Compare commits
2 Commits
main
...
feature/vi
| Author | SHA1 | Date |
|---|---|---|
|
|
b2ff4238af | |
|
|
939a3d11a7 |
|
|
@ -24,7 +24,7 @@ allow_origins = ["*"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
|
# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
|
||||||
unsafe_allow_html = false
|
unsafe_allow_html = true
|
||||||
|
|
||||||
# Process and display mathematical expressions. This can clash with "$" characters in messages.
|
# Process and display mathematical expressions. This can clash with "$" characters in messages.
|
||||||
latex = false
|
latex = false
|
||||||
|
|
@ -57,7 +57,7 @@ reaction_on_message_received = false
|
||||||
# 3. For specific file extensions:
|
# 3. For specific file extensions:
|
||||||
# accept = { "application/octet-stream" = [".xyz", ".pdb"] }
|
# accept = { "application/octet-stream" = [".xyz", ".pdb"] }
|
||||||
# Note: Using "*/*" is not recommended as it may cause browser warnings
|
# Note: Using "*/*" is not recommended as it may cause browser warnings
|
||||||
accept = ["*/*"]
|
accept = ["*"]
|
||||||
max_files = 20
|
max_files = 20
|
||||||
max_size_mb = 500
|
max_size_mb = 500
|
||||||
|
|
||||||
|
|
@ -86,11 +86,11 @@ reaction_on_message_received = false
|
||||||
|
|
||||||
[UI]
|
[UI]
|
||||||
# Name of the assistant.
|
# Name of the assistant.
|
||||||
name = "Assistant"
|
name = "Ai Station DFFM"
|
||||||
|
|
||||||
# default_theme = "dark"
|
default_theme = "dark"
|
||||||
|
|
||||||
# layout = "wide"
|
layout = "wide"
|
||||||
|
|
||||||
default_sidebar_state = "open"
|
default_sidebar_state = "open"
|
||||||
|
|
||||||
|
|
@ -104,6 +104,14 @@ cot = "full"
|
||||||
# The CSS file can be served from the public directory or via an external link.
|
# The CSS file can be served from the public directory or via an external link.
|
||||||
# custom_css = "/public/test.css"
|
# custom_css = "/public/test.css"
|
||||||
|
|
||||||
|
# CSS personalizzato
|
||||||
|
custom_css = "/public/custom.css"
|
||||||
|
|
||||||
|
# Logo custom
|
||||||
|
[UI.theme]
|
||||||
|
primary_color = "#0066CC" # Colore brand
|
||||||
|
background_color = "#1a1a1a"
|
||||||
|
|
||||||
# Specify additional attributes for a custom CSS file
|
# Specify additional attributes for a custom CSS file
|
||||||
# custom_css_attributes = "media=\"print\""
|
# custom_css_attributes = "media=\"print\""
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 487 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,463 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
import shutil
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, List, Any
|
||||||
|
import chainlit as cl
|
||||||
|
import ollama
|
||||||
|
from docling.document_converter import DocumentConverter
|
||||||
|
from qdrant_client import AsyncQdrantClient
|
||||||
|
# CORREZIONE IMPORT: Importiamo le classi necessarie direttamente dalla libreria
|
||||||
|
from qdrant_client.models import PointStruct, Distance, VectorParams, SparseVectorParams, Prefetch
|
||||||
|
from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
|
||||||
|
from chainlit.types import ThreadDict
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
# === FIX IMPORT ROBUSTO ===
|
||||||
|
try:
|
||||||
|
from chainlit.data.storage_clients import BaseStorageClient
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from chainlit.data.base import BaseStorageClient
|
||||||
|
except ImportError:
|
||||||
|
from chainlit.data.storage_clients.base import BaseStorageClient
|
||||||
|
|
||||||
|
# === CONFIGURAZIONE ===
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station")
|
||||||
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://192.168.1.243:11434")
|
||||||
|
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
||||||
|
BGE_API_URL = os.getenv("BGE_API_URL", "http://192.168.1.243:8001/embed")
|
||||||
|
|
||||||
|
VISION_MODEL = "minicpm-v"
|
||||||
|
DEFAULT_TEXT_MODEL = "glm-4.6:cloud"
|
||||||
|
|
||||||
|
WORKSPACES_DIR = "./workspaces"
|
||||||
|
STORAGE_DIR = "./.files"
|
||||||
|
|
||||||
|
os.makedirs(STORAGE_DIR, exist_ok=True)
|
||||||
|
os.makedirs(WORKSPACES_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# === MAPPING UTENTI ===
|
||||||
|
USER_PROFILES = {
|
||||||
|
"giuseppe@defranceschi.pro": { "role": "admin", "name": "Giuseppe", "workspace": "admin_workspace", "rag_collection": "admin_docs", "capabilities": ["debug", "all"], "show_code": True },
|
||||||
|
"giuseppe.defranceschi@gmail.com": { "role": "admin", "name": "Giuseppe", "workspace": "admin_workspace", "rag_collection": "admin_docs", "capabilities": ["debug", "all"], "show_code": True },
|
||||||
|
"federica.tecchio@gmail.com": { "role": "business", "name": "Federica", "workspace": "business_workspace", "rag_collection": "contabilita", "capabilities": ["basic_chat"], "show_code": False },
|
||||||
|
"riccardob545@gmail.com": { "role": "engineering", "name": "Riccardo", "workspace": "engineering_workspace", "rag_collection": "engineering_docs", "capabilities": ["code"], "show_code": True },
|
||||||
|
"giuliadefranceschi05@gmail.com": { "role": "architecture", "name": "Giulia", "workspace": "architecture_workspace", "rag_collection": "architecture_manuals", "capabilities": ["visual"], "show_code": False }
|
||||||
|
}
|
||||||
|
|
||||||
|
# === STORAGE CLIENT ===
|
||||||
|
class LocalStorageClient(BaseStorageClient):
|
||||||
|
def __init__(self, storage_path: str):
|
||||||
|
self.storage_path = storage_path
|
||||||
|
os.makedirs(storage_path, exist_ok=True)
|
||||||
|
async def upload_file(self, object_key: str, data: bytes, mime: str = "application/octet-stream", overwrite: bool = True) -> Dict[str, str]:
|
||||||
|
file_path = os.path.join(self.storage_path, object_key)
|
||||||
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
|
with open(file_path, "wb") as f: f.write(data)
|
||||||
|
return {"object_key": object_key, "url": f"/files/{object_key}"}
|
||||||
|
async def get_read_url(self, object_key: str) -> str: return f"/files/{object_key}"
|
||||||
|
async def delete_file(self, object_key: str) -> bool:
|
||||||
|
path = os.path.join(self.storage_path, object_key)
|
||||||
|
if os.path.exists(path): os.remove(path); return True
|
||||||
|
return False
|
||||||
|
async def close(self): pass
|
||||||
|
|
||||||
|
@cl.data_layer
|
||||||
|
def get_data_layer():
|
||||||
|
return SQLAlchemyDataLayer(conninfo=DATABASE_URL, storage_provider=LocalStorageClient(STORAGE_DIR))
|
||||||
|
|
||||||
|
# === OAUTH & UTILS ===
|
||||||
|
@cl.oauth_callback
|
||||||
|
def oauth_callback(provider_id: str, token: str, raw_user_data: Dict[str, str], default_user: cl.User) -> Optional[cl.User]:
|
||||||
|
if provider_id == "google":
|
||||||
|
email = raw_user_data.get("email", "").lower()
|
||||||
|
profile = USER_PROFILES.get(email, USER_PROFILES.get("guest", {"role": "guest", "name": "Guest", "workspace": "guest", "rag_collection": "public", "show_code": False}))
|
||||||
|
default_user.metadata.update({"role": profile["role"], "workspace": profile["workspace"], "rag_collection": profile["rag_collection"], "show_code": profile["show_code"], "display_name": profile["name"]})
|
||||||
|
return default_user
|
||||||
|
return default_user
|
||||||
|
|
||||||
|
def create_workspace(workspace_name: str) -> str:
|
||||||
|
path = os.path.join(WORKSPACES_DIR, workspace_name)
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# === CORE: DOCLING ===
|
||||||
|
def process_file_with_docling(file_path: str) -> str:
|
||||||
|
try:
|
||||||
|
converter = DocumentConverter()
|
||||||
|
result = converter.convert(file_path)
|
||||||
|
return result.document.export_to_markdown()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Docling Error: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# === CORE: BGE-M3 CLIENT ===
|
||||||
|
def get_bge_embeddings(text: str) -> Optional[Dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
payload = {"texts": [text[:8000]]}
|
||||||
|
response = requests.post(BGE_API_URL, json=payload, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json().get("data", [])
|
||||||
|
if data:
|
||||||
|
return data[0]
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ BGE API Error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# === CORE: QDRANT ===
|
||||||
|
async def ensure_collection(collection_name: str):
|
||||||
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
||||||
|
if not await client.collection_exists(collection_name):
|
||||||
|
await client.create_collection(
|
||||||
|
collection_name=collection_name,
|
||||||
|
vectors_config={"dense": VectorParams(size=1024, distance=Distance.COSINE)},
|
||||||
|
sparse_vectors_config={"sparse": SparseVectorParams()}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def index_document(file_name: str, content: str, collection_name: str):
|
||||||
|
await ensure_collection(collection_name)
|
||||||
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
||||||
|
|
||||||
|
chunk_size = 2000
|
||||||
|
overlap = 200
|
||||||
|
|
||||||
|
points = []
|
||||||
|
for i in range(0, len(content), chunk_size - overlap):
|
||||||
|
chunk = content[i : i + chunk_size]
|
||||||
|
embedding_data = get_bge_embeddings(chunk)
|
||||||
|
|
||||||
|
if embedding_data:
|
||||||
|
points.append(PointStruct(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
vector={
|
||||||
|
"dense": embedding_data["dense"],
|
||||||
|
"sparse": embedding_data["sparse"]
|
||||||
|
},
|
||||||
|
payload={
|
||||||
|
"file_name": file_name,
|
||||||
|
"content": chunk,
|
||||||
|
"indexed_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
if points:
|
||||||
|
await client.upsert(collection_name=collection_name, points=points)
|
||||||
|
return len(points)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def search_hybrid(query: str, collection_name: str, limit: int = 4) -> str:
|
||||||
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
||||||
|
if not await client.collection_exists(collection_name): return ""
|
||||||
|
|
||||||
|
query_emb = get_bge_embeddings(query)
|
||||||
|
if not query_emb: return ""
|
||||||
|
|
||||||
|
# CORREZIONE QUI: Usiamo l'oggetto Prefetch importato correttamente
|
||||||
|
results = await client.query_points(
|
||||||
|
collection_name=collection_name,
|
||||||
|
prefetch=[
|
||||||
|
Prefetch(
|
||||||
|
query=query_emb["sparse"],
|
||||||
|
using="sparse",
|
||||||
|
limit=limit * 2
|
||||||
|
)
|
||||||
|
],
|
||||||
|
query=query_emb["dense"],
|
||||||
|
using="dense",
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
context = []
|
||||||
|
for hit in results.points:
|
||||||
|
context.append(f"--- DA {hit.payload['file_name']} ---\n{hit.payload['content']}")
|
||||||
|
|
||||||
|
return "\n\n".join(context)
|
||||||
|
|
||||||
|
# === Caching Embeddings ===
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_bge_embeddings_cached(text: str):
|
||||||
|
"""Cache per query ripetute"""
|
||||||
|
return get_bge_embeddings(text)
|
||||||
|
|
||||||
|
# === CHAINLIT HANDLERS ===
|
||||||
|
@cl.on_chat_start
|
||||||
|
async def start():
|
||||||
|
# 1. Profilo utente
|
||||||
|
user = cl.user_session.get("user")
|
||||||
|
email = user.identifier if user else "guest"
|
||||||
|
profile = USER_PROFILES.get(email, USER_PROFILES["giuseppe@defranceschi.pro"])
|
||||||
|
|
||||||
|
cl.user_session.set("profile", profile)
|
||||||
|
create_workspace(profile["workspace"])
|
||||||
|
|
||||||
|
# 2. Badge HTML personalizzato
|
||||||
|
role_color = {
|
||||||
|
"admin": "#e74c3c",
|
||||||
|
"engineering": "#3498db",
|
||||||
|
"business": "#2ecc71",
|
||||||
|
"architecture": "#9b59b6",
|
||||||
|
}.get(profile["role"], "#95a5a6")
|
||||||
|
|
||||||
|
badge_html = f"""
|
||||||
|
<div style="background:{role_color}; padding:8px; border-radius:8px; margin-bottom:16px;">
|
||||||
|
👤 <b>{profile['name']}</b> | 🔧 {profile['role'].upper()} | 📁 {profile['workspace']}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
await cl.Message(content=badge_html).send()
|
||||||
|
|
||||||
|
# 3. Settings UI
|
||||||
|
settings = await cl.ChatSettings(
|
||||||
|
[
|
||||||
|
cl.input_widget.Slider(
|
||||||
|
id="top_k",
|
||||||
|
label="Numero Documenti RAG",
|
||||||
|
initial=4,
|
||||||
|
min=1,
|
||||||
|
max=10,
|
||||||
|
step=1,
|
||||||
|
),
|
||||||
|
cl.input_widget.Select(
|
||||||
|
id="vision_detail",
|
||||||
|
label="Dettaglio Analisi Immagini",
|
||||||
|
values=["auto", "low", "high"],
|
||||||
|
initial_value="auto",
|
||||||
|
),
|
||||||
|
cl.input_widget.TextInput(
|
||||||
|
id="system_instruction",
|
||||||
|
label="Istruzione Sistema Custom (opzionale)",
|
||||||
|
initial="",
|
||||||
|
placeholder="Es: Rispondi sempre in formato tecnico...",
|
||||||
|
),
|
||||||
|
cl.input_widget.Select(
|
||||||
|
id="model",
|
||||||
|
label="Modello di Ragionamento",
|
||||||
|
values=[DEFAULT_TEXT_MODEL, "llama3.2", "mistral", "qwen2.5-coder:32b"],
|
||||||
|
initial_value=DEFAULT_TEXT_MODEL,
|
||||||
|
),
|
||||||
|
cl.input_widget.Slider(
|
||||||
|
id="temperature",
|
||||||
|
label="Creatività (Temperatura)",
|
||||||
|
initial=0.3,
|
||||||
|
min=0,
|
||||||
|
max=1,
|
||||||
|
step=0.1,
|
||||||
|
),
|
||||||
|
cl.input_widget.Switch(
|
||||||
|
id="rag_enabled",
|
||||||
|
label="Usa Conoscenza Documenti (RAG)",
|
||||||
|
initial=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
).send()
|
||||||
|
|
||||||
|
cl.user_session.set("settings", settings)
|
||||||
|
|
||||||
|
# 4. Messaggio iniziale (opzionale)
|
||||||
|
await cl.Message(
|
||||||
|
content=(
|
||||||
|
f"🚀 **Vision-RAG Hybrid System Online**\n"
|
||||||
|
f"Utente: {profile['name']} | Workspace: {profile['workspace']}\n"
|
||||||
|
f"Engine: Docling + BGE-M3 + {VISION_MODEL}"
|
||||||
|
)
|
||||||
|
).send()
|
||||||
|
|
||||||
|
|
||||||
|
cl.user_session.set("settings", settings)
|
||||||
|
|
||||||
|
await cl.Message(f"🚀 **Vision-RAG Hybrid System Online**\nUtente: {profile['name']} | Workspace: {profile['workspace']}\nEngine: Docling + BGE-M3 + {VISION_MODEL}").send()
|
||||||
|
|
||||||
|
@cl.on_settings_update
|
||||||
|
async def setup_agent(settings):
|
||||||
|
cl.user_session.set("settings", settings)
|
||||||
|
await cl.Message(content=f"✅ Impostazioni aggiornate: Modello {settings['model']}").send()
|
||||||
|
|
||||||
|
async def log_metrics(metrics: dict):
|
||||||
|
# Versione minima: log su stdout
|
||||||
|
print("[METRICS]", metrics)
|
||||||
|
|
||||||
|
# In futuro puoi:
|
||||||
|
# - salvarle in Postgres
|
||||||
|
# - mandarle a Prometheus / Grafana
|
||||||
|
# - scriverle su file JSON per analisi settimanale
|
||||||
|
|
||||||
|
# - Resume Chat Handler
|
||||||
|
|
||||||
|
@cl.on_chat_resume
|
||||||
|
async def on_chat_resume(thread: ThreadDict):
|
||||||
|
"""
|
||||||
|
Viene chiamato quando l'utente clicca 'Riprendi' su una chat archiviata.
|
||||||
|
Chainlit carica già i messaggi nella UI, qui puoi solo ripristinare la sessione.
|
||||||
|
"""
|
||||||
|
# Se vuoi, puoi recuperare l'identifier dell’utente dal thread
|
||||||
|
user_identifier = thread.get("userIdentifier")
|
||||||
|
profile = USER_PROFILES.get(
|
||||||
|
user_identifier,
|
||||||
|
USER_PROFILES["giuseppe@defranceschi.pro"],
|
||||||
|
)
|
||||||
|
cl.user_session.set("profile", profile)
|
||||||
|
|
||||||
|
# Puoi anche ripristinare eventuale stato custom (es: impostazioni di default)
|
||||||
|
# oppure semplicemente salutare l’utente
|
||||||
|
await cl.Message(
|
||||||
|
content="👋 Bentornato! Possiamo riprendere da questa conversazione."
|
||||||
|
).send()
|
||||||
|
|
||||||
|
@cl.on_message
|
||||||
|
async def main(message: cl.Message):
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
profile = cl.user_session.get("profile")
|
||||||
|
settings = cl.user_session.get("settings", {})
|
||||||
|
|
||||||
|
selected_model = settings.get("model", DEFAULT_TEXT_MODEL)
|
||||||
|
temperature = settings.get("temperature", 0.3)
|
||||||
|
rag_enabled = settings.get("rag_enabled", True)
|
||||||
|
|
||||||
|
workspace = create_workspace(profile["workspace"])
|
||||||
|
|
||||||
|
images_for_vision = []
|
||||||
|
doc_context = ""
|
||||||
|
rag_context = "" # ← la inizializzi qui, così esiste sempre
|
||||||
|
|
||||||
|
# 1. GESTIONE FILE
|
||||||
|
if message.elements:
|
||||||
|
for element in message.elements:
|
||||||
|
file_path = os.path.join(workspace, element.name)
|
||||||
|
shutil.copy(element.path, file_path)
|
||||||
|
|
||||||
|
if "image" in element.mime:
|
||||||
|
images_for_vision.append(file_path)
|
||||||
|
msg_img = cl.Message(
|
||||||
|
content=f"👁️ Analizzo immagine **{element.name}** con {VISION_MODEL}..."
|
||||||
|
)
|
||||||
|
await msg_img.send()
|
||||||
|
|
||||||
|
with open(file_path, "rb") as img_file:
|
||||||
|
img_bytes = img_file.read()
|
||||||
|
|
||||||
|
client_sync = ollama.Client(host=OLLAMA_URL)
|
||||||
|
res = client_sync.chat(
|
||||||
|
model=VISION_MODEL,
|
||||||
|
messages=[{
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
"Analizza questa immagine tecnica. Trascrivi testi, codici "
|
||||||
|
"e descrivi diagrammi o tabelle in dettaglio."
|
||||||
|
),
|
||||||
|
"images": [img_bytes],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
desc = res["message"]["content"]
|
||||||
|
doc_context += f"\n\n[DESCRIZIONE IMMAGINE {element.name}]:\n{desc}"
|
||||||
|
msg_img.content = f"✅ Immagine analizzata:\n{desc[:200]}..."
|
||||||
|
await msg_img.update()
|
||||||
|
|
||||||
|
elif element.name.endswith((".pdf", ".docx")):
|
||||||
|
msg_doc = cl.Message(
|
||||||
|
content=f"📄 Leggo **{element.name}** con Docling (tabelle/formule)..."
|
||||||
|
)
|
||||||
|
await msg_doc.send()
|
||||||
|
|
||||||
|
markdown_content = process_file_with_docling(file_path)
|
||||||
|
if markdown_content:
|
||||||
|
chunks = await index_document(
|
||||||
|
element.name, markdown_content, profile["rag_collection"]
|
||||||
|
)
|
||||||
|
msg_doc.content = (
|
||||||
|
f"✅ **{element.name}**: Convertito e salvato {chunks} "
|
||||||
|
"frammenti nel DB vettoriale."
|
||||||
|
)
|
||||||
|
doc_context += (
|
||||||
|
f"\n\n[CONTENUTO FILE {element.name}]:\n"
|
||||||
|
f"{markdown_content[:1000]}..."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg_doc.content = f"❌ Errore lettura {element.name}"
|
||||||
|
await msg_doc.update()
|
||||||
|
|
||||||
|
# 2. RAG RETRIEVAL
|
||||||
|
if rag_enabled and not images_for_vision:
|
||||||
|
rag_context = await search_hybrid(
|
||||||
|
message.content, profile["rag_collection"]
|
||||||
|
)
|
||||||
|
|
||||||
|
final_context = ""
|
||||||
|
if rag_context:
|
||||||
|
final_context += f"CONTESTO RAG:\n{rag_context}\n"
|
||||||
|
if doc_context:
|
||||||
|
final_context += f"CONTESTO SESSIONE CORRENTE:\n{doc_context}\n"
|
||||||
|
|
||||||
|
system_prompt = (
|
||||||
|
"Sei un assistente tecnico esperto. Usa il contesto fornito "
|
||||||
|
"(incluso Markdown di tabelle e descrizioni immagini) per "
|
||||||
|
"rispondere con precisione. Cita i documenti fonte."
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = cl.Message(content="")
|
||||||
|
await msg.send()
|
||||||
|
|
||||||
|
error = None
|
||||||
|
|
||||||
|
# 3. GENERAZIONE
|
||||||
|
try:
|
||||||
|
client_async = ollama.AsyncClient(host=OLLAMA_URL)
|
||||||
|
stream = await client_async.chat(
|
||||||
|
model=selected_model,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": f"Domanda: {message.content}\n\n{final_context}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options={"temperature": temperature},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async for chunk in stream:
|
||||||
|
content = chunk["message"]["content"]
|
||||||
|
await msg.stream_token(content)
|
||||||
|
await msg.update()
|
||||||
|
except Exception as e:
|
||||||
|
error = str(e)
|
||||||
|
await msg.stream_token(f"❌ Errore AI: {error}")
|
||||||
|
await msg.update()
|
||||||
|
|
||||||
|
# 4. SALVATAGGIO CODICE
|
||||||
|
if profile["show_code"]:
|
||||||
|
code_blocks = re.findall(r"``````", msg.content, re.DOTALL)
|
||||||
|
if code_blocks:
|
||||||
|
for i, code in enumerate(code_blocks):
|
||||||
|
fname = f"script_{datetime.now().strftime('%H%M%S')}_{i}.py"
|
||||||
|
with open(os.path.join(workspace, fname), "w") as f:
|
||||||
|
f.write(code.strip())
|
||||||
|
await cl.Message(
|
||||||
|
content=f"💾 Script salvato: `{fname}`"
|
||||||
|
).send()
|
||||||
|
|
||||||
|
# 5. METRICHE (ALLA FINE)
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
# Se rag_context è una stringa concatenata, puoi stimare i "rag_hits"
|
||||||
|
# contando i separatori che usi in search_hybrid (es. '--- DA ')
|
||||||
|
if rag_context:
|
||||||
|
rag_hits = rag_context.count("--- DA ")
|
||||||
|
else:
|
||||||
|
rag_hits = 0
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
"response_time": elapsed,
|
||||||
|
"rag_hits": rag_hits,
|
||||||
|
"model": selected_model,
|
||||||
|
"user_role": profile["role"],
|
||||||
|
"error": error,
|
||||||
|
}
|
||||||
|
|
||||||
|
await log_metrics(metrics)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -3,9 +3,12 @@ FROM python:3.11-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Installa dipendenze sistema
|
# Installa dipendenze sistema
|
||||||
|
# Aggiunte libgl1 e libglib2.0-0 per il supporto Docling/CV2
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gcc \
|
gcc \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
|
libgl1 \
|
||||||
|
libglib2.0-0 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copia requirements e installa
|
# Copia requirements e installa
|
||||||
|
|
|
||||||
593
README.md
593
README.md
|
|
@ -1,325 +1,342 @@
|
||||||
# AI Station - Document Analysis Platform
|
# AI Station DFFM - Vision-RAG Hybrid System
|
||||||
|
|
||||||
## 📋 Overview
|
Sistema AI multi-utente con supporto RAG (Retrieval-Augmented Generation), analisi immagini e gestione documenti avanzata, basato su Chainlit, Ollama e BGE-M3.
|
||||||
|
|
||||||
**AI Station** è una piattaforma di analisi documentale basata su AI che utilizza **Retrieval-Augmented Generation (RAG)** per analizzare PDF e documenti testuali con il modello **GLM-4.6:Cloud**.
|
## 🌟 Features
|
||||||
|
|
||||||
|
### Core AI
|
||||||
|
- **RAG Hybrid Search** con BGE-M3 (dense + sparse embeddings)
|
||||||
|
- **Vision Analysis** tramite MiniCPM-V per OCR e descrizione immagini
|
||||||
|
- **Document Processing** con Docling (PDF, DOCX) con preservazione tabelle/formule
|
||||||
|
- **Multi-Model Support** (Ollama locale + cloud models)
|
||||||
|
- **Streaming Responses** con latenza ridotta
|
||||||
|
|
||||||
|
### Multi-Utente
|
||||||
|
- **OAuth2 Google** con profili personalizzati per ruolo
|
||||||
|
- **Workspace isolati** per utente/team
|
||||||
|
- **RAG Collections dedicate** per knowledge base separate
|
||||||
|
- **Permessi granulari** (admin, engineering, business, architecture)
|
||||||
|
|
||||||
|
### UI/UX
|
||||||
|
- **Badge ruolo personalizzato** con colori dedicati
|
||||||
|
- **Settings dinamici** (temperatura, top_k RAG, modello, istruzioni custom)
|
||||||
|
- **Chat history persistente** con ripresa conversazioni
|
||||||
|
- **Auto-save codice Python** estratto dalle risposte
|
||||||
|
- **Metriche real-time** (response time, RAG hits, errori)
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Caching embeddings** (LRU cache 1000 query)
|
||||||
|
- **Chunking intelligente** (2000 char con overlap 200)
|
||||||
|
- **Async operations** su Qdrant e Ollama
|
||||||
|
- **PostgreSQL** per persistenza thread e metadata
|
||||||
|
|
||||||
|
## 🏗️ Architettura
|
||||||
|
|
||||||
|
### Hardware Setup
|
||||||
|
|
||||||
|
#### AI-SRV (Chainlit VM)
|
||||||
|
- **IP**: 192.168.1.244
|
||||||
|
- **CPU**: 16 core (QEMU Virtual)
|
||||||
|
- **RAM**: 64 GB
|
||||||
|
- **Storage**: 195 GB
|
||||||
|
- **Ruolo**: Host Chainlit app + PostgreSQL + Qdrant
|
||||||
|
|
||||||
|
#### AI-Server (GPU Workstation)
|
||||||
|
- **IP**: 192.168.1.243
|
||||||
|
- **CPU**: Intel Core Ultra 7 265 (20 core, max 6.5 GHz)
|
||||||
|
- **RAM**: 32 GB
|
||||||
|
- **GPU**: NVIDIA RTX A1000 (8 GB VRAM)
|
||||||
|
- **Storage**: 936 GB NVMe
|
||||||
|
- **Ruolo**: Ollama models + BGE-M3 embeddings service
|
||||||
|
|
||||||
### Stack Tecnologico
|
### Stack Tecnologico
|
||||||
- **Backend**: Python + Chainlit (LLM UI framework)
|
|
||||||
- **LLM**: GLM-4.6:Cloud (via Ollama Cloud)
|
|
||||||
- **Vector DB**: Qdrant (semantic search)
|
|
||||||
- **PDF Processing**: PyMuPDF (fitz)
|
|
||||||
- **Database**: PostgreSQL + SQLAlchemy ORM
|
|
||||||
- **Containerization**: Docker Compose
|
|
||||||
- **Embeddings**: nomic-embed-text (via Ollama local)
|
|
||||||
|
|
||||||
---
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Chainlit UI (ai-srv) │
|
||||||
|
│ Badge + Settings + Chat History │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────▼──────────────────────────┐
|
||||||
|
│ Python Backend (app.py) │
|
||||||
|
│ - OAuth2 Google │
|
||||||
|
│ - Multi-user profiles │
|
||||||
|
│ - File processing orchestration │
|
||||||
|
└─┬────────┬──────────┬──────────┬────────┘
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
┌─────┐ ┌─────┐ ┌────────┐ ┌──────────┐
|
||||||
|
│ PG │ │Qdrant│ │ Ollama │ │ BGE API │
|
||||||
|
│ │ │Vector│ │ GPU │ │ CPU │
|
||||||
|
│ │ │ DB │ │ Server │ │ Server │
|
||||||
|
└─────┘ └─────┘ └────────┘ └──────────┘
|
||||||
|
ai-srv ai-srv ai-server ai-server
|
||||||
|
|
||||||
## 🚀 Quick Start
|
text
|
||||||
|
|
||||||
### Prerequisites
|
## 📋 Requisiti
|
||||||
- Docker & Docker Compose
|
|
||||||
- Ollama installed locally (for embeddings)
|
|
||||||
- Ollama Cloud account (for glm-4.6:cloud)
|
|
||||||
|
|
||||||
### 1️⃣ Clone & Setup
|
### Sistema
|
||||||
|
- Docker 24.x+ con Docker Compose
|
||||||
|
- Accesso a Google Cloud Console (per OAuth2)
|
||||||
|
- 2 server (o VM) con networking condiviso
|
||||||
|
|
||||||
|
### Modelli Ollama (da installare su ai-server)
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:your-username/ai-station.git
|
ollama pull minicpm-v # Vision model (5.5 GB)
|
||||||
|
ollama pull glm-4.6:cloud # Cloud reasoning
|
||||||
|
ollama pull qwen2.5-coder:32b # Code generation (9 GB)
|
||||||
|
ollama pull llama3.2 # Fast general purpose (4.7 GB)
|
||||||
|
🚀 Installazione
|
||||||
|
1. Clone Repository
|
||||||
|
bash
|
||||||
|
git clone <your-repo>
|
||||||
cd ai-station
|
cd ai-station
|
||||||
|
2. Configurazione Ambiente
|
||||||
|
Crea .env:
|
||||||
|
|
||||||
# Configure environment
|
bash
|
||||||
cat > .env << 'EOF'
|
# Database
|
||||||
DATABASE_URL=postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station
|
DATABASE_URL=postgresql+asyncpg://ai_user:CHANGE_ME@postgres:5432/ai_station
|
||||||
|
|
||||||
|
# AI Services
|
||||||
OLLAMA_URL=http://192.168.1.243:11434
|
OLLAMA_URL=http://192.168.1.243:11434
|
||||||
QDRANT_URL=http://qdrant:6333
|
QDRANT_URL=http://qdrant:6333
|
||||||
EOF
|
BGE_API_URL=http://192.168.1.243:8001/embed
|
||||||
```
|
|
||||||
|
|
||||||
### 2️⃣ Authenticate Ollama Cloud
|
# OAuth Google
|
||||||
```bash
|
OAUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
||||||
ollama signin
|
OAUTH_GOOGLE_CLIENT_SECRET=your-secret
|
||||||
# Follow the link to authenticate with your Ollama account
|
CHAINLIT_AUTH_SECRET=$(openssl rand -base64 32)
|
||||||
```
|
|
||||||
|
|
||||||
### 3️⃣ Start Services
|
3. Configurazione OAuth Google
|
||||||
```bash
|
Vai su Google Cloud Console
|
||||||
docker compose up -d
|
|
||||||
|
Crea nuovo progetto → API e servizi → Credenziali
|
||||||
|
|
||||||
|
Crea "ID client OAuth 2.0"
|
||||||
|
|
||||||
|
Aggiungi URI autorizzati:
|
||||||
|
|
||||||
|
https://ai.dffm.it/auth/oauth/google/callback
|
||||||
|
|
||||||
|
http://localhost:8000/auth/oauth/google/callback (dev)
|
||||||
|
|
||||||
|
Copia Client ID e Secret in .env
|
||||||
|
|
||||||
|
4. Personalizza Utenti
|
||||||
|
Modifica app.py → USER_PROFILES:
|
||||||
|
|
||||||
|
python
|
||||||
|
USER_PROFILES = {
|
||||||
|
"tuo.email@example.com": {
|
||||||
|
"role": "admin",
|
||||||
|
"name": "Nome",
|
||||||
|
"workspace": "workspace_name",
|
||||||
|
"rag_collection": "docs_collection",
|
||||||
|
"capabilities": ["debug", "all"],
|
||||||
|
"show_code": True,
|
||||||
|
},
|
||||||
|
# ... altri utenti
|
||||||
|
}
|
||||||
|
5. Deploy
|
||||||
|
bash
|
||||||
|
# Build e avvio
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# Verifica logs
|
||||||
docker compose logs -f chainlit-app
|
docker compose logs -f chainlit-app
|
||||||
|
|
||||||
|
# Dovresti vedere:
|
||||||
|
# ✅ Tutte le tabelle create con successo.
|
||||||
|
# Your app is available at http://localhost:8000
|
||||||
|
6. Setup BGE-M3 Service (su ai-server)
|
||||||
|
bash
|
||||||
|
# Installa dependencies
|
||||||
|
pip install fastapi uvicorn FlagEmbedding torch
|
||||||
|
|
||||||
|
# Salva il file bge_service.py (vedi docs/)
|
||||||
|
python bge_service.py
|
||||||
|
# Listening on http://0.0.0.0:8001
|
||||||
|
🎯 Utilizzo
|
||||||
|
Login
|
||||||
|
Accedi via browser: https://ai.dffm.it (o http://localhost:8000)
|
||||||
|
|
||||||
|
Click su "Continue with Google"
|
||||||
|
|
||||||
|
Autorizza con account configurato in USER_PROFILES
|
||||||
|
|
||||||
|
Chat con RAG
|
||||||
|
Carica PDF/DOCX → Sistema li indicizza automaticamente
|
||||||
|
|
||||||
|
Fai domande → Risposta con contesto dai documenti
|
||||||
|
|
||||||
|
Regola top_k (numero documenti) via settings
|
||||||
|
|
||||||
|
Analisi Immagini
|
||||||
|
Carica screenshot/diagrammi
|
||||||
|
|
||||||
|
Il sistema:
|
||||||
|
|
||||||
|
Estrae testo (OCR)
|
||||||
|
|
||||||
|
Descrive grafici/tabelle
|
||||||
|
|
||||||
|
Usa descrizione come contesto per rispondere
|
||||||
|
|
||||||
|
Settings Disponibili
|
||||||
|
Numero Documenti RAG (1-10): Quanti chunk recuperare
|
||||||
|
|
||||||
|
Modello: Scegli tra locale/cloud
|
||||||
|
|
||||||
|
Temperatura (0-1): Creatività risposta
|
||||||
|
|
||||||
|
RAG Enabled: On/Off recupero documenti
|
||||||
|
|
||||||
|
Istruzione Custom: Prompt system personalizzato
|
||||||
|
|
||||||
|
Ripresa Chat
|
||||||
|
Sidebar → Chat History
|
||||||
|
|
||||||
|
Click su conversazione → "Riprendi"
|
||||||
|
|
||||||
|
Continua da dove avevi lasciato
|
||||||
|
|
||||||
|
📊 Metriche
|
||||||
|
Ogni risposta logga (stdout):
|
||||||
|
|
||||||
|
json
|
||||||
|
{
|
||||||
|
"response_time": 18.65,
|
||||||
|
"rag_hits": 4,
|
||||||
|
"model": "glm-4.6:cloud",
|
||||||
|
"user_role": "admin",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
Raccogli con:
|
||||||
|
|
||||||
|
bash
|
||||||
|
docker logs ai-station-app | grep METRICS > metrics.log
|
||||||
|
🔧 Troubleshooting
|
||||||
|
RAG non trova documenti
|
||||||
|
Verifica collection name in USER_PROFILES[email]["rag_collection"]
|
||||||
|
|
||||||
|
Controlla Qdrant: curl http://localhost:6333/collections
|
||||||
|
|
||||||
|
Badge HTML non si vede
|
||||||
|
Abilita in .chainlit/config.toml:
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4️⃣ Access UI
|
```text
|
||||||
Navigate to: **http://localhost:8000**
|
[features]
|
||||||
|
unsafe_allow_html = true
|
||||||
---
|
Modello Ollama non risponde
|
||||||
|
bash
|
||||||
## 📁 Project Structure
|
# Testa connessione
|
||||||
|
curl http://192.168.1.243:11434/api/tags
|
||||||
|
|
||||||
|
# Verifica modello disponibile
|
||||||
|
ollama list
|
||||||
|
BGE embeddings fail
|
||||||
```
|
```
|
||||||
|
```bash
|
||||||
|
# Testa API
|
||||||
|
curl -X POST http://192.168.1.243:8001/embed \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"texts": ["test"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
📁 Struttura Progetto
|
||||||
|
```bash
|
||||||
ai-station/
|
ai-station/
|
||||||
├── app.py # Main Chainlit application
|
├── app.py # Main Chainlit app
|
||||||
├── requirements.txt # Python dependencies
|
├── init_db.py # Database schema init
|
||||||
├── docker-compose.yml # Docker services config
|
├── requirements.txt # Python deps
|
||||||
├── .env # Environment variables (gitignored)
|
├── Dockerfile # Container config
|
||||||
├── workspaces/ # User workspace directories
|
├── docker-compose.yaml # Multi-service orchestration
|
||||||
│ └── admin/ # Admin user files
|
├── .chainlit/
|
||||||
└── README.md # This file
|
│ └── config.toml # UI/features config
|
||||||
|
├── public/
|
||||||
|
│ └── custom.css # Custom styling
|
||||||
|
├── workspaces/ # User file storage (volume)
|
||||||
|
│ ├── admin_workspace/
|
||||||
|
│ ├── engineering_workspace/
|
||||||
|
│ └── ...
|
||||||
|
└── .files/ # Chainlit storage (volume)
|
||||||
```
|
```
|
||||||
|
🔐 Sicurezza
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Features
|
|
||||||
|
|
||||||
### ✅ Implemented
|
|
||||||
- **PDF Upload & Processing**: Extract text from PDF documents using PyMuPDF
|
|
||||||
- **Document Indexing**: Automatic chunking and semantic indexing via Qdrant
|
|
||||||
- **RAG Search**: Retrieve relevant document chunks based on semantic similarity
|
|
||||||
- **Intelligent Analysis**: GLM-4.6:Cloud analyzes documents with full context
|
|
||||||
- **Code Extraction**: Automatically save Python code blocks from responses
|
|
||||||
- **Chat History**: Persistent conversation storage via SQLAlchemy
|
|
||||||
- **Streaming Responses**: Real-time token streaming via Chainlit
|
|
||||||
|
|
||||||
### 🔄 Workflow
|
|
||||||
1. User uploads PDF or TXT file
|
|
||||||
2. System extracts text and creates semantic chunks
|
|
||||||
3. Chunks indexed in Qdrant vector database
|
|
||||||
4. User asks questions about documents
|
|
||||||
5. RAG retrieves relevant chunks
|
|
||||||
6. GLM-4.6:Cloud analyzes with full context
|
|
||||||
7. Streaming response to user
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Technical Details
|
|
||||||
|
|
||||||
### Document Processing Pipeline
|
|
||||||
|
|
||||||
```
|
|
||||||
PDF Upload
|
|
||||||
↓
|
|
||||||
PyMuPDF Text Extraction
|
|
||||||
↓
|
|
||||||
Text Chunking (1500 chars, 200 char overlap)
|
|
||||||
↓
|
|
||||||
nomic-embed-text Embeddings (Ollama local)
|
|
||||||
↓
|
|
||||||
Qdrant Vector Storage
|
|
||||||
↓
|
|
||||||
Semantic Search on User Query
|
|
||||||
↓
|
|
||||||
GLM-4.6:Cloud Analysis with RAG Context
|
|
||||||
↓
|
|
||||||
Chainlit Streaming Response
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Functions
|
|
||||||
|
|
||||||
| Function | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `extract_text_from_pdf()` | Convert PDF to text using PyMuPDF |
|
|
||||||
| `chunk_text()` | Split text into overlapping chunks |
|
|
||||||
| `get_embeddings()` | Generate embeddings via Ollama |
|
|
||||||
| `index_document()` | Store chunks in Qdrant |
|
|
||||||
| `search_qdrant()` | Retrieve relevant context |
|
|
||||||
| `on_message()` | Process user queries with RAG |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Environment Variables
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URL=postgresql+asyncpg://user:pass@postgres:5432/ai_station
|
|
||||||
OLLAMA_URL=http://192.168.1.243:11434 # Local Ollama for embeddings
|
|
||||||
QDRANT_URL=http://qdrant:6333 # Vector database
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: GLM-4.6:Cloud authentication is handled automatically via `ollama signin`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐳 Docker Services
|
|
||||||
|
|
||||||
| Service | Port | Purpose |
|
|
||||||
|---------|------|---------|
|
|
||||||
| `chainlit-app` | 8000 | Chainlit UI & API |
|
|
||||||
| `postgres` | 5432 | Conversation persistence |
|
|
||||||
| `qdrant` | 6333 | Vector database |
|
|
||||||
| `ollama` | 11434 | Local embeddings (external) |
|
|
||||||
|
|
||||||
Start/Stop:
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d # Start all services
|
OAuth2 obbligatorio (no accesso anonimo)
|
||||||
docker compose down # Stop all services
|
|
||||||
docker compose logs -f # View logs
|
Workspace isolation (file separati per utente)
|
||||||
docker compose restart # Restart services
|
|
||||||
|
HTML sanitization (configurable via unsafe_allow_html)
|
||||||
|
|
||||||
|
Environment secrets (.env mai committato)
|
||||||
|
|
||||||
|
PostgreSQL passwords cambiate da default
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Usage Examples
|
🚦 Roadmap
|
||||||
|
|
||||||
### Example 1: Analyze Tax Document
|
|
||||||
```
|
|
||||||
User: "Qual è l'importo totale del documento?"
|
|
||||||
AI Station:
|
|
||||||
✅ Extracts PDF content
|
|
||||||
✅ Searches relevant sections
|
|
||||||
✅ Analyzes with GLM-4.6:Cloud
|
|
||||||
📄 Returns: "Based on the document, the total amount is..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Multi-Document Analysis
|
|
||||||
```
|
|
||||||
1. Upload multiple PDFs (invoices, contracts)
|
|
||||||
2. All documents automatically indexed
|
|
||||||
3. Query across all documents simultaneously
|
|
||||||
4. RAG retrieves most relevant chunks
|
|
||||||
5. GLM-4.6:Cloud synthesizes answer
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Development
|
|
||||||
|
|
||||||
### Install Dependencies
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
Re-ranking con cross-encoder
|
||||||
|
|
||||||
|
Query expansion automatica
|
||||||
|
|
||||||
|
Feedback loop (👍👎 su risposte)
|
||||||
|
|
||||||
|
Export conversazioni PDF/Markdown
|
||||||
|
|
||||||
|
Multi-query RAG parallelo
|
||||||
|
|
||||||
|
Prometheus/Grafana monitoring
|
||||||
|
|
||||||
|
Adaptive chunking per tipo documento
|
||||||
|
|
||||||
|
Audio input support
|
||||||
```
|
```
|
||||||
|
|
||||||
### Requirements
|
|
||||||
```
|
## 📝 Licenza
|
||||||
chainlit==1.3.2
|
```tect
|
||||||
pydantic==2.9.2
|
MIT License - vedi file [LICENSE](LICENSE) per dettagli.
|
||||||
ollama>=0.1.0
|
Crea file LICENSE nella root del progetto
|
||||||
asyncpg>=0.29.0
|
text
|
||||||
psycopg2-binary
|
MIT License
|
||||||
qdrant-client>=1.10.0
|
|
||||||
sqlalchemy>=2.0.0
|
Copyright (c) 2026 DFFM / Giuseppe De Franceschi
|
||||||
greenlet>=3.0.0
|
|
||||||
sniffio
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
aiohttp
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
alembic
|
in the Software without restriction, including without limitation the rights
|
||||||
pymupdf
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
python-dotenv
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Testing (without Docker)
|
👥 Contributors
|
||||||
```bash
|
Giuseppe De Franceschi - @defranceschi
|
||||||
# Start Ollama, PostgreSQL, Qdrant manually
|
|
||||||
ollama serve &
|
|
||||||
chainlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
🙏 Credits
|
||||||
|
Chainlit - UI framework
|
||||||
|
|
||||||
## 🔄 Model Details
|
Ollama - LLM runtime
|
||||||
|
|
||||||
### GLM-4.6:Cloud
|
Qdrant - Vector DB
|
||||||
- **Provider**: Zhipu AI via Ollama Cloud
|
|
||||||
- **Capabilities**: Long context, reasoning, multilingual
|
|
||||||
- **Cost**: Free tier available
|
|
||||||
- **Authentication**: Device key (automatic via `ollama signin`)
|
|
||||||
|
|
||||||
### nomic-embed-text
|
BGE-M3 - Embeddings
|
||||||
- **Local embedding model** for chunking/retrieval
|
|
||||||
- **Dimensions**: 768
|
|
||||||
- **Speed**: Fast, runs locally
|
|
||||||
- **Used for**: RAG semantic search
|
|
||||||
|
|
||||||
---
|
Docling - Document processing
|
||||||
|
|
||||||
## 📈 Monitoring & Logs
|
## **Status**: 🔨 Pre-Production | **Last Update**: 2026-01-01
|
||||||
|
|
||||||
### Check Service Health
|
|
||||||
```bash
|
|
||||||
# View all logs
|
|
||||||
docker compose logs
|
|
||||||
|
|
||||||
# Follow live logs
|
|
||||||
docker compose logs -f chainlit-app
|
|
||||||
|
|
||||||
# Check specific container
|
|
||||||
docker inspect ai-station-chainlit-app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
| Issue | Solution |
|
|
||||||
|-------|----------|
|
|
||||||
| `unauthorized` error | Run `ollama signin` on server |
|
|
||||||
| Database connection failed | Check PostgreSQL is running |
|
|
||||||
| Qdrant unavailable | Verify `docker-compose up` completed |
|
|
||||||
| PDF not extracted | Ensure PyMuPDF installed: `pip install pymupdf` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deployment
|
|
||||||
|
|
||||||
### Production Checklist
|
|
||||||
- [ ] Set secure PostgreSQL credentials in `.env`
|
|
||||||
- [ ] Enable SSL/TLS for Chainlit endpoints
|
|
||||||
- [ ] Configure CORS for frontend
|
|
||||||
- [ ] Setup log aggregation (ELK, Datadog, etc.)
|
|
||||||
- [ ] Implement rate limiting
|
|
||||||
- [ ] Add API authentication
|
|
||||||
- [ ] Configure backup strategy for Qdrant
|
|
||||||
|
|
||||||
### Cloud Deployment Options
|
|
||||||
- **AWS**: ECS + RDS + VectorDB
|
|
||||||
- **Google Cloud**: Cloud Run + Cloud SQL
|
|
||||||
- **DigitalOcean**: App Platform + Managed Databases
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 API Reference
|
|
||||||
|
|
||||||
### REST Endpoints (via Chainlit)
|
|
||||||
- `POST /api/chat` - Send message with context
|
|
||||||
- `GET /api/threads` - List conversations
|
|
||||||
- `POST /api/upload` - Upload document
|
|
||||||
|
|
||||||
### WebSocket
|
|
||||||
- Real-time streaming responses via Chainlit protocol
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔮 Future Features
|
|
||||||
|
|
||||||
- [ ] OAuth2 Google authentication
|
|
||||||
- [ ] Document metadata extraction (dates, amounts, entities)
|
|
||||||
- [ ] Advanced search filters (type, date range, language)
|
|
||||||
- [ ] Export results (PDF, CSV, JSON)
|
|
||||||
- [ ] Analytics dashboard
|
|
||||||
- [ ] Multi-language support
|
|
||||||
- [ ] Document versioning
|
|
||||||
- [ ] Compliance reporting (GDPR, audit trails)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
1. Check logs: `docker compose logs chainlit-app`
|
|
||||||
2. Verify Ollama authentication: `ollama show glm-4.6:cloud`
|
|
||||||
3. Test Qdrant connection: `curl http://localhost:6333/health`
|
|
||||||
4. Inspect PostgreSQL: `docker compose exec postgres psql -U ai_user -d ai_station`
|
|
||||||
|
|
||||||
### Performance Tips
|
|
||||||
- Increase chunk overlap for better context retrieval
|
|
||||||
- Adjust embedding model based on latency requirements
|
|
||||||
- Monitor Qdrant memory usage for large document sets
|
|
||||||
- Implement caching for frequent queries
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
MIT License - See LICENSE file
|
|
||||||
|
|
||||||
## 👤 Author
|
|
||||||
|
|
||||||
AI Station Team
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: December 26, 2025
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Status**: Production Ready ✅
|
|
||||||
621
app.py
621
app.py
|
|
@ -2,17 +2,22 @@ import os
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
import shutil
|
import shutil
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, List
|
from typing import Optional, Dict, List, Any
|
||||||
import chainlit as cl
|
import chainlit as cl
|
||||||
import ollama
|
import ollama
|
||||||
import fitz # PyMuPDF
|
from docling.document_converter import DocumentConverter
|
||||||
from qdrant_client import AsyncQdrantClient
|
from qdrant_client import AsyncQdrantClient
|
||||||
from qdrant_client.models import PointStruct, Distance, VectorParams
|
# CORREZIONE IMPORT: Importiamo le classi necessarie direttamente dalla libreria
|
||||||
|
from qdrant_client.models import PointStruct, Distance, VectorParams, SparseVectorParams, Prefetch
|
||||||
from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
|
from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
|
||||||
|
from chainlit.types import ThreadDict
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
# === FIX IMPORT ROBUSTO ===
|
# === FIX IMPORT ROBUSTO ===
|
||||||
# Gestisce le differenze tra le versioni di Chainlit 2.x
|
|
||||||
try:
|
try:
|
||||||
from chainlit.data.storage_clients import BaseStorageClient
|
from chainlit.data.storage_clients import BaseStorageClient
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -25,320 +30,434 @@ except ImportError:
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station")
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station")
|
||||||
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://192.168.1.243:11434")
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://192.168.1.243:11434")
|
||||||
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
||||||
|
BGE_API_URL = os.getenv("BGE_API_URL", "http://192.168.1.243:8001/embed")
|
||||||
|
|
||||||
|
VISION_MODEL = "minicpm-v"
|
||||||
|
DEFAULT_TEXT_MODEL = "glm-4.6:cloud"
|
||||||
|
|
||||||
WORKSPACES_DIR = "./workspaces"
|
WORKSPACES_DIR = "./workspaces"
|
||||||
STORAGE_DIR = "./.files"
|
STORAGE_DIR = "./.files"
|
||||||
|
|
||||||
os.makedirs(STORAGE_DIR, exist_ok=True)
|
os.makedirs(STORAGE_DIR, exist_ok=True)
|
||||||
os.makedirs(WORKSPACES_DIR, exist_ok=True)
|
os.makedirs(WORKSPACES_DIR, exist_ok=True)
|
||||||
|
|
||||||
# === MAPPING UTENTI E RUOLI ===
|
# === MAPPING UTENTI ===
|
||||||
USER_PROFILES = {
|
USER_PROFILES = {
|
||||||
"giuseppe@defranceschi.pro": {
|
"giuseppe@defranceschi.pro": { "role": "admin", "name": "Giuseppe", "workspace": "admin_workspace", "rag_collection": "admin_docs", "capabilities": ["debug", "all"], "show_code": True },
|
||||||
"role": "admin",
|
"giuseppe.defranceschi@gmail.com": { "role": "admin", "name": "Giuseppe", "workspace": "admin_workspace", "rag_collection": "admin_docs", "capabilities": ["debug", "all"], "show_code": True },
|
||||||
"name": "Giuseppe",
|
"federica.tecchio@gmail.com": { "role": "business", "name": "Federica", "workspace": "business_workspace", "rag_collection": "contabilita", "capabilities": ["basic_chat"], "show_code": False },
|
||||||
"workspace": "admin_workspace",
|
"riccardob545@gmail.com": { "role": "engineering", "name": "Riccardo", "workspace": "engineering_workspace", "rag_collection": "engineering_docs", "capabilities": ["code"], "show_code": True },
|
||||||
"rag_collection": "admin_docs",
|
"giuliadefranceschi05@gmail.com": { "role": "architecture", "name": "Giulia", "workspace": "architecture_workspace", "rag_collection": "architecture_manuals", "capabilities": ["visual"], "show_code": False }
|
||||||
"capabilities": ["debug", "system_prompts", "user_management", "all_models"],
|
|
||||||
"show_code": True
|
|
||||||
},
|
|
||||||
"federica.tecchio@gmail.com": {
|
|
||||||
"role": "business",
|
|
||||||
"name": "Federica",
|
|
||||||
"workspace": "business_workspace",
|
|
||||||
"rag_collection": "contabilita",
|
|
||||||
"capabilities": ["pdf_upload", "basic_chat"],
|
|
||||||
"show_code": False
|
|
||||||
},
|
|
||||||
"giuseppe.defranceschi@gmail.com": {
|
|
||||||
"role": "admin",
|
|
||||||
"name": "Giuseppe",
|
|
||||||
"workspace": "admin_workspace",
|
|
||||||
"rag_collection": "admin_docs",
|
|
||||||
"capabilities": ["debug", "system_prompts", "user_management", "all_models"],
|
|
||||||
"show_code": True
|
|
||||||
},
|
|
||||||
"riccardob545@gmail.com": {
|
|
||||||
"role": "engineering",
|
|
||||||
"name": "Riccardo",
|
|
||||||
"workspace": "engineering_workspace",
|
|
||||||
"rag_collection": "engineering_docs",
|
|
||||||
"capabilities": ["code_execution", "data_viz", "advanced_chat"],
|
|
||||||
"show_code": True
|
|
||||||
},
|
|
||||||
"giuliadefranceschi05@gmail.com": {
|
|
||||||
"role": "architecture",
|
|
||||||
"name": "Giulia",
|
|
||||||
"workspace": "architecture_workspace",
|
|
||||||
"rag_collection": "architecture_manuals",
|
|
||||||
"capabilities": ["visual_chat", "pdf_upload", "image_gen"],
|
|
||||||
"show_code": False
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# === CUSTOM LOCAL STORAGE CLIENT (FIXED) ===# Questa classe ora implementa tutti i metodi astratti richiesti da Chainlit 2.8.3
|
# === STORAGE CLIENT ===
|
||||||
class LocalStorageClient(BaseStorageClient):
|
class LocalStorageClient(BaseStorageClient):
|
||||||
"""Storage locale su filesystem per file/elementi"""
|
|
||||||
|
|
||||||
def __init__(self, storage_path: str):
|
def __init__(self, storage_path: str):
|
||||||
self.storage_path = storage_path
|
self.storage_path = storage_path
|
||||||
os.makedirs(storage_path, exist_ok=True)
|
os.makedirs(storage_path, exist_ok=True)
|
||||||
|
async def upload_file(self, object_key: str, data: bytes, mime: str = "application/octet-stream", overwrite: bool = True) -> Dict[str, str]:
|
||||||
async def upload_file(
|
|
||||||
self,
|
|
||||||
object_key: str,
|
|
||||||
data: bytes,
|
|
||||||
mime: str = "application/octet-stream",
|
|
||||||
overwrite: bool = True,
|
|
||||||
) -> Dict[str, str]:
|
|
||||||
file_path = os.path.join(self.storage_path, object_key)
|
file_path = os.path.join(self.storage_path, object_key)
|
||||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f: f.write(data)
|
||||||
f.write(data)
|
|
||||||
return {"object_key": object_key, "url": f"/files/{object_key}"}
|
return {"object_key": object_key, "url": f"/files/{object_key}"}
|
||||||
|
async def get_read_url(self, object_key: str) -> str: return f"/files/{object_key}"
|
||||||
# Implementazione metodi obbligatori mancanti nella versione precedente
|
|
||||||
async def get_read_url(self, object_key: str) -> str:
|
|
||||||
return f"/files/{object_key}"
|
|
||||||
|
|
||||||
async def delete_file(self, object_key: str) -> bool:
|
async def delete_file(self, object_key: str) -> bool:
|
||||||
file_path = os.path.join(self.storage_path, object_key)
|
path = os.path.join(self.storage_path, object_key)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(path): os.remove(path); return True
|
||||||
os.remove(file_path)
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
async def close(self): pass
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# === DATA LAYER ===
|
|
||||||
@cl.data_layer
|
@cl.data_layer
|
||||||
def get_data_layer():
|
def get_data_layer():
|
||||||
return SQLAlchemyDataLayer(
|
return SQLAlchemyDataLayer(conninfo=DATABASE_URL, storage_provider=LocalStorageClient(STORAGE_DIR))
|
||||||
conninfo=DATABASE_URL,
|
|
||||||
user_thread_limit=1000,
|
|
||||||
storage_provider=LocalStorageClient(storage_path=STORAGE_DIR)
|
|
||||||
)
|
|
||||||
|
|
||||||
# === OAUTH CALLBACK ===
|
# === OAUTH & UTILS ===
|
||||||
@cl.oauth_callback
|
@cl.oauth_callback
|
||||||
def oauth_callback(
|
def oauth_callback(provider_id: str, token: str, raw_user_data: Dict[str, str], default_user: cl.User) -> Optional[cl.User]:
|
||||||
provider_id: str,
|
|
||||||
token: str,
|
|
||||||
raw_user_data: Dict[str, str],
|
|
||||||
default_user: cl.User,
|
|
||||||
) -> Optional[cl.User]:
|
|
||||||
if provider_id == "google":
|
if provider_id == "google":
|
||||||
email = raw_user_data.get("email", "").lower()
|
email = raw_user_data.get("email", "").lower()
|
||||||
|
profile = USER_PROFILES.get(email, USER_PROFILES.get("guest", {"role": "guest", "name": "Guest", "workspace": "guest", "rag_collection": "public", "show_code": False}))
|
||||||
# Verifica se utente è autorizzato (opzionale: blocca se non in lista)
|
default_user.metadata.update({"role": profile["role"], "workspace": profile["workspace"], "rag_collection": profile["rag_collection"], "show_code": profile["show_code"], "display_name": profile["name"]})
|
||||||
# if email not in USER_PROFILES:
|
|
||||||
# return None
|
|
||||||
|
|
||||||
# Recupera profilo o usa default Guest
|
|
||||||
profile = USER_PROFILES.get(email, get_user_profile("guest"))
|
|
||||||
|
|
||||||
default_user.metadata.update({
|
|
||||||
"picture": raw_user_data.get("picture", ""),
|
|
||||||
"role": profile["role"],
|
|
||||||
"workspace": profile["workspace"],
|
|
||||||
"rag_collection": profile["rag_collection"],
|
|
||||||
"capabilities": profile["capabilities"],
|
|
||||||
"show_code": profile["show_code"],
|
|
||||||
"display_name": profile["name"]
|
|
||||||
})
|
|
||||||
return default_user
|
return default_user
|
||||||
return default_user
|
return default_user
|
||||||
|
|
||||||
# === UTILITY FUNCTIONS ===
|
|
||||||
def get_user_profile(user_email: str) -> Dict:
|
|
||||||
return USER_PROFILES.get(user_email.lower(), {
|
|
||||||
"role": "guest",
|
|
||||||
"name": "Ospite",
|
|
||||||
"workspace": "guest_workspace",
|
|
||||||
"rag_collection": "documents",
|
|
||||||
"capabilities": [],
|
|
||||||
"show_code": False
|
|
||||||
})
|
|
||||||
|
|
||||||
def create_workspace(workspace_name: str) -> str:
|
def create_workspace(workspace_name: str) -> str:
|
||||||
path = os.path.join(WORKSPACES_DIR, workspace_name)
|
path = os.path.join(WORKSPACES_DIR, workspace_name)
|
||||||
os.makedirs(path, exist_ok=True)
|
os.makedirs(path, exist_ok=True)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def save_code_to_file(code: str, workspace: str) -> str:
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
file_name = f"code_{timestamp}.py"
|
|
||||||
file_path = os.path.join(WORKSPACES_DIR, workspace, file_name)
|
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(code)
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def extract_text_from_pdf(pdf_path: str) -> str:
|
# === CORE: DOCLING ===
|
||||||
|
def process_file_with_docling(file_path: str) -> str:
|
||||||
try:
|
try:
|
||||||
doc = fitz.open(pdf_path)
|
converter = DocumentConverter()
|
||||||
text = "\n".join([page.get_text() for page in doc])
|
result = converter.convert(file_path)
|
||||||
doc.close()
|
return result.document.export_to_markdown()
|
||||||
return text
|
except Exception as e:
|
||||||
except Exception:
|
print(f"❌ Docling Error: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# === QDRANT FUNCTIONS ===
|
# === CORE: BGE-M3 CLIENT ===
|
||||||
async def get_qdrant_client() -> AsyncQdrantClient:
|
def get_bge_embeddings(text: str) -> Optional[Dict[str, Any]]:
|
||||||
return AsyncQdrantClient(url=QDRANT_URL)
|
try:
|
||||||
|
payload = {"texts": [text[:8000]]}
|
||||||
|
response = requests.post(BGE_API_URL, json=payload, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json().get("data", [])
|
||||||
|
if data:
|
||||||
|
return data[0]
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ BGE API Error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# === CORE: QDRANT ===
|
||||||
async def ensure_collection(collection_name: str):
|
async def ensure_collection(collection_name: str):
|
||||||
client = await get_qdrant_client()
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
||||||
if not await client.collection_exists(collection_name):
|
if not await client.collection_exists(collection_name):
|
||||||
await client.create_collection(
|
await client.create_collection(
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
vectors_config=VectorParams(size=768, distance=Distance.COSINE)
|
vectors_config={"dense": VectorParams(size=1024, distance=Distance.COSINE)},
|
||||||
|
sparse_vectors_config={"sparse": SparseVectorParams()}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_embeddings(text: str) -> list:
|
async def index_document(file_name: str, content: str, collection_name: str):
|
||||||
client = ollama.Client(host=OLLAMA_URL)
|
await ensure_collection(collection_name)
|
||||||
try:
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
||||||
response = client.embed(model='nomic-embed-text', input=text[:2000])
|
|
||||||
if 'embeddings' in response: return response['embeddings'][0]
|
chunk_size = 2000
|
||||||
return response.get('embedding', [])
|
overlap = 200
|
||||||
except: return []
|
|
||||||
|
points = []
|
||||||
async def index_document(file_name: str, content: str, collection_name: str) -> bool:
|
for i in range(0, len(content), chunk_size - overlap):
|
||||||
try:
|
chunk = content[i : i + chunk_size]
|
||||||
await ensure_collection(collection_name)
|
embedding_data = get_bge_embeddings(chunk)
|
||||||
embedding = await get_embeddings(content)
|
|
||||||
if not embedding: return False
|
|
||||||
|
|
||||||
qdrant = await get_qdrant_client()
|
if embedding_data:
|
||||||
await qdrant.upsert(
|
points.append(PointStruct(
|
||||||
collection_name=collection_name,
|
|
||||||
points=[PointStruct(
|
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
vector=embedding,
|
vector={
|
||||||
payload={"file_name": file_name, "content": content[:3000], "indexed_at": datetime.now().isoformat()}
|
"dense": embedding_data["dense"],
|
||||||
)]
|
"sparse": embedding_data["sparse"]
|
||||||
)
|
},
|
||||||
return True
|
payload={
|
||||||
except: return False
|
"file_name": file_name,
|
||||||
|
"content": chunk,
|
||||||
|
"indexed_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
if points:
|
||||||
|
await client.upsert(collection_name=collection_name, points=points)
|
||||||
|
return len(points)
|
||||||
|
return 0
|
||||||
|
|
||||||
async def search_qdrant(query: str, collection: str) -> str:
|
async def search_hybrid(query: str, collection_name: str, limit: int = 4) -> str:
|
||||||
try:
|
client = AsyncQdrantClient(url=QDRANT_URL)
|
||||||
client = await get_qdrant_client()
|
if not await client.collection_exists(collection_name): return ""
|
||||||
if not await client.collection_exists(collection): return ""
|
|
||||||
emb = await get_embeddings(query)
|
query_emb = get_bge_embeddings(query)
|
||||||
if not emb: return ""
|
if not query_emb: return ""
|
||||||
res = await client.query_points(collection_name=collection, query=emb, limit=3)
|
|
||||||
return "\n\n".join([hit.payload['content'] for hit in res.points if hit.payload])
|
# CORREZIONE QUI: Usiamo l'oggetto Prefetch importato correttamente
|
||||||
except: return ""
|
results = await client.query_points(
|
||||||
|
collection_name=collection_name,
|
||||||
|
prefetch=[
|
||||||
|
Prefetch(
|
||||||
|
query=query_emb["sparse"],
|
||||||
|
using="sparse",
|
||||||
|
limit=limit * 2
|
||||||
|
)
|
||||||
|
],
|
||||||
|
query=query_emb["dense"],
|
||||||
|
using="dense",
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
context = []
|
||||||
|
for hit in results.points:
|
||||||
|
context.append(f"--- DA {hit.payload['file_name']} ---\n{hit.payload['content']}")
|
||||||
|
|
||||||
|
return "\n\n".join(context)
|
||||||
|
|
||||||
|
# === Caching Embeddings ===
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_bge_embeddings_cached(text: str):
|
||||||
|
"""Cache per query ripetute"""
|
||||||
|
return get_bge_embeddings(text)
|
||||||
|
|
||||||
# === CHAINLIT HANDLERS ===
|
# === CHAINLIT HANDLERS ===
|
||||||
|
|
||||||
@cl.on_chat_start
|
@cl.on_chat_start
|
||||||
async def on_chat_start():
|
async def start():
|
||||||
|
# 1. Profilo utente
|
||||||
user = cl.user_session.get("user")
|
user = cl.user_session.get("user")
|
||||||
|
email = user.identifier if user else "guest"
|
||||||
|
profile = USER_PROFILES.get(email, USER_PROFILES["giuseppe@defranceschi.pro"])
|
||||||
|
|
||||||
if not user:
|
cl.user_session.set("profile", profile)
|
||||||
# Fallback locale se non c'è auth
|
|
||||||
user_email = "guest@local"
|
|
||||||
profile = get_user_profile(user_email)
|
|
||||||
else:
|
|
||||||
user_email = user.identifier
|
|
||||||
# I metadati sono già popolati dalla callback oauth
|
|
||||||
profile = USER_PROFILES.get(user_email, get_user_profile("guest"))
|
|
||||||
|
|
||||||
# Salva in sessione
|
|
||||||
cl.user_session.set("email", user_email)
|
|
||||||
cl.user_session.set("role", profile["role"])
|
|
||||||
cl.user_session.set("workspace", profile["workspace"])
|
|
||||||
cl.user_session.set("rag_collection", profile["rag_collection"])
|
|
||||||
cl.user_session.set("show_code", profile["show_code"])
|
|
||||||
|
|
||||||
create_workspace(profile["workspace"])
|
create_workspace(profile["workspace"])
|
||||||
|
|
||||||
# === SETTINGS WIDGETS ===
|
# 2. Badge HTML personalizzato
|
||||||
settings_widgets = [
|
role_color = {
|
||||||
cl.input_widget.Select(
|
"admin": "#e74c3c",
|
||||||
id="model",
|
"engineering": "#3498db",
|
||||||
label="Modello AI",
|
"business": "#2ecc71",
|
||||||
values=["glm-4.6:cloud", "llama3.2", "mistral", "qwen2.5-coder:32b"],
|
"architecture": "#9b59b6",
|
||||||
initial_value="glm-4.6:cloud",
|
}.get(profile["role"], "#95a5a6")
|
||||||
),
|
|
||||||
cl.input_widget.Slider(
|
|
||||||
id="temperature",
|
|
||||||
label="Temperatura",
|
|
||||||
initial=0.7, min=0, max=2, step=0.1,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
if profile["role"] == "admin":
|
|
||||||
settings_widgets.append(cl.input_widget.Switch(id="rag_enabled", label="Abilita RAG", initial=True))
|
|
||||||
|
|
||||||
await cl.ChatSettings(settings_widgets).send()
|
badge_html = f"""
|
||||||
|
<div style="background:{role_color}; padding:8px; border-radius:8px; margin-bottom:16px;">
|
||||||
await cl.Message(
|
👤 <b>{profile['name']}</b> | 🔧 {profile['role'].upper()} | 📁 {profile['workspace']}
|
||||||
content=f"👋 Ciao **{profile['name']}**!\n"
|
</div>
|
||||||
f"Ruolo: `{profile['role']}` | Workspace: `{profile['workspace']}`\n"
|
"""
|
||||||
|
await cl.Message(content=badge_html).send()
|
||||||
|
|
||||||
|
# 3. Settings UI
|
||||||
|
settings = await cl.ChatSettings(
|
||||||
|
[
|
||||||
|
cl.input_widget.Slider(
|
||||||
|
id="top_k",
|
||||||
|
label="Numero Documenti RAG",
|
||||||
|
initial=4,
|
||||||
|
min=1,
|
||||||
|
max=10,
|
||||||
|
step=1,
|
||||||
|
),
|
||||||
|
cl.input_widget.Select(
|
||||||
|
id="vision_detail",
|
||||||
|
label="Dettaglio Analisi Immagini",
|
||||||
|
values=["auto", "low", "high"],
|
||||||
|
initial_value="auto",
|
||||||
|
),
|
||||||
|
cl.input_widget.TextInput(
|
||||||
|
id="system_instruction",
|
||||||
|
label="Istruzione Sistema Custom (opzionale)",
|
||||||
|
initial="",
|
||||||
|
placeholder="Es: Rispondi sempre in formato tecnico...",
|
||||||
|
),
|
||||||
|
cl.input_widget.Select(
|
||||||
|
id="model",
|
||||||
|
label="Modello di Ragionamento",
|
||||||
|
values=[DEFAULT_TEXT_MODEL, "llama3.2", "mistral", "qwen2.5-coder:32b"],
|
||||||
|
initial_value=DEFAULT_TEXT_MODEL,
|
||||||
|
),
|
||||||
|
cl.input_widget.Slider(
|
||||||
|
id="temperature",
|
||||||
|
label="Creatività (Temperatura)",
|
||||||
|
initial=0.3,
|
||||||
|
min=0,
|
||||||
|
max=1,
|
||||||
|
step=0.1,
|
||||||
|
),
|
||||||
|
cl.input_widget.Switch(
|
||||||
|
id="rag_enabled",
|
||||||
|
label="Usa Conoscenza Documenti (RAG)",
|
||||||
|
initial=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
).send()
|
).send()
|
||||||
|
|
||||||
@cl.on_settings_update
|
|
||||||
async def on_settings_update(settings):
|
|
||||||
cl.user_session.set("settings", settings)
|
cl.user_session.set("settings", settings)
|
||||||
await cl.Message(content="✅ Impostazioni aggiornate").send()
|
|
||||||
|
# 4. Messaggio iniziale (opzionale)
|
||||||
|
await cl.Message(
|
||||||
|
content=(
|
||||||
|
f"🚀 **Vision-RAG Hybrid System Online**\n"
|
||||||
|
f"Utente: {profile['name']} | Workspace: {profile['workspace']}\n"
|
||||||
|
f"Engine: Docling + BGE-M3 + {VISION_MODEL}"
|
||||||
|
)
|
||||||
|
).send()
|
||||||
|
|
||||||
|
|
||||||
|
cl.user_session.set("settings", settings)
|
||||||
|
|
||||||
|
await cl.Message(f"🚀 **Vision-RAG Hybrid System Online**\nUtente: {profile['name']} | Workspace: {profile['workspace']}\nEngine: Docling + BGE-M3 + {VISION_MODEL}").send()
|
||||||
|
|
||||||
|
@cl.on_settings_update
|
||||||
|
async def setup_agent(settings):
|
||||||
|
cl.user_session.set("settings", settings)
|
||||||
|
await cl.Message(content=f"✅ Impostazioni aggiornate: Modello {settings['model']}").send()
|
||||||
|
|
||||||
|
async def log_metrics(metrics: dict):
|
||||||
|
# Versione minima: log su stdout
|
||||||
|
print("[METRICS]", metrics)
|
||||||
|
|
||||||
|
# In futuro puoi:
|
||||||
|
# - salvarle in Postgres
|
||||||
|
# - mandarle a Prometheus / Grafana
|
||||||
|
# - scriverle su file JSON per analisi settimanale
|
||||||
|
|
||||||
|
# - Resume Chat Handler
|
||||||
|
|
||||||
|
@cl.on_chat_resume
|
||||||
|
async def on_chat_resume(thread: ThreadDict):
|
||||||
|
"""
|
||||||
|
Viene chiamato quando l'utente clicca 'Riprendi' su una chat archiviata.
|
||||||
|
Chainlit carica già i messaggi nella UI, qui puoi solo ripristinare la sessione.
|
||||||
|
"""
|
||||||
|
# Se vuoi, puoi recuperare l'identifier dell’utente dal thread
|
||||||
|
user_identifier = thread.get("userIdentifier")
|
||||||
|
profile = USER_PROFILES.get(
|
||||||
|
user_identifier,
|
||||||
|
USER_PROFILES["giuseppe@defranceschi.pro"],
|
||||||
|
)
|
||||||
|
cl.user_session.set("profile", profile)
|
||||||
|
|
||||||
|
# Puoi anche ripristinare eventuale stato custom (es: impostazioni di default)
|
||||||
|
# oppure semplicemente salutare l’utente
|
||||||
|
await cl.Message(
|
||||||
|
content="👋 Bentornato! Possiamo riprendere da questa conversazione."
|
||||||
|
).send()
|
||||||
|
|
||||||
@cl.on_message
|
@cl.on_message
|
||||||
async def on_message(message: cl.Message):
|
async def main(message: cl.Message):
|
||||||
workspace = cl.user_session.get("workspace")
|
start_time = time.time()
|
||||||
rag_collection = cl.user_session.get("rag_collection")
|
|
||||||
user_role = cl.user_session.get("role")
|
profile = cl.user_session.get("profile")
|
||||||
show_code = cl.user_session.get("show_code")
|
|
||||||
|
|
||||||
settings = cl.user_session.get("settings", {})
|
settings = cl.user_session.get("settings", {})
|
||||||
model = settings.get("model", "glm-4.6:cloud")
|
|
||||||
temperature = settings.get("temperature", 0.7)
|
selected_model = settings.get("model", DEFAULT_TEXT_MODEL)
|
||||||
rag_enabled = settings.get("rag_enabled", True) if user_role == "admin" else True
|
temperature = settings.get("temperature", 0.3)
|
||||||
|
rag_enabled = settings.get("rag_enabled", True)
|
||||||
|
|
||||||
|
workspace = create_workspace(profile["workspace"])
|
||||||
|
|
||||||
|
images_for_vision = []
|
||||||
|
doc_context = ""
|
||||||
|
rag_context = "" # ← la inizializzi qui, così esiste sempre
|
||||||
|
|
||||||
# 1. GESTIONE FILE
|
# 1. GESTIONE FILE
|
||||||
if message.elements:
|
if message.elements:
|
||||||
for element in message.elements:
|
for element in message.elements:
|
||||||
dest = os.path.join(WORKSPACES_DIR, workspace, element.name)
|
file_path = os.path.join(workspace, element.name)
|
||||||
shutil.copy(element.path, dest)
|
shutil.copy(element.path, file_path)
|
||||||
if element.name.endswith(".pdf"):
|
|
||||||
text = extract_text_from_pdf(dest)
|
if "image" in element.mime:
|
||||||
if text:
|
images_for_vision.append(file_path)
|
||||||
await index_document(element.name, text, rag_collection)
|
msg_img = cl.Message(
|
||||||
await cl.Message(content=f"✅ **{element.name}** indicizzato.").send()
|
content=f"👁️ Analizzo immagine **{element.name}** con {VISION_MODEL}..."
|
||||||
|
)
|
||||||
|
await msg_img.send()
|
||||||
|
|
||||||
|
with open(file_path, "rb") as img_file:
|
||||||
|
img_bytes = img_file.read()
|
||||||
|
|
||||||
|
client_sync = ollama.Client(host=OLLAMA_URL)
|
||||||
|
res = client_sync.chat(
|
||||||
|
model=VISION_MODEL,
|
||||||
|
messages=[{
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
"Analizza questa immagine tecnica. Trascrivi testi, codici "
|
||||||
|
"e descrivi diagrammi o tabelle in dettaglio."
|
||||||
|
),
|
||||||
|
"images": [img_bytes],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
desc = res["message"]["content"]
|
||||||
|
doc_context += f"\n\n[DESCRIZIONE IMMAGINE {element.name}]:\n{desc}"
|
||||||
|
msg_img.content = f"✅ Immagine analizzata:\n{desc[:200]}..."
|
||||||
|
await msg_img.update()
|
||||||
|
|
||||||
|
elif element.name.endswith((".pdf", ".docx")):
|
||||||
|
msg_doc = cl.Message(
|
||||||
|
content=f"📄 Leggo **{element.name}** con Docling (tabelle/formule)..."
|
||||||
|
)
|
||||||
|
await msg_doc.send()
|
||||||
|
|
||||||
|
markdown_content = process_file_with_docling(file_path)
|
||||||
|
if markdown_content:
|
||||||
|
chunks = await index_document(
|
||||||
|
element.name, markdown_content, profile["rag_collection"]
|
||||||
|
)
|
||||||
|
msg_doc.content = (
|
||||||
|
f"✅ **{element.name}**: Convertito e salvato {chunks} "
|
||||||
|
"frammenti nel DB vettoriale."
|
||||||
|
)
|
||||||
|
doc_context += (
|
||||||
|
f"\n\n[CONTENUTO FILE {element.name}]:\n"
|
||||||
|
f"{markdown_content[:1000]}..."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg_doc.content = f"❌ Errore lettura {element.name}"
|
||||||
|
await msg_doc.update()
|
||||||
|
|
||||||
|
# 2. RAG RETRIEVAL
|
||||||
|
if rag_enabled and not images_for_vision:
|
||||||
|
rag_context = await search_hybrid(
|
||||||
|
message.content, profile["rag_collection"]
|
||||||
|
)
|
||||||
|
|
||||||
|
final_context = ""
|
||||||
|
if rag_context:
|
||||||
|
final_context += f"CONTESTO RAG:\n{rag_context}\n"
|
||||||
|
if doc_context:
|
||||||
|
final_context += f"CONTESTO SESSIONE CORRENTE:\n{doc_context}\n"
|
||||||
|
|
||||||
|
system_prompt = (
|
||||||
|
"Sei un assistente tecnico esperto. Usa il contesto fornito "
|
||||||
|
"(incluso Markdown di tabelle e descrizioni immagini) per "
|
||||||
|
"rispondere con precisione. Cita i documenti fonte."
|
||||||
|
)
|
||||||
|
|
||||||
# 2. RAG
|
|
||||||
context = ""
|
|
||||||
if rag_enabled:
|
|
||||||
context = await search_qdrant(message.content, rag_collection)
|
|
||||||
|
|
||||||
system_prompt = "Sei un assistente esperto."
|
|
||||||
if context: system_prompt += f"\n\nCONTESTO:\n{context}"
|
|
||||||
|
|
||||||
# 3. GENERAZIONE
|
|
||||||
client = ollama.AsyncClient(host=OLLAMA_URL)
|
|
||||||
msg = cl.Message(content="")
|
msg = cl.Message(content="")
|
||||||
await msg.send()
|
await msg.send()
|
||||||
|
|
||||||
stream = await client.chat(
|
error = None
|
||||||
model=model,
|
|
||||||
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": message.content}],
|
# 3. GENERAZIONE
|
||||||
options={"temperature": temperature},
|
try:
|
||||||
stream=True
|
client_async = ollama.AsyncClient(host=OLLAMA_URL)
|
||||||
)
|
stream = await client_async.chat(
|
||||||
|
model=selected_model,
|
||||||
full_resp = ""
|
messages=[
|
||||||
async for chunk in stream:
|
{"role": "system", "content": system_prompt},
|
||||||
token = chunk['message']['content']
|
{
|
||||||
full_resp += token
|
"role": "user",
|
||||||
await msg.stream_token(token)
|
"content": f"Domanda: {message.content}\n\n{final_context}",
|
||||||
await msg.update()
|
},
|
||||||
|
],
|
||||||
|
options={"temperature": temperature},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async for chunk in stream:
|
||||||
|
content = chunk["message"]["content"]
|
||||||
|
await msg.stream_token(content)
|
||||||
|
await msg.update()
|
||||||
|
except Exception as e:
|
||||||
|
error = str(e)
|
||||||
|
await msg.stream_token(f"❌ Errore AI: {error}")
|
||||||
|
await msg.update()
|
||||||
|
|
||||||
# 4. SALVATAGGIO CODICE
|
# 4. SALVATAGGIO CODICE
|
||||||
if show_code:
|
if profile["show_code"]:
|
||||||
blocks = re.findall(r"``````", full_resp, re.DOTALL)
|
code_blocks = re.findall(r"``````", msg.content, re.DOTALL)
|
||||||
elements = []
|
if code_blocks:
|
||||||
for code in blocks:
|
for i, code in enumerate(code_blocks):
|
||||||
path = save_code_to_file(code.strip(), workspace)
|
fname = f"script_{datetime.now().strftime('%H%M%S')}_{i}.py"
|
||||||
elements.append(cl.File(name=os.path.basename(path), path=path, display="inline"))
|
with open(os.path.join(workspace, fname), "w") as f:
|
||||||
if elements:
|
f.write(code.strip())
|
||||||
await cl.Message(content="💾 Codice salvato", elements=elements).send()
|
await cl.Message(
|
||||||
|
content=f"💾 Script salvato: `{fname}`"
|
||||||
|
).send()
|
||||||
|
|
||||||
|
# 5. METRICHE (ALLA FINE)
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
# Se rag_context è una stringa concatenata, puoi stimare i "rag_hits"
|
||||||
|
# contando i separatori che usi in search_hybrid (es. '--- DA ')
|
||||||
|
if rag_context:
|
||||||
|
rag_hits = rag_context.count("--- DA ")
|
||||||
|
else:
|
||||||
|
rag_hits = 0
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
"response_time": elapsed,
|
||||||
|
"rag_hits": rag_hits,
|
||||||
|
"model": selected_model,
|
||||||
|
"user_role": profile["role"],
|
||||||
|
"error": error,
|
||||||
|
}
|
||||||
|
|
||||||
|
await log_metrics(metrics)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, List
|
||||||
|
import chainlit as cl
|
||||||
|
import ollama
|
||||||
|
import fitz # PyMuPDF
|
||||||
|
from qdrant_client import AsyncQdrantClient
|
||||||
|
from qdrant_client.models import PointStruct, Distance, VectorParams
|
||||||
|
from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
|
||||||
|
|
||||||
|
# === FIX IMPORT ROBUSTO ===
|
||||||
|
# Gestisce le differenze tra le versioni di Chainlit 2.x
|
||||||
|
try:
|
||||||
|
from chainlit.data.storage_clients import BaseStorageClient
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from chainlit.data.base import BaseStorageClient
|
||||||
|
except ImportError:
|
||||||
|
from chainlit.data.storage_clients.base import BaseStorageClient
|
||||||
|
|
||||||
|
# === CONFIGURAZIONE ===
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station")
|
||||||
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://192.168.1.243:11434")
|
||||||
|
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
||||||
|
WORKSPACES_DIR = "./workspaces"
|
||||||
|
STORAGE_DIR = "./.files"
|
||||||
|
|
||||||
|
os.makedirs(STORAGE_DIR, exist_ok=True)
|
||||||
|
os.makedirs(WORKSPACES_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# === MAPPING UTENTI E RUOLI ===
|
||||||
|
USER_PROFILES = {
|
||||||
|
"giuseppe@defranceschi.pro": {
|
||||||
|
"role": "admin",
|
||||||
|
"name": "Giuseppe",
|
||||||
|
"workspace": "admin_workspace",
|
||||||
|
"rag_collection": "admin_docs",
|
||||||
|
"capabilities": ["debug", "system_prompts", "user_management", "all_models"],
|
||||||
|
"show_code": True
|
||||||
|
},
|
||||||
|
"federica.tecchio@gmail.com": {
|
||||||
|
"role": "business",
|
||||||
|
"name": "Federica",
|
||||||
|
"workspace": "business_workspace",
|
||||||
|
"rag_collection": "contabilita",
|
||||||
|
"capabilities": ["pdf_upload", "basic_chat"],
|
||||||
|
"show_code": False
|
||||||
|
},
|
||||||
|
"giuseppe.defranceschi@gmail.com": {
|
||||||
|
"role": "admin",
|
||||||
|
"name": "Giuseppe",
|
||||||
|
"workspace": "admin_workspace",
|
||||||
|
"rag_collection": "admin_docs",
|
||||||
|
"capabilities": ["debug", "system_prompts", "user_management", "all_models"],
|
||||||
|
"show_code": True
|
||||||
|
},
|
||||||
|
"riccardob545@gmail.com": {
|
||||||
|
"role": "engineering",
|
||||||
|
"name": "Riccardo",
|
||||||
|
"workspace": "engineering_workspace",
|
||||||
|
"rag_collection": "engineering_docs",
|
||||||
|
"capabilities": ["code_execution", "data_viz", "advanced_chat"],
|
||||||
|
"show_code": True
|
||||||
|
},
|
||||||
|
"giuliadefranceschi05@gmail.com": {
|
||||||
|
"role": "architecture",
|
||||||
|
"name": "Giulia",
|
||||||
|
"workspace": "architecture_workspace",
|
||||||
|
"rag_collection": "architecture_manuals",
|
||||||
|
"capabilities": ["visual_chat", "pdf_upload", "image_gen"],
|
||||||
|
"show_code": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# === CUSTOM LOCAL STORAGE CLIENT (FIXED) ===# Questa classe ora implementa tutti i metodi astratti richiesti da Chainlit 2.8.3
|
||||||
|
class LocalStorageClient(BaseStorageClient):
|
||||||
|
"""Storage locale su filesystem per file/elementi"""
|
||||||
|
|
||||||
|
def __init__(self, storage_path: str):
|
||||||
|
self.storage_path = storage_path
|
||||||
|
os.makedirs(storage_path, exist_ok=True)
|
||||||
|
|
||||||
|
async def upload_file(
|
||||||
|
self,
|
||||||
|
object_key: str,
|
||||||
|
data: bytes,
|
||||||
|
mime: str = "application/octet-stream",
|
||||||
|
overwrite: bool = True,
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
file_path = os.path.join(self.storage_path, object_key)
|
||||||
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
return {"object_key": object_key, "url": f"/files/{object_key}"}
|
||||||
|
|
||||||
|
# Implementazione metodi obbligatori mancanti nella versione precedente
|
||||||
|
async def get_read_url(self, object_key: str) -> str:
|
||||||
|
return f"/files/{object_key}"
|
||||||
|
|
||||||
|
async def delete_file(self, object_key: str) -> bool:
|
||||||
|
file_path = os.path.join(self.storage_path, object_key)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === DATA LAYER ===
|
||||||
|
@cl.data_layer
|
||||||
|
def get_data_layer():
|
||||||
|
return SQLAlchemyDataLayer(
|
||||||
|
conninfo=DATABASE_URL,
|
||||||
|
user_thread_limit=1000,
|
||||||
|
storage_provider=LocalStorageClient(storage_path=STORAGE_DIR)
|
||||||
|
)
|
||||||
|
|
||||||
|
# === OAUTH CALLBACK ===
|
||||||
|
@cl.oauth_callback
|
||||||
|
def oauth_callback(
|
||||||
|
provider_id: str,
|
||||||
|
token: str,
|
||||||
|
raw_user_data: Dict[str, str],
|
||||||
|
default_user: cl.User,
|
||||||
|
) -> Optional[cl.User]:
|
||||||
|
if provider_id == "google":
|
||||||
|
email = raw_user_data.get("email", "").lower()
|
||||||
|
|
||||||
|
# Verifica se utente è autorizzato (opzionale: blocca se non in lista)
|
||||||
|
# if email not in USER_PROFILES:
|
||||||
|
# return None
|
||||||
|
|
||||||
|
# Recupera profilo o usa default Guest
|
||||||
|
profile = USER_PROFILES.get(email, get_user_profile("guest"))
|
||||||
|
|
||||||
|
default_user.metadata.update({
|
||||||
|
"picture": raw_user_data.get("picture", ""),
|
||||||
|
"role": profile["role"],
|
||||||
|
"workspace": profile["workspace"],
|
||||||
|
"rag_collection": profile["rag_collection"],
|
||||||
|
"capabilities": profile["capabilities"],
|
||||||
|
"show_code": profile["show_code"],
|
||||||
|
"display_name": profile["name"]
|
||||||
|
})
|
||||||
|
return default_user
|
||||||
|
return default_user
|
||||||
|
|
||||||
|
# === UTILITY FUNCTIONS ===
|
||||||
|
def get_user_profile(user_email: str) -> Dict:
|
||||||
|
return USER_PROFILES.get(user_email.lower(), {
|
||||||
|
"role": "guest",
|
||||||
|
"name": "Ospite",
|
||||||
|
"workspace": "guest_workspace",
|
||||||
|
"rag_collection": "documents",
|
||||||
|
"capabilities": [],
|
||||||
|
"show_code": False
|
||||||
|
})
|
||||||
|
|
||||||
|
def create_workspace(workspace_name: str) -> str:
|
||||||
|
path = os.path.join(WORKSPACES_DIR, workspace_name)
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def save_code_to_file(code: str, workspace: str) -> str:
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
file_name = f"code_{timestamp}.py"
|
||||||
|
file_path = os.path.join(WORKSPACES_DIR, workspace, file_name)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(code)
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
def extract_text_from_pdf(pdf_path: str) -> str:
|
||||||
|
try:
|
||||||
|
doc = fitz.open(pdf_path)
|
||||||
|
text = "\n".join([page.get_text() for page in doc])
|
||||||
|
doc.close()
|
||||||
|
return text
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# === QDRANT FUNCTIONS ===
|
||||||
|
async def get_qdrant_client() -> AsyncQdrantClient:
|
||||||
|
return AsyncQdrantClient(url=QDRANT_URL)
|
||||||
|
|
||||||
|
async def ensure_collection(collection_name: str):
|
||||||
|
client = await get_qdrant_client()
|
||||||
|
if not await client.collection_exists(collection_name):
|
||||||
|
await client.create_collection(
|
||||||
|
collection_name=collection_name,
|
||||||
|
vectors_config=VectorParams(size=768, distance=Distance.COSINE)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_embeddings(text: str) -> list:
|
||||||
|
client = ollama.Client(host=OLLAMA_URL)
|
||||||
|
try:
|
||||||
|
response = client.embed(model='nomic-embed-text', input=text[:2000])
|
||||||
|
if 'embeddings' in response: return response['embeddings'][0]
|
||||||
|
return response.get('embedding', [])
|
||||||
|
except: return []
|
||||||
|
|
||||||
|
async def index_document(file_name: str, content: str, collection_name: str) -> bool:
|
||||||
|
try:
|
||||||
|
await ensure_collection(collection_name)
|
||||||
|
embedding = await get_embeddings(content)
|
||||||
|
if not embedding: return False
|
||||||
|
|
||||||
|
qdrant = await get_qdrant_client()
|
||||||
|
await qdrant.upsert(
|
||||||
|
collection_name=collection_name,
|
||||||
|
points=[PointStruct(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
vector=embedding,
|
||||||
|
payload={"file_name": file_name, "content": content[:3000], "indexed_at": datetime.now().isoformat()}
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except: return False
|
||||||
|
|
||||||
|
async def search_qdrant(query: str, collection: str) -> str:
|
||||||
|
try:
|
||||||
|
client = await get_qdrant_client()
|
||||||
|
if not await client.collection_exists(collection): return ""
|
||||||
|
emb = await get_embeddings(query)
|
||||||
|
if not emb: return ""
|
||||||
|
res = await client.query_points(collection_name=collection, query=emb, limit=3)
|
||||||
|
return "\n\n".join([hit.payload['content'] for hit in res.points if hit.payload])
|
||||||
|
except: return ""
|
||||||
|
|
||||||
|
# === CHAINLIT HANDLERS ===
|
||||||
|
|
||||||
|
@cl.on_chat_start
|
||||||
|
async def on_chat_start():
|
||||||
|
user = cl.user_session.get("user")
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
# Fallback locale se non c'è auth
|
||||||
|
user_email = "guest@local"
|
||||||
|
profile = get_user_profile(user_email)
|
||||||
|
else:
|
||||||
|
user_email = user.identifier
|
||||||
|
# I metadati sono già popolati dalla callback oauth
|
||||||
|
profile = USER_PROFILES.get(user_email, get_user_profile("guest"))
|
||||||
|
|
||||||
|
# Salva in sessione
|
||||||
|
cl.user_session.set("email", user_email)
|
||||||
|
cl.user_session.set("role", profile["role"])
|
||||||
|
cl.user_session.set("workspace", profile["workspace"])
|
||||||
|
cl.user_session.set("rag_collection", profile["rag_collection"])
|
||||||
|
cl.user_session.set("show_code", profile["show_code"])
|
||||||
|
|
||||||
|
create_workspace(profile["workspace"])
|
||||||
|
|
||||||
|
# === SETTINGS WIDGETS ===
|
||||||
|
settings_widgets = [
|
||||||
|
cl.input_widget.Select(
|
||||||
|
id="model",
|
||||||
|
label="Modello AI",
|
||||||
|
values=["glm-4.6:cloud", "llama3.2", "mistral", "qwen2.5-coder:32b"],
|
||||||
|
initial_value="glm-4.6:cloud",
|
||||||
|
),
|
||||||
|
cl.input_widget.Slider(
|
||||||
|
id="temperature",
|
||||||
|
label="Temperatura",
|
||||||
|
initial=0.7, min=0, max=2, step=0.1,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if profile["role"] == "admin":
|
||||||
|
settings_widgets.append(cl.input_widget.Switch(id="rag_enabled", label="Abilita RAG", initial=True))
|
||||||
|
|
||||||
|
await cl.ChatSettings(settings_widgets).send()
|
||||||
|
|
||||||
|
await cl.Message(
|
||||||
|
content=f"👋 Ciao **{profile['name']}**!\n"
|
||||||
|
f"Ruolo: `{profile['role']}` | Workspace: `{profile['workspace']}`\n"
|
||||||
|
).send()
|
||||||
|
|
||||||
|
@cl.on_settings_update
|
||||||
|
async def on_settings_update(settings):
|
||||||
|
cl.user_session.set("settings", settings)
|
||||||
|
await cl.Message(content="✅ Impostazioni aggiornate").send()
|
||||||
|
|
||||||
|
@cl.on_message
|
||||||
|
async def on_message(message: cl.Message):
|
||||||
|
workspace = cl.user_session.get("workspace")
|
||||||
|
rag_collection = cl.user_session.get("rag_collection")
|
||||||
|
user_role = cl.user_session.get("role")
|
||||||
|
show_code = cl.user_session.get("show_code")
|
||||||
|
|
||||||
|
settings = cl.user_session.get("settings", {})
|
||||||
|
model = settings.get("model", "glm-4.6:cloud")
|
||||||
|
temperature = settings.get("temperature", 0.7)
|
||||||
|
rag_enabled = settings.get("rag_enabled", True) if user_role == "admin" else True
|
||||||
|
|
||||||
|
# 1. GESTIONE FILE
|
||||||
|
if message.elements:
|
||||||
|
for element in message.elements:
|
||||||
|
dest = os.path.join(WORKSPACES_DIR, workspace, element.name)
|
||||||
|
shutil.copy(element.path, dest)
|
||||||
|
if element.name.endswith(".pdf"):
|
||||||
|
text = extract_text_from_pdf(dest)
|
||||||
|
if text:
|
||||||
|
await index_document(element.name, text, rag_collection)
|
||||||
|
await cl.Message(content=f"✅ **{element.name}** indicizzato.").send()
|
||||||
|
|
||||||
|
# 2. RAG
|
||||||
|
context = ""
|
||||||
|
if rag_enabled:
|
||||||
|
context = await search_qdrant(message.content, rag_collection)
|
||||||
|
|
||||||
|
system_prompt = "Sei un assistente esperto."
|
||||||
|
if context: system_prompt += f"\n\nCONTESTO:\n{context}"
|
||||||
|
|
||||||
|
# 3. GENERAZIONE
|
||||||
|
client = ollama.AsyncClient(host=OLLAMA_URL)
|
||||||
|
msg = cl.Message(content="")
|
||||||
|
await msg.send()
|
||||||
|
|
||||||
|
stream = await client.chat(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": message.content}],
|
||||||
|
options={"temperature": temperature},
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
full_resp = ""
|
||||||
|
async for chunk in stream:
|
||||||
|
token = chunk['message']['content']
|
||||||
|
full_resp += token
|
||||||
|
await msg.stream_token(token)
|
||||||
|
await msg.update()
|
||||||
|
|
||||||
|
# 4. SALVATAGGIO CODICE
|
||||||
|
if show_code:
|
||||||
|
blocks = re.findall(r"``````", full_resp, re.DOTALL)
|
||||||
|
elements = []
|
||||||
|
for code in blocks:
|
||||||
|
path = save_code_to_file(code.strip(), workspace)
|
||||||
|
elements.append(cl.File(name=os.path.basename(path), path=path, display="inline"))
|
||||||
|
if elements:
|
||||||
|
await cl.Message(content="💾 Codice salvato", elements=elements).send()
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
Architecture: x86_64
|
||||||
|
CPU op-mode(s): 32-bit, 64-bit
|
||||||
|
Address sizes: 40 bits physical, 48 bits virtual
|
||||||
|
Byte Order: Little Endian
|
||||||
|
CPU(s): 16
|
||||||
|
On-line CPU(s) list: 0-15
|
||||||
|
Vendor ID: GenuineIntel
|
||||||
|
Model name: QEMU Virtual CPU version 2.5+
|
||||||
|
CPU family: 15
|
||||||
|
Model: 107
|
||||||
|
Thread(s) per core: 1
|
||||||
|
Core(s) per socket: 4
|
||||||
|
Socket(s): 4
|
||||||
|
Stepping: 1
|
||||||
|
BogoMIPS: 4999.99
|
||||||
|
Flags: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid tsc_known_freq pni ssse3 cx16 sse4_1 sse4_2 x2apic popcnt aes hypervisor lahf_lm cpuid_fault pti
|
||||||
|
Hypervisor vendor: KVM
|
||||||
|
Virtualization type: full
|
||||||
|
L1d cache: 512 KiB (16 instances)
|
||||||
|
L1i cache: 512 KiB (16 instances)
|
||||||
|
L2 cache: 64 MiB (16 instances)
|
||||||
|
L3 cache: 64 MiB (4 instances)
|
||||||
|
NUMA node(s): 1
|
||||||
|
NUMA node0 CPU(s): 0-15
|
||||||
|
Vulnerability Gather data sampling: Not affected
|
||||||
|
Vulnerability Itlb multihit: KVM: Mitigation: VMX unsupported
|
||||||
|
Vulnerability L1tf: Mitigation; PTE Inversion
|
||||||
|
Vulnerability Mds: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
|
||||||
|
Vulnerability Meltdown: Mitigation; PTI
|
||||||
|
Vulnerability Mmio stale data: Unknown: No mitigations
|
||||||
|
Vulnerability Reg file data sampling: Not affected
|
||||||
|
Vulnerability Retbleed: Not affected
|
||||||
|
Vulnerability Spec rstack overflow: Not affected
|
||||||
|
Vulnerability Spec store bypass: Vulnerable
|
||||||
|
Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization
|
||||||
|
Vulnerability Spectre v2: Mitigation; Retpolines; STIBP disabled; RSB filling; PBRSB-eIBRS Not affected; BHI Retpoline
|
||||||
|
Vulnerability Srbds: Not affected
|
||||||
|
Vulnerability Tsx async abort: Not affected
|
||||||
|
Vulnerability Vmscape: Not affected
|
||||||
|
|
@ -0,0 +1,717 @@
|
||||||
|
ai-station-app | [INFO] 2025-12-31 10:20:23,724 [RapidOCR] download_file.py:82: Download size: 25.67MB
|
||||||
|
ai-station-app | [INFO] 2025-12-31 10:20:27,451 [RapidOCR] download_file.py:95: Successfully saved to: /usr/local/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_rec_infer.pth
|
||||||
|
ai-station-app | [INFO] 2025-12-31 10:20:27,460 [RapidOCR] main.py:50: Using /usr/local/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_rec_infer.pth
|
||||||
|
ai-station-app | 2025-12-31 10:20:28 - Auto OCR model selected rapidocr with torch.
|
||||||
|
ai-station-app | 2025-12-31 10:20:28 - Loading plugin 'docling_defaults'
|
||||||
|
ai-station-app | 2025-12-31 10:20:28 - Registered layout engines: ['docling_layout_default', 'docling_experimental_table_crops_layout']
|
||||||
|
ai-station-app | 2025-12-31 10:20:28 - Accelerator device: 'cpu'
|
||||||
|
ai-station-app | 2025-12-31 10:20:59 - Loading plugin 'docling_defaults'
|
||||||
|
ai-station-app | 2025-12-31 10:20:59 - Registered table structure engines: ['docling_tableformer']
|
||||||
|
ai-station-app | 2025-12-31 10:22:00 - Accelerator device: 'cpu'
|
||||||
|
ai-station-app | 2025-12-31 10:22:02 - Processing document esempio manuale Omron.pdf
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Finished converting document esempio manuale Omron.pdf in 173.75 sec.
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - An unexpected error occurred:
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Translation file for it-IT not found. Using default translation en-US.
|
||||||
|
ai-station-qdrant | 2025-12-31T10:23:07.266024Z INFO actix_web::middleware::logger: 172.18.0.4 "GET /collections/admin_docs/exists HTTP/1.1" 200 69 "-" "python-client/1.16.2 python/3.11.14" 0.001423
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - HTTP Request: GET http://qdrant:6333/collections/admin_docs/exists "HTTP/1.1 200 OK"
|
||||||
|
ai-station-qdrant | 2025-12-31T10:23:07.422109Z INFO storage::content_manager::toc::collection_meta_ops: Creating collection admin_docs
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-426' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-429' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-432' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-434' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-437' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-439' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-441' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-443' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-446' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-448' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-450' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-452' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-454' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-457' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-459' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-461' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-463' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-465' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-467' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-469' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-471' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-474' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-478' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-480' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-482' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-484' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-486' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-488' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-490' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-492' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-494' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-496' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-498' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-501' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-503' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - Task exception was never retrieved
|
||||||
|
ai-station-app | future: <Task finished name='Task-505' coro=<AsyncServer._handle_event_internal() done, defined at /usr/local/lib/python3.11/site-packages/socketio/async_server.py:605> exception=ValueError('Session not found')>
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/asyncio/tasks.py", line 277, in __step
|
||||||
|
ai-station-app | result = coro.send(None)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 607, in _handle_event_internal
|
||||||
|
ai-station-app | r = await server._trigger_event(data[0], namespace, sid, *data[1:])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/socketio/async_server.py", line 634, in _trigger_event
|
||||||
|
ai-station-app | ret = await handler(*args)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/socket.py", line 323, in window_message
|
||||||
|
ai-station-app | session = WebsocketSession.require(sid)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/session.py", line 354, in require
|
||||||
|
ai-station-app | raise ValueError("Session not found")
|
||||||
|
ai-station-app | ValueError: Session not found
|
||||||
|
ai-station-app | post request handler error
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/async_server.py", line 306, in handle_request
|
||||||
|
ai-station-app | await socket.handle_post_request(environ)
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/async_socket.py", line 109, in handle_post_request
|
||||||
|
ai-station-app | p = payload.Payload(encoded_payload=body)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/payload.py", line 13, in __init__
|
||||||
|
ai-station-app | self.decode(encoded_payload)
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/payload.py", line 44, in decode
|
||||||
|
ai-station-app | raise ValueError('Too many packets in payload')
|
||||||
|
ai-station-app | ValueError: Too many packets in payload
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - post request handler error
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/async_server.py", line 306, in handle_request
|
||||||
|
ai-station-app | await socket.handle_post_request(environ)
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/async_socket.py", line 109, in handle_post_request
|
||||||
|
ai-station-app | p = payload.Payload(encoded_payload=body)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/payload.py", line 13, in __init__
|
||||||
|
ai-station-app | self.decode(encoded_payload)
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/engineio/payload.py", line 44, in decode
|
||||||
|
ai-station-app | raise ValueError('Too many packets in payload')
|
||||||
|
ai-station-app | ValueError: Too many packets in payload
|
||||||
|
ai-station-qdrant | 2025-12-31T10:23:07.898915Z INFO actix_web::middleware::logger: 172.18.0.4 "PUT /collections/admin_docs HTTP/1.1" 200 57 "-" "python-client/1.16.2 python/3.11.14" 0.478796
|
||||||
|
ai-station-app | 2025-12-31 10:23:07 - HTTP Request: PUT http://qdrant:6333/collections/admin_docs "HTTP/1.1 200 OK"
|
||||||
|
ai-station-app | 2025-12-31 10:23:08 - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
||||||
|
ai-station-qdrant | 2025-12-31T10:23:12.256007Z INFO actix_web::middleware::logger: 172.18.0.4 "PUT /collections/admin_docs/points?wait=true HTTP/1.1" 200 84 "-" "python-client/1.16.2 python/3.11.14" 0.128378
|
||||||
|
ai-station-app | 2025-12-31 10:23:12 - HTTP Request: PUT http://qdrant:6333/collections/admin_docs/points?wait=true "HTTP/1.1 200 OK"
|
||||||
|
ai-station-app | 2025-12-31 10:23:12 - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
||||||
|
ai-station-qdrant | 2025-12-31T10:23:12.413564Z INFO actix_web::middleware::logger: 172.18.0.4 "GET /collections/admin_docs/exists HTTP/1.1" 200 68 "-" "python-client/1.16.2 python/3.11.14" 0.006418
|
||||||
|
ai-station-app | 2025-12-31 10:23:12 - HTTP Request: GET http://qdrant:6333/collections/admin_docs/exists "HTTP/1.1 200 OK"
|
||||||
|
ai-station-app | 2025-12-31 10:23:12 - module 'chainlit.data' has no attribute 'qdrant_client'
|
||||||
|
ai-station-app | Traceback (most recent call last):
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/utils.py", line 57, in wrapper
|
||||||
|
ai-station-app | return await user_function(**params_values)
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/usr/local/lib/python3.11/site-packages/chainlit/callbacks.py", line 161, in with_parent_id
|
||||||
|
ai-station-app | await func(message)
|
||||||
|
ai-station-app | File "/app/app.py", line 250, in main
|
||||||
|
ai-station-app | rag_context = await search_hybrid(message.content, profile["rag_collection"])
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | File "/app/app.py", line 166, in search_hybrid
|
||||||
|
ai-station-app | cl.data.qdrant_client.models.Prefetch(
|
||||||
|
ai-station-app | ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
ai-station-app | AttributeError: module 'chainlit.data' has no attribute 'qdrant_client'
|
||||||
|
ai-station-app | 2025-12-31 10:23:28 - Translation file for it-IT not found. Using default translation en-US.
|
||||||
|
ai-station-postgres | 2025-12-31 10:23:35.822 UTC [28] LOG: checkpoint starting: time
|
||||||
|
ai-station-postgres | 2025-12-31 10:23:37.312 UTC [28] LOG: checkpoint complete: wrote 17 buffers (0.1%); 0 WAL file(s) added, 0 removed, 0 recycled; write=1.428 s, sync=0.019 s, total=1.491 s; sync files=11, longest=0.009 s, average=0.002 s; distance=68 kB, estimate=68 kB
|
||||||
|
^C
|
||||||
|
giuseppe@ai-srv:~/ai-station$
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
chainlit-app:
|
chainlit-app:
|
||||||
build: .
|
build: .
|
||||||
|
|
@ -12,7 +10,7 @@ services:
|
||||||
- DATABASE_URL=postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station
|
- DATABASE_URL=postgresql+asyncpg://ai_user:secure_password_here@postgres:5432/ai_station
|
||||||
- OLLAMA_URL=http://192.168.1.243:11434
|
- OLLAMA_URL=http://192.168.1.243:11434
|
||||||
- QDRANT_URL=http://qdrant:6333
|
- QDRANT_URL=http://qdrant:6333
|
||||||
- BGE_API_URL=http://192.168.1.243:8001
|
- BGE_API_URL=http://192.168.1.243:8001/embed
|
||||||
volumes:
|
volumes:
|
||||||
- ./workspaces:/app/workspaces
|
- ./workspaces:/app/workspaces
|
||||||
- ./public:/app/public # ⬅️ VERIFICA QUESTO
|
- ./public:/app/public # ⬅️ VERIFICA QUESTO
|
||||||
|
|
|
||||||
133
error.log
133
error.log
|
|
@ -1,133 +0,0 @@
|
||||||
qdrant-1 | _ _
|
|
||||||
chainlit-app-1 | 2025-12-25 18:05:12 - INFO - chainlit - Your app is available at http://0.0.0.0:8000
|
|
||||||
postgres-1 |
|
|
||||||
postgres-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
|
|
||||||
postgres-1 |
|
|
||||||
postgres-1 | 2025-12-25 16:38:01.071 UTC [1] LOG: starting PostgreSQL 18.1 (Debian 18.1-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
|
|
||||||
qdrant-1 | __ _ __| |_ __ __ _ _ __ | |_
|
|
||||||
qdrant-1 | / _` |/ _` | '__/ _` | '_ \| __|
|
|
||||||
qdrant-1 | | (_| | (_| | | | (_| | | | | |_
|
|
||||||
qdrant-1 | \__, |\__,_|_| \__,_|_| |_|\__|
|
|
||||||
postgres-1 | 2025-12-25 16:38:01.072 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
|
||||||
postgres-1 | 2025-12-25 16:38:01.072 UTC [1] LOG: listening on IPv6 address "::", port 5432
|
|
||||||
postgres-1 | 2025-12-25 16:38:01.093 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
|
||||||
postgres-1 | 2025-12-25 16:38:01.126 UTC [32] LOG: database system was shut down at 2025-12-25 14:34:55 UTC
|
|
||||||
postgres-1 | 2025-12-25 16:38:01.155 UTC [1] LOG: database system is ready to accept connections
|
|
||||||
qdrant-1 | |_|
|
|
||||||
qdrant-1 |
|
|
||||||
qdrant-1 | Access web UI at https://ui.qdrant.tech/?v=v1.0.0
|
|
||||||
qdrant-1 |
|
|
||||||
qdrant-1 | [2025-12-25T16:38:00.816Z INFO storage::content_manager::consensus::persistent] Initializing new raft state at ./storage/raft_state
|
|
||||||
qdrant-1 | [2025-12-25T16:38:00.861Z INFO qdrant] Distributed mode disabled
|
|
||||||
qdrant-1 | [2025-12-25T16:38:00.861Z INFO qdrant] Telemetry reporting enabled, id: e6113e43-627c-471d-8374-0f1b61799d76
|
|
||||||
qdrant-1 | [2025-12-25T16:38:00.872Z INFO qdrant::tonic] Qdrant gRPC listening on 6334
|
|
||||||
qdrant-1 | [2025-12-25T16:38:00.890Z INFO actix_server::builder] Starting 3 workers
|
|
||||||
qdrant-1 | [2025-12-25T16:38:00.890Z INFO actix_server::server] Actix runtime found; starting in Actix runtime
|
|
||||||
qdrant-1 | [2025-12-25T16:39:02.504Z INFO actix_server::server] SIGTERM received; starting graceful shutdown
|
|
||||||
qdrant-1 | [2025-12-25T16:39:02.505Z INFO actix_server::worker] Shutting down idle worker
|
|
||||||
qdrant-1 | [2025-12-25T16:39:02.508Z INFO actix_server::accept] Accept thread stopped
|
|
||||||
qdrant-1 | [2025-12-25T16:39:02.508Z INFO actix_server::worker] Shutting down idle worker
|
|
||||||
qdrant-1 | [2025-12-25T16:39:02.508Z INFO actix_server::worker] Shutting down idle worker
|
|
||||||
qdrant-1 | _ _
|
|
||||||
qdrant-1 | __ _ __| |_ __ __ _ _ __ | |_
|
|
||||||
qdrant-1 | / _` |/ _` | '__/ _` | '_ \| __|
|
|
||||||
qdrant-1 | | (_| | (_| | | | (_| | | | | |_
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.495 UTC [1] LOG: received fast shutdown request
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.505 UTC [1] LOG: aborting any active transactions
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.521 UTC [1] LOG: background worker "logical replication launcher" (PID 35) exited with exit code 1
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.521 UTC [30] LOG: shutting down
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.533 UTC [30] LOG: checkpoint starting: shutdown immediate
|
|
||||||
chainlit-app-1 | 2025-12-25 18:05:25 - INFO - httpx - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | /app/app.py:43: UserWarning: Qdrant client version 1.16.2 is incompatible with server version 1.0.0. Major versions should match and minor version difference must not exceed 1. Set check_compatibility=False to skip version check.
|
|
||||||
chainlit-app-1 | return QdrantClient(url=QDRANT_URL)
|
|
||||||
qdrant-1 | \__, |\__,_|_| \__,_|_| |_|\__|
|
|
||||||
chainlit-app-1 | 2025-12-25 18:05:25 - INFO - httpx - HTTP Request: GET http://qdrant:6333/collections/documents "HTTP/1.1 404 Not Found"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:08 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:10 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
qdrant-1 | |_|
|
|
||||||
qdrant-1 |
|
|
||||||
qdrant-1 | Access web UI at https://ui.qdrant.tech/?v=v1.0.0
|
|
||||||
qdrant-1 |
|
|
||||||
qdrant-1 | [2025-12-25T16:43:53.592Z INFO storage::content_manager::consensus::persistent] Loading raft state from ./storage/raft_state
|
|
||||||
qdrant-1 | [2025-12-25T16:43:53.612Z INFO qdrant] Distributed mode disabled
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.601 UTC [30] LOG: checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.019 s, sync=0.009 s, total=0.079 s; sync files=2, longest=0.005 s, average=0.005 s; distance=0 kB, estimate=0 kB; lsn=0/1BEF980, redo lsn=0/1BEF980
|
|
||||||
postgres-1 | 2025-12-25 16:39:02.644 UTC [1] LOG: database system is shut down
|
|
||||||
postgres-1 |
|
|
||||||
postgres-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
|
|
||||||
postgres-1 |
|
|
||||||
postgres-1 | 2025-12-25 16:43:53.946 UTC [1] LOG: starting PostgreSQL 18.1 (Debian 18.1-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
|
|
||||||
postgres-1 | 2025-12-25 16:43:53.947 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
|
||||||
postgres-1 | 2025-12-25 16:43:53.947 UTC [1] LOG: listening on IPv6 address "::", port 5432
|
|
||||||
qdrant-1 | [2025-12-25T16:43:53.612Z INFO qdrant] Telemetry reporting enabled, id: 2a83356a-9770-47d3-a0bd-638f75769522
|
|
||||||
qdrant-1 | [2025-12-25T16:43:53.615Z INFO qdrant::tonic] Qdrant gRPC listening on 6334
|
|
||||||
qdrant-1 | [2025-12-25T16:43:53.616Z INFO actix_server::builder] Starting 3 workers
|
|
||||||
postgres-1 | 2025-12-25 16:43:53.965 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
|
||||||
postgres-1 | 2025-12-25 16:43:53.990 UTC [32] LOG: database system was shut down at 2025-12-25 16:39:02 UTC
|
|
||||||
postgres-1 | 2025-12-25 16:43:54.013 UTC [1] LOG: database system is ready to accept connections
|
|
||||||
postgres-1 | 2025-12-25 16:48:54.089 UTC [30] LOG: checkpoint starting: time
|
|
||||||
postgres-1 | 2025-12-25 16:48:54.175 UTC [30] LOG: checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.036 s, sync=0.009 s, total=0.088 s; sync files=2, longest=0.005 s, average=0.005 s; distance=0 kB, estimate=0 kB; lsn=0/1BEFA88, redo lsn=0/1BEFA30
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.002 UTC [1] LOG: received fast shutdown request
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.018 UTC [1] LOG: aborting any active transactions
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.026 UTC [1] LOG: background worker "logical replication launcher" (PID 35) exited with exit code 1
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.030 UTC [30] LOG: shutting down
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.039 UTC [30] LOG: checkpoint starting: shutdown immediate
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.086 UTC [30] LOG: checkpoint complete: wrote 0 buffers (0.0%), wrote 0 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.004 s, sync=0.001 s, total=0.057 s; sync files=0, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=0 kB; lsn=0/1BEFB38, redo lsn=0/1BEFB38
|
|
||||||
postgres-1 | 2025-12-25 16:56:42.131 UTC [1] LOG: database system is shut down
|
|
||||||
postgres-1 |
|
|
||||||
postgres-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
|
|
||||||
postgres-1 |
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:10 - WARNING - chainlit - Translated markdown file for it-IT not found. Defaulting to chainlit.md.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:13 - INFO - chainlit - Missing custom logo. Falling back to default logo.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:21 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:21 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
qdrant-1 | [2025-12-25T16:43:53.617Z INFO actix_server::server] Actix runtime found; starting in Actix runtime
|
|
||||||
qdrant-1 | [2025-12-25T16:56:42.005Z INFO actix_server::server] SIGTERM received; starting graceful shutdown
|
|
||||||
qdrant-1 | [2025-12-25T16:56:42.006Z INFO actix_server::worker] Shutting down idle worker
|
|
||||||
qdrant-1 | [2025-12-25T16:56:42.006Z INFO actix_server::worker] Shutting down idle worker
|
|
||||||
qdrant-1 | [2025-12-25T16:56:42.007Z INFO actix_server::worker] Shutting down idle worker
|
|
||||||
qdrant-1 | [2025-12-25T16:56:42.007Z INFO actix_server::accept] Accept thread stopped
|
|
||||||
qdrant-1 | _ _
|
|
||||||
qdrant-1 | __ _ __| |_ __ __ _ _ __ | |_
|
|
||||||
qdrant-1 | / _` |/ _` | '__/ _` | '_ \| __|
|
|
||||||
qdrant-1 | | (_| | (_| | | | (_| | | | | |_
|
|
||||||
qdrant-1 | \__, |\__,_|_| \__,_|_| |_|\__|
|
|
||||||
qdrant-1 | |_|
|
|
||||||
qdrant-1 |
|
|
||||||
qdrant-1 | Access web UI at https://ui.qdrant.tech/?v=v1.0.0
|
|
||||||
qdrant-1 |
|
|
||||||
qdrant-1 | [2025-12-25T16:56:52.790Z INFO storage::content_manager::consensus::persistent] Loading raft state from ./storage/raft_state
|
|
||||||
qdrant-1 | [2025-12-25T16:56:52.796Z INFO qdrant] Distributed mode disabled
|
|
||||||
qdrant-1 | [2025-12-25T16:56:52.796Z INFO qdrant] Telemetry reporting enabled, id: f821b8ea-9ee5-497e-a172-dfebf253f7b1
|
|
||||||
qdrant-1 | [2025-12-25T16:56:52.797Z INFO qdrant::tonic] Qdrant gRPC listening on 6334
|
|
||||||
qdrant-1 | [2025-12-25T16:56:52.798Z INFO actix_server::builder] Starting 3 workers
|
|
||||||
qdrant-1 | [2025-12-25T16:56:52.798Z INFO actix_server::server] Actix runtime found; starting in Actix runtime
|
|
||||||
qdrant-1 | [2025-12-25T18:05:25.183Z INFO actix_web::middleware::logger] 172.18.0.4 "GET /collections/documents HTTP/1.1" 404 110 "-" "python-client/1.16.2 python/3.10.19" 0.007704
|
|
||||||
qdrant-1 | [2025-12-25T18:05:30.499Z INFO actix_web::middleware::logger] 172.18.0.4 "PUT /collections/documents HTTP/1.1" 200 71 "-" "python-client/1.16.2 python/3.10.19" 5.311157
|
|
||||||
qdrant-1 | [2025-12-25T18:06:22.662Z INFO actix_web::middleware::logger] 172.18.0.4 "GET /collections/documents HTTP/1.1" 200 413 "-" "python-client/1.16.2 python/3.10.19" 0.005606
|
|
||||||
postgres-1 | 2025-12-25 16:56:43.530 UTC [1] LOG: starting PostgreSQL 18.1 (Debian 18.1-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
|
|
||||||
postgres-1 | 2025-12-25 16:56:43.532 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
|
||||||
postgres-1 | 2025-12-25 16:56:43.532 UTC [1] LOG: listening on IPv6 address "::", port 5432
|
|
||||||
postgres-1 | 2025-12-25 16:56:43.552 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
|
||||||
postgres-1 | 2025-12-25 16:56:43.585 UTC [32] LOG: database system was shut down at 2025-12-25 16:56:42 UTC
|
|
||||||
postgres-1 | 2025-12-25 16:56:43.616 UTC [1] LOG: database system is ready to accept connections
|
|
||||||
postgres-1 | 2025-12-25 17:01:43.645 UTC [30] LOG: checkpoint starting: time
|
|
||||||
postgres-1 | 2025-12-25 17:01:43.712 UTC [30] LOG: checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.019 s, sync=0.009 s, total=0.068 s; sync files=2, longest=0.005 s, average=0.005 s; distance=0 kB, estimate=0 kB; lsn=0/1BEFC40, redo lsn=0/1BEFBE8
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:21 - WARNING - chainlit - Translated markdown file for it-IT not found. Defaulting to chainlit.md.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:22 - INFO - httpx - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:22 - INFO - httpx - HTTP Request: GET http://qdrant:6333/collections/documents "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:28 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:36 - INFO - httpx - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:37 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/embed "HTTP/1.1 500 Internal Server Error"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:39 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/chat "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:06:48 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:07:02 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:07:16 - INFO - httpx - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:07:22 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/embed "HTTP/1.1 500 Internal Server Error"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:07:22 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/chat "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:07:49 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:07:54 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:08:15 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/chat "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:08:30 - WARNING - chainlit - Translation file for it-IT not found. Using parent translation it.
|
|
||||||
chainlit-app-1 | 2025-12-25 18:08:57 - INFO - httpx - HTTP Request: GET http://qdrant:6333 "HTTP/1.1 200 OK"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:09:03 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/embed "HTTP/1.1 500 Internal Server Error"
|
|
||||||
chainlit-app-1 | 2025-12-25 18:09:03 - INFO - httpx - HTTP Request: POST http://192.168.1.243:11434/api/chat "HTTP/1.1 200 OK"
|
|
||||||
|
|
@ -1,36 +1,12 @@
|
||||||
/* dFm AI Station - Perplexity Clean Style */
|
.user-badge {
|
||||||
:root {
|
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||||||
--bg-color: #0B0F1A;
|
padding: 4px 12px;
|
||||||
--card-color: #161B2C;
|
border-radius: 12px;
|
||||||
--accent-color: #6366F1;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
body { background-color: var(--bg-color) !important; color: #F1F5F9 !important; }
|
/* Evidenzia codice */
|
||||||
|
.message pre {
|
||||||
/* Header e Logo */
|
background: #2d2d2d;
|
||||||
header {
|
border-left: 4px solid #0066CC;
|
||||||
background: rgba(11, 15, 26, 0.8) !important;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
border-bottom: 1px solid #23293F !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.MuiDrawer-paper {
|
|
||||||
background-color: var(--bg-color) !important;
|
|
||||||
border-right: 1px solid #23293F !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Messaggi */
|
|
||||||
div[class*="user"] {
|
|
||||||
background: #1E253A !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
border-left: 4px solid var(--accent-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input Area */
|
|
||||||
form {
|
|
||||||
background: var(--card-color) !important;
|
|
||||||
border-radius: 20px !important;
|
|
||||||
border: 1px solid #334155 !important;
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5) !important;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,7 @@ aiofiles>=23.0.0
|
||||||
sniffio
|
sniffio
|
||||||
aiohttp
|
aiohttp
|
||||||
boto3>=1.28.0
|
boto3>=1.28.0
|
||||||
azure-storage-file-datalake>=12.14.0
|
azure-storage-file-datalake>=12.14.0
|
||||||
|
docling
|
||||||
|
pillow
|
||||||
|
requests
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import httpx
|
||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configurazione
|
||||||
|
OLLAMA_URL = "http://192.168.1.243:11434"
|
||||||
|
MODEL = "minicpm-v"
|
||||||
|
|
||||||
|
print(f"👁️ Test Visione su {OLLAMA_URL} con modello {MODEL}...")
|
||||||
|
|
||||||
|
# 1. Controlla se il modello è caricato
|
||||||
|
try:
|
||||||
|
r = httpx.get(f"{OLLAMA_URL}/api/tags")
|
||||||
|
models = [m['name'] for m in r.json()['models']]
|
||||||
|
if MODEL not in str(models):
|
||||||
|
print(f"❌ Errore: Il modello {MODEL} non è stato trovato su Ollama!")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"✅ Modello {MODEL} trovato.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Errore connessione Ollama: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("🚀 Tutto pronto per l'implementazione!")
|
||||||
Loading…
Reference in New Issue