AiAnonymize_v3 / web /services /debug_payload.py
Alessandro Tomassini
deploy(hf): overlay README/Dockerfile da huggingface/, senza docs/binari/model
c42a5a1
Raw
History Blame Contribute Delete
4.25 kB
"""Costruzione dei payload di debug/giudice a partire dalle tracce dei cluster.
Funzioni pure (nessun I/O, nessun rilancio) condivise dai vari path di
`AnalysisService`: sia "calcola e salva" sia "leggi dalla cache" usano queste,
così la rappresentazione del debug non può divergere tra i due. Tenute fuori dal
servizio per mantenerlo focalizzato sull'orchestrazione.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import asdict
from config.catalog.severity import SEVERITY_ORDER
from core.contracts import Severity
from core.output.trace import ClusterTrace
# Limiti oltre i quali uno span non è più una "parola/entità" ma uno spezzone
# di frase (chunk): certi layer ML (NER/baseline con aggregation) fondono token
# adiacenti in span lunghi. Nel confronto mostriamo solo i termini discreti.
_MAX_TERM_CHARS = 60
_MAX_TERM_WORDS = 8
def _is_term(text: str) -> bool:
"""True se `text` è un termine discreto (entità/parola), non un chunk."""
return 0 < len(text) <= _MAX_TERM_CHARS and len(text.split()) <= _MAX_TERM_WORDS
def _parse_severities(values: list[str] | None) -> set[Severity] | None:
if values is None:
return None
valid = {s.value for s in SEVERITY_ORDER}
return {Severity(v) for v in values if v in valid}
def _llm_status(verify: bool, has_verifier: bool, simulate: bool = False) -> str:
"""Stato del giudice per il riepilogo debug: off (non richiesto), unavailable
(richiesto ma non costruito), simulated (prompt costruiti ma LLM non valutato),
active (agganciato ed eseguito)."""
if not verify:
return "off"
if not has_verifier:
return "unavailable"
return "simulated" if simulate else "active"
def build_debug_payload(
traces: Sequence[ClusterTrace], threshold: float, method: str,
llm_status: str,
) -> dict:
"""Costruisce la risposta di debug dalle tracce (pura: nessun rilancio).
Unica fonte del dizionario di debug: la usano sia il path "calcola e salva"
sia il path "leggi dalla cache", così non possono divergere.
"""
clusters = [{**asdict(t), "discarded_reason": t.discarded_reason} for t in traces]
kept = sum(1 for t in traces if t.kept)
below = sum(1 for t in traces if not t.passed_threshold)
overlap = sum(1 for t in traces if t.passed_threshold and not t.kept)
llm_rejected = sum(
1 for t in traces if t.llm_vote is not None and t.llm_vote < 0
)
llm_scored = sum(1 for t in traces if t.llm_vote is not None)
# Prompt costruiti (anche senza valutazione): in simulazione sono tutti i
# prompt mostrati, con LLM reale coincidono con le frasi inviate al giudice.
llm_prompts = sum(1 for t in traces if t.llm_prompt is not None)
return {
"method": method,
"threshold": round(threshold, 4),
"clusters": clusters,
"summary": {
"total_clusters": len(traces),
"kept": kept,
"below_threshold": below,
"dropped_overlap": overlap,
"llm_active": llm_status == "active",
"llm_simulated": llm_status == "simulated",
"llm_status": llm_status,
"llm_scored": llm_scored,
"llm_prompts": llm_prompts,
"llm_rejected": llm_rejected,
},
}
def build_llm_check(
traces: Sequence[ClusterTrace], llm_status: str, llm_error: str | None = None
) -> dict:
"""Stato sintetico del giudice per l'avviso nel frontend (puro).
`scored`/`total`: frasi valutate sul totale dei cluster.
`obscured_without_score`: frasi tenute (oscurate) ma senza voto del giudice —
il modello non ha restituito un punteggio per quelle frasi.
`error`: messaggio se il modello LLM ha FALLITO (es. non si è caricato): il FE
lo mostra come errore, distinto dal caso "ha risposto ma senza punteggi utili".
"""
scored = sum(1 for t in traces if t.llm_vote is not None)
obscured_without_score = sum(
1 for t in traces if t.kept and t.llm_vote is None
)
return {
"status": llm_status,
"scored": scored,
"total": len(traces),
"obscured_without_score": obscured_without_score,
"error": llm_error,
}