diff --git a/step-7/README.md b/step-7/README.md deleted file mode 100644 index c5a5b72..0000000 --- a/step-7/README.md +++ /dev/null @@ -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 -``` - ---- - -## Prossimo step - -```bash -python step-8/ingest.py --stem -``` diff --git a/step-7/check_env.py b/step-7/check_env.py deleted file mode 100644 index 071c48d..0000000 --- a/step-7/check_env.py +++ /dev/null @@ -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 ") - 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()) diff --git a/step-9/README.md b/step-9/README.md deleted file mode 100644 index 48fd47d..0000000 --- a/step-9/README.md +++ /dev/null @@ -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 -``` - -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 | -|---|---| -| `` | Risposta basata sul documento | -| ` -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 -``` - -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 | -|---|---| -| `` | Chunk più simili con score di similarità (testo troncato a 200 car.) | -| ` -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`. diff --git a/step-9/config.py b/step-9/config.py deleted file mode 100644 index 8d83cd9..0000000 --- a/step-9/config.py +++ /dev/null @@ -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.\"" -) diff --git a/step-9/rag.py b/step-9/rag.py deleted file mode 100644 index ffd0402..0000000 --- a/step-9/rag.py +++ /dev/null @@ -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/ (collection ChromaDB) -Output: risposta a schermo - -Uso: - python step-9/rag.py --stem - -Nel loop interattivo: - Domanda: → risposta - Domanda: -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 ", - "", - "Loop interattivo:", - " risposta basata sul documento", - " -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 " - ), - ) - 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()) diff --git a/step-9/retrieve.py b/step-9/retrieve.py deleted file mode 100644 index aad0d63..0000000 --- a/step-9/retrieve.py +++ /dev/null @@ -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/ (collection ChromaDB) -Output: lista chunk con score di similarità - -Uso: - python step-9/retrieve.py --stem - -Nel loop interattivo: - Query: → chunk più simili con score - Query: -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 ", - "", - "Nel loop interattivo:", - " chunk più simili con score (testo troncato)", - " -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()) diff --git a/step-9/test_ollama.py b/step-9/test_ollama.py deleted file mode 100644 index 8b683f1..0000000 --- a/step-9/test_ollama.py +++ /dev/null @@ -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())