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
}