Spaces:
Sleeping
Sleeping
File size: 10,467 Bytes
cc11e77 | 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 | /**
* 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) };
}
|