From 12effa1a51a5a4a282832ee0204b4049355b01d0 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 17 Apr 2026 18:50:31 +0200 Subject: [PATCH] refactor: elimina step-7 e step-9, consolida script alla root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- config.py | 54 +++++++++ ollama/README.md | 4 +- ollama/check_env.py | 6 +- ollama/test_ollama.py | 66 +++++++++++ rag.py | 252 ++++++++++++++++++++++++++++++++++++++++++ retrieve.py | 217 ++++++++++++++++++++++++++++++++++++ step-8/README.md | 16 +-- step-8/ingest.py | 10 +- 8 files changed, 605 insertions(+), 20 deletions(-) create mode 100644 config.py create mode 100644 ollama/test_ollama.py create mode 100644 rag.py create mode 100644 retrieve.py diff --git a/config.py b/config.py new file mode 100644 index 0000000..870c2d3 --- /dev/null +++ b/config.py @@ -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.\"" +) diff --git a/ollama/README.md b/ollama/README.md index 053049a..79012ce 100644 --- a/ollama/README.md +++ b/ollama/README.md @@ -57,7 +57,7 @@ Alternative supportate: - `bge-m3` - `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) @@ -65,7 +65,7 @@ Se cambi embedding model rispetto a quello usato in step-8, riesegui ingest con 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 diff --git a/ollama/check_env.py b/ollama/check_env.py index cd50ccc..359f0e9 100644 --- a/ollama/check_env.py +++ b/ollama/check_env.py @@ -22,7 +22,7 @@ from pathlib import Path # ─── 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 = [ "qwen3-embedding", "nomic-embed-text-v2-moe", @@ -60,9 +60,7 @@ def _parse_ollama_models(raw_output: str) -> list[str]: return models -# ─── Modelli configurati in 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")) +sys.path.insert(0, str(Path(__file__).parent.parent)) try: from config import EMBED_MODEL as CONFIGURED_EMBED, OLLAMA_MODEL as CONFIGURED_LLM except Exception: diff --git a/ollama/test_ollama.py b/ollama/test_ollama.py new file mode 100644 index 0000000..3054d59 --- /dev/null +++ b/ollama/test_ollama.py @@ -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()) diff --git a/rag.py b/rag.py new file mode 100644 index 0000000..f8f406e --- /dev/null +++ b/rag.py @@ -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/ (collection ChromaDB) +Output: risposta a schermo + +Uso: + python 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 +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 ", + "", + "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=( + "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("─── 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/retrieve.py b/retrieve.py new file mode 100644 index 0000000..03b26a1 --- /dev/null +++ b/retrieve.py @@ -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/ (collection ChromaDB) +Output: lista chunk con score di similarità + +Uso: + python 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 +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 ", + "", + "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=( + "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()) diff --git a/step-8/README.md b/step-8/README.md index 322b38a..afcef49 100644 --- a/step-8/README.md +++ b/step-8/README.md @@ -15,14 +15,14 @@ salva in ChromaDB (vector store persistente su disco). ## Configurazione modello -Il modello di embedding viene letto da **`step-9/config.py`**: +Il modello di embedding viene letto da **`config.py`**: ```python -# step-9/config.py +# config.py 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`. --- @@ -54,7 +54,7 @@ distanza coseno. La directory è ignorata da git (generata automaticamente). ## 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 (`ollama pull `). @@ -81,16 +81,16 @@ Prima scelta: `qwen3-embedding:0.6b`. `qwen3-embedding` + `qwen3.5` condividono tokenizer e spazio semantico — 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.** -ChromaDB memorizza i vettori generati con un certo modello. Se step-9 usa un +**`EMBED_MODEL` deve essere identico in `ingest.py` e `rag.py`.** +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 e il retrieval restituisce risultati casuali — senza alcun errore visibile. **Dopo aver cambiato `EMBED_MODEL`, riesegui sempre con `--force`.** 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 # Cambio modello → ricrea sempre la collection diff --git a/step-8/ingest.py b/step-8/ingest.py index 8db0329..7dda557 100644 --- a/step-8/ingest.py +++ b/step-8/ingest.py @@ -5,9 +5,9 @@ Step 8 — Vettorizzazione Legge i chunk prodotti da step-6, genera gli embedding tramite Ollama 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 -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//chunks.json Output: chroma_db/ (collection ChromaDB) @@ -36,9 +36,7 @@ project_root = Path(__file__).parent.parent CHUNKS_DIR = project_root / "step-6" CHROMA_DIR = project_root / "chroma_db" -# Legge EMBED_MODEL e OLLAMA_URL da step-9/config.py (fonte di verità). -# Per spostare config.py alla root: cambia solo la riga qui sotto. -sys.path.insert(0, str(project_root / "step-9")) +sys.path.insert(0, str(project_root)) from config import EMBED_MODEL, OLLAMA_URL # noqa: E402 EMBED_ENDPOINT = f"{OLLAMA_URL}/api/embeddings" @@ -205,7 +203,7 @@ def main() -> int: parser.add_argument("--force", action="store_true", help="Sovrascrive la collection se già esistente") 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() print("─── Step 8 — Vettorizzazione ─────────────────────────────────────────\n")