Spaces:
Sleeping
Sleeping
| /** | |
| * crossCriticBlock.ts — P-CROSS-CRITIC: verifica incrociata multi-modello | |
| * | |
| * Tier 4 del sistema di verifica (dopo postResponseVerifier Tier 3). | |
| * Si attiva SOLO per task ad alta complessità (complexity >= 7) dove | |
| * GoalVerifier non era pienamente sicuro (routeQualityScore < 0.85). | |
| * | |
| * Architettura: | |
| * - Fire-and-forget: si esegue DOPO che la risposta è già visibile all'utente | |
| * - Provider fisso: Groq llama-3.1-8b-instant (veloce, economico, diverso dal main) | |
| * - Timeout: 5000ms con AbortController — fail-open silenzioso | |
| * - Rate limit: max 1 per sessione browser (module-level flag, reset su ricarica) | |
| * - Output: recordError (selfLearning) + badge opzionale — mai blocca UI | |
| * | |
| * Trigger: | |
| * complexity >= 7 AND routeQualityScore < 0.85 | |
| * (task davvero complesso + GoalVerifier non pienamente sicuro) | |
| * | |
| * Invarianti: | |
| * - Mai lancia eccezioni — ogni path ha try/catch | |
| * - Non modifica mai la risposta visibile all'utente | |
| * - Non usa callBudgetCoordinator (turno già concluso al momento del trigger) | |
| * - Fail-open totale: timeout/rate-limit/token-missing → noop silenzioso | |
| * - Safari-safe: no Worker, no SharedArrayBuffer | |
| * - Zero regressione sul path normale (complexity < 7 → mai invocato) | |
| */ | |
| import * as telemetry from "../agentTelemetry"; | |
| const CRITIC_MODEL = "llama-3.1-8b-instant"; | |
| const GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"; | |
| const CRITIC_TIMEOUT = 5000; | |
| const CRITIC_MAX_TOK = 60; | |
| /** Rate limiter per-sessione: max 1 call critica per caricamento pagina. */ | |
| let _criticUsedThisSession = false; | |
| /** | |
| * Tenta di riservare il slot critica per questa sessione. | |
| * Chiamare PRIMA di scheduleCrossCritic — se false, non schedulare. | |
| * @returns true se disponibile, false se già usato questa sessione. | |
| */ | |
| export function acquireCriticCall(): boolean { | |
| if (_criticUsedThisSession) return false; | |
| _criticUsedThisSession = true; | |
| return true; | |
| } | |
| /** Solo per unit test — non usare in produzione. */ | |
| export function _resetCriticRateLimit(): void { | |
| _criticUsedThisSession = false; | |
| } | |
| export interface CrossCriticOpts { | |
| finalText: string; | |
| lastUserMsg: string; | |
| routeQualityScore: number; | |
| groqToken: string | null; | |
| recordError: (error: string, context: string, hint: string) => void; | |
| scheduleIdle: (fn: () => void, delayMs?: number) => void; | |
| onBadge?: (status: "ok" | "warn", label: string) => void; | |
| } | |
| function _buildCriticPrompt(lastUserMsg: string, finalText: string): string { | |
| const q = lastUserMsg.slice(0, 200).replace(/\n/g, " "); | |
| const a = finalText.slice(0, 900).replace(/\n/g, " "); | |
| return [ | |
| "You are a concise quality reviewer. Evaluate this AI response.", | |
| `USER QUESTION: ${q}`, | |
| `AI RESPONSE (truncated): ${a}`, | |
| "", | |
| "Reply with exactly ONE of:", | |
| "PASS", | |
| "FAIL: <reason in ≤8 words>", | |
| "UNCERTAIN", | |
| "", | |
| "No explanation. No formatting. One line only.", | |
| ].join("\n"); | |
| } | |
| async function _callCriticLlm( | |
| prompt: string, | |
| groqToken: string, | |
| ): Promise<"pass" | "fail" | "uncertain"> { | |
| const ctrl = new AbortController(); | |
| const timer = setTimeout(() => ctrl.abort(), CRITIC_TIMEOUT); | |
| try { | |
| const res = await fetch(GROQ_ENDPOINT, { | |
| method: "POST", | |
| signal: ctrl.signal, | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${groqToken}`, | |
| }, | |
| body: JSON.stringify({ | |
| model: CRITIC_MODEL, | |
| max_tokens: CRITIC_MAX_TOK, | |
| temperature: 0.0, | |
| messages: [{ role: "user", content: prompt }], | |
| }), | |
| }); | |
| if (!res.ok) return "uncertain"; | |
| const data = await res.json() as { | |
| choices?: Array<{ message?: { content?: string } }>; | |
| }; | |
| const text = (data.choices?.[0]?.message?.content ?? "").trim().toUpperCase(); | |
| if (text.startsWith("PASS")) return "pass"; | |
| if (text.startsWith("FAIL")) return "fail"; | |
| return "uncertain"; | |
| } catch { | |
| return "uncertain"; // timeout o network error — fail-open | |
| } finally { | |
| clearTimeout(timer); | |
| } | |
| } | |
| /** | |
| * Schedula la verifica critica fire-and-forget. | |
| * Chiamare DOPO processCase3FinalBlock (risposta già streamata all'utente). | |
| * Usa scheduleIdle per non bloccare il render post-risposta. | |
| * | |
| * @param opts - Parametri iniettati (no import diretto da selfLearning/agentLoop) | |
| */ | |
| export function scheduleCrossCritic(opts: CrossCriticOpts): void { | |
| const { | |
| finalText, lastUserMsg, routeQualityScore, | |
| groqToken, recordError, scheduleIdle, onBadge, | |
| } = opts; | |
| if (!groqToken || !finalText || !lastUserMsg) return; | |
| scheduleIdle(async () => { | |
| try { | |
| const prompt = _buildCriticPrompt(lastUserMsg, finalText); | |
| const verdict = await _callCriticLlm(prompt, groqToken); | |
| telemetry.record("crossCritic", verdict); | |
| if (verdict === "fail") { | |
| try { | |
| recordError( | |
| `CROSS_CRITIC_FAIL: task complesso (score=${Math.round(routeQualityScore * 100)}%) segnalato come errato dal critic cross-model`, | |
| lastUserMsg.slice(0, 80), | |
| "Su task ad alta complessità, verifica proattivamente la correttezza della risposta prima di concludere.", | |
| ); | |
| } catch { /* non-blocking */ } | |
| try { onBadge?.("warn", "⚠ Cross-model: verifica manuale"); } catch { /* non-blocking */ } | |
| } else if (verdict === "pass") { | |
| try { onBadge?.("ok", "✓ Cross-model OK"); } catch { /* non-blocking */ } | |
| } | |
| // UNCERTAIN → noop silenzioso (non abbastanza informazioni per giudicare) | |
| } catch { /* fail-open totale — nessuna eccezione propaga */ } | |
| }, 1000); // 1000ms delay — più tardivo di postResponseVerify (500ms) per non competere su rete | |
| } | |