feat(step-9): aggiungi retrieve.py per retrieval puro senza LLM
Nuovo script interattivo che vettorizza la query e restituisce i chunk più simili da ChromaDB senza chiamare Ollama per la generation. Utile per debug del retrieval e verifica della qualità dei chunk. Aggiornato README con rag.py e retrieve.py come opzioni alla pari.
This commit is contained in:
+51
-27
@@ -1,43 +1,30 @@
|
|||||||
# Step 9 — Pipeline RAG
|
# Step 9 — Interrogazione del documento
|
||||||
|
|
||||||
Loop interattivo che risponde a domande in linguaggio naturale sul documento indicato.
|
Due modalità di interrogazione, entrambe con loop interattivo:
|
||||||
Per ogni domanda: vettorizza la query, recupera i chunk più rilevanti da ChromaDB e genera la risposta tramite Ollama.
|
|
||||||
|
| Script | Modalità | Quando usarlo |
|
||||||
|
|---|---|---|
|
||||||
|
| `rag.py` | Retrieval + generazione LLM | Risposta in linguaggio naturale |
|
||||||
|
| `retrieve.py` | Solo retrieval (no LLM) | Debug, verifica chunk, ricerca semantica |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisiti
|
## Prerequisiti
|
||||||
|
|
||||||
- Step 8 completato (`chroma_db/` popolata)
|
- Step 8 completato (`chroma_db/` popolata)
|
||||||
- Ollama attivo con il modello di embedding e il modello LLM scaricati
|
- Ollama attivo con il modello di embedding scaricato
|
||||||
|
- Per `rag.py`: anche il modello LLM scaricato
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Avvio
|
## rag.py — Risposta in linguaggio naturale
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
python step-9/rag.py --stem <nome>
|
python step-9/rag.py --stem <nome>
|
||||||
```
|
```
|
||||||
|
|
||||||
`--stem` è l'unico argomento CLI. Tutti gli altri parametri si configurano in `config.py`.
|
Per ogni domanda: vettorizza la query, recupera i chunk più rilevanti da ChromaDB e genera la risposta tramite Ollama.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configurazione (`config.py`)
|
|
||||||
|
|
||||||
| Parametro | Default | Descrizione |
|
|
||||||
|---|---|---|
|
|
||||||
| `TOP_K` | `6` | Chunk recuperati per ogni domanda. Valori consigliati: `3`–`10` |
|
|
||||||
| `TEMPERATURE` | `0.0` | Deterministico a `0.0`, creativo verso `1.0`. Per RAG consigliato `0.0` |
|
|
||||||
| `NO_THINK` | `True` | Disabilita il chain-of-thought interno dei modelli Qwen3/Qwen3.5. `True` = risposta diretta, più veloce |
|
|
||||||
| `EMBED_MODEL` | `"nomic-embed-text"` | Deve corrispondere al modello usato in step-8. Se cambiato, rieseguire step-8 con `--force` |
|
|
||||||
| `OLLAMA_URL` | `"http://localhost:11434"` | Modifica solo se Ollama gira su porta o host diversi |
|
|
||||||
| `OLLAMA_MODEL` | `"qwen3.5:0.8b"` | Modello LLM. Vedi `step-7/README.md` per la scelta |
|
|
||||||
| `SYSTEM_PROMPT` | *(vedi file)* | Istruzioni di comportamento inviate al LLM. Modifica per cambiare tono, lingua o condizione di fallback |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Loop interattivo
|
|
||||||
|
|
||||||
```
|
```
|
||||||
── Loop RAG ─────────────────────────────────────── (exit per uscire)
|
── Loop RAG ─────────────────────────────────────── (exit per uscire)
|
||||||
@@ -51,9 +38,7 @@ Domanda:
|
|||||||
| `<testo> -v` | Risposta + chunk recuperati con score di similarità |
|
| `<testo> -v` | Risposta + chunk recuperati con score di similarità |
|
||||||
| `exit` | Esce dal programma |
|
| `exit` | Esce dal programma |
|
||||||
|
|
||||||
---
|
Flusso interno:
|
||||||
|
|
||||||
## Flusso interno
|
|
||||||
|
|
||||||
```
|
```
|
||||||
domanda
|
domanda
|
||||||
@@ -74,6 +59,45 @@ Il LLM risponde esclusivamente dal contesto fornito. Se il contesto è irrilevan
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## retrieve.py — Retrieval puro (senza LLM)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
python step-9/retrieve.py --stem <nome>
|
||||||
|
```
|
||||||
|
|
||||||
|
Vettorizza la query e restituisce i chunk più simili con score di similarità — senza chiamare Ollama per la generation. Utile per verificare la qualità del retrieval e diagnosticare risposte sbagliate.
|
||||||
|
|
||||||
|
```
|
||||||
|
── Loop retrieval ──────────────────────── (exit per uscire, -f per testo completo)
|
||||||
|
|
||||||
|
Query:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Sintassi | Comportamento |
|
||||||
|
|---|---|
|
||||||
|
| `<testo>` | Chunk più simili con score di similarità (testo troncato a 200 car.) |
|
||||||
|
| `<testo> -f` | Chunk più simili con testo completo |
|
||||||
|
| `exit` | Esce dal programma |
|
||||||
|
|
||||||
|
Accetta `--top-k N` per sovrascrivere il valore di `config.py` per quella sessione.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configurazione (`config.py`)
|
||||||
|
|
||||||
|
| Parametro | Default | Descrizione |
|
||||||
|
|---|---|---|
|
||||||
|
| `TOP_K` | `6` | Chunk recuperati per ogni domanda. Valori consigliati: `3`–`10` |
|
||||||
|
| `TEMPERATURE` | `0.0` | Deterministico a `0.0`, creativo verso `1.0`. Per RAG consigliato `0.0` |
|
||||||
|
| `NO_THINK` | `True` | Disabilita il chain-of-thought interno dei modelli Qwen3/Qwen3.5. `True` = risposta diretta, più veloce |
|
||||||
|
| `EMBED_MODEL` | `"nomic-embed-text"` | Deve corrispondere al modello usato in step-8. Se cambiato, rieseguire step-8 con `--force` |
|
||||||
|
| `OLLAMA_URL` | `"http://localhost:11434"` | Modifica solo se Ollama gira su porta o host diversi |
|
||||||
|
| `OLLAMA_MODEL` | `"qwen3.5:0.8b"` | Modello LLM. Vedi `step-7/README.md` per la scelta |
|
||||||
|
| `SYSTEM_PROMPT` | *(vedi file)* | Istruzioni di comportamento inviate al LLM. Modifica per cambiare tono, lingua o condizione di fallback |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test senza RAG
|
## Test senza RAG
|
||||||
|
|
||||||
Per verificare che Ollama risponda correttamente prima di interrogare il documento:
|
Per verificare che Ollama risponda correttamente prima di interrogare il documento:
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Step 9 — Retrieval puro (senza generazione LLM)
|
||||||
|
|
||||||
|
Loop interattivo: inserisci una query, ottieni i chunk più simili dalla
|
||||||
|
collection ChromaDB tramite embedding semantico — senza chiamare Ollama
|
||||||
|
per la generation.
|
||||||
|
|
||||||
|
Utile per:
|
||||||
|
- verificare la qualità del retrieval prima di diagnosticare risposte sbagliate
|
||||||
|
- controllare che i chunk giusti vengano recuperati per una query
|
||||||
|
- usare la pipeline come motore di ricerca semantica
|
||||||
|
|
||||||
|
Input: chroma_db/<stem> (collection ChromaDB)
|
||||||
|
Output: lista chunk con score di similarità
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python step-9/retrieve.py --stem <nome>
|
||||||
|
|
||||||
|
Nel loop interattivo:
|
||||||
|
Query: <testo> → chunk più simili con score
|
||||||
|
Query: <testo> -f → testo completo dei chunk
|
||||||
|
Query: exit → uscita
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import chromadb
|
||||||
|
|
||||||
|
# ─── Configurazione ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
import config as _cfg
|
||||||
|
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
CHROMA_DIR = project_root / "chroma_db"
|
||||||
|
|
||||||
|
OLLAMA_URL = _cfg.OLLAMA_URL
|
||||||
|
EMBED_MODEL = _cfg.EMBED_MODEL
|
||||||
|
TOP_K = _cfg.TOP_K
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Embedding ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def embed(text: str) -> list[float]:
|
||||||
|
"""Genera il vettore della query tramite Ollama."""
|
||||||
|
payload = json.dumps({"model": EMBED_MODEL, "prompt": text}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{OLLAMA_URL}/api/embeddings",
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
return json.loads(resp.read())["embedding"]
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Retrieval ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
vector = embed(query)
|
||||||
|
results = collection.query(
|
||||||
|
query_embeddings=[vector],
|
||||||
|
n_results=top_k,
|
||||||
|
include=["documents", "metadatas", "distances"],
|
||||||
|
)
|
||||||
|
chunks = []
|
||||||
|
for rank, (text, meta, dist) in enumerate(
|
||||||
|
zip(
|
||||||
|
results["documents"][0],
|
||||||
|
results["metadatas"][0],
|
||||||
|
results["distances"][0],
|
||||||
|
),
|
||||||
|
start=1,
|
||||||
|
):
|
||||||
|
chunks.append({
|
||||||
|
"rank": rank,
|
||||||
|
"similarity": round(1 - dist, 4),
|
||||||
|
"sezione": meta.get("sezione", ""),
|
||||||
|
"titolo": meta.get("titolo", ""),
|
||||||
|
"text": text,
|
||||||
|
})
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Output ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def print_results(chunks: list[dict], full: bool = False) -> None:
|
||||||
|
print(f"── {len(chunks)} chunk recuperati ─────────────────────────────────\n")
|
||||||
|
for c in chunks:
|
||||||
|
loc = c["sezione"]
|
||||||
|
if c["titolo"]:
|
||||||
|
loc += f" > {c['titolo']}"
|
||||||
|
print(f" [{c['rank']}] similarità: {c['similarity']:.4f} | {loc}")
|
||||||
|
if full:
|
||||||
|
print()
|
||||||
|
print(c["text"])
|
||||||
|
else:
|
||||||
|
print(f" {c['text'][:200].replace(chr(10), ' ')}")
|
||||||
|
if len(c["text"]) > 200:
|
||||||
|
print(f" … ({len(c['text'])} caratteri totali)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Loop interattivo ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_loop(collection: chromadb.Collection, top_k: int) -> None:
|
||||||
|
print("── Loop retrieval ──────────────────────── (exit per uscire, -f per testo completo)\n")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
raw = input("Query: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nUscita.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
if raw.lower() == "exit":
|
||||||
|
break
|
||||||
|
|
||||||
|
full = raw.endswith(" -f")
|
||||||
|
query = raw[:-3].strip() if full else raw
|
||||||
|
|
||||||
|
try:
|
||||||
|
chunks = retrieve(collection, query, top_k)
|
||||||
|
except (urllib.error.URLError, OSError) as e:
|
||||||
|
print(f"❌ Errore embedding (Ollama raggiungibile?): {e}\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print()
|
||||||
|
print_results(chunks, full=full)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Entry point ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build_epilog() -> str:
|
||||||
|
lines = [
|
||||||
|
"Uso:",
|
||||||
|
" python step-9/retrieve.py --stem <nome>",
|
||||||
|
"",
|
||||||
|
"Nel loop interattivo:",
|
||||||
|
" <query> chunk più simili con score (testo troncato)",
|
||||||
|
" <query> -f testo completo dei chunk",
|
||||||
|
" exit termina",
|
||||||
|
]
|
||||||
|
if CHROMA_DIR.exists():
|
||||||
|
try:
|
||||||
|
client = chromadb.PersistentClient(path=str(CHROMA_DIR))
|
||||||
|
names = [c.name for c in client.list_collections()]
|
||||||
|
if names:
|
||||||
|
lines += ["", f"Collection disponibili: {', '.join(names)}"]
|
||||||
|
else:
|
||||||
|
lines += ["", "Nessuna collection trovata — eseguire prima: python step-8/ingest.py"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Step 9 — Retrieval puro (senza LLM)\n\n"
|
||||||
|
"Loop interattivo: inserisci una query e ottieni i chunk più simili\n"
|
||||||
|
"tramite embedding semantico, senza generazione LLM."
|
||||||
|
),
|
||||||
|
epilog=_build_epilog(),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stem",
|
||||||
|
required=True,
|
||||||
|
help="Nome della collection ChromaDB da interrogare.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--top-k",
|
||||||
|
type=int,
|
||||||
|
default=TOP_K,
|
||||||
|
metavar="N",
|
||||||
|
help=f"Numero di chunk da restituire per query (default: {TOP_K} da config.py).",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("─── Step 9 — Retrieval puro ──────────────────────────────────────────\n")
|
||||||
|
print(f" Documento : {args.stem}")
|
||||||
|
print(f" Embed model : {EMBED_MODEL}")
|
||||||
|
print(f" Top-K : {args.top_k}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not CHROMA_DIR.exists():
|
||||||
|
print("❌ chroma_db/ non trovata — esegui prima step-8", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
client = chromadb.PersistentClient(path=str(CHROMA_DIR))
|
||||||
|
collections = [c.name for c in client.list_collections()]
|
||||||
|
if args.stem not in collections:
|
||||||
|
print(f"❌ Collection '{args.stem}' non trovata in chroma_db/", file=sys.stderr)
|
||||||
|
print(f" → python step-8/ingest.py --stem {args.stem}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
collection = client.get_collection(args.stem)
|
||||||
|
print(f"✅ Collection '{args.stem}' caricata ({collection.count()} chunk)\n")
|
||||||
|
|
||||||
|
run_loop(collection, args.top_k)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user