Spaces:
Sleeping
Sleeping
| /** | |
| * 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); | |
| } | |