Add fallback target logging and unified failures

This commit is contained in:
2026-05-18 20:22:47 +03:00
parent 507b15aa5f
commit 1773b44edd
6 changed files with 74 additions and 4 deletions
+2 -2
View File
@@ -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.
+21
View File
@@ -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)
: {}),
},
});
},
+21
View File
@@ -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)
: {}),
},
});
},
@@ -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}`);
}
@@ -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),
};
}
+3 -2
View File
@@ -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);
}
}
}