Toolkit Python/Flask per generare dataset di ground truth Q&A da pagine, PDF e sitemap UniCT, interrogare chatbot RAG e valutarne qualità e prestazioni tramite LLM-judge, metriche RAGAS e load test concorrenti.
  • Python 76.8%
  • HTML 19.6%
  • CSS 3.6%
Find a file
2026-07-28 09:35:48 +02:00
static Initial import 2026-07-27 13:58:22 +02:00
templates Initial import 2026-07-27 13:58:22 +02:00
.env.example Initial import 2026-07-27 13:58:22 +02:00
.gitignore Initial import 2026-07-27 13:58:22 +02:00
app.py Initial import 2026-07-27 13:58:22 +02:00
build_sitemap_dataset.py Initial import 2026-07-27 13:58:22 +02:00
chatbot_client.py Use sources field in chatbot clients 2026-07-28 09:35:48 +02:00
chatbot_client_didattica.py Use sources field in chatbot clients 2026-07-28 09:35:48 +02:00
chatbot_client_servizi.py Use sources field in chatbot clients 2026-07-28 09:35:48 +02:00
chatbot_client_servizi_async.py Use sources field in chatbot clients 2026-07-28 09:35:48 +02:00
chatbot_client_servizifix.py Initial import 2026-07-27 13:58:22 +02:00
chatbot_clients.py Initial import 2026-07-27 13:58:22 +02:00
evaluate_chatbot.py Initial import 2026-07-27 13:58:22 +02:00
evaluate_ragas.py Initial import 2026-07-27 13:58:22 +02:00
generate_qa.py Initial import 2026-07-27 13:58:22 +02:00
llm_client.py Initial import 2026-07-27 13:58:22 +02:00
loadtest_chatbot.py Initial import 2026-07-27 13:58:22 +02:00
pyproject.toml Initial import 2026-07-27 13:58:22 +02:00
README.md Initial import 2026-07-27 13:58:22 +02:00
requirements.txt Initial import 2026-07-27 13:58:22 +02:00
sitemap_scraper.py Initial import 2026-07-27 13:58:22 +02:00
sitemapDidattica.xml Initial import 2026-07-27 13:58:22 +02:00
uv.lock Initial import 2026-07-27 13:58:22 +02:00

Test RAG chatbot di ateneo (UniCT)

Toolkit per valutare il chatbot RAG dell'Università di Catania. Il flusso è:

  1. Genera ground truth — da una pagina web (testo + PDF collegati) un LLM crea coppie domanda/risposta di riferimento.
  2. Interroga il chatbot con quelle domande.
  3. 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: il f1 di RAGAS penalizza la verbosità (risposte lunghe ma corrette → precision bassa); recall misura 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 --limit in 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 1 e con --users 10 a 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.8 va abilitato nella console Bedrock della regione scelta (AWS_REGION), e il --model è un inference profile (es. eu.anthropic.claude-opus-4-8, prefisso eu./us. a seconda della regione — verificabile con list_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 la temperature: 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).