refactor: pulizia files

This commit is contained in:
2026-04-17 18:52:13 +02:00
parent af9ffc0559
commit e4dc0856bb
7 changed files with 0 additions and 1053 deletions
-147
View File
@@ -1,147 +0,0 @@
# Step 7 — Verifica ambiente
Prima di procedere con la vettorizzazione (step 8) devi avere installato:
- **Ollama** — server locale per LLM e embedding
- un **modello di embedding** (es. `nomic-embed-text`, `bge-m3`)
- un **modello LLM** (es. `qwen3.5:4b`, `qwen3:4b`)
- **chromadb** — libreria Python per il vector store
---
## 1. Installa Ollama
```bash
curl -fsSL https://ollama.com/install.sh | sh
```
Verifica che il servizio sia attivo:
```bash
ollama list
```
### Disinstalla Ollama
```bash
# Ferma e rimuovi il servizio systemd
sudo systemctl stop ollama
sudo systemctl disable ollama
sudo rm /etc/systemd/system/ollama.service
sudo systemctl daemon-reload
# Rimuovi il binario
sudo rm /usr/local/bin/ollama
# Rimuovi modelli e dati (opzionale — occupa spazio su disco)
# I modelli sono salvati sotto l'utente di sistema "ollama", non nella tua home
sudo rm -rf /usr/share/ollama
# Rimuovi l'utente e il gruppo di sistema creati dall'installer (opzionale)
sudo userdel ollama
sudo groupdel ollama
```
---
## 2. Scarica i modelli
### Modello di embedding
Per testi in italiano serve un modello multilingue — i modelli English-first producono embeddings di qualità inferiore su lingue diverse dall'inglese, con retrieval meno preciso.
Prima scelta consigliata:
```bash
ollama pull qwen3-embedding:0.6b
```
Stessa famiglia del LLM in uso (`qwen3.5`), multilingue, recente, gira comodamente in CPU.
| Modello | Dim | Dimensione | Lingue | Consigliato |
|---|---|---|---|---|
| `qwen3-embedding:0.6b` | 1024 | ~522 MB | multilingue | ✅ prima scelta |
| `nomic-embed-text-v2-moe` | 768 | ~523 MB | multilingue | ✅ seconda scelta |
| `bge-m3` | 1024 | ~1.2 GB | 100+ lingue incl. IT | ✅ terza scelta |
| `nomic-embed-text` | 768 | ~274 MB | principalmente EN | ⚠️ default corrente |
| `mxbai-embed-large` | 1024 | ~670 MB | principalmente EN | ❌ |
| `paraphrase-multilingual` | 768 | ~278 MB | multilingue | ❌ obsoleto |
| `all-minilm` | 384 | ~46 MB | principalmente EN | ❌ troppo piccolo |
Se cambi modello rispetto a quello usato in step-8, devi rieseguire la vettorizzazione con `--force` e aggiornare `EMBED_MODEL` in `step-9/config.py`.
### Modello LLM
Per RAG su testi italiani servono: buon instruction following, supporto multilingue e context window ampia (i prompt RAG includono più chunk).
Prima scelta consigliata per 8 GB RAM:
```bash
ollama pull qwen3.5:4b
```
Il progetto è pensato per la famiglia **Qwen3.5** — stessa famiglia dell'embedding consigliato (`qwen3-embedding`), context window 256K, ottimo italiano. Altri modelli sono compatibili ma non testati.
| Modello | RAM | Note |
|---|---|---|
| `qwen3.5:0.8b` | ≥ 1 GB | minimo assoluto |
| `qwen3.5:2b` | ≥ 3 GB | leggero |
| `qwen3.5:4b` | ≥ 5 GB | **consigliato per 8 GB** |
| `qwen3.5:9b` | ≥ 8 GB | lento su CPU, meglio con GPU |
Se usi un modello diverso da `qwen3.5:4b`, aggiorna `OLLAMA_MODEL` in `step-9/config.py`.
### Disinstalla un modello
```bash
ollama rm qwen3.5:4b
ollama rm nomic-embed-text
```
Per vedere tutti i modelli installati:
```bash
ollama list
```
---
## 3. Installa le dipendenze nel venv
Assicurati di avere `chromadb` installato nel `.venv`:
```bash
source .venv/bin/activate
pip install -r requirements.txt
```
---
## 4. Verifica tutto
```bash
source .venv/bin/activate
python step-7/check_env.py
```
Output atteso se tutto è a posto:
```
✅ ollama trovato nel PATH
✅ ollama risponde correttamente
✅ modello embedding trovato: nomic-embed-text:latest
✅ modello LLM trovato: qwen3.5:4b
✅ chromadb importabile
✅ Ambiente pronto — procedi con la vettorizzazione:
python step-8/ingest.py --stem <nome>
```
---
## Prossimo step
```bash
python step-8/ingest.py --stem <nome>
```
-208
View File
@@ -1,208 +0,0 @@
#!/usr/bin/env python3
"""
Step 7 — Verifica ambiente
Controlla che tutti i prerequisiti per la vettorizzazione siano soddisfatti:
1. ollama è nel PATH e risponde
2. Almeno un modello di embedding è scaricato
3. Almeno un modello LLM è scaricato
4. chromadb è importabile
Output: report a schermo con ✅ / ❌ per ogni componente.
Nessun file scritto. Exit 0 se tutto OK, 1 altrimenti.
Uso:
python step-7/check_env.py
"""
import shutil
import subprocess
import sys
from pathlib import Path
# ─── Lista canonica di modelli embedding supportati ───────────────────────────
# Ordine: prima scelta → ultima scelta (come da README step-7)
EMBED_MODELS = [
"qwen3-embedding",
"nomic-embed-text-v2-moe",
"bge-m3",
"nomic-embed-text",
"mxbai-embed-large",
"paraphrase-multilingual",
"all-minilm",
]
def _is_embed(model_name: str) -> bool:
"""True se il modello è riconosciuto come embedding (lista canonica o keyword)."""
base = model_name.split(":")[0].lower()
return any(base == e or base.startswith(e) for e in EMBED_MODELS) or "embed" in base
# ─── Modelli configurati in step-9/config.py ─────────────────────────────────
# Per spostare config.py alla root: cambia solo la riga qui sotto.
sys.path.insert(0, str(Path(__file__).parent.parent / "step-9"))
try:
from config import EMBED_MODEL as CONFIGURED_EMBED, OLLAMA_MODEL as CONFIGURED_LLM
except Exception:
CONFIGURED_EMBED = None
CONFIGURED_LLM = None
REQUIRED_LIBS = ["chromadb"]
# ─── Checks ───────────────────────────────────────────────────────────────────
def check_ollama_in_path() -> bool:
"""Verifica che ollama sia nel PATH."""
found = shutil.which("ollama") is not None
if found:
print("✅ ollama trovato nel PATH")
else:
print("❌ ollama non trovato nel PATH")
print(" → Installa con: curl -fsSL https://ollama.com/install.sh | sh")
return found
def check_ollama_running() -> list[str] | None:
"""
Esegue 'ollama list' e ritorna la lista dei modelli disponibili.
Ritorna None se ollama non risponde.
"""
try:
result = subprocess.run(
["ollama", "list"],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
print("❌ ollama non risponde (errore all'avvio)")
print(" → Avvia il servizio con: ollama serve")
return None
lines = result.stdout.strip().splitlines()
models = []
for line in lines[1:]: # salta l'header
parts = line.split()
if parts:
models.append(parts[0])
print("✅ ollama risponde correttamente")
return models
except FileNotFoundError:
print("❌ ollama non trovato (FileNotFoundError)")
return None
except subprocess.TimeoutExpired:
print("❌ ollama non risponde (timeout)")
print(" → Avvia il servizio con: ollama serve")
return None
def _match(model_name: str, available: list[str]) -> str | None:
"""
Ritorna il nome completo del modello trovato in 'available' che corrisponde
a 'model_name' (confronto per prefisso), oppure None.
"""
for m in available:
if m == model_name or m.startswith(model_name + ":") or m.startswith(model_name + "-"):
return m
return None
def check_embed_model(available: list[str]) -> bool:
"""Verifica che il modello di embedding configurato sia disponibile."""
if CONFIGURED_EMBED:
print(f" modello configurato (step-9/config.py): {CONFIGURED_EMBED}")
found = _match(CONFIGURED_EMBED, available)
if found:
print(f"✅ embedding disponibile: {found}")
return True
print(f"{CONFIGURED_EMBED} non trovato in Ollama")
print(f" → ollama pull {CONFIGURED_EMBED}")
return False
# fallback: config.py non leggibile
found = next((m for m in available if _is_embed(m)), None)
if found:
print(f"✅ modello embedding trovato: {found}")
return True
print("❌ nessun modello di embedding trovato")
print(f" → Prima scelta: ollama pull qwen3-embedding:0.6b")
return False
def check_llm_model(available: list[str]) -> bool:
"""Verifica che il modello LLM configurato sia disponibile."""
if CONFIGURED_LLM:
print(f" modello configurato (step-9/config.py): {CONFIGURED_LLM}")
found = _match(CONFIGURED_LLM, available)
if found:
print(f"✅ LLM disponibile: {found}")
return True
print(f"{CONFIGURED_LLM} non trovato in Ollama")
print(f" → ollama pull {CONFIGURED_LLM}")
return False
# fallback: config.py non leggibile
llm_candidates = [m for m in available if not _is_embed(m)]
if llm_candidates:
print(f"✅ modello LLM trovato: {llm_candidates[0]}")
return True
print("❌ nessun modello LLM trovato")
print(f" → Consigliato per 8 GB RAM: ollama pull qwen3.5:4b")
return False
def check_library(lib: str) -> bool:
"""Verifica che una libreria Python sia importabile."""
try:
__import__(lib)
print(f"{lib} importabile")
return True
except ImportError:
print(f"{lib} non importabile")
print(f" → Installa con: pip install {lib}")
return False
# ─── Entry point ──────────────────────────────────────────────────────────────
def main() -> int:
print("─── Step 7 — Verifica ambiente ───────────────────────────────────────\n")
results: list[bool] = []
# 1. ollama nel PATH
in_path = check_ollama_in_path()
results.append(in_path)
# 2. ollama risponde + modelli
if in_path:
available = check_ollama_running()
if available is None:
results.extend([False, False, False])
else:
results.append(True)
results.append(check_embed_model(available))
results.append(check_llm_model(available))
else:
results.extend([False, False, False])
print("⚠️ modelli non verificabili (ollama non trovato)")
# 3. Librerie Python
print()
for lib in REQUIRED_LIBS:
results.append(check_library(lib))
# ── Riepilogo ─────────────────────────────────────────────────────────────
print()
print("──────────────────────────────────────────────────────────────────────")
all_ok = all(results)
if all_ok:
print("✅ Ambiente pronto — procedi con la vettorizzazione:")
print(" python step-8/ingest.py --stem <nome>")
else:
n_fail = sum(1 for r in results if not r)
print(f"⚠️ {n_fail} problema/i rilevato/i — risolvi prima di procedere con step-8.")
return 0 if all_ok else 1
if __name__ == "__main__":
sys.exit(main())
-109
View File
@@ -1,109 +0,0 @@
# Step 9 — Interrogazione del documento
Due modalità di interrogazione, entrambe con loop interattivo:
| 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
- Step 8 completato (`chroma_db/` popolata)
- Ollama attivo con il modello di embedding scaricato
- Per `rag.py`: anche il modello LLM scaricato
---
## rag.py — Risposta in linguaggio naturale
```bash
source .venv/bin/activate
python step-9/rag.py --stem <nome>
```
Per ogni domanda: vettorizza la query, recupera i chunk più rilevanti da ChromaDB e genera la risposta tramite Ollama.
```
── Loop RAG ─────────────────────────────────────── (exit per uscire)
Domanda:
```
| Sintassi | Comportamento |
|---|---|
| `<testo>` | Risposta basata sul documento |
| `<testo> -v` | Risposta + chunk recuperati con score di similarità |
| `exit` | Esce dal programma |
Flusso interno:
```
domanda
▼ embed (EMBED_MODEL, Ollama)
vettore N-dim
▼ query ChromaDB — similarità coseno, top-K
chunk rilevanti
▼ build_prompt (SYSTEM_PROMPT + contesti + domanda)
▼ generate (OLLAMA_MODEL, Ollama)
risposta
```
Il LLM risponde esclusivamente dal contesto fornito. Se il contesto è irrilevante rispetto alla domanda, lo dichiara esplicitamente.
---
## 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
Per verificare che Ollama risponda correttamente prima di interrogare il documento:
```bash
python step-9/test_ollama.py
```
Chat diretta con il modello, senza ChromaDB. Usa gli stessi parametri di `config.py`.
-54
View File
@@ -1,54 +0,0 @@
# ─── Step 9 — Configurazione RAG ─────────────────────────────────────────────
#
# Modifica questo file per cambiare i parametri della pipeline.
#
# Uso:
# python step-9/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.\""
)
-252
View File
@@ -1,252 +0,0 @@
#!/usr/bin/env python3
"""
Step 9 — 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 step-9/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.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 step-9/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=(
"Step 9 — 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("─── Step 9 — 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
View File
@@ -1,217 +0,0 @@
#!/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())
-66
View File
@@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""
Test chat locale Ollama — senza RAG, senza ChromaDB.
Uso: python step-9/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))
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())