feat(rag): adatta pipeline allo schema chunks AST-based + ottimizza system prompt
config.py: - EMBED_MODEL: qwen3-embedding:0.6b → bge-m3 (multilingua, migliore su testi accademici) - SYSTEM_PROMPT: lingua esplicita, anti-allucinazione rafforzata, citazione strutturata con percorso sezione, passaggi numerati per spiegazioni, fallback al plurale ingestion/ingest.py: - embed su content_for_embedding (prefisso header contestuale) - store content_original in ChromaDB (testo pulito per retrieval) - metadata aggiornati: header_path, chunk_index, content_type, flags, start/end_line rag.py, retrieve.py: - sostituisce sezione/titolo (schema vecchio) con header_path (schema AST) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ NO_THINK = True
|
|||||||
# Modello di embedding usato da Ollama.
|
# Modello di embedding usato da Ollama.
|
||||||
# Deve corrispondere al modello usato durante la vettorizzazione (ingest.py).
|
# Deve corrispondere al modello usato durante la vettorizzazione (ingest.py).
|
||||||
# Se cambi questo, devi rieseguire ingest.py con --force.
|
# 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.
|
# Caratteri massimi inviati al modello di embedding.
|
||||||
# Il testo viene troncato SOLO per il vettore; il documento completo
|
# 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.
|
# Mantieni le risposte ancorate al contesto per evitare allucinazioni.
|
||||||
SYSTEM_PROMPT = (
|
SYSTEM_PROMPT = (
|
||||||
"Sei un assistente specializzato nell'analisi di documenti accademici. "
|
"Sei un assistente specializzato nell'analisi di documenti accademici. "
|
||||||
"Rispondi alla domanda basandoti esclusivamente sul contesto fornito. "
|
"Rispondi sempre in italiano. "
|
||||||
"Sii preciso e conciso; cita la sezione di riferimento quando è utile. "
|
"Basati esclusivamente sui contesti numerati forniti: non aggiungere nozioni esterne, "
|
||||||
"Se il contesto non contiene informazioni sufficienti, rispondi: "
|
"non inventare informazioni non presenti nel testo. "
|
||||||
"\"Non trovo questa informazione nel documento.\""
|
"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.\""
|
||||||
)
|
)
|
||||||
|
|||||||
+15
-5
@@ -139,18 +139,28 @@ def _ingest_stem(stem: str, collection: chromadb.Collection,
|
|||||||
|
|
||||||
for i, chunk in enumerate(chunks, start=1):
|
for i, chunk in enumerate(chunks, start=1):
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
vector = embed(chunk["text"], model)
|
vector = embed(chunk["content_for_embedding"], model)
|
||||||
t1 = time.monotonic()
|
t1 = time.monotonic()
|
||||||
durations.append(t1 - t0)
|
durations.append(t1 - t0)
|
||||||
|
|
||||||
|
hp = chunk.get("header_path", [])
|
||||||
|
flags = chunk.get("flags", {})
|
||||||
|
|
||||||
ids.append(f"{stem}__{chunk['chunk_id']}")
|
ids.append(f"{stem}__{chunk['chunk_id']}")
|
||||||
embeddings.append(vector)
|
embeddings.append(vector)
|
||||||
documents.append(chunk["text"])
|
documents.append(chunk["content_original"])
|
||||||
metadatas.append({
|
metadatas.append({
|
||||||
"source": stem,
|
"source": stem,
|
||||||
"sezione": chunk.get("sezione", ""),
|
"chunk_index": chunk.get("chunk_index", i - 1),
|
||||||
"titolo": chunk.get("titolo", ""),
|
"content_type": chunk.get("content_type", ""),
|
||||||
"sub_index": chunk.get("sub_index", 0),
|
"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)
|
avg = sum(durations) / len(durations)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def call_ollama(prompt: str, system: str = "") -> str:
|
|||||||
def retrieve(collection: chromadb.Collection, question: str) -> list[dict]:
|
def retrieve(collection: chromadb.Collection, question: str) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Genera l'embedding della domanda e recupera i TOP_K chunk più simili.
|
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)
|
vector = embed(question)
|
||||||
results = collection.query(
|
results = collection.query(
|
||||||
@@ -102,8 +102,7 @@ def retrieve(collection: chromadb.Collection, question: str) -> list[dict]:
|
|||||||
chunks.append({
|
chunks.append({
|
||||||
"text": text,
|
"text": text,
|
||||||
"source": meta.get("source", ""),
|
"source": meta.get("source", ""),
|
||||||
"sezione": meta.get("sezione", ""),
|
"header_path": meta.get("header_path", ""),
|
||||||
"titolo": meta.get("titolo", ""),
|
|
||||||
"distance": dist,
|
"distance": dist,
|
||||||
})
|
})
|
||||||
return chunks
|
return chunks
|
||||||
@@ -116,10 +115,8 @@ def build_prompt(question: str, chunks: list[dict]) -> str:
|
|||||||
context_parts = []
|
context_parts = []
|
||||||
for i, c in enumerate(chunks, start=1):
|
for i, c in enumerate(chunks, start=1):
|
||||||
header = f"[Contesto {i}"
|
header = f"[Contesto {i}"
|
||||||
if c["sezione"]:
|
if c["header_path"]:
|
||||||
header += f" — {c['sezione']}"
|
header += f" — {c['header_path']}"
|
||||||
if c["titolo"]:
|
|
||||||
header += f" > {c['titolo']}"
|
|
||||||
header += "]"
|
header += "]"
|
||||||
context_parts.append(f"{header}\n{c['text']}")
|
context_parts.append(f"{header}\n{c['text']}")
|
||||||
|
|
||||||
@@ -140,11 +137,9 @@ def answer(question: str, collection: chromadb.Collection, verbose: bool) -> Non
|
|||||||
if verbose:
|
if verbose:
|
||||||
print("\n── Chunk recuperati ──────────────────────────────────────────")
|
print("\n── Chunk recuperati ──────────────────────────────────────────")
|
||||||
for i, c in enumerate(chunks, start=1):
|
for i, c in enumerate(chunks, start=1):
|
||||||
loc = c["sezione"]
|
|
||||||
if c["titolo"]:
|
|
||||||
loc += f" > {c['titolo']}"
|
|
||||||
sim = 1 - c["distance"]
|
sim = 1 - c["distance"]
|
||||||
src = f"[{c['source']}] " if c.get("source") else ""
|
src = f"[{c['source']}] " if c.get("source") else ""
|
||||||
|
loc = c["header_path"] or ""
|
||||||
print(f" [{i}] {src}{loc} (similarità: {sim:.3f})")
|
print(f" [{i}] {src}{loc} (similarità: {sim:.3f})")
|
||||||
print(f" {c['text'][:120].replace(chr(10), ' ')}...")
|
print(f" {c['text'][:120].replace(chr(10), ' ')}...")
|
||||||
print("──────────────────────────────────────────────────────────────\n")
|
print("──────────────────────────────────────────────────────────────\n")
|
||||||
|
|||||||
+3
-6
@@ -65,7 +65,7 @@ def embed(text: str) -> list[float]:
|
|||||||
def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[dict]:
|
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.
|
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)
|
vector = embed(query)
|
||||||
results = collection.query(
|
results = collection.query(
|
||||||
@@ -86,8 +86,7 @@ def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[di
|
|||||||
"rank": rank,
|
"rank": rank,
|
||||||
"similarity": round(1 - dist, 4),
|
"similarity": round(1 - dist, 4),
|
||||||
"source": meta.get("source", ""),
|
"source": meta.get("source", ""),
|
||||||
"sezione": meta.get("sezione", ""),
|
"header_path": meta.get("header_path", ""),
|
||||||
"titolo": meta.get("titolo", ""),
|
|
||||||
"text": text,
|
"text": text,
|
||||||
})
|
})
|
||||||
return chunks
|
return chunks
|
||||||
@@ -99,9 +98,7 @@ def print_results(chunks: list[dict], full: bool = False) -> None:
|
|||||||
print(f"── {len(chunks)} chunk recuperati ─────────────────────────────────\n")
|
print(f"── {len(chunks)} chunk recuperati ─────────────────────────────────\n")
|
||||||
for c in chunks:
|
for c in chunks:
|
||||||
src = f"[{c['source']}] " if c.get("source") else ""
|
src = f"[{c['source']}] " if c.get("source") else ""
|
||||||
loc = c["sezione"]
|
loc = c["header_path"] or ""
|
||||||
if c["titolo"]:
|
|
||||||
loc += f" > {c['titolo']}"
|
|
||||||
print(f" [{c['rank']}] similarità: {c['similarity']:.4f} | {src}{loc}")
|
print(f" [{c['rank']}] similarità: {c['similarity']:.4f} | {src}{loc}")
|
||||||
if full:
|
if full:
|
||||||
print()
|
print()
|
||||||
|
|||||||
Reference in New Issue
Block a user