/** * 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 = { 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 = { 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); }