File size: 9,510 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/**
 * errorNarrator.ts — S768+S769: traduzione errori tecnici → narrativa italiana user-friendly
 *
 * Centralizza TUTTA la narrativa utente che non riguarda i tool running state
 * (i tool running state sono in toolStatusLabels.ts).
 *
 * Architettura:
 *   toolStatusLabels.ts  → step running state label (narrativeLabel)
 *   errorNarrator.ts     → tutto il resto:
 *     narrateApiError()         errori HTTP provider
 *     narrateProviderFallback() fallback chain attivata
 *     narrateProviderSelected() provider selezionato post-fallback  ← S769
 *     narrateLoopPhase()        fasi del loop (elaborating/direct/…) ← S769
 *
 * Pattern: module-level cb registrato da agentLoop.ts via setNarratorStatusCb().
 * Nessuna dipendenza React / store. Zero breaking changes.
 *
 * @module errorNarrator
 */

// ─── Module-level narration callback ─────────────────────────────────────────
let _narratorCb: ((msg: string) => void) | null = null;

/**
 * Registra il callback onStatus del loop corrente.
 * Chiamare subito dopo setPyodideStatus(onStatus) in agentLoop.ts.
 */
export function setNarratorStatusCb(cb: ((msg: string) => void) | null): void {
  _narratorCb = cb;
}

/** Emette sul callback registrato O su quello esplicito (priorità a quello esplicito). */
function _emit(msg: string, explicitCb?: (msg: string) => void): void {
  try { (explicitCb ?? _narratorCb)?.(msg); } catch { /* non-fatal */ }
}

// ─── Provider friendly names ─────────────────────────────────────────────────
const _PROVIDER_NAMES: Record<string, string> = {
  groq:        "Groq",
  "groq-fast": "Groq Fast",
  cerebras:    "Cerebras",
  sambanova:   "SambaNova",
  gemini:      "Gemini",
  deepseek:    "DeepSeek",
  openrouter:  "OpenRouter",
  hf:          "HuggingFace",
  "hf-sl":     "HuggingFace Serverless",
  openai:      "OpenAI",
  cf:          "Cloudflare AI",
  github:      "GitHub Models", // S-LOOP9: aggiunto per narrateApiError
  together:    "Together AI",
  or:          "OpenRouter",
};

function _providerLabel(key: string): string {
  const base = (key ?? "").split(":")[0];
  return _PROVIDER_NAMES[base] ?? key;
}

// ─── 1. API Error → narrativa ─────────────────────────────────────────────────

/**
 * Traduce un errore HTTP API in un messaggio italiano comprensibile per l'utente.
 *
 * @param status   - Codice HTTP (401, 403, 429, 400, 422, 503, 500, …)
 * @param errText  - Body dell'errore (per rilevare quota/safety/model errors)
 * @param provider - Chiave provider opzionale (es. "groq", "gemini")
 * @param onStatus - Callback esplicito (opzionale — fallback al cb registrato)
 */
export function narrateApiError(
  status: number,
  errText: string,
  provider?: string,
  onStatus?: (msg: string) => void,
): void {
  const cb = onStatus ?? _narratorCb;
  if (!cb) return;

  const provLabel = provider ? _providerLabel(provider) : "il provider AI";
  const body = (errText ?? "").toLowerCase();
  let msg: string;

  if (status === 401 || status === 403) {
    msg = `🔑 Le credenziali di ${provLabel} non sono valide — cambio provider, non si perde nulla`;
  } else if (
    status === 429 ||
    body.includes("rate limit") ||
    body.includes("quota") ||
    body.includes("too many requests")
  ) {
    msg = `⏳ ${provLabel} ha finito i turni disponibili — mi sposto su un altro provider`;
  } else if (
    body.includes("safety") ||
    body.includes("content_filter") ||
    body.includes("filtered") ||
    body.includes("blocked by") ||
    body.includes("policy violation")
  ) {
    msg = `🛡️ Il filtro di sicurezza ha bloccato questa richiesta — la riformulo in modo diverso e riprovo`;
  } else if (
    body.includes("context_length") ||
    body.includes("too long") ||
    body.includes("max_tokens") ||
    status === 413
  ) {
    msg = `📏 La conversazione è diventata troppo lunga per ${provLabel} — la comprimo e riprovo subito`;
  } else if (
    body.includes("model_not_found") ||
    body.includes("model not found") ||
    body.includes("no such model") ||
    status === 404
  ) {
    msg = `🔄 Questo modello non è più disponibile su ${provLabel} — ne scelgo uno equivalente`;
  } else if (
    status === 503 ||
    body.includes("overloaded") ||
    body.includes("service unavailable") ||
    body.includes("capacity")
  ) {
    msg = `🔄 ${provLabel} è sotto pressione in questo momento — mi sposto su un'alternativa più libera`;
  } else if (status === 400 || status === 422) {
    msg = `⚠️ ${provLabel} non ha accettato la richiesta — la riformatto e riprovo con una configurazione diversa`;
  } else if (status === 500) {
    msg = `⚠️ ${provLabel} ha avuto un errore interno — cambio provider e ci riprovo`;
  } else {
    msg = `⚠️ ${provLabel} non risponde (errore ${status}) — passo a un provider alternativo`;
  }

  _emit(msg, onStatus);
}

// ─── 2. Provider fallback chain → narrativa ───────────────────────────────────

/**
 * Emette un messaggio narrativo quando il routing smart passa al fallback chain.
 * Chiamare nel blocco S196 di providerChain.ts (tutti i provider prioritari falliti).
 *
 * @param failedCount - Numero di provider prioritari falliti
 * @param reasons     - Array di motivi (da _loopReasons nel routing loop)
 */
export function narrateProviderFallback(
  failedCount: number,
  reasons: string[],
): void {
  if (!_narratorCb) return;

  const hasQuota    = reasons.some(r => /quota|429|rate|limit/i.test(r));
  const hasAuth     = reasons.some(r => /auth|401|403|token/i.test(r));
  const hasOverload = reasons.some(r => /overload|503|sovrac|capacity/i.test(r));

  let msg: string;
  if (hasAuth) {
    msg = `🔑 Alcune chiavi API risultano non valide — provo i provider alternativi senza interrompere`;
  } else if (hasQuota) {
    msg = `⏳ I provider principali hanno esaurito i turni (${failedCount} su ${failedCount}) — attivo i canali di backup`;
  } else if (hasOverload) {
    msg = `🔄 I provider principali sono sotto carico — mi sposto sui modelli di riserva`;
  } else {
    msg = `🔄 ${failedCount} provider non hanno risposto — cerco il canale più veloce disponibile`;
  }
  _emit(msg);
}

// ─── 3. Provider selected after fallback → narrativa ──────────────────────────

/**
 * Emette un messaggio di conferma quando il routing trova un provider funzionante
 * dopo che almeno uno prioritario aveva fallito (wasFallback=true).
 * Se wasFallback=false (provider normale trovato al primo tentativo) → no-op.
 *
 * @param providerKey - Chiave interna provider (es. "cerebras", "groq")
 * @param model       - Nome completo del modello selezionato
 * @param wasFallback - true solo se _loopFailed > 0 nel routing loop
 */
export function narrateProviderSelected(
  providerKey: string,
  model: string,
  wasFallback: boolean,
): void {
  if (!_narratorCb || !wasFallback) return;
  const name = _providerLabel(providerKey);
  const modelShort = model.split("/").pop()?.replace(/-\d{4}-\d{2}-\d{2}/, "") ?? model;
  _emit(`✓ Connesso su ${name} (${modelShort}) — rispondo subito`);
}

// ─── 4. Loop phase transitions → narrativa ────────────────────────────────────

/**
 * Fasi del loop agente disponibili per la narrazione.
 * Centralizza tutte le chiamate onStatus("...") delle fasi in apiErrorRecovery.ts.
 */
export type LoopPhase =
  | "elaborating"  // Elaboro risposta con i dati raccolti
  | "direct"       // Risposta diretta (no tool o dati aggiuntivi)
  | "with-data"    // Elaboro risposta con dati VFS/tool già presenti
  | "data-ready"   // Tool ha restituito dati, sintesi completata
  | "retrying"     // Riprovo con approccio o provider diverso
  | "finalizing"   // Finalizzo e verifico la risposta
  | "searching";   // Cerco dati aggiuntivi prima di rispondere

const _LOOP_PHASE_LABELS: Record<LoopPhase, string> = {
  elaborating:  "Sto elaborando la risposta con i dati che ho…",
  direct:       "Rispondo direttamente…",
  "with-data":  "Ho i dati, sto scrivendo la risposta…",
  "data-ready": "Ho tutto quello che serve ✓",
  retrying:     "Ci riprovo con un approccio diverso…",
  finalizing:   "Sto finalizzando…",
  searching:    "Sto cercando qualche informazione in più…",
};

/**
 * Emette un messaggio narrativo per la fase corrente del loop agente.
 * Sostituisce i singoli onStatus("...") hardcoded in apiErrorRecovery.ts.
 *
 * @param phase    - Fase del loop (LoopPhase)
 * @param onStatus - Callback esplicito (opzionale — fallback al cb registrato)
 */
/**
 * Emette un messaggio di recovery dall'interno delle toolRecoveryChains.
 * Usa il callback registrato dal loop corrente (setNarratorStatusCb).
 * Zero dipendenze circolari — non importa toolRecoveryChains.
 */
export function narrateRecovery(msg: string): void {
  _emit(msg);
}

export function narrateLoopPhase(
  phase: LoopPhase,
  onStatus?: (msg: string) => void,
): void {
  const msg = _LOOP_PHASE_LABELS[phase];
  if (!msg) return;
  _emit(msg, onStatus);
}