- Python 76.8%
- HTML 19.6%
- CSS 3.6%
| static | ||
| templates | ||
| .env.example | ||
| .gitignore | ||
| app.py | ||
| build_sitemap_dataset.py | ||
| chatbot_client.py | ||
| chatbot_client_didattica.py | ||
| chatbot_client_servizi.py | ||
| chatbot_client_servizi_async.py | ||
| chatbot_client_servizifix.py | ||
| chatbot_clients.py | ||
| evaluate_chatbot.py | ||
| evaluate_ragas.py | ||
| generate_qa.py | ||
| llm_client.py | ||
| loadtest_chatbot.py | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
| sitemap_scraper.py | ||
| sitemapDidattica.xml | ||
| uv.lock | ||
Test RAG chatbot di ateneo (UniCT)
Toolkit per valutare il chatbot RAG dell'Università di Catania. Il flusso è:
- Genera ground truth — da una pagina web (testo + PDF collegati) un LLM crea coppie domanda/risposta di riferimento.
- Interroga il chatbot con quelle domande.
- Valuta le risposte del chatbot contro la ground truth, con due approcci (LLM-judge custom e metriche RAGAS).
Tutto è pilotabile da interfaccia web o da CLI. Le risposte di riferimento sono generate da un LLM, quindi vanno considerate una base di partenza da rivedere.
pagina/sitemap ─► generate_qa.py ─► dataset JSON (output/)
│
▼
chatbot_client.py ◄─► evaluate_chatbot.py (LLM-judge) ─► results/
(chatbot RAG) ◄─► evaluate_ragas.py (metriche RAGAS) ─► results/
│
▼
app.py (web UI: lancio + visualizzazione + export PDF)
Componenti
| File | Cosa fa |
|---|---|
generate_qa.py |
Da un URL, da file .md locali o da una sitemap XML → dataset Q&A di ground truth (formato RAGAS). |
sitemap_scraper.py |
Scarica una sitemap XML in una cartella di file .md (frontmatter + Markdown), input per --from-md. Usato anche da --from-sitemap. |
build_sitemap_dataset.py |
Accoda su task spooler (tsp) la generazione per tutte le URL di una sitemap XML. |
chatbot_client.py |
Client del chatbot RAG di ateneo (endpoint Azure /api/chat). È l'agente default; altri agenti sono file chatbot_client_<NOME>.py. |
chatbot_clients.py |
Registro degli agenti: scopre i client chatbot_client_<NOME>.py e li rende selezionabili con --agent. |
evaluate_chatbot.py |
Valutazione LLM-judge: verdetto correct/partial/incorrect + source hit. |
evaluate_ragas.py |
Valutazione con metriche RAGAS (factual correctness, faithfulness, context recall/precision). |
app.py |
Web app Flask: lancia gli stadi e visualizza/esporta i risultati. |
output/ |
Dataset di ground truth (un JSON per pagina). output/sitemap/ per il crawl. |
results/ |
Risultati delle valutazioni (un JSON per run). |
Gli LLM girano di default su Azure OpenAI / AI Foundry tramite endpoint
OpenAI-compatible (gpt-5.5 su
https://<foundry-resource-name>.services.ai.azure.com/openai/v1), oppure su
Ollama Cloud (gpt-oss:120b) impostando LLM_PROVIDER=ollama, oppure su
AWS Bedrock (Claude Opus 4.8) impostando LLM_PROVIDER=bedrock.
Setup
Richiede uv e Python ≥ 3.10. Per i PDF della web app servono le librerie di sistema di WeasyPrint (pango/cairo, di solito già presenti).
uv sync # crea .venv e installa le dipendenze (da pyproject.toml)
cp .env.example .env # poi inserisci la chiave del provider scelto
Esegui con uv run python ..., oppure attiva la venv: source .venv/bin/activate.
Alternativa senza uv: pip install -r requirements.txt.
Configurazione LLM
Default Azure OpenAI / AI Foundry:
LLM_PROVIDER=azure
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_BASE_URL=https://<foundry-resource-name>.services.ai.azure.com/openai/v1
AZURE_OPENAI_MODEL=gpt-5.5
Ollama Cloud (opt-in):
LLM_PROVIDER=ollama
OLLAMA_API_KEY=...
AWS Bedrock — Claude Opus 4.8 (opt-in). L'autenticazione accetta o la Bedrock API key (un solo token) o le credenziali AWS classiche (access key/secret, profilo o ruolo IAM gestiti da boto3):
LLM_PROVIDER=bedrock
BEDROCK_MODEL=eu.anthropic.claude-opus-4-8 # inference profile EU
AWS_REGION=eu-north-1
# opzione A — Bedrock API key
AWS_BEARER_TOKEN_BEDROCK=...
# opzione B — credenziali AWS classiche (in alternativa)
# AWS_ACCESS_KEY_ID=... / AWS_SECRET_ACCESS_KEY=... / AWS_PROFILE=...
Da CLI puoi anche forzare la scelta per singolo comando:
uv run python generate_qa.py "https://www.unict.it/it/..." \
--provider azure --model gpt-5.5 \
--base-url https://<foundry-resource-name>.services.ai.azure.com/openai/v1
# oppure Claude Opus 4.8 su AWS Bedrock
uv run python generate_qa.py "https://www.unict.it/it/..." \
--provider bedrock --model eu.anthropic.claude-opus-4-8
Interfaccia web (modo consigliato)
uv run python app.py # apri http://localhost:8501 (porta: variabile PORT)
Dal browser puoi fare tutto: generare Q&A, lanciare le due valutazioni (i job girano in background con log in tempo reale) e sfogliare i risultati. La pagina Risultati mostra per ogni domanda risposta del chatbot e ground truth affiancate, con verdetto/metriche, filtri e motivazione del giudice; un pulsante 📄 Esporta PDF scarica il report completo.
Le pagine web lanciano esattamente gli stessi script CLI descritti sotto, quindi puoi usare indifferentemente web o terminale.
Stadio 1 — Generare il dataset di ground truth
1a. Singola pagina (da URL)
uv run python generate_qa.py "https://www.unict.it/it/..." -n 6 --languages it,en
Scarica la pagina + i PDF collegati, e genera ~N domande per lingua. Crea un
file output/qa_<timestamp>.json (o usa -o per un nome fisso).
| Opzione | Default | Descrizione |
|---|---|---|
url (posizionale) |
— | URL della pagina (ometti se usi --from-md/--from-sitemap) |
--from-md [PATH] |
off | Genera dai file Markdown locali invece che da un URL (vedi 1b) |
--from-sitemap SITEMAP |
off | Scarica prima l'intera sitemap XML in una cartella di .md, poi genera da lì (vedi 1c) |
--sitemap-out DIR |
sitemap-md-index |
Cartella dove --from-sitemap salva i .md |
--overwrite-sitemap |
off | Rigenera la cartella da zero (altrimenti riprende i file già scaricati) |
--sitemap-limit N |
tutte | Con --from-sitemap, scarica solo le prime N URL |
--scrape-only |
off | Con --from-sitemap, si ferma alla cartella .md senza generare le Q&A (non richiede credenziali LLM) |
-n, --num-questions |
8 |
Domande per lingua |
--languages |
it,en |
Lingue separate da virgola |
--provider |
azure |
Provider LLM (azure, ollama o bedrock) |
--model |
provider-specifico | Modello/deployment LLM (gpt-5.5 su Azure, gpt-oss:120b su Ollama, eu.anthropic.claude-opus-4-8 su Bedrock) |
--base-url |
endpoint Azure configurato | Base URL OpenAI-compatible per Azure |
--max-context |
120000 |
Caratteri max di contesto (oltre → chunking automatico) |
--no-pdf |
off | Ignora i PDF (collegati o allegati) |
-o, --output |
timestamp | Forza il file di output (sovrascrive) |
1b. Da file Markdown locali (scraping già fatto)
Se il sito è già stato scaricato e convertito in file .md (una pagina/allegato
per file, con l'URL di provenienza nel frontmatter YAML), si può generare la
ground truth offline senza rifare lo scraping:
# usa la cartella di default (sitemap-rag-md-index) o env MD_SOURCE_DIR
uv run python generate_qa.py --from-md -n 5 --languages it
# oppure una cartella/file specifici
uv run python generate_qa.py --from-md path/alla/cartella-md -n 5
uv run python generate_qa.py --from-md path/al/singolo-file.md -n 5
Lo script legge ricorsivamente i .md, ricava source_url/page_title dal
frontmatter e raggruppa ogni allegato PDF con la sua pagina (via
rag_parent_url), esattamente come la modalità web unisce pagina + PDF
collegati. Genera Q&A per ogni unità e le accoda in un unico dataset. Frontmatter
riconosciuto (chiavi principali):
---
url: "https://www.unict.it/it/didattica/..." # provenienza → source_url
title: "…" # → page_title
content_type: html | pdf # pdf → allegato
rag_parent_url: "https://…" # (solo pdf) pagina genitore
---
--no-pdf in questa modalità ignora i file con content_type: pdf.
1c. Da una sitemap XML (scarica il sito, poi genera)
Se hai una sitemap XML (anche con l'estensione rag: di ateneo, es.
sitemapServizi.xml) ma non hai ancora i file .md, questa modalità fa
tutto in un passo: scarica ogni URL della sitemap in una cartella di file .md
(stessa struttura di sitemap-rag-md-index/: frontmatter YAML + corpo in
Markdown) e poi genera le Q&A da quella cartella, esattamente come --from-md.
# scarica tutta la sitemap in 'sitemap-md-index/' e genera (IT, 5 domande)
uv run python generate_qa.py --from-sitemap sitemapServizi.xml -n 5 --languages it
# cartella di output personalizzata + solo le prime 20 URL
uv run python generate_qa.py --from-sitemap sitemapServizi.xml \
--sitemap-out sitemap-servizi-md --sitemap-limit 20 -n 5
# rigenera la cartella da zero (altrimenti riprende i .md già scaricati)
uv run python generate_qa.py --from-sitemap sitemapServizi.xml --overwrite-sitemap
# SOLO scaricamento: crea la cartella .md senza generare le Q&A
# (non richiede credenziali LLM; le Q&A si generano poi con --from-md)
uv run python generate_qa.py --from-sitemap sitemapServizi.xml \
--sitemap-out sitemap-servizi-md --scrape-only
Lo scaricamento è resumabile: senza --overwrite-sitemap i file .md già
presenti nella cartella vengono saltati. La cartella prodotta è riutilizzabile
in seguito con --from-md (per rigenerare senza riscaricare). Lo scraping HTML
usa markdownify; i PDF collegati (via rag:mime/content_type: pdf) sono
inclusi salvo --no-pdf. È anche disponibile lo script autonomo
sitemap_scraper.py per il solo scaricamento:
uv run python sitemap_scraper.py sitemapServizi.xml -o sitemap-servizi-md
1d. Intera sitemap (batch con task spooler)
Per costruire il dataset su molte pagine, build_sitemap_dataset.py accoda un job
per ogni URL della sitemap su tsp (task spooler), che li elabora in
background uno alla volta. È resumabile: salta le pagine già fatte.
# anteprima (non accoda nulla)
uv run python build_sitemap_dataset.py sitemapDidattica.xml --dry-run
# accoda tutte le pagine (IT, 5 domande each) → output/sitemap/<slug>.json
uv run python build_sitemap_dataset.py sitemapDidattica.xml -n 5 --languages it
# solo un lotto, e ripulendo i file vuoti (pagine-indice senza fatti)
uv run python build_sitemap_dataset.py sitemapDidattica.xml --limit 100 --clean-empty
Controllo della coda tsp:
tsp # stato (queued / running / finished)
tsp -c <id> # output/log di un job
tsp -S 2 # 2 job in parallelo (più veloce, occhio ai rate limit)
tsp -K # ferma tutto (azzera la coda)
Rilanciare lo stesso comando riprende da dove era rimasto (rigenera solo i file mancanti o vuoti).
Stadio 2 — Valutare il chatbot
Entrambi gli script interrogano il chatbot (chatbot_client.py) e salvano un
JSON in results/ + un report a console. Usano come giudice un LLM sul provider
configurato (default Azure gpt-5.5). Accettano uno o più dataset (anche glob:
output/sitemap/*.json).
Scelta dell'agente da valutare (--agent). Puoi valutare chatbot diversi:
ogni agente è un file chatbot_client_<NOME>.py con la stessa interfaccia
(send_message, check_health). L'opzione --agent <NOME> seleziona quale
interrogare; senza l'opzione si usa l'agente default (chatbot_client.py).
Il nome dell'agente è salvato nel JSON dei risultati e mostrato nei report.
# valuta l'agente 'didattica' (file chatbot_client_didattica.py)
uv run python evaluate_chatbot.py output/qa_*.json --agent didattica --language it
2a. LLM-judge — correttezza della risposta
uv run python evaluate_chatbot.py output/qa_*.json --language it --limit 50
Per ogni domanda: interroga il chatbot, un giudice LLM confronta la risposta
con la ground truth (verdetto correct/partial/incorrect + punteggio), e
calcola il source hit rate (la fonte attesa è tra le citazioni del chatbot?).
Registra anche il tempo di risposta di ogni chiamata al chatbot
(response_time per domanda) e, a fine run, il tempo totale (somma dei
singoli tempi) e il tempo medio (media), riportati nel report a console, nel
JSON dei risultati (summary.total_response_time, summary.mean_response_time)
e nella pagina/PDF dei risultati.
Opzioni: --agent, --provider, --model, --base-url,
--language {all,it,en}, --limit N, --delay, -o/--output, --no-health.
2b. RAGAS — metriche standard
uv run python evaluate_ragas.py output/qa_*.json --language it --limit 50
Interroga il chatbot, scarica le pagine/PDF citati come retrieved_contexts
(proxy del retrieval: il backend non espone i chunk) e calcola:
| Metrica | Misura |
|---|---|
factual_correctness (recall) |
fatti del ground_truth presenti nella risposta |
faithfulness |
risposta fondata sulle fonti citate (anti-allucinazione) |
context_recall |
le citazioni coprono i fatti attesi |
llm_context_precision_with_reference |
le citazioni sono pertinenti |
Opzioni: --agent, --provider, --model, --base-url, --language,
--limit N, --delay, --context-cap, --max-workers,
--factual-mode {recall,precision,f1}, -o/--output.
factual-mode=recallè il default: ilf1di RAGAS penalizza la verbosità (risposte lunghe ma corrette → precision bassa);recallmisura la copertura dei fatti attesi, più adatto a una ground truth.
Quale usare? Il judge dà un verdetto robusto sulla correttezza; RAGAS aggiunge le metriche standard di retrieval/faithfulness. Si completano.
⚠️ RAGAS salva il JSON solo a fine run: per campioni grandi spezza con
--limitin più lotti, così un'interruzione non fa perdere tutto.
2c. Test di carico — utenti concorrenti
uv run python loadtest_chatbot.py output/qa_*.json --users 10 --rounds 3 --limit 5
Misura le prestazioni del backend sotto carico (nessun LLM-judge, quindi
veloce ed economico). Per ogni domanda del dataset simula N utenti
contemporanei × M ondate: le domande sono processate una alla volta, e per
ciascuna si lanciano N chiamate concorrenti (ThreadPoolExecutor) attendendo il
completamento dell'ondata prima della successiva, così N è il vero tetto di
concorrenza sul backend.
Il report (console + JSON in results/loadtest_<timestamp>.json + pagina/PDF web)
riporta, per-domanda e globalmente:
| Metrica | Misura |
|---|---|
Latenze min/max/mean/p50/p95/p99 |
tempi di risposta delle chiamate riuscite |
throughput_rps + wall_time_s |
richieste al secondo e durata totale reale |
error_rate + error_types |
% di errori con classificazione (timeout, http_5xx, http_4xx, connection, other) |
total_requests = domande × M × N |
parametri di concorrenza usati |
Opzioni: --agent, -u/--users, -r/--rounds, --language {all,it,en},
--limit N, --timeout (per richiesta; applicato se il client lo supporta),
--warmup (richieste di riscaldamento non conteggiate, per domanda),
-o/--output, --no-health.
💡 Per stimare la scalabilità confronta la durata totale con
--users 1e con--users 10a parità di richieste: se il tempo cresce meno che linearmente, le richieste stanno venendo servite in parallelo.
Formato dei dati
Dataset (output/*.json) — lista, compatibile RAGAS:
{
"question": "Quali sono le scadenze per l'immatricolazione?",
"ground_truth": "Le immatricolazioni sono aperte dal ... al ...",
"contexts": ["passaggio sorgente esatto..."],
"language": "it",
"source_url": "https://www.unict.it/...",
"source_documents": ["https://.../bando.pdf"],
"page_title": "...",
"generated_at": "..."
}
Risultati (results/*.json) — oggetto con summary (metriche aggregate) e
results (dettaglio per domanda). Visualizzabili nella web app o esportabili in PDF.
Note e limiti
- Limite Ollama Cloud: l'account ha un limite d'uso (sessione e settimanale).
Su batch grandi le chiamate iniziano a fallire con HTTP 429: ridurre con
--limit, attendere il reset o fare upgrade. Vale per generazione e valutazione. - AWS Bedrock: il modello
Claude Opus 4.8va abilitato nella console Bedrock della regione scelta (AWS_REGION), e il--modelè un inference profile (es.eu.anthropic.claude-opus-4-8, prefissoeu./us.a seconda della regione — verificabile conlist_inference_profiles). Bedrock non ha una "JSON mode": ci si affida ai prompt (che già impongono JSON) e al parsing tollerante. Opus 4.8 ha deprecato latemperature: il codice la omette automaticamente (sia nella generazione sia nel valutatore RAGAS). - Le risposte di ground truth sono generate da un LLM: ancorate al contenuto estratto (prompt anti-allucinazione), ma da rivedere prima dell'uso ufficiale.
- I PDF scansionati (senza testo) non sono supportati (servirebbe OCR).
- Pagine rese via JavaScript possono non esporre testo a
requests(servirebbe un browser headless).