Spaces:
Sleeping
Sleeping
| /** | |
| * apiErrorRecovery.ts — S502: extracted from agentLoop.ts | |
| * | |
| * Gestisce la risposta HTTP in errore (!res.ok) dopo callWithFallback: | |
| * - Codici 400/401/403/422/503: recovery progressivo con tool + streamFallback | |
| * - Codici non gestiti: segnala al caller di throw HFApiError | |
| * | |
| * Invarianti: | |
| * - Non modifica loopMessages (solo steps e display via onSteps) | |
| * - Non emette testo al di fuori di streamFallback / onChunk | |
| * - Fire-and-forget per recordTaskOutcome, contextBridge, invalidateRepoMap | |
| * | |
| * @module apiErrorRecovery | |
| */ | |
| import type { AgentStep } from "../storage"; | |
| import type { StreamOptions } from "../types"; | |
| import type { TaskType } from "../taskClassifier"; | |
| import { HFApiError } from "../types"; | |
| import { selfLearning } from "../selfLearning"; | |
| import { contextBridge } from "../contextBridge"; | |
| import { semanticSearch } from "../agent/SemanticSearch"; | |
| import { invalidateRepoMap } from "../context/repoMap"; | |
| import { detectIntent } from "./intentParser"; | |
| import { streamFallback } from "./streamHelpers"; | |
| import { _localSynthesize } from "./toolExecutor"; | |
| import { TOOL_STATUS_LABELS } from "./toolStatusLabels"; | |
| import { narrateApiError, narrateLoopPhase } from "./errorNarrator"; // S768+S769 | |
| import { recordOutcome as recordTaskOutcome } from "../agent/SuccessRateMonitor"; | |
| import { generateSuggestions } from "../agent/SmartSuggestions"; | |
| import type { ApiMsg } from "./networkTools"; | |
| export interface ApiErrorCtx { | |
| res: Response; | |
| iter: number; | |
| token: string; | |
| options: StreamOptions; | |
| signal?: AbortSignal; | |
| initialMessages: Array<{ role: string; content: string }>; | |
| loopMessages: ApiMsg[]; | |
| lastUserMsg: string; | |
| steps: AgentStep[]; | |
| onStatus: (msg: string) => void; | |
| onChunk: (chunk: string) => void; | |
| onSteps: (steps: AgentStep[]) => void; | |
| detectedIntent: { tool: string; args: Record<string, unknown> } | null; | |
| realDataInjected: boolean; | |
| preCls: { type: string; isDirectAnswer?: boolean } | null; | |
| preferredModel?: string; | |
| maxTokensBudget: number; | |
| executeToolGated: (name: string, args: Record<string, unknown>) => Promise<string>; | |
| writeSess: () => void; | |
| cleanupSseBridge: () => void; | |
| onSuggestions?: (s: ReturnType<typeof generateSuggestions>) => void; | |
| } | |
| export type ApiErrorAction = "return" | "throw-hf-error"; | |
| export interface ApiErrorResult { | |
| action: ApiErrorAction; | |
| hfError?: HFApiError; | |
| } | |
| /** | |
| * Gestisce !res.ok. | |
| * Ritorna "return" se il caller deve fare return, | |
| * "throw-hf-error" se deve throw HFApiError. | |
| */ | |
| export async function handleApiError(ctx: ApiErrorCtx): Promise<ApiErrorResult> { | |
| const { | |
| res, iter, token, options, signal, | |
| initialMessages, loopMessages, lastUserMsg, steps, | |
| onStatus, onChunk, onSteps, | |
| detectedIntent: _detectedIntent, realDataInjected: _realDataInjected, | |
| preCls: _preCls, preferredModel: _preferredModel, | |
| executeToolGated, writeSess: _writeSess, cleanupSseBridge: _cleanupSseBridge, | |
| onSuggestions, | |
| } = ctx; | |
| const writeSess = _writeSess; | |
| const errText = await res.text().catch(() => ""); | |
| console.error(`[AgentLoop] Errore API ${res.status}:`, errText); // check-console-log: ok | |
| narrateApiError(res.status, errText, undefined, onStatus); // S768: friendly error narration | |
| if ([400, 422, 503, 401, 403].includes(res.status)) { | |
| // On first iteration: run detected tool first, then stream with enriched context | |
| if (iter === 0) { | |
| // DIRECT-FIX: per task a risposta diretta, non fare fallback a web_search in recovery | |
| const fi = (_preCls?.isDirectAnswer ?? false) | |
| ? null | |
| : (_detectedIntent ?? detectIntent(lastUserMsg) ?? { tool: "web_search", args: { query: lastUserMsg.slice(0, 200) } }); // S609: 120→200 | |
| if (!fi) { | |
| // isDirectAnswer in errore API → stream diretto senza tool | |
| await streamFallback(token || "", initialMessages, onChunk, signal, { | |
| ...options, ...(_preferredModel ? { model: _preferredModel } : {}), | |
| }); | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } | |
| writeSess(); // S-ARC3: salva sessione su isDirectAnswer fallback | |
| return { action: "return" }; | |
| } | |
| // S380-C: preCheck proattivo (selfLearning) — usa fi come sorgente di tool/args | |
| const _diPreChk = (() => { | |
| try { return selfLearning.preCheck(fi.tool, fi.args as Record<string, unknown>); } catch { return null; } | |
| })(); | |
| const _diTool = _diPreChk != null && _diPreChk.hasKnownIssue && _diPreChk.altTool | |
| ? _diPreChk.altTool | |
| : fi.tool; | |
| const _diArgs = fi.args as Record<string, unknown>; | |
| onStatus(TOOL_STATUS_LABELS[_diTool] ?? `${_diTool}…`); | |
| const fr = await executeToolGated(_diTool, _diArgs); | |
| try { contextBridge.recordToolResult(_diTool, _diArgs, fr); } catch { /* non-blocking */ } | |
| try { semanticSearch.invalidateCache(); } catch { /* non-blocking */ } | |
| try { invalidateRepoMap(); } catch { /* non-blocking — S206 */ } | |
| const fs: AgentStep = { tool: _diTool, args: _diArgs, result: fr, status: fr.startsWith("❌") ? "error" : "done" }; | |
| steps.push(fs); onSteps([...steps]); | |
| if (!fr.startsWith("❌")) { | |
| const enriched = [ | |
| ...initialMessages, | |
| { role: "user" as const, content: `OSSERVAZIONE [${fi.tool}]:\n${fr}\n\nUsa questi dati reali per rispondere in italiano in modo chiaro e completo.` }, | |
| ]; | |
| narrateLoopPhase("elaborating", onStatus); // S769 | |
| await streamFallback(token || "", enriched, onChunk, signal, { | |
| ...options, ...(_preferredModel ? { model: _preferredModel } : {}), | |
| }); | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } | |
| writeSess(); // S-ARC3: salva sessione su enriched fallback | |
| return { action: "return" }; | |
| } | |
| // S303-INARRESTABILE: tool primario fallito → tenta web_search come backup prima di cedere | |
| if (fi.tool !== "web_search") { | |
| try { | |
| const _wsBak = await executeToolGated("web_search", { query: lastUserMsg.slice(0, 200) }); // S609: 100→200 | |
| if (!_wsBak.startsWith("❌") && _wsBak.length > 30) { | |
| const _enrichedWs = [ | |
| ...initialMessages, | |
| { role: "user" as const, content: `OSSERVAZIONE [web_search]:\n${_wsBak}\n\nUsa questi dati reali per rispondere in italiano in modo chiaro e completo.` }, | |
| ]; | |
| narrateLoopPhase("elaborating", onStatus); // S769 | |
| await streamFallback(token || "", _enrichedWs, onChunk, signal, { ...options }); | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } | |
| writeSess(); // S-ARC3: salva sessione su API error recovery web fallback | |
| return { action: "return" }; | |
| } | |
| } catch { /* non-blocking */ } | |
| } | |
| } | |
| narrateLoopPhase("direct", onStatus); // S769 | |
| // S303-INARRESTABILE: se steps ha risultati validi, usa loopMessages arricchiti | |
| // S255: se dati reali già iniettati in loopMessages, NON perdere il contesto | |
| const _hasStepsData = steps.some((s: AgentStep) => !s.result.startsWith("❌") && s.result.length > 30); | |
| if (_realDataInjected || _hasStepsData) { | |
| // S304-B: prova sintesi locale istantanea per dati fattuali — se riesce non serve LLM | |
| if (_localSynthesize(steps, onChunk, signal)) { | |
| narrateLoopPhase("data-ready", onStatus); // S769 | |
| try { onSuggestions?.(generateSuggestions(lastUserMsg, steps)); } catch {} | |
| try { recordTaskOutcome(_preCls!.type as TaskType, "success"); } catch { /* non-blocking */ } | |
| _writeSess(); | |
| _cleanupSseBridge(); signal?.removeEventListener("abort", _cleanupSseBridge); | |
| return { action: "return" }; | |
| } | |
| narrateLoopPhase("with-data", onStatus); // S769 | |
| await streamFallback( | |
| token || "", loopMessages as Array<{ role: string; content: string }>, onChunk, signal, options, | |
| ); | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } | |
| writeSess(); // S-ARC3: salva sessione su loopMessages fallback | |
| return { action: "return" }; | |
| } | |
| // S169-FixD / S255: prima di cedere, tenta web_search di recovery (solo se iter>0) | |
| if (iter > 0 && !steps.some(s => s.tool === "web_search" && !s.result.startsWith("❌"))) { | |
| try { | |
| const _wsResult = await executeToolGated("web_search", { query: lastUserMsg.slice(0, 200) }); // S609: 100→200 | |
| if (!_wsResult.startsWith("❌") && _wsResult.length > 20) { | |
| const _enriched = [ | |
| ...initialMessages, | |
| { role: "user" as const, content: `OBSERVATION [web_search|REAL_DATA]:\n${_wsResult}\n\nUsa questi dati per rispondere in modo completo e diretto.` }, | |
| ]; | |
| await streamFallback(token || "", _enriched, onChunk, signal, options); | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } | |
| writeSess(); // S-ARC3: salva sessione su API error recovery web enriched | |
| return { action: "return" }; | |
| } | |
| } catch { /* non-blocking */ } | |
| } | |
| // S471: graceful 503 — disclaimer anti-hallucination + fallback finale | |
| const _noDataMsg = `⚠️ NOTA SISTEMA: i tool di ricerca non hanno restituito dati (errore/timeout). Rispondi SOLO con ciò che sai con certezza. Se la domanda riguarda meteo, notizie o dati in tempo reale, dì esplicitamente "Non ho dati aggiornati in questo momento — riprova tra poco" e NON inventare numeri o fatti specifici.`; | |
| const _noDataMsgs = [...initialMessages, { role: "user" as const, content: _noDataMsg }]; | |
| try { | |
| await streamFallback(token || "", _noDataMsgs, onChunk, signal, options); | |
| } catch { | |
| onChunk("\n\n⚠️ Tutti i provider AI sono occupati in questo momento — di solito si liberano in pochi secondi. Riprova tra un momento."); | |
| try { recordTaskOutcome(_preCls!.type as TaskType, "failure"); } catch { /* non-blocking */ } | |
| } | |
| writeSess(); // S-ARC3: salva sessione su API error — tutti i provider down | |
| return { action: "return" }; | |
| } | |
| // Codice HTTP non gestito (500, 404, ecc.) → throw HFApiError al caller | |
| return { action: "throw-hf-error", hfError: new HFApiError(errText || `HTTP ${res.status}`, res.status) }; | |
| } | |