From be116dcbde53aa214875796cf70ccbe8902b8bde Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 10 Apr 2026 22:34:10 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=9B=D0=9E=D0=91=D0=90=D0=9B=D0=AC?= =?UTF-8?q?=D0=9D=D0=AB=D0=99=20=D0=A0=D0=95=D0=A4=D0=90=D0=9A=D0=A2=D0=9E?= =?UTF-8?q?=D0=A0=D0=98=D0=9D=D0=93=20=D0=90=D0=A0=D0=A5=D0=98=D0=A2=D0=95?= =?UTF-8?q?=D0=9A=D0=A2=D0=A3=D0=A0=D0=AB=20-=20=D0=A0=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D1=8D=D1=82=D0=B0?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=202.36=20=D0=B2=D1=8B=D0=BD=D0=BE=D1=81=20tr?= =?UTF-8?q?yHandleLivingChat=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20runtime-handler=20=D1=87=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D0=B5?= =?UTF-8?q?=D1=89=D0=B5=20=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D1=88=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=B1=D0=BB=D0=BE=D0=BA.=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20living-chat=20handler-=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BF=D1=82=D0=B5=D1=80=20=D1=81=20try/catch=20+=20finalize=20?= =?UTF-8?q?+=20warn=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TECH/1CLLMARCH-FACT.md | 32 +++- ...ssistantLivingChatHandlerRuntimeAdapter.js | 73 ++++++++ .../backend/dist/services/assistantService.js | 166 +++++++----------- ...ssistantLivingChatHandlerRuntimeAdapter.ts | 125 +++++++++++++ .../backend/src/services/assistantService.ts | 166 +++++++----------- ...antLivingChatHandlerRuntimeAdapter.test.ts | 117 ++++++++++++ 6 files changed, 482 insertions(+), 197 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js create mode 100644 llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/tests/assistantLivingChatHandlerRuntimeAdapter.test.ts diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index f2216d9..c6a978c 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -1156,7 +1156,37 @@ Validation: - `assistantLivingRouter.test.ts` - `assistantWave10SettlementCorrectiveRegression.test.ts` -Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 completed)** +Implemented in current pass (Phase 2.36): +1. Extracted living-chat handler branch (`tryHandleLivingChat`) from `assistantService` into dedicated runtime adapter: + - `assistantLivingChatHandlerRuntimeAdapter.ts` + - introduced: + - `tryHandleAssistantLivingChatRuntime(...)` +2. Centralized living-chat handler sequence (behavior-preserving): + - living-chat runtime invocation (deterministic/LLM guard chain); + - chat finalization invocation for `assistant_message_chat` response path; + - warn-log fallback path for runtime failures. +3. Rewired `assistantService` `tryHandleLivingChat(...)` closure to consume handler runtime adapter output. +4. Added focused unit tests: + - `assistantLivingChatHandlerRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `assistantLivingChatHandlerRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseRuntimeAdapter.test.ts` + - `assistantAddressRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisRuntimeAdapter.test.ts` + - `assistantDeepTurnNormalizationRuntimeAdapter.test.ts` + - `assistantAddressToolGateRuntimeAdapter.test.ts` + - `assistantAddressOrchestrationRuntimeAdapter.test.ts` + - `assistantAddressLaneRuntimeAdapter.test.ts` + - `assistantAddressFollowupContext.test.ts` + - `assistantLivingChatMode.test.ts` + - `assistantLivingRouter.test.ts` + - `assistantWave10SettlementCorrectiveRegression.test.ts` + +Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js new file mode 100644 index 0000000..825050b --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js @@ -0,0 +1,73 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tryHandleAssistantLivingChatRuntime = tryHandleAssistantLivingChatRuntime; +const assistantLivingChatRuntimeAdapter_1 = require("./assistantLivingChatRuntimeAdapter"); +const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = require("./assistantLivingChatTurnFinalizeRuntimeAdapter"); +async function tryHandleAssistantLivingChatRuntime(input) { + const runLivingChatRuntimeSafe = input.runLivingChatRuntime ?? assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime; + const finalizeLivingChatTurnSafe = input.finalizeLivingChatTurn ?? assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn; + try { + const runtime = await runLivingChatRuntimeSafe({ + userMessage: input.userMessage, + sessionItems: input.sessionItems, + modeDecision: input.modeDecision, + sessionScope: input.sessionScope, + addressRuntimeMeta: input.addressRuntimeMeta, + traceIdFactory: input.traceIdFactory, + toNonEmptyString: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: input.resolveDataScopeProbe, + executeLlmChat: input.executeLlmChat, + applyScriptGuard: input.applyScriptGuard, + applyGroundingGuard: input.applyGroundingGuard, + buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply + }); + if (!runtime.handled || !runtime.debug) { + return null; + } + const finalization = finalizeLivingChatTurnSafe({ + sessionId: input.sessionId, + userMessage: input.userMessage, + assistantReply: runtime.chatText, + replyType: "factual_with_explanation", + debug: runtime.debug, + modeDecision: input.modeDecision, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent, + messageIdFactory: input.messageIdFactory + }); + return finalization.response; + } + catch (error) { + input.logEvent({ + timestamp: input.nowIso(), + level: "warn", + service: "assistant_loop", + message: "assistant_living_chat_failed_fallback_to_deep", + sessionId: input.sessionId, + details: { + session_id: input.sessionId, + user_message: input.userMessage, + reason: error instanceof Error ? error.message : String(error) + } + }); + return null; + } +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 4bdbda4..7691968 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -79,8 +79,7 @@ const assistantDeepTurnNormalizationRuntimeAdapter_1 = __importStar(require("./a const assistantDeepTurnResponseRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnResponseRuntimeAdapter")); const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter")); const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter")); -const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter")); -const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter")); +const assistantLivingChatHandlerRuntimeAdapter_1 = __importStar(require("./assistantLivingChatHandlerRuntimeAdapter")); const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning")); const iconv_lite_1 = __importDefault(require("iconv-lite")); const DATA_SCOPE_CACHE_TTL_MS = 60_000; @@ -4454,102 +4453,73 @@ class AssistantService { return runtime.response; }; const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => { - try { - const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({ - userMessage, - sessionItems: session.items, - modeDecision, - sessionScope: { - knownOrganizations: sessionOrganizationScope.knownOrganizations, - selectedOrganization: sessionOrganizationScope.selectedOrganization, - activeOrganization: sessionOrganizationScope.activeOrganization - }, - addressRuntimeMeta, - traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`, - toNonEmptyString, - mergeKnownOrganizations, - hasAssistantDataScopeMetaQuestionSignal, - shouldHandleAsAssistantCapabilityMetaQuery, - hasDestructiveDataActionSignal, - hasDangerOrCoercionSignal, - hasOperationalAdminActionRequestSignal, - hasOrganizationFactLookupSignal, - hasOrganizationFactFollowupSignal, - shouldEmitOrganizationSelectionReply, - hasAssistantCapabilityQuestionSignal, - resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), - executeLlmChat: async () => { - const conversationWindow = buildLivingChatContextWindow(session.items); - const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow); - const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520); - const chatResponse = await this.chatClient.chat({ - llmProvider: payload.llmProvider, - apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''), - model: String(payload.model ?? config_1.DEFAULT_MODEL), - baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL, - temperature: payload.temperature ?? 0.35, - maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)) - }, { - systemPrompt: [ - 'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.', - 'Работай честно: не заявляй действия, которые недоступны в этом рантайме.', - 'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.', - 'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.', - 'Если пользователь спрашивает про возможности, отвечай только по этому контракту.', - `Канон поведения: ${canonExcerpt}` - ].join(' '), - developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.', - userMessage: userPrompt, - maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)), - temperature: payload.temperature ?? 0.35 - }); - return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.'); - }, - applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), - applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), - buildAssistantSafetyRefusalReply, - buildAssistantDataScopeContractReply, - buildAssistantOrganizationFactBoundaryReply, - buildAssistantDataScopeSelectionReply, - buildAssistantOperationalBoundaryReply, - buildAssistantCapabilityContractReply - }); - if (!runtime.handled || !runtime.debug) { - return null; - } - const chatText = runtime.chatText; - const debug = runtime.debug; - const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({ - sessionId, - userMessage, - assistantReply: chatText, - replyType: 'factual_with_explanation', - debug, - modeDecision, - appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), - getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), - persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), - cloneConversation: (items) => cloneItems(items), - logEvent: (payload) => (0, log_1.logJson)(payload), - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}` - }); - return finalization.response; - } - catch (error) { - (0, log_1.logJson)({ - timestamp: new Date().toISOString(), - level: 'warn', - service: 'assistant_loop', - message: 'assistant_living_chat_failed_fallback_to_deep', - sessionId, - details: { - session_id: sessionId, - user_message: userMessage, - reason: error instanceof Error ? error.message : String(error) - } - }); - return null; - } + return (0, assistantLivingChatHandlerRuntimeAdapter_1.tryHandleAssistantLivingChatRuntime)({ + sessionId, + userMessage, + sessionItems: session.items, + modeDecision, + sessionScope: { + knownOrganizations: sessionOrganizationScope.knownOrganizations, + selectedOrganization: sessionOrganizationScope.selectedOrganization, + activeOrganization: sessionOrganizationScope.activeOrganization + }, + addressRuntimeMeta, + traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`, + toNonEmptyString, + mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), + executeLlmChat: async () => { + const conversationWindow = buildLivingChatContextWindow(session.items); + const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow); + const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520); + const chatResponse = await this.chatClient.chat({ + llmProvider: payload.llmProvider, + apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''), + model: String(payload.model ?? config_1.DEFAULT_MODEL), + baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL, + temperature: payload.temperature ?? 0.35, + maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)) + }, { + systemPrompt: [ + 'РўС‹ Р¶РёРІРѕР№ русскоязычный ассистент для чтения Рё анализа данных 1РЎ.', + 'Работай честно: РЅРµ заявляй действия, которые недоступны РІ этом рантайме.', + 'Разрешено: анализ Рё объяснение данных, формулировка запросов, подсказки РїРѕ следующему шагу.', + 'Запрещено: обещать настройку 1РЎ, админ-действия, создание/проведение документов или любые изменения РІ базе.', + 'Если пользователь спрашивает РїСЂРѕ возможности, отвечай только РїРѕ этому контракту.', + `Канон поведения: ${canonExcerpt}` + ].join(' '), + developerPrompt: 'Формат: коротко Рё РїРѕ сути, без JSON Рё без служебных блоков. Пиши человеко-понятно.', + userMessage: userPrompt, + maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)), + temperature: payload.temperature ?? 0.35 + }); + return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'РџРѕРЅСЏР». Сформулируйте, что именно РЅСѓР¶РЅРѕ РїРѕ данным 1РЎ, Рё СЏ РїРѕРјРѕРіСѓ РїРѕ шагам.'); + }, + applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), + applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), + buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply, + appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), + getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), + persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), + cloneConversation: (items) => cloneItems(items), + logEvent: (payload) => (0, log_1.logJson)(payload), + messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, + nowIso: () => new Date().toISOString() + }); }; let addressRuntimeMetaForDeep = null; const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => { diff --git a/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts new file mode 100644 index 0000000..befa2b6 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts @@ -0,0 +1,125 @@ +import { + runAssistantLivingChatRuntime, + type AssistantLivingChatRuntimeInput, + type AssistantLivingChatRuntimeOutput +} from "./assistantLivingChatRuntimeAdapter"; +import { + finalizeAssistantLivingChatTurn, + type FinalizeAssistantLivingChatTurnInput +} from "./assistantLivingChatTurnFinalizeRuntimeAdapter"; + +export interface TryHandleAssistantLivingChatRuntimeInput { + sessionId: string; + userMessage: string; + sessionItems: unknown[]; + modeDecision?: AssistantLivingChatRuntimeInput["modeDecision"]; + sessionScope: AssistantLivingChatRuntimeInput["sessionScope"]; + addressRuntimeMeta?: AssistantLivingChatRuntimeInput["addressRuntimeMeta"]; + traceIdFactory: AssistantLivingChatRuntimeInput["traceIdFactory"]; + toNonEmptyString: AssistantLivingChatRuntimeInput["toNonEmptyString"]; + mergeKnownOrganizations: AssistantLivingChatRuntimeInput["mergeKnownOrganizations"]; + hasAssistantDataScopeMetaQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantDataScopeMetaQuestionSignal"]; + shouldHandleAsAssistantCapabilityMetaQuery: AssistantLivingChatRuntimeInput["shouldHandleAsAssistantCapabilityMetaQuery"]; + hasDestructiveDataActionSignal: AssistantLivingChatRuntimeInput["hasDestructiveDataActionSignal"]; + hasDangerOrCoercionSignal: AssistantLivingChatRuntimeInput["hasDangerOrCoercionSignal"]; + hasOperationalAdminActionRequestSignal: AssistantLivingChatRuntimeInput["hasOperationalAdminActionRequestSignal"]; + hasOrganizationFactLookupSignal: AssistantLivingChatRuntimeInput["hasOrganizationFactLookupSignal"]; + hasOrganizationFactFollowupSignal: AssistantLivingChatRuntimeInput["hasOrganizationFactFollowupSignal"]; + shouldEmitOrganizationSelectionReply: AssistantLivingChatRuntimeInput["shouldEmitOrganizationSelectionReply"]; + hasAssistantCapabilityQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantCapabilityQuestionSignal"]; + resolveDataScopeProbe: AssistantLivingChatRuntimeInput["resolveDataScopeProbe"]; + executeLlmChat: AssistantLivingChatRuntimeInput["executeLlmChat"]; + applyScriptGuard: AssistantLivingChatRuntimeInput["applyScriptGuard"]; + applyGroundingGuard: AssistantLivingChatRuntimeInput["applyGroundingGuard"]; + buildAssistantSafetyRefusalReply: AssistantLivingChatRuntimeInput["buildAssistantSafetyRefusalReply"]; + buildAssistantDataScopeContractReply: AssistantLivingChatRuntimeInput["buildAssistantDataScopeContractReply"]; + buildAssistantOrganizationFactBoundaryReply: AssistantLivingChatRuntimeInput["buildAssistantOrganizationFactBoundaryReply"]; + buildAssistantDataScopeSelectionReply: AssistantLivingChatRuntimeInput["buildAssistantDataScopeSelectionReply"]; + buildAssistantOperationalBoundaryReply: AssistantLivingChatRuntimeInput["buildAssistantOperationalBoundaryReply"]; + buildAssistantCapabilityContractReply: AssistantLivingChatRuntimeInput["buildAssistantCapabilityContractReply"]; + appendItem: FinalizeAssistantLivingChatTurnInput["appendItem"]; + getSession: FinalizeAssistantLivingChatTurnInput["getSession"]; + persistSession: FinalizeAssistantLivingChatTurnInput["persistSession"]; + cloneConversation: FinalizeAssistantLivingChatTurnInput["cloneConversation"]; + logEvent: (payload: Record) => void; + messageIdFactory: FinalizeAssistantLivingChatTurnInput["messageIdFactory"]; + nowIso: () => string; + runLivingChatRuntime?: ( + input: AssistantLivingChatRuntimeInput + ) => Promise; + finalizeLivingChatTurn?: ( + input: FinalizeAssistantLivingChatTurnInput + ) => { + response: ResponseType; + }; +} + +export async function tryHandleAssistantLivingChatRuntime( + input: TryHandleAssistantLivingChatRuntimeInput +): Promise { + const runLivingChatRuntimeSafe = input.runLivingChatRuntime ?? runAssistantLivingChatRuntime; + const finalizeLivingChatTurnSafe = input.finalizeLivingChatTurn ?? finalizeAssistantLivingChatTurn; + try { + const runtime = await runLivingChatRuntimeSafe({ + userMessage: input.userMessage, + sessionItems: input.sessionItems, + modeDecision: input.modeDecision, + sessionScope: input.sessionScope, + addressRuntimeMeta: input.addressRuntimeMeta, + traceIdFactory: input.traceIdFactory, + toNonEmptyString: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: input.resolveDataScopeProbe, + executeLlmChat: input.executeLlmChat, + applyScriptGuard: input.applyScriptGuard, + applyGroundingGuard: input.applyGroundingGuard, + buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply + }); + if (!runtime.handled || !runtime.debug) { + return null; + } + const finalization = finalizeLivingChatTurnSafe({ + sessionId: input.sessionId, + userMessage: input.userMessage, + assistantReply: runtime.chatText, + replyType: "factual_with_explanation", + debug: runtime.debug, + modeDecision: input.modeDecision, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent as FinalizeAssistantLivingChatTurnInput["logEvent"], + messageIdFactory: input.messageIdFactory + }); + return finalization.response as ResponseType; + } catch (error) { + input.logEvent({ + timestamp: input.nowIso(), + level: "warn", + service: "assistant_loop", + message: "assistant_living_chat_failed_fallback_to_deep", + sessionId: input.sessionId, + details: { + session_id: input.sessionId, + user_message: input.userMessage, + reason: error instanceof Error ? error.message : String(error) + } + }); + return null; + } +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 563cde5..770bda2 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -33,8 +33,7 @@ import * as assistantDeepTurnNormalizationRuntimeAdapter_1 from "./assistantDeep import * as assistantDeepTurnResponseRuntimeAdapter_1 from "./assistantDeepTurnResponseRuntimeAdapter"; import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter"; import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter"; -import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter"; -import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter"; +import * as assistantLivingChatHandlerRuntimeAdapter_1 from "./assistantLivingChatHandlerRuntimeAdapter"; import * as assistantQueryPlanning_1 from "./assistantQueryPlanning"; import iconv from "iconv-lite"; const DATA_SCOPE_CACHE_TTL_MS = 60_000; @@ -4409,102 +4408,73 @@ export class AssistantService { return runtime.response; }; const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => { - try { - const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({ - userMessage, - sessionItems: session.items, - modeDecision, - sessionScope: { - knownOrganizations: sessionOrganizationScope.knownOrganizations, - selectedOrganization: sessionOrganizationScope.selectedOrganization, - activeOrganization: sessionOrganizationScope.activeOrganization - }, - addressRuntimeMeta, - traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`, - toNonEmptyString, - mergeKnownOrganizations, - hasAssistantDataScopeMetaQuestionSignal, - shouldHandleAsAssistantCapabilityMetaQuery, - hasDestructiveDataActionSignal, - hasDangerOrCoercionSignal, - hasOperationalAdminActionRequestSignal, - hasOrganizationFactLookupSignal, - hasOrganizationFactFollowupSignal, - shouldEmitOrganizationSelectionReply, - hasAssistantCapabilityQuestionSignal, - resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), - executeLlmChat: async () => { - const conversationWindow = buildLivingChatContextWindow(session.items); - const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow); - const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520); - const chatResponse = await this.chatClient.chat({ - llmProvider: payload.llmProvider, - apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''), - model: String(payload.model ?? config_1.DEFAULT_MODEL), - baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL, - temperature: payload.temperature ?? 0.35, - maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)) - }, { - systemPrompt: [ - 'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.', - 'Работай честно: не заявляй действия, которые недоступны в этом рантайме.', - 'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.', - 'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.', - 'Если пользователь спрашивает про возможности, отвечай только по этому контракту.', - `Канон поведения: ${canonExcerpt}` - ].join(' '), - developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.', - userMessage: userPrompt, - maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)), - temperature: payload.temperature ?? 0.35 - }); - return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.'); - }, - applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), - applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), - buildAssistantSafetyRefusalReply, - buildAssistantDataScopeContractReply, - buildAssistantOrganizationFactBoundaryReply, - buildAssistantDataScopeSelectionReply, - buildAssistantOperationalBoundaryReply, - buildAssistantCapabilityContractReply - }); - if (!runtime.handled || !runtime.debug) { - return null; - } - const chatText = runtime.chatText; - const debug = runtime.debug; - const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({ - sessionId, - userMessage, - assistantReply: chatText, - replyType: 'factual_with_explanation', - debug, - modeDecision, - appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), - getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), - persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), - cloneConversation: (items) => cloneItems(items), - logEvent: (payload) => (0, log_1.logJson)(payload), - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}` - }); - return finalization.response; - } - catch (error) { - (0, log_1.logJson)({ - timestamp: new Date().toISOString(), - level: 'warn', - service: 'assistant_loop', - message: 'assistant_living_chat_failed_fallback_to_deep', - sessionId, - details: { - session_id: sessionId, - user_message: userMessage, - reason: error instanceof Error ? error.message : String(error) - } - }); - return null; - } + return (0, assistantLivingChatHandlerRuntimeAdapter_1.tryHandleAssistantLivingChatRuntime)({ + sessionId, + userMessage, + sessionItems: session.items, + modeDecision, + sessionScope: { + knownOrganizations: sessionOrganizationScope.knownOrganizations, + selectedOrganization: sessionOrganizationScope.selectedOrganization, + activeOrganization: sessionOrganizationScope.activeOrganization + }, + addressRuntimeMeta, + traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`, + toNonEmptyString, + mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), + executeLlmChat: async () => { + const conversationWindow = buildLivingChatContextWindow(session.items); + const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow); + const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520); + const chatResponse = await this.chatClient.chat({ + llmProvider: payload.llmProvider, + apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''), + model: String(payload.model ?? config_1.DEFAULT_MODEL), + baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL, + temperature: payload.temperature ?? 0.35, + maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)) + }, { + systemPrompt: [ + 'РўС‹ Р¶РёРІРѕР№ русскоязычный ассистент для чтения Рё анализа данных 1РЎ.', + 'Работай честно: РЅРµ заявляй действия, которые недоступны РІ этом рантайме.', + 'Разрешено: анализ Рё объяснение данных, формулировка запросов, подсказки РїРѕ следующему шагу.', + 'Запрещено: обещать настройку 1РЎ, админ-действия, создание/проведение документов или любые изменения РІ базе.', + 'Если пользователь спрашивает РїСЂРѕ возможности, отвечай только РїРѕ этому контракту.', + `Канон поведения: ${canonExcerpt}` + ].join(' '), + developerPrompt: 'Формат: коротко Рё РїРѕ сути, без JSON Рё без служебных блоков. Пиши человеко-понятно.', + userMessage: userPrompt, + maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)), + temperature: payload.temperature ?? 0.35 + }); + return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'РџРѕРЅСЏР». Сформулируйте, что именно РЅСѓР¶РЅРѕ РїРѕ данным 1РЎ, Рё СЏ РїРѕРјРѕРіСѓ РїРѕ шагам.'); + }, + applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), + applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), + buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply, + appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), + getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), + persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), + cloneConversation: (items) => cloneItems(items), + logEvent: (payload) => (0, log_1.logJson)(payload), + messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, + nowIso: () => new Date().toISOString() + }); }; let addressRuntimeMetaForDeep = null; const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => { diff --git a/llm_normalizer/backend/tests/assistantLivingChatHandlerRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantLivingChatHandlerRuntimeAdapter.test.ts new file mode 100644 index 0000000..055afe3 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantLivingChatHandlerRuntimeAdapter.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it, vi } from "vitest"; +import { tryHandleAssistantLivingChatRuntime } from "../src/services/assistantLivingChatHandlerRuntimeAdapter"; + +function buildInput(overrides: Record = {}) { + return { + sessionId: "asst-1", + userMessage: "question", + sessionItems: [], + modeDecision: { mode: "chat", reason: "living_chat_signal_detected" }, + sessionScope: { + knownOrganizations: [], + selectedOrganization: null, + activeOrganization: null + }, + addressRuntimeMeta: null, + traceIdFactory: () => "chat-trace-1", + toNonEmptyString: (value: unknown) => (typeof value === "string" && value.trim() ? value.trim() : null), + mergeKnownOrganizations: (values: unknown[]) => values.map((v) => String(v)), + hasAssistantDataScopeMetaQuestionSignal: () => false, + shouldHandleAsAssistantCapabilityMetaQuery: () => false, + hasDestructiveDataActionSignal: () => false, + hasDangerOrCoercionSignal: () => false, + hasOperationalAdminActionRequestSignal: () => false, + hasOrganizationFactLookupSignal: () => false, + hasOrganizationFactFollowupSignal: () => false, + shouldEmitOrganizationSelectionReply: () => false, + hasAssistantCapabilityQuestionSignal: () => false, + resolveDataScopeProbe: async () => null, + executeLlmChat: async () => "chat answer", + applyScriptGuard: (text: string) => ({ text, applied: false, reason: null }), + applyGroundingGuard: (payload: { chatText: string }) => ({ + text: payload.chatText, + applied: false, + reason: null + }), + buildAssistantSafetyRefusalReply: () => "safety", + buildAssistantDataScopeContractReply: () => "scope", + buildAssistantOrganizationFactBoundaryReply: () => "boundary", + buildAssistantDataScopeSelectionReply: () => "selection", + buildAssistantOperationalBoundaryReply: () => "operational", + buildAssistantCapabilityContractReply: () => "capability", + appendItem: () => {}, + getSession: () => ({ session_id: "asst-1", updated_at: "", items: [], investigation_state: null } as any), + persistSession: () => {}, + cloneConversation: (items: any[]) => items, + logEvent: vi.fn(), + messageIdFactory: () => "msg-1", + nowIso: () => "2026-04-10T00:00:00.000Z", + ...overrides + } as any; +} + +describe("assistant living chat handler runtime adapter", () => { + it("returns finalized response when runtime is handled", async () => { + const runLivingChatRuntime = vi.fn(async () => ({ + handled: true, + chatText: "chat", + debug: { trace_id: "chat-1" } + })); + const finalizeLivingChatTurn = vi.fn(() => ({ + response: { ok: true, lane: "chat" } + })); + + const response = await tryHandleAssistantLivingChatRuntime( + buildInput({ + runLivingChatRuntime, + finalizeLivingChatTurn + }) + ); + + expect(runLivingChatRuntime).toHaveBeenCalledTimes(1); + expect(finalizeLivingChatTurn).toHaveBeenCalledWith( + expect.objectContaining({ + assistantReply: "chat", + replyType: "factual_with_explanation" + }) + ); + expect(response).toEqual({ ok: true, lane: "chat" }); + }); + + it("returns null when runtime is not handled", async () => { + const finalizeLivingChatTurn = vi.fn(); + const response = await tryHandleAssistantLivingChatRuntime( + buildInput({ + runLivingChatRuntime: async () => ({ + handled: false, + chatText: "", + debug: null + }), + finalizeLivingChatTurn + }) + ); + + expect(response).toBeNull(); + expect(finalizeLivingChatTurn).not.toHaveBeenCalled(); + }); + + it("logs warn and returns null on runtime error", async () => { + const logEvent = vi.fn(); + const response = await tryHandleAssistantLivingChatRuntime( + buildInput({ + runLivingChatRuntime: async () => { + throw new Error("boom"); + }, + logEvent + }) + ); + + expect(response).toBeNull(); + expect(logEvent).toHaveBeenCalledWith( + expect.objectContaining({ + level: "warn", + message: "assistant_living_chat_failed_fallback_to_deep" + }) + ); + }); +});