refactor: pulizia files
This commit is contained in:
@@ -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>
|
||||
```
|
||||
@@ -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())
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user