diff --git a/config.py b/config.py index 4061879..02b918a 100644 --- a/config.py +++ b/config.py @@ -24,7 +24,7 @@ NO_THINK = True # Modello di embedding usato da Ollama. # Deve corrispondere al modello usato durante la vettorizzazione (ingest.py). # Se cambi questo, devi rieseguire ingest.py con --force. -EMBED_MODEL = "qwen3-embedding:0.6b" +EMBED_MODEL = "bge-m3" # Caratteri massimi inviati al modello di embedding. # Il testo viene troncato SOLO per il vettore; il documento completo @@ -45,8 +45,12 @@ OLLAMA_MODEL = "qwen3.5:4b" # Mantieni le risposte ancorate al contesto per evitare allucinazioni. SYSTEM_PROMPT = ( "Sei un assistente specializzato nell'analisi di documenti accademici. " - "Rispondi alla domanda basandoti esclusivamente sul contesto fornito. " - "Sii preciso e conciso; cita la sezione di riferimento quando è utile. " - "Se il contesto non contiene informazioni sufficienti, rispondi: " - "\"Non trovo questa informazione nel documento.\"" + "Rispondi sempre in italiano. " + "Basati esclusivamente sui contesti numerati forniti: non aggiungere nozioni esterne, " + "non inventare informazioni non presenti nel testo. " + "Quando citi un risultato indica la sezione tra parentesi quadre usando il percorso " + "fornito (es. [Sezione > Sottosezione]). " + "Per spiegazioni strutturate usa passaggi numerati. " + "Se il contesto non contiene informazioni sufficienti rispondi esattamente: " + "\"Non trovo questa informazione nei documenti forniti.\"" ) diff --git a/ingestion/ingest.py b/ingestion/ingest.py index c9f90b6..8bd1119 100644 --- a/ingestion/ingest.py +++ b/ingestion/ingest.py @@ -139,18 +139,28 @@ def _ingest_stem(stem: str, collection: chromadb.Collection, for i, chunk in enumerate(chunks, start=1): t0 = time.monotonic() - vector = embed(chunk["text"], model) + vector = embed(chunk["content_for_embedding"], model) t1 = time.monotonic() durations.append(t1 - t0) + hp = chunk.get("header_path", []) + flags = chunk.get("flags", {}) + ids.append(f"{stem}__{chunk['chunk_id']}") embeddings.append(vector) - documents.append(chunk["text"]) + documents.append(chunk["content_original"]) metadatas.append({ - "source": stem, - "sezione": chunk.get("sezione", ""), - "titolo": chunk.get("titolo", ""), - "sub_index": chunk.get("sub_index", 0), + "source": stem, + "chunk_index": chunk.get("chunk_index", i - 1), + "content_type": chunk.get("content_type", ""), + "header_path": " > ".join(h["text"] for h in hp), + "start_line": chunk.get("start_line", 0), + "end_line": chunk.get("end_line", 0), + "chars": chunk.get("chars", 0), + "has_code": flags.get("has_code", False), + "has_table": flags.get("has_table", False), + "has_math": flags.get("has_math", False), + "is_overflow": flags.get("is_overflow", False), }) avg = sum(durations) / len(durations) diff --git a/rag.py b/rag.py index 992aa54..4bc4e36 100644 --- a/rag.py +++ b/rag.py @@ -85,7 +85,7 @@ def call_ollama(prompt: str, system: str = "") -> str: def retrieve(collection: chromadb.Collection, question: str) -> list[dict]: """ Genera l'embedding della domanda e recupera i TOP_K chunk più simili. - Ritorna lista di dict con chiavi: text, sezione, titolo, distance. + Ritorna lista di dict con chiavi: text, source, header_path, distance. """ vector = embed(question) results = collection.query( @@ -100,11 +100,10 @@ def retrieve(collection: chromadb.Collection, question: str) -> list[dict]: results["distances"][0], ): chunks.append({ - "text": text, - "source": meta.get("source", ""), - "sezione": meta.get("sezione", ""), - "titolo": meta.get("titolo", ""), - "distance": dist, + "text": text, + "source": meta.get("source", ""), + "header_path": meta.get("header_path", ""), + "distance": dist, }) return chunks @@ -116,10 +115,8 @@ def build_prompt(question: str, chunks: list[dict]) -> str: context_parts = [] for i, c in enumerate(chunks, start=1): header = f"[Contesto {i}" - if c["sezione"]: - header += f" — {c['sezione']}" - if c["titolo"]: - header += f" > {c['titolo']}" + if c["header_path"]: + header += f" — {c['header_path']}" header += "]" context_parts.append(f"{header}\n{c['text']}") @@ -140,11 +137,9 @@ def answer(question: str, collection: chromadb.Collection, verbose: bool) -> Non if verbose: print("\n── Chunk recuperati ──────────────────────────────────────────") for i, c in enumerate(chunks, start=1): - loc = c["sezione"] - if c["titolo"]: - loc += f" > {c['titolo']}" sim = 1 - c["distance"] src = f"[{c['source']}] " if c.get("source") else "" + loc = c["header_path"] or "" print(f" [{i}] {src}{loc} (similarità: {sim:.3f})") print(f" {c['text'][:120].replace(chr(10), ' ')}...") print("──────────────────────────────────────────────────────────────\n") diff --git a/retrieve.py b/retrieve.py index 02a3868..a2d92ee 100644 --- a/retrieve.py +++ b/retrieve.py @@ -65,7 +65,7 @@ def embed(text: str) -> list[float]: def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[dict]: """ Genera l'embedding della query e recupera i top_k chunk più simili. - Ritorna lista di dict con chiavi: rank, similarity, sezione, titolo, text. + Ritorna lista di dict con chiavi: rank, similarity, source, header_path, text. """ vector = embed(query) results = collection.query( @@ -83,12 +83,11 @@ def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[di start=1, ): chunks.append({ - "rank": rank, - "similarity": round(1 - dist, 4), - "source": meta.get("source", ""), - "sezione": meta.get("sezione", ""), - "titolo": meta.get("titolo", ""), - "text": text, + "rank": rank, + "similarity": round(1 - dist, 4), + "source": meta.get("source", ""), + "header_path": meta.get("header_path", ""), + "text": text, }) return chunks @@ -99,9 +98,7 @@ def print_results(chunks: list[dict], full: bool = False) -> None: print(f"── {len(chunks)} chunk recuperati ─────────────────────────────────\n") for c in chunks: src = f"[{c['source']}] " if c.get("source") else "" - loc = c["sezione"] - if c["titolo"]: - loc += f" > {c['titolo']}" + loc = c["header_path"] or "" print(f" [{c['rank']}] similarità: {c['similarity']:.4f} | {src}{loc}") if full: print()