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