Spaces:
Sleeping
Sleeping
Alessandro Tomassini
deploy(hf): overlay README/Dockerfile da huggingface/, senza docs/binari/model
c42a5a1 | """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, | |
| } | |