diff --git a/PIPELINE_TODO.md b/PIPELINE_TODO.md index 294f881..a917099 100644 --- a/PIPELINE_TODO.md +++ b/PIPELINE_TODO.md @@ -99,7 +99,7 @@ - [x] Для `continue_without_stage` писать короткий debug/audit без user notification. - [x] Для `use_alternate_target` логировать исходный и alternate target. - [x] Для `fail_request` завершать request через единый error path. -- [ ] Добавить локализацию fallback messages. +- [x] Добавить локализацию fallback messages. - [x] Добавить отдельные тексты для RAG failure, STT failure, TTS failure, tool failure. - [x] Не спамить пользователя несколькими fallback notifications за один request. - [x] Сохранять fallback notification в `request_audit.details`. diff --git a/locales/en.json b/locales/en.json index e067487..c6c6e5f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -8,6 +8,13 @@ }, "providerChoice.default": "Default", "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...", "analyzingPictureText": "🔍 Analyzing the image...", "analyzingPicturesText": "🔍 Analyzing the images...", diff --git a/locales/ru.json b/locales/ru.json index e75fe00..04f0d39 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -8,6 +8,13 @@ }, "providerChoice.default": "По умолчанию", "errorText": "⚠️ Произошла ошибка.", + "pipelineFallback.generic": "⚠️ Мне пришлось пропустить часть запроса, но я могу продолжить.", + "pipelineFallback.notifyUser": "⚠️ Возникла проблема, и я продолжу с запасным вариантом.", + "pipelineFallback.failRequest": "⚠️ Я не смог завершить этот запрос.", + "pipelineFallback.documentRag": "⚠️ Не удалось получить документы, поэтому я отвечу без RAG.", + "pipelineFallback.speechToText": "⚠️ Не удалось распознать речь, поэтому я продолжу без расшифровки аудио.", + "pipelineFallback.textToSpeech": "⚠️ Не удалось выполнить синтез речи, поэтому я продолжу без аудио.", + "pipelineFallback.toolLoop": "⚠️ Не удалось выполнить инструменты, поэтому я продолжу без них.", "waitThinkText": "⏳ Дайте-ка подумать...", "analyzingPictureText": "🔍 Анализирую изображение...", "analyzingPicturesText": "🔍 Анализирую изображения...", diff --git a/locales/ua.json b/locales/ua.json index 4574531..947c1ab 100644 --- a/locales/ua.json +++ b/locales/ua.json @@ -8,6 +8,13 @@ }, "providerChoice.default": "За замовчуванням", "errorText": "⚠️ Сталася помилка.", + "pipelineFallback.generic": "⚠️ Мені довелося пропустити частину запиту, але я можу продовжити.", + "pipelineFallback.notifyUser": "⚠️ Виникла проблема, і я продовжу із запасним варіантом.", + "pipelineFallback.failRequest": "⚠️ Я не зміг завершити цей запит.", + "pipelineFallback.documentRag": "⚠️ Не вдалося отримати документи, тому я відповім без RAG.", + "pipelineFallback.speechToText": "⚠️ Не вдалося розпізнати мовлення, тому я продовжу без розшифровки аудіо.", + "pipelineFallback.textToSpeech": "⚠️ Не вдалося виконати синтез мовлення, тому я продовжу без аудіо.", + "pipelineFallback.toolLoop": "⚠️ Не вдалося виконати інструменти, тому я продовжу без них.", "waitThinkText": "⏳ Дайте-но подумати...", "analyzingPictureText": "🔍 Аналізую зображення...", "analyzingPicturesText": "🔍 Аналізую зображення...", diff --git a/src/ai/unified-ai-request-pipeline.ts b/src/ai/unified-ai-request-pipeline.ts index 97b641a..ca355b1 100644 --- a/src/ai/unified-ai-request-pipeline.ts +++ b/src/ai/unified-ai-request-pipeline.ts @@ -292,7 +292,7 @@ export async function prepareUnifiedAiRequestPipeline(params: { ]; const state = createAiRequestPipelineState(options); - const fallbackNotifier = new PipelineFallbackNotifier(options.msg); + const fallbackNotifier = new PipelineFallbackNotifier(options.msg, options.responseLanguage); const pipeline = new UserRequestPipeline({ stages, stageNames: [ diff --git a/src/ai/unified-ai-response-pipeline.ts b/src/ai/unified-ai-response-pipeline.ts index 0452381..b80da70 100644 --- a/src/ai/unified-ai-response-pipeline.ts +++ b/src/ai/unified-ai-response-pipeline.ts @@ -167,7 +167,7 @@ export async function runUnifiedAiResponsePipeline(params: { }): Promise { const {options, config, downloads, prepared, streamMessage, controller} = params; const state = createResponsePipelineState(options); - const fallbackNotifier = new PipelineFallbackNotifier(options.msg); + const fallbackNotifier = new PipelineFallbackNotifier(options.msg, options.responseLanguage); const adapter = getProviderAdapter(options.provider); let selectedToolNames: string[] = []; let filteredTools: unknown[] = []; diff --git a/src/ai/user-request-pipeline/fallback-notifier-text.ts b/src/ai/user-request-pipeline/fallback-notifier-text.ts index d375395..854e8ee 100644 --- a/src/ai/user-request-pipeline/fallback-notifier-text.ts +++ b/src/ai/user-request-pipeline/fallback-notifier-text.ts @@ -1,27 +1,26 @@ +import {Localization} from "../../common/localization.js"; import type {PipelineFallbackAction, PipelineStageName} from "./types.js"; -const DEFAULT_TEXT = "⚠️ I had to skip part of the request, but I can continue."; -const NOTIFY_TEXT = "⚠️ I hit a problem and need to continue with a fallback."; -const FAIL_TEXT = "⚠️ I could not finish this request."; -const RAG_TEXT = "⚠️ Document retrieval failed, so I will answer without RAG."; -const STT_TEXT = "⚠️ Speech transcription failed, so I will continue without the audio transcript."; -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 { +export function resolvePipelineFallbackText( + stage: PipelineStageName, + action: PipelineFallbackAction, + locale?: string, +): string | 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) { 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": - return RAG_TEXT; + return Localization.text("pipelineFallback.documentRag", {}, "⚠️ Document retrieval failed, so I will answer without RAG.", locale); 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": - return TTS_TEXT; + return Localization.text("pipelineFallback.textToSpeech", {}, "⚠️ Text-to-speech failed, so I will continue without audio output.", locale); 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); } } diff --git a/src/ai/user-request-pipeline/fallback-notifier.ts b/src/ai/user-request-pipeline/fallback-notifier.ts index f4ae06c..fd58f90 100644 --- a/src/ai/user-request-pipeline/fallback-notifier.ts +++ b/src/ai/user-request-pipeline/fallback-notifier.ts @@ -1,4 +1,5 @@ import type {Message} from "typescript-telegram-bot-api"; +import {Localization} from "../../common/localization.js"; import {replyToMessage, logError} from "../../util/utils.js"; import type {PipelineFallbackDecision} from "./fallback-executor.js"; import {PipelineFallbackNotificationRegistry} from "./fallback-notifier-registry.js"; @@ -9,6 +10,7 @@ export class PipelineFallbackNotifier { constructor( private readonly sourceMessage: Message, + private readonly responseLanguage?: string, private readonly sendFallbackMessage: (text: string) => Promise = async text => { await replyToMessage({ message: this.sourceMessage, @@ -22,7 +24,10 @@ export class PipelineFallbackNotifier { 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) { return {notified: false}; } diff --git a/test/pipeline-fallback-notifier.test.mjs b/test/pipeline-fallback-notifier.test.mjs index 43805eb..d98d054 100644 --- a/test/pipeline-fallback-notifier.test.mjs +++ b/test/pipeline-fallback-notifier.test.mjs @@ -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); }); +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", () => { assert.equal(resolvePipelineFallbackText("document_rag", "continue_without_stage"), undefined); assert.equal(resolvePipelineFallbackText("tool_loop", "continue_without_stage"), undefined);