""" critic.py — Critic Model Secondo passaggio: verifica output, trova errori, suggerisce miglioramenti. Usa AIClient generico (non OllamaClient) per compatibilità HF Space. """ import json import re from typing import Any import logging _logger = logging.getLogger("agents.critic") CRITIC_SYSTEM = """Sei un critico AI. Valuta l'output dato e rispondi SOLO con JSON valido: { "quality": 0-10, "issues": ["lista problemi trovati — solo se GRAVI, non dettagli stilistici"], "suggestions": ["lista miglioramenti concreti"], "is_complete": true/false, "needs_retry": true/false, "confidence": 0.0-1.0 } Criteri: - quality 8-10: risposta corretta, completa, con codice/calcoli se richiesti - quality 5-7: risposta parziale ma utile, mancano dettagli non essenziali - quality 0-4: risposta sbagliata, vuota, o fuori tema → needs_retry: true - needs_retry: true SOLO se quality <= 3 (non per risposte corrette ma incomplete) - Se la risposta ha codice funzionante, calcoli corretti o dati reali → quality >= 7 Nessun testo prima o dopo il JSON.""" def _extract_json_balanced(raw: str) -> str | None: """P16-B3: depth-counting bilanciato — sostituisce regex greedy r'{[\s\S]+}'. Gestisce oggetti JSON annidati correttamente (es. patch con sub-oggetti). """ depth = 0 start = -1 for i, ch in enumerate(raw): if ch == '{': if depth == 0: start = i depth += 1 elif ch == '}': depth -= 1 if depth == 0 and start != -1: return raw[start:i + 1] return None class Critic: def __init__(self, llm_client: Any): self.llm = llm_client async def evaluate(self, task: str, output: str, model: str | None = None) -> dict: messages = [ {"role": "system", "content": CRITIC_SYSTEM}, { "role": "user", "content": ( f"Task originale: {task}\n\n" f"Output da valutare:\n{output[:2000]}" ), }, ] try: raw = await self.llm.chat(messages, temperature=0.2, max_tokens=512) json_match = _extract_json_balanced(raw) if json_match: result = json.loads(json_match) result["_evaluated"] = True return result except Exception as _exc: _logger.debug("[critic] silenced %s", type(_exc).__name__) # noqa: BLE001 # Fallback euristico (nessuna chiamata LLM) quality = 5 issues: list[str] = [] if len(output) < 50: quality -= 3 issues.append("Output troppo breve") if "errore" in output.lower() or "error" in output.lower(): quality -= 2 issues.append("Potenziali errori nell'output") if len(output) > 100: quality += 2 # Penalizza description leak dei tool if "usa il tool" in output.lower() or "esegui il comando" in output.lower(): quality -= 3 issues.append("Agente descrive tool invece di usarli") return { "quality": max(0, min(10, quality)), "issues": issues, "suggestions": ["Verifica la completezza della risposta"], "is_complete": len(output) > 100, "needs_retry": quality < 3, # S192: alzato soglia 4→3 — meno falsi negativi "confidence": 0.5, "_fallback": True, }