From 9cef64f9eacc24451b208d4187d3f5c6214ab61b Mon Sep 17 00:00:00 2001 From: AI Station Server Date: Tue, 30 Dec 2025 08:51:10 +0100 Subject: [PATCH] implementazione BGE-M3 Dense --- .../9ea4f57e-fd04-417a-a467-f5594c405972 | Bin 0 -> 5720 bytes .../013a0fb6-459d-462f-827c-e0d9c227bfa5 | Bin 0 -> 5720 bytes .../d65f66a2-2673-4cc2-9b84-2ad316a17f75 | Bin 0 -> 5720 bytes .../Fatture 2025.xlsx | Bin 0 -> 5720 bytes .../Fatture 2025.xlsx | Bin 0 -> 5720 bytes .../Fatture 2025.xlsx | Bin 0 -> 5720 bytes app.py | 357 ++++++++---------- requirements.txt | 6 +- 8 files changed, 170 insertions(+), 193 deletions(-) create mode 100644 .files/3656e301-423c-41a3-95cc-aa9c1d0a6d60/9ea4f57e-fd04-417a-a467-f5594c405972 create mode 100644 .files/c1942915-e472-4493-8745-0d07b7bba423/013a0fb6-459d-462f-827c-e0d9c227bfa5 create mode 100644 .files/f49178c6-be52-47f5-93c1-9114b9a67a29/d65f66a2-2673-4cc2-9b84-2ad316a17f75 create mode 100644 .files/fe508c80-f90f-471d-a7f8-fc98d4a018d6/013a0fb6-459d-462f-827c-e0d9c227bfa5/Fatture 2025.xlsx create mode 100644 .files/fe508c80-f90f-471d-a7f8-fc98d4a018d6/9ea4f57e-fd04-417a-a467-f5594c405972/Fatture 2025.xlsx create mode 100644 .files/unknown/d65f66a2-2673-4cc2-9b84-2ad316a17f75/Fatture 2025.xlsx diff --git a/.files/3656e301-423c-41a3-95cc-aa9c1d0a6d60/9ea4f57e-fd04-417a-a467-f5594c405972 b/.files/3656e301-423c-41a3-95cc-aa9c1d0a6d60/9ea4f57e-fd04-417a-a467-f5594c405972 new file mode 100644 index 0000000000000000000000000000000000000000..1e1163965f78cc0368b45ea33263cd88dbec2a2e GIT binary patch literal 5720 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 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) os.makedirs(os.path.dirname(file_path), exist_ok=True) - with open(file_path, "wb") as f: - f.write(data) + 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 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 + 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 - 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) - ) + return SQLAlchemyDataLayer(conninfo=DATABASE_URL, user_thread_limit=1000, storage_provider=LocalStorageClient(STORAGE_DIR)) -# === OAUTH CALLBACK === +# === OAUTH === @cl.oauth_callback -def oauth_callback( - provider_id: str, - token: str, - raw_user_data: Dict[str, str], - default_user: cl.User, -) -> Optional[cl.User]: +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"] + "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 get_user_profile(email: str) -> Dict: + return USER_PROFILES.get(email.lower(), {"role": "guest", "name": "Ospite", "workspace": "guest_workspace", "rag_collection": "documents", "show_code": False}) -def create_workspace(workspace_name: str) -> str: - path = os.path.join(WORKSPACES_DIR, workspace_name) +def create_workspace(name: str) -> str: + path = os.path.join(WORKSPACES_DIR, 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 + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + path = os.path.join(WORKSPACES_DIR, workspace, f"code_{ts}.py") + with open(path, "w", encoding="utf-8") as f: f.write(code) + return path -def extract_text_from_pdf(pdf_path: str) -> str: +# === PARSING DOCUMENTI === +def extract_text_from_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: + doc = fitz.open(path) + return "\n".join([page.get_text() for page in doc]) + except: return "" + +def extract_text_from_excel(path: str) -> str: + """Estrae testo da Excel convertendo i fogli in Markdown""" + try: + xl = pd.read_excel(path, sheet_name=None) + text_content = [] + for sheet, df in xl.items(): + text_content.append(f"\n--- Foglio Excel: {sheet} ---\n") + # Pulisce NaN e converte in stringa + clean_df = df.fillna("").astype(str) + text_content.append(clean_df.to_markdown(index=False)) + return "\n".join(text_content) + except Exception as e: + print(f"โŒ Errore Excel: {e}") 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) - ) - +# === AI & EMBEDDINGS (Remoto) === 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 with httpx.AsyncClient(timeout=30.0) as client: + resp = await client.post( + f"{BGE_API_URL}/embed", + json={"texts": [text], "normalize": True} + ) + if resp.status_code == 200: + data = resp.json() + # Gestisce sia il vecchio formato (lista diretta) che il nuovo (dict) + if isinstance(data, list): return data[0] # Vecchia API + if "dense" in data: return data["dense"][0] # Nuova API Hybrid + if "embeddings" in data: return data["embeddings"][0] # API precedente + except Exception as e: + print(f"โš ๏ธ Errore Embedding: {e}") + 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()} - )] +async def ensure_collection(name: str): + client = AsyncQdrantClient(url=QDRANT_URL) + if not await client.collection_exists(name): + # Creiamo una collezione ottimizzata + await client.create_collection( + collection_name=name, + vectors_config={ + "bge_dense": VectorParams(size=1024, distance=Distance.COSINE) + } + # Se in futuro abilitiamo lo sparse output dal .243, aggiungeremo: + # sparse_vectors_config={"bge_sparse": SparseVectorParams(index=SparseIndexParams(on_disk=False))} ) - return True - except: return False + +async def index_document(filename: str, content: str, collection: str) -> bool: + try: + await ensure_collection(collection) + chunks = [content[i:i+3000] for i in range(0, len(content), 3000)] + + qdrant = AsyncQdrantClient(url=QDRANT_URL) + points = [] + + for i, chunk in enumerate(chunks): + # Ottieni embedding (assume che get_embeddings ritorni la lista float) + # Nota: Se hai aggiornato l'API .243 per ritornare un dict {"dense": ...}, + # devi aggiornare get_embeddings per estrarre ["dense"]! + + # Vedere funzione get_embeddings aggiornata sotto + emb = await get_embeddings(chunk) + + if emb: + points.append(PointStruct( + id=str(uuid.uuid4()), + # Vettori nominati + vector={"bge_dense": emb}, + payload={"file_name": filename, "content": chunk, "chunk_id": i} + )) + + if points: + await qdrant.upsert(collection_name=collection, points=points) + return True + except Exception as e: + print(f"Index Error: {e}") + return False async def search_qdrant(query: str, collection: str) -> str: try: - client = await get_qdrant_client() + client = AsyncQdrantClient(url=QDRANT_URL) 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]) + + # Ricerca mirata sul vettore BGE + res = await client.query_points( + collection_name=collection, + query=emb, + using="bge_dense", # Specifica quale indice usare + limit=5 + ) + return "\n\n".join([f"๐Ÿ“„ {hit.payload['file_name']}:\n{hit.payload['content']}" for hit in res.points if hit.payload]) except: return "" -# === CHAINLIT HANDLERS === - +# === CHAT LOGIC === @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) + email = "guest@local" + profile = get_user_profile(email) else: - user_email = user.identifier - # I metadati sono giร  popolati dalla callback oauth - profile = USER_PROFILES.get(user_email, get_user_profile("guest")) + email = user.identifier + profile = USER_PROFILES.get(email, get_user_profile("guest")) - # Salva in sessione - cl.user_session.set("email", user_email) + cl.user_session.set("email", email) cl.user_session.set("role", profile["role"]) cl.user_session.set("workspace", profile["workspace"]) cl.user_session.set("rag_collection", profile["rag_collection"]) @@ -254,91 +253,65 @@ async def on_chat_start(): 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, - ), + settings = [ + cl.input_widget.Select(id="model", label="Modello", values=["glm-4.6:cloud", "llama3"], initial_value="glm-4.6:cloud"), + cl.input_widget.Slider(id="temp", label="Temperatura", initial=0.5, min=0, max=1, step=0.1) ] if profile["role"] == "admin": - settings_widgets.append(cl.input_widget.Switch(id="rag_enabled", label="Abilita RAG", initial=True)) + settings.append(cl.input_widget.Switch(id="rag", label="RAG Attivo", 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() + await cl.ChatSettings(settings).send() + await cl.Message(content=f"๐Ÿ‘‹ Ciao **{profile['name']}**! Pronto per l'automazione.").send() @cl.on_settings_update -async def on_settings_update(settings): - cl.user_session.set("settings", settings) - await cl.Message(content="โœ… Impostazioni aggiornate").send() +async def on_settings_update(s): cl.user_session.set("settings", s) @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") - + role = cl.user_session.get("role") 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 + + # 1. FILE UPLOAD (PDF & EXCEL) 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() + for el in message.elements: + dest = os.path.join(WORKSPACES_DIR, workspace, el.name) + shutil.copy(el.path, dest) + + content = "" + if el.name.endswith(".pdf"): + content = extract_text_from_pdf(dest) + elif el.name.endswith((".xlsx", ".xls")): + await cl.Message(content=f"๐Ÿ“Š Analisi Excel **{el.name}**...").send() + content = extract_text_from_excel(dest) + + if content: + ok = await index_document(el.name, content, rag_collection) + icon = "โœ…" if ok else "โŒ" + await cl.Message(content=f"{icon} **{el.name}** elaborato.").send() - # 2. RAG - context = "" - if rag_enabled: - context = await search_qdrant(message.content, rag_collection) + # 2. RAG & GENERATION + rag_active = settings.get("rag", True) if role == "admin" else True + context = await search_qdrant(message.content, rag_collection) if rag_active else "" - system_prompt = "Sei un assistente esperto." - if context: system_prompt += f"\n\nCONTESTO:\n{context}" + prompt = "Sei un esperto di automazione industriale." + if context: prompt += f"\n\nUSA QUESTO CONTESTO (Manuali/Excel):\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 - ) + try: + client = ollama.AsyncClient(host=OLLAMA_URL) + stream = await client.chat( + model=settings.get("model", "glm-4.6:cloud"), + messages=[{"role": "system", "content": prompt}, {"role": "user", "content": message.content}], + options={"temperature": settings.get("temp", 0.5)}, + stream=True + ) + async for chunk in stream: + await msg.stream_token(chunk['message']['content']) + except Exception as e: + await msg.stream_token(f"Errore connessione AI: {e}") - 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() \ No newline at end of file + await msg.update() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0f574f8c..e363a569 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,8 @@ aiofiles>=23.0.0 sniffio aiohttp boto3>=1.28.0 -azure-storage-file-datalake>=12.14.0 \ No newline at end of file +azure-storage-file-datalake>=12.14.0 +# NUOVI PER EXCEL +pandas +openpyxl +tabulate