Spaces:
Configuration error
Configuration error
File size: 5,777 Bytes
95eb75a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | /**
* 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
}
|