refactor: elimina step-7 e step-9, consolida script alla root
- step-9/: config.py, rag.py, retrieve.py → root; test_ollama.py → ollama/ - step-7/: eliminata, già coperta da ollama/ - sys.path aggiornati in rag.py, retrieve.py, ingest.py, check_env.py (step-7 e ollama) - Riferimenti step-9/config.py → config.py in tutti i file
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
# ─── Configurazione RAG ───────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Modifica questo file per cambiare i parametri della pipeline.
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# python rag.py --stem nietzsche
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── Retrieval ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Numero di chunk da recuperare per ogni domanda.
|
||||||
|
# Valori più alti = più contesto, risposte potenzialmente più complete,
|
||||||
|
# ma prompt più lunghi e generazione più lenta.
|
||||||
|
TOP_K = 6
|
||||||
|
|
||||||
|
# ── Generazione ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Temperatura del modello LLM.
|
||||||
|
# 0.0 = completamente deterministico (stessa risposta ad ogni run)
|
||||||
|
# 0.7 = più creativo e vario
|
||||||
|
TEMPERATURE = 0.0
|
||||||
|
|
||||||
|
# Disabilita il "thinking" (ragionamento interno) nei modelli Qwen3/Qwen3.5.
|
||||||
|
# True = risposta diretta, più veloce
|
||||||
|
# False = ragionamento interno abilitato (più lento ma potenzialmente più accurato)
|
||||||
|
NO_THINK = True
|
||||||
|
|
||||||
|
# ── Embedding ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Modello di embedding usato da Ollama.
|
||||||
|
# Deve corrispondere al modello usato durante la vettorizzazione (step-8).
|
||||||
|
# Se cambi questo, devi rieseguire step-8 con --force.
|
||||||
|
EMBED_MODEL = "nomic-embed-text"
|
||||||
|
|
||||||
|
# ── Ollama ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# URL del server Ollama (default: locale sulla porta 11434).
|
||||||
|
OLLAMA_URL = "http://localhost:11434"
|
||||||
|
|
||||||
|
# Modello LLM. Scegli in base alla RAM disponibile (vedi README).
|
||||||
|
OLLAMA_MODEL = "qwen3.5:0.8b"
|
||||||
|
|
||||||
|
# ── Prompt di sistema ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Istruzioni di comportamento inviate al LLM prima del contesto e della domanda.
|
||||||
|
# Modifica per cambiare il tono, la lingua, il grado di libertà interpretativa
|
||||||
|
# o le condizioni di fallback ("non so rispondere").
|
||||||
|
SYSTEM_PROMPT = (
|
||||||
|
"Sei un assistente che risponde usando il contesto fornito. "
|
||||||
|
"Sintetizza e interpreta liberamente i passaggi del contesto per rispondere alla domanda. "
|
||||||
|
"Se il contesto contiene informazioni pertinenti, anche indirette, usale per costruire una risposta. "
|
||||||
|
"Solo se il contesto è completamente irrilevante, rispondi: "
|
||||||
|
"\"Non trovo questa informazione nel documento.\""
|
||||||
|
)
|
||||||
+2
-2
@@ -57,7 +57,7 @@ Alternative supportate:
|
|||||||
- `bge-m3`
|
- `bge-m3`
|
||||||
- `nomic-embed-text`
|
- `nomic-embed-text`
|
||||||
|
|
||||||
Se cambi embedding model rispetto a quello usato in step-8, riesegui ingest con `--force` e aggiorna `EMBED_MODEL` in `step-9/config.py`.
|
Se cambi embedding model rispetto a quello usato in step-8, riesegui ingest con `--force` e aggiorna `EMBED_MODEL` in `config.py`.
|
||||||
|
|
||||||
### Modello LLM (consigliato per 8 GB RAM)
|
### Modello LLM (consigliato per 8 GB RAM)
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ Se cambi embedding model rispetto a quello usato in step-8, riesegui ingest con
|
|||||||
ollama pull qwen3.5:4b
|
ollama pull qwen3.5:4b
|
||||||
```
|
```
|
||||||
|
|
||||||
Se usi un modello diverso, aggiorna `OLLAMA_MODEL` in `step-9/config.py`.
|
Se usi un modello diverso, aggiorna `OLLAMA_MODEL` in `config.py`.
|
||||||
|
|
||||||
### Disinstalla un modello
|
### Disinstalla un modello
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -22,7 +22,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
# ─── Lista canonica di modelli embedding supportati ───────────────────────────
|
# ─── Lista canonica di modelli embedding supportati ───────────────────────────
|
||||||
# Ordine: prima scelta → ultima scelta (come da README step-7)
|
# Ordine: prima scelta → ultima scelta (come da ollama/README.md)
|
||||||
EMBED_MODELS = [
|
EMBED_MODELS = [
|
||||||
"qwen3-embedding",
|
"qwen3-embedding",
|
||||||
"nomic-embed-text-v2-moe",
|
"nomic-embed-text-v2-moe",
|
||||||
@@ -60,9 +60,7 @@ def _parse_ollama_models(raw_output: str) -> list[str]:
|
|||||||
return models
|
return models
|
||||||
|
|
||||||
|
|
||||||
# ─── Modelli configurati in config.py ─────────────────────────────────────────
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
# Per spostare config.py alla root: cambia solo la riga qui sotto.
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent / "step-9"))
|
|
||||||
try:
|
try:
|
||||||
from config import EMBED_MODEL as CONFIGURED_EMBED, OLLAMA_MODEL as CONFIGURED_LLM
|
from config import EMBED_MODEL as CONFIGURED_EMBED, OLLAMA_MODEL as CONFIGURED_LLM
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test chat locale Ollama — senza RAG, senza ChromaDB.
|
||||||
|
Uso: python ollama/test_ollama.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
import config as _cfg
|
||||||
|
|
||||||
|
OLLAMA_URL = _cfg.OLLAMA_URL
|
||||||
|
MODEL = _cfg.OLLAMA_MODEL
|
||||||
|
TEMPERATURE = _cfg.TEMPERATURE
|
||||||
|
NO_THINK = _cfg.NO_THINK
|
||||||
|
|
||||||
|
|
||||||
|
def chat(prompt: str) -> str:
|
||||||
|
payload = json.dumps({
|
||||||
|
"model": MODEL,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False,
|
||||||
|
"think": not NO_THINK,
|
||||||
|
"options": {"temperature": TEMPERATURE},
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{OLLAMA_URL}/api/generate",
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
||||||
|
return json.loads(resp.read())["response"].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print(f"─── Chat Ollama ──────────────────────────────── (exit per uscire)")
|
||||||
|
print(f" Modello : {MODEL}")
|
||||||
|
print(f" Thinking : {'off' if NO_THINK else 'on'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user = input("Tu: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nUscita.")
|
||||||
|
break
|
||||||
|
if not user:
|
||||||
|
continue
|
||||||
|
if user.lower() == "exit":
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
reply = chat(user)
|
||||||
|
print(f"\nAssistente: {reply}\n")
|
||||||
|
except (urllib.error.URLError, OSError) as e:
|
||||||
|
print(f"❌ Errore: {e}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Pipeline RAG interattiva
|
||||||
|
|
||||||
|
Riceve una domanda, recupera i chunk più rilevanti da ChromaDB (retrieval)
|
||||||
|
e genera una risposta tramite Ollama (generation).
|
||||||
|
|
||||||
|
Input: chroma_db/<stem> (collection ChromaDB)
|
||||||
|
Output: risposta a schermo
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python rag.py --stem <nome>
|
||||||
|
|
||||||
|
Nel loop interattivo:
|
||||||
|
Domanda: <testo> → risposta
|
||||||
|
Domanda: <testo> -v → risposta + chunk recuperati
|
||||||
|
Domanda: 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
|
||||||
|
CHROMA_DIR = project_root / "chroma_db"
|
||||||
|
|
||||||
|
OLLAMA_URL = _cfg.OLLAMA_URL
|
||||||
|
EMBED_MODEL = _cfg.EMBED_MODEL
|
||||||
|
LLM_MODEL = _cfg.OLLAMA_MODEL
|
||||||
|
TOP_K = _cfg.TOP_K
|
||||||
|
TEMPERATURE = _cfg.TEMPERATURE
|
||||||
|
NO_THINK = _cfg.NO_THINK
|
||||||
|
SYSTEM_PROMPT = _cfg.SYSTEM_PROMPT
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Embedding ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def embed(text: str) -> list[float]:
|
||||||
|
"""Genera il vettore della domanda 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"]
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Generazione ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def call_ollama(prompt: str, system: str = "") -> str:
|
||||||
|
"""Chiama Ollama /api/generate e ritorna la risposta."""
|
||||||
|
payload = json.dumps({
|
||||||
|
"model": LLM_MODEL,
|
||||||
|
"system": system,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False,
|
||||||
|
"think": not NO_THINK,
|
||||||
|
"options": {"temperature": TEMPERATURE},
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{OLLAMA_URL}/api/generate",
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
||||||
|
return json.loads(resp.read())["response"].strip()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Retrieval ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
vector = embed(question)
|
||||||
|
results = collection.query(
|
||||||
|
query_embeddings=[vector],
|
||||||
|
n_results=TOP_K,
|
||||||
|
include=["documents", "metadatas", "distances"],
|
||||||
|
)
|
||||||
|
chunks = []
|
||||||
|
for text, meta, dist in zip(
|
||||||
|
results["documents"][0],
|
||||||
|
results["metadatas"][0],
|
||||||
|
results["distances"][0],
|
||||||
|
):
|
||||||
|
chunks.append({
|
||||||
|
"text": text,
|
||||||
|
"sezione": meta.get("sezione", ""),
|
||||||
|
"titolo": meta.get("titolo", ""),
|
||||||
|
"distance": dist,
|
||||||
|
})
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Prompt ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def build_prompt(question: str, chunks: list[dict]) -> str:
|
||||||
|
"""Ritorna (system, user_prompt) separati per l'API Ollama."""
|
||||||
|
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']}"
|
||||||
|
header += "]"
|
||||||
|
context_parts.append(f"{header}\n{c['text']}")
|
||||||
|
|
||||||
|
context = "\n\n".join(context_parts)
|
||||||
|
user_prompt = f"{context}\n\nDomanda: {question}"
|
||||||
|
return SYSTEM_PROMPT, user_prompt
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Loop interattivo ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def answer(question: str, collection: chromadb.Collection, verbose: bool) -> None:
|
||||||
|
try:
|
||||||
|
chunks = retrieve(collection, question)
|
||||||
|
except (urllib.error.URLError, OSError) as e:
|
||||||
|
print(f"❌ Errore embedding: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
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"]
|
||||||
|
print(f" [{i}] {loc} (similarità: {sim:.3f})")
|
||||||
|
print(f" {c['text'][:120].replace(chr(10), ' ')}...")
|
||||||
|
print("──────────────────────────────────────────────────────────────\n")
|
||||||
|
|
||||||
|
system, prompt = build_prompt(question, chunks)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = call_ollama(prompt, system=system)
|
||||||
|
except (urllib.error.URLError, OSError) as e:
|
||||||
|
print(f"❌ Errore generazione: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{response}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def run_loop(collection: chromadb.Collection) -> None:
|
||||||
|
print("── Loop RAG ─────────────────────────────────────── (exit per uscire)\n")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
raw = input("Domanda: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nUscita.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
if raw.lower() == "exit":
|
||||||
|
break
|
||||||
|
|
||||||
|
verbose = raw.endswith(" -v")
|
||||||
|
question = raw[:-3].strip() if verbose else raw
|
||||||
|
|
||||||
|
answer(question, collection, verbose)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Entry point ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build_epilog() -> str:
|
||||||
|
lines = [
|
||||||
|
"Uso:",
|
||||||
|
" python rag.py --stem <nome>",
|
||||||
|
"",
|
||||||
|
"Loop interattivo:",
|
||||||
|
" <domanda> risposta basata sul documento",
|
||||||
|
" <domanda> -v risposta + chunk recuperati con score di similarità",
|
||||||
|
" 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=(
|
||||||
|
"Pipeline RAG interattiva\n\n"
|
||||||
|
"Risponde a domande in linguaggio naturale su un documento\n"
|
||||||
|
"indicizzato in ChromaDB da step-8/ingest.py."
|
||||||
|
),
|
||||||
|
epilog=_build_epilog(),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stem",
|
||||||
|
required=True,
|
||||||
|
help=(
|
||||||
|
"Nome della collection ChromaDB da interrogare. "
|
||||||
|
"Le collection vengono create da: python step-8/ingest.py --stem <nome>"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("─── Pipeline RAG ────────────────────────────────────────────\n")
|
||||||
|
print(f" Documento : {args.stem}")
|
||||||
|
print(f" Modello : {LLM_MODEL}")
|
||||||
|
print(f" Top-K : {TOP_K}")
|
||||||
|
print(f" Thinking : {'off' if NO_THINK else 'on'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not CHROMA_DIR.exists():
|
||||||
|
print("❌ chroma_db/ non trovata — esegui prima step-8")
|
||||||
|
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/")
|
||||||
|
print(f" → python step-8/ingest.py --stem {args.stem}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
collection = client.get_collection(args.stem)
|
||||||
|
print(f"✅ Collection '{args.stem}' caricata ({collection.count()} chunk)\n")
|
||||||
|
|
||||||
|
run_loop(collection)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
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 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
|
||||||
|
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 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=(
|
||||||
|
"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("─── 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())
|
||||||
+8
-8
@@ -15,14 +15,14 @@ salva in ChromaDB (vector store persistente su disco).
|
|||||||
|
|
||||||
## Configurazione modello
|
## Configurazione modello
|
||||||
|
|
||||||
Il modello di embedding viene letto da **`step-9/config.py`**:
|
Il modello di embedding viene letto da **`config.py`**:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# step-9/config.py
|
# config.py
|
||||||
EMBED_MODEL = "nomic-embed-text" # ← cambia qui
|
EMBED_MODEL = "nomic-embed-text" # ← cambia qui
|
||||||
```
|
```
|
||||||
|
|
||||||
> Il modello scelto qui deve corrispondere a quello usato in step-9.
|
> Il modello scelto qui deve corrispondere a quello usato in rag.py.
|
||||||
> Se lo cambi dopo aver già vettorizzato, devi rieseguire step-8 con `--force`.
|
> Se lo cambi dopo aver già vettorizzato, devi rieseguire step-8 con `--force`.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -54,7 +54,7 @@ distanza coseno. La directory è ignorata da git (generata automaticamente).
|
|||||||
|
|
||||||
## Modelli supportati
|
## Modelli supportati
|
||||||
|
|
||||||
Stessi modelli raccomandati nel [README di step-7](../step-7/README.md).
|
Stessi modelli raccomandati nel [README di ollama](../ollama/README.md).
|
||||||
Il modello deve essere scaricato in Ollama prima di eseguire questo script
|
Il modello deve essere scaricato in Ollama prima di eseguire questo script
|
||||||
(`ollama pull <modello>`).
|
(`ollama pull <modello>`).
|
||||||
|
|
||||||
@@ -81,16 +81,16 @@ Prima scelta: `qwen3-embedding:0.6b`.
|
|||||||
`qwen3-embedding` + `qwen3.5` condividono tokenizer e spazio semantico —
|
`qwen3-embedding` + `qwen3.5` condividono tokenizer e spazio semantico —
|
||||||
il retrieval è più coerente rispetto a modelli di famiglie diverse.
|
il retrieval è più coerente rispetto a modelli di famiglie diverse.
|
||||||
|
|
||||||
### Coerenza tra step-8 e step-9
|
### Coerenza tra ingest e retrieval
|
||||||
|
|
||||||
**`EMBED_MODEL` deve essere identico in step-8 e step-9.**
|
**`EMBED_MODEL` deve essere identico in `ingest.py` e `rag.py`.**
|
||||||
ChromaDB memorizza i vettori generati con un certo modello. Se step-9 usa un
|
ChromaDB memorizza i vettori generati con un certo modello. Se `rag.py` usa un
|
||||||
modello diverso per la query di ricerca, gli spazi vettoriali non corrispondono
|
modello diverso per la query di ricerca, gli spazi vettoriali non corrispondono
|
||||||
e il retrieval restituisce risultati casuali — senza alcun errore visibile.
|
e il retrieval restituisce risultati casuali — senza alcun errore visibile.
|
||||||
|
|
||||||
**Dopo aver cambiato `EMBED_MODEL`, riesegui sempre con `--force`.**
|
**Dopo aver cambiato `EMBED_MODEL`, riesegui sempre con `--force`.**
|
||||||
Senza `--force` lo script salta la collection già esistente — i vecchi vettori
|
Senza `--force` lo script salta la collection già esistente — i vecchi vettori
|
||||||
(generati col modello precedente) restano e continuano a essere usati da step-9.
|
(generati col modello precedente) restano e continuano a essere usati da `rag.py`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Cambio modello → ricrea sempre la collection
|
# Cambio modello → ricrea sempre la collection
|
||||||
|
|||||||
+4
-6
@@ -5,9 +5,9 @@ Step 8 — Vettorizzazione
|
|||||||
Legge i chunk prodotti da step-6, genera gli embedding tramite Ollama
|
Legge i chunk prodotti da step-6, genera gli embedding tramite Ollama
|
||||||
e li indicizza in ChromaDB (persistente).
|
e li indicizza in ChromaDB (persistente).
|
||||||
|
|
||||||
Il modello di embedding viene letto da step-9/config.py (EMBED_MODEL).
|
Il modello di embedding viene letto da config.py (EMBED_MODEL).
|
||||||
Puoi sovrascriverlo con --model, ma deve corrispondere al modello che
|
Puoi sovrascriverlo con --model, ma deve corrispondere al modello che
|
||||||
userai in step-9 — altrimenti riesegui con --force dopo aver cambiato.
|
userai in rag.py — altrimenti riesegui con --force dopo aver cambiato.
|
||||||
|
|
||||||
Input: step-6/<stem>/chunks.json
|
Input: step-6/<stem>/chunks.json
|
||||||
Output: chroma_db/<stem> (collection ChromaDB)
|
Output: chroma_db/<stem> (collection ChromaDB)
|
||||||
@@ -36,9 +36,7 @@ project_root = Path(__file__).parent.parent
|
|||||||
CHUNKS_DIR = project_root / "step-6"
|
CHUNKS_DIR = project_root / "step-6"
|
||||||
CHROMA_DIR = project_root / "chroma_db"
|
CHROMA_DIR = project_root / "chroma_db"
|
||||||
|
|
||||||
# Legge EMBED_MODEL e OLLAMA_URL da step-9/config.py (fonte di verità).
|
sys.path.insert(0, str(project_root))
|
||||||
# Per spostare config.py alla root: cambia solo la riga qui sotto.
|
|
||||||
sys.path.insert(0, str(project_root / "step-9"))
|
|
||||||
from config import EMBED_MODEL, OLLAMA_URL # noqa: E402
|
from config import EMBED_MODEL, OLLAMA_URL # noqa: E402
|
||||||
|
|
||||||
EMBED_ENDPOINT = f"{OLLAMA_URL}/api/embeddings"
|
EMBED_ENDPOINT = f"{OLLAMA_URL}/api/embeddings"
|
||||||
@@ -205,7 +203,7 @@ def main() -> int:
|
|||||||
parser.add_argument("--force", action="store_true",
|
parser.add_argument("--force", action="store_true",
|
||||||
help="Sovrascrive la collection se già esistente")
|
help="Sovrascrive la collection se già esistente")
|
||||||
parser.add_argument("--model", default=EMBED_MODEL,
|
parser.add_argument("--model", default=EMBED_MODEL,
|
||||||
help=f"Modello embedding Ollama (default da step-9/config.py: {EMBED_MODEL})")
|
help=f"Modello embedding Ollama (default da config.py: {EMBED_MODEL})")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
print("─── Step 8 — Vettorizzazione ─────────────────────────────────────────\n")
|
print("─── Step 8 — Vettorizzazione ─────────────────────────────────────────\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user