Localize pipeline fallback notifications

This commit is contained in:
2026-05-18 20:31:04 +03:00
parent 1773b44edd
commit df39d89ea8
9 changed files with 49 additions and 19 deletions
+1 -1
View File
@@ -99,7 +99,7 @@
- [x] Для `continue_without_stage` писать короткий debug/audit без user notification. - [x] Для `continue_without_stage` писать короткий debug/audit без user notification.
- [x] Для `use_alternate_target` логировать исходный и alternate target. - [x] Для `use_alternate_target` логировать исходный и alternate target.
- [x] Для `fail_request` завершать request через единый error path. - [x] Для `fail_request` завершать request через единый error path.
- [ ] Добавить локализацию fallback messages. - [x] Добавить локализацию fallback messages.
- [x] Добавить отдельные тексты для RAG failure, STT failure, TTS failure, tool failure. - [x] Добавить отдельные тексты для RAG failure, STT failure, TTS failure, tool failure.
- [x] Не спамить пользователя несколькими fallback notifications за один request. - [x] Не спамить пользователя несколькими fallback notifications за один request.
- [x] Сохранять fallback notification в `request_audit.details`. - [x] Сохранять fallback notification в `request_audit.details`.
+7
View File
@@ -8,6 +8,13 @@
}, },
"providerChoice.default": "Default", "providerChoice.default": "Default",
"errorText": "⚠️ An error occurred.", "errorText": "⚠️ An error occurred.",
"pipelineFallback.generic": "⚠️ I had to skip part of the request, but I can continue.",
"pipelineFallback.notifyUser": "⚠️ I hit a problem and need to continue with a fallback.",
"pipelineFallback.failRequest": "⚠️ I could not finish this request.",
"pipelineFallback.documentRag": "⚠️ Document retrieval failed, so I will answer without RAG.",
"pipelineFallback.speechToText": "⚠️ Speech transcription failed, so I will continue without the audio transcript.",
"pipelineFallback.textToSpeech": "⚠️ Text-to-speech failed, so I will continue without audio output.",
"pipelineFallback.toolLoop": "⚠️ Tool execution failed, so I will continue without that tool.",
"waitThinkText": "⏳ Let me think...", "waitThinkText": "⏳ Let me think...",
"analyzingPictureText": "🔍 Analyzing the image...", "analyzingPictureText": "🔍 Analyzing the image...",
"analyzingPicturesText": "🔍 Analyzing the images...", "analyzingPicturesText": "🔍 Analyzing the images...",
+7
View File
@@ -8,6 +8,13 @@
}, },
"providerChoice.default": "По умолчанию", "providerChoice.default": "По умолчанию",
"errorText": "⚠️ Произошла ошибка.", "errorText": "⚠️ Произошла ошибка.",
"pipelineFallback.generic": "⚠️ Мне пришлось пропустить часть запроса, но я могу продолжить.",
"pipelineFallback.notifyUser": "⚠️ Возникла проблема, и я продолжу с запасным вариантом.",
"pipelineFallback.failRequest": "⚠️ Я не смог завершить этот запрос.",
"pipelineFallback.documentRag": "⚠️ Не удалось получить документы, поэтому я отвечу без RAG.",
"pipelineFallback.speechToText": "⚠️ Не удалось распознать речь, поэтому я продолжу без расшифровки аудио.",
"pipelineFallback.textToSpeech": "⚠️ Не удалось выполнить синтез речи, поэтому я продолжу без аудио.",
"pipelineFallback.toolLoop": "⚠️ Не удалось выполнить инструменты, поэтому я продолжу без них.",
"waitThinkText": "⏳ Дайте-ка подумать...", "waitThinkText": "⏳ Дайте-ка подумать...",
"analyzingPictureText": "🔍 Анализирую изображение...", "analyzingPictureText": "🔍 Анализирую изображение...",
"analyzingPicturesText": "🔍 Анализирую изображения...", "analyzingPicturesText": "🔍 Анализирую изображения...",
+7
View File
@@ -8,6 +8,13 @@
}, },
"providerChoice.default": "За замовчуванням", "providerChoice.default": "За замовчуванням",
"errorText": "⚠️ Сталася помилка.", "errorText": "⚠️ Сталася помилка.",
"pipelineFallback.generic": "⚠️ Мені довелося пропустити частину запиту, але я можу продовжити.",
"pipelineFallback.notifyUser": "⚠️ Виникла проблема, і я продовжу із запасним варіантом.",
"pipelineFallback.failRequest": "⚠️ Я не зміг завершити цей запит.",
"pipelineFallback.documentRag": "⚠️ Не вдалося отримати документи, тому я відповім без RAG.",
"pipelineFallback.speechToText": "⚠️ Не вдалося розпізнати мовлення, тому я продовжу без розшифровки аудіо.",
"pipelineFallback.textToSpeech": "⚠️ Не вдалося виконати синтез мовлення, тому я продовжу без аудіо.",
"pipelineFallback.toolLoop": "⚠️ Не вдалося виконати інструменти, тому я продовжу без них.",
"waitThinkText": "⏳ Дайте-но подумати...", "waitThinkText": "⏳ Дайте-но подумати...",
"analyzingPictureText": "🔍 Аналізую зображення...", "analyzingPictureText": "🔍 Аналізую зображення...",
"analyzingPicturesText": "🔍 Аналізую зображення...", "analyzingPicturesText": "🔍 Аналізую зображення...",
+1 -1
View File
@@ -292,7 +292,7 @@ export async function prepareUnifiedAiRequestPipeline(params: {
]; ];
const state = createAiRequestPipelineState(options); const state = createAiRequestPipelineState(options);
const fallbackNotifier = new PipelineFallbackNotifier(options.msg); const fallbackNotifier = new PipelineFallbackNotifier(options.msg, options.responseLanguage);
const pipeline = new UserRequestPipeline({ const pipeline = new UserRequestPipeline({
stages, stages,
stageNames: [ stageNames: [
+1 -1
View File
@@ -167,7 +167,7 @@ export async function runUnifiedAiResponsePipeline(params: {
}): Promise<void> { }): Promise<void> {
const {options, config, downloads, prepared, streamMessage, controller} = params; const {options, config, downloads, prepared, streamMessage, controller} = params;
const state = createResponsePipelineState(options); const state = createResponsePipelineState(options);
const fallbackNotifier = new PipelineFallbackNotifier(options.msg); const fallbackNotifier = new PipelineFallbackNotifier(options.msg, options.responseLanguage);
const adapter = getProviderAdapter(options.provider); const adapter = getProviderAdapter(options.provider);
let selectedToolNames: string[] = []; let selectedToolNames: string[] = [];
let filteredTools: unknown[] = []; let filteredTools: unknown[] = [];
@@ -1,27 +1,26 @@
import {Localization} from "../../common/localization.js";
import type {PipelineFallbackAction, PipelineStageName} from "./types.js"; import type {PipelineFallbackAction, PipelineStageName} from "./types.js";
const DEFAULT_TEXT = "⚠️ I had to skip part of the request, but I can continue."; export function resolvePipelineFallbackText(
const NOTIFY_TEXT = "⚠️ I hit a problem and need to continue with a fallback."; stage: PipelineStageName,
const FAIL_TEXT = "⚠️ I could not finish this request."; action: PipelineFallbackAction,
const RAG_TEXT = "⚠️ Document retrieval failed, so I will answer without RAG."; locale?: string,
const STT_TEXT = "⚠️ Speech transcription failed, so I will continue without the audio transcript."; ): string | undefined {
const TTS_TEXT = "⚠️ Text-to-speech failed, so I will continue without audio output.";
const TOOL_TEXT = "⚠️ Tool execution failed, so I will continue without that tool.";
export function resolvePipelineFallbackText(stage: PipelineStageName, action: PipelineFallbackAction): string | undefined {
if (action === "continue_without_stage") return undefined; if (action === "continue_without_stage") return undefined;
if (action === "fail_request") return FAIL_TEXT; if (action === "fail_request") return Localization.text("pipelineFallback.failRequest", {}, "⚠️ I could not finish this request.", locale);
switch (stage) { switch (stage) {
case "speech_to_text": case "speech_to_text":
return STT_TEXT; return Localization.text("pipelineFallback.speechToText", {}, "⚠️ Speech transcription failed, so I will continue without the audio transcript.", locale);
case "document_rag": case "document_rag":
return RAG_TEXT; return Localization.text("pipelineFallback.documentRag", {}, "⚠️ Document retrieval failed, so I will answer without RAG.", locale);
case "tool_loop": case "tool_loop":
return TOOL_TEXT; return Localization.text("pipelineFallback.toolLoop", {}, "⚠️ Tool execution failed, so I will continue without that tool.", locale);
case "text_to_speech": case "text_to_speech":
return TTS_TEXT; return Localization.text("pipelineFallback.textToSpeech", {}, "⚠️ Text-to-speech failed, so I will continue without audio output.", locale);
default: default:
return action === "notify_user" ? NOTIFY_TEXT : DEFAULT_TEXT; return action === "notify_user"
? Localization.text("pipelineFallback.notifyUser", {}, "⚠️ I hit a problem and need to continue with a fallback.", locale)
: Localization.text("pipelineFallback.generic", {}, "⚠️ I had to skip part of the request, but I can continue.", locale);
} }
} }
@@ -1,4 +1,5 @@
import type {Message} from "typescript-telegram-bot-api"; import type {Message} from "typescript-telegram-bot-api";
import {Localization} from "../../common/localization.js";
import {replyToMessage, logError} from "../../util/utils.js"; import {replyToMessage, logError} from "../../util/utils.js";
import type {PipelineFallbackDecision} from "./fallback-executor.js"; import type {PipelineFallbackDecision} from "./fallback-executor.js";
import {PipelineFallbackNotificationRegistry} from "./fallback-notifier-registry.js"; import {PipelineFallbackNotificationRegistry} from "./fallback-notifier-registry.js";
@@ -9,6 +10,7 @@ export class PipelineFallbackNotifier {
constructor( constructor(
private readonly sourceMessage: Message, private readonly sourceMessage: Message,
private readonly responseLanguage?: string,
private readonly sendFallbackMessage: (text: string) => Promise<void> = async text => { private readonly sendFallbackMessage: (text: string) => Promise<void> = async text => {
await replyToMessage({ await replyToMessage({
message: this.sourceMessage, message: this.sourceMessage,
@@ -22,7 +24,10 @@ export class PipelineFallbackNotifier {
return {notified: false}; return {notified: false};
} }
const text = resolvePipelineFallbackText(decision.stage, decision.action); const locale = this.responseLanguage === "default"
? Localization.currentLocale()
: Localization.normalizeLocale(this.responseLanguage) ?? Localization.currentLocale();
const text = resolvePipelineFallbackText(decision.stage, decision.action, locale);
if (!text) { if (!text) {
return {notified: false}; return {notified: false};
} }
+5
View File
@@ -10,6 +10,11 @@ test("pipeline fallback text maps notify_user to a user-facing message", () => {
assert.match(resolvePipelineFallbackText("tool_loop", "notify_user"), /tool/i); assert.match(resolvePipelineFallbackText("tool_loop", "notify_user"), /tool/i);
}); });
test("pipeline fallback text is localized when locale is provided", () => {
assert.match(resolvePipelineFallbackText("document_rag", "notify_user", "ru"), /RAG|документ/i);
assert.match(resolvePipelineFallbackText("text_to_speech", "notify_user", "ua"), /аудіо|мовлення/i);
});
test("pipeline fallback text stays silent for continue_without_stage", () => { test("pipeline fallback text stays silent for continue_without_stage", () => {
assert.equal(resolvePipelineFallbackText("document_rag", "continue_without_stage"), undefined); assert.equal(resolvePipelineFallbackText("document_rag", "continue_without_stage"), undefined);
assert.equal(resolvePipelineFallbackText("tool_loop", "continue_without_stage"), undefined); assert.equal(resolvePipelineFallbackText("tool_loop", "continue_without_stage"), undefined);