AUDIT / src /lib /agentLoop /crossCriticBlock.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
5.78 kB
/**
* 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
}