From 1773b44edd7c059f3e9ac36b13cc51dc6c3d998b Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 18 May 2026 20:22:47 +0300 Subject: [PATCH] Add fallback target logging and unified failures --- PIPELINE_TODO.md | 4 ++-- src/ai/unified-ai-request-pipeline.ts | 21 +++++++++++++++++++ src/ai/unified-ai-response-pipeline.ts | 21 +++++++++++++++++++ .../user-request-pipeline/fallback-failure.ts | 12 +++++++++++ .../fallback-target-details.ts | 15 +++++++++++++ src/ai/user-request-pipeline/pipeline.ts | 5 +++-- 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/ai/user-request-pipeline/fallback-failure.ts create mode 100644 src/ai/user-request-pipeline/fallback-target-details.ts diff --git a/PIPELINE_TODO.md b/PIPELINE_TODO.md index 4320088..294f881 100644 --- a/PIPELINE_TODO.md +++ b/PIPELINE_TODO.md @@ -97,8 +97,8 @@ - [x] Добавить `PipelineFallbackNotifier`. - [x] Для `notify_user` отправлять пользователю понятное сообщение. - [x] Для `continue_without_stage` писать короткий debug/audit без user notification. -- [ ] Для `use_alternate_target` логировать исходный и alternate target. -- [ ] Для `fail_request` завершать request через единый error path. +- [x] Для `use_alternate_target` логировать исходный и alternate target. +- [x] Для `fail_request` завершать request через единый error path. - [ ] Добавить локализацию fallback messages. - [x] Добавить отдельные тексты для RAG failure, STT failure, TTS failure, tool failure. - [x] Не спамить пользователя несколькими fallback notifications за один request. diff --git a/src/ai/unified-ai-request-pipeline.ts b/src/ai/unified-ai-request-pipeline.ts index 2a570ea..97b641a 100644 --- a/src/ai/unified-ai-request-pipeline.ts +++ b/src/ai/unified-ai-request-pipeline.ts @@ -3,6 +3,7 @@ import {AI_VOICE_MODE_TRANSCRIPT, DEFAULT_AI_RESPONSE_LANGUAGE} from "../common/ import {Environment} from "../common/environment"; import {UserRequestPipeline, type UserRequestPipelineState, type UserRequestPipelineStage} from "./user-request-pipeline"; import {PipelineFallbackNotifier} from "./user-request-pipeline/fallback-notifier"; +import {buildToolRankFallbackTargetDetails} from "./user-request-pipeline/fallback-target-details"; import type {AiDownloadedFile} from "./telegram-attachments"; import type {TelegramStreamMessage} from "./telegram-stream-message"; import type {ChatMessage} from "./chat-messages-types"; @@ -304,6 +305,23 @@ export async function prepareUnifiedAiRequestPipeline(params: { "audit_finish", ], onFallback: async decision => { + if (decision.action === "use_alternate_target") { + aiLog("warn", "request.fallback.use_alternate_target", { + provider: options.provider, + stage: decision.stage, + reason: decision.reason, + ...buildToolRankFallbackTargetDetails(options.provider, config), + }); + } + + if (decision.action === "fail_request") { + aiLog("error", "request.fallback.fail_request", { + provider: options.provider, + stage: decision.stage, + reason: decision.reason, + }); + } + const notification = await fallbackNotifier.notify(state.requestId, decision); state.audit.push({ stage: decision.stage, @@ -315,6 +333,9 @@ export async function prepareUnifiedAiRequestPipeline(params: { fallbackNotification: notification.text, fallbackNotified: notification.notified, reason: decision.reason, + ...(decision.action === "use_alternate_target" + ? buildToolRankFallbackTargetDetails(options.provider, config) + : {}), }, }); }, diff --git a/src/ai/unified-ai-response-pipeline.ts b/src/ai/unified-ai-response-pipeline.ts index 8497627..0452381 100644 --- a/src/ai/unified-ai-response-pipeline.ts +++ b/src/ai/unified-ai-response-pipeline.ts @@ -25,6 +25,7 @@ import {summarizeModelOutput} from "./response-model-output"; import {summarizeToolLoop} from "./tool-loop-summary"; import {persistToolLoopSummaryArtifactAttachment} from "./tool-loop-artifact-store"; import {PipelineFallbackNotifier} from "./user-request-pipeline/fallback-notifier"; +import {buildToolRankFallbackTargetDetails} from "./user-request-pipeline/fallback-target-details"; import { resolveTextToSpeechProviderForUser, sendSynthesizedSpeech, @@ -395,6 +396,23 @@ export async function runUnifiedAiResponsePipeline(params: { "audit_finish", ], onFallback: async decision => { + if (decision.action === "use_alternate_target") { + aiLog("warn", "response.fallback.use_alternate_target", { + provider: options.provider, + stage: decision.stage, + reason: decision.reason, + ...buildToolRankFallbackTargetDetails(options.provider, config), + }); + } + + if (decision.action === "fail_request") { + aiLog("error", "response.fallback.fail_request", { + provider: options.provider, + stage: decision.stage, + reason: decision.reason, + }); + } + const notification = await fallbackNotifier.notify(state.requestId, decision); state.audit.push({ stage: decision.stage, @@ -406,6 +424,9 @@ export async function runUnifiedAiResponsePipeline(params: { fallbackNotification: notification.text, fallbackNotified: notification.notified, reason: decision.reason, + ...(decision.action === "use_alternate_target" + ? buildToolRankFallbackTargetDetails(options.provider, config) + : {}), }, }); }, diff --git a/src/ai/user-request-pipeline/fallback-failure.ts b/src/ai/user-request-pipeline/fallback-failure.ts new file mode 100644 index 0000000..688c234 --- /dev/null +++ b/src/ai/user-request-pipeline/fallback-failure.ts @@ -0,0 +1,12 @@ +import type {PipelineFallbackDecision} from "./fallback-executor.js"; + +export class PipelineRequestFailure extends Error { + constructor(public readonly decision: PipelineFallbackDecision, message: string) { + super(message); + this.name = "PipelineRequestFailure"; + } +} + +export function raisePipelineRequestFailure(decision: PipelineFallbackDecision, stageName: string): never { + throw new PipelineRequestFailure(decision, `Pipeline send failed at stage ${stageName} with fallback action ${decision.action}`); +} diff --git a/src/ai/user-request-pipeline/fallback-target-details.ts b/src/ai/user-request-pipeline/fallback-target-details.ts new file mode 100644 index 0000000..0329a2d --- /dev/null +++ b/src/ai/user-request-pipeline/fallback-target-details.ts @@ -0,0 +1,15 @@ +import {AiProvider} from "../../model/ai-provider.js"; +import type {RuntimeConfigSnapshot} from "../unified-ai-runner.shared.js"; +import {aiLogProviderTarget} from "../../logging/ai-logger.js"; +import {buildRankerTarget} from "../tool-ranker-pipeline.js"; +import {providerChatTarget} from "../unified-ai-runner.shared.js"; + +export function buildToolRankFallbackTargetDetails(provider: AiProvider, config: RuntimeConfigSnapshot) { + const sourceTarget = buildRankerTarget(config, provider); + const alternateTarget = providerChatTarget(provider, config); + + return { + sourceTarget: aiLogProviderTarget(sourceTarget), + alternateTarget: aiLogProviderTarget(alternateTarget), + }; +} diff --git a/src/ai/user-request-pipeline/pipeline.ts b/src/ai/user-request-pipeline/pipeline.ts index df56104..0971c38 100644 --- a/src/ai/user-request-pipeline/pipeline.ts +++ b/src/ai/user-request-pipeline/pipeline.ts @@ -1,5 +1,6 @@ import {DEFAULT_PIPELINE_FALLBACK_POLICIES, USER_REQUEST_PIPELINE_STAGES} from "./blueprint.js"; import {decidePipelineFallback, type PipelineFallbackDecision} from "./fallback-executor.js"; +import {raisePipelineRequestFailure} from "./fallback-failure.js"; import type { PipelineAuditEvent, PipelineFallbackPolicy, @@ -66,7 +67,7 @@ export class UserRequestPipeline { }, })); if (decision.shouldFailRequest) { - throw new Error(`Required pipeline stage is not registered: ${stageName}`); + raisePipelineRequestFailure(decision, stageName); } continue; } @@ -112,7 +113,7 @@ export class UserRequestPipeline { error: error instanceof Error ? error.message : String(error), })); if (decision.shouldFailRequest) { - throw error; + raisePipelineRequestFailure(decision, stageName); } } }