From 205daeccc52384df8db7f3f3ca0b4b24a4a131cc Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 10 Apr 2026 19:37:56 +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.25=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20address-=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=BA=D0=B8=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=20adapter=20\=20=D0=9F=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B8=D0=BB=20?= =?UTF-8?q?assistantService=20=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20adapter=20(=D0=B1=D0=B5=D0=B7=20=D1=81=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TECH/1CLLMARCH-FACT.md | 71 ++++++- ...istantAddressTurnFinalizeRuntimeAdapter.js | 118 +++++++++++ ...assistantDeepTurnFinalizeRuntimeAdapter.js | 32 +++ .../backend/dist/services/assistantService.js | 128 +++--------- ...istantAddressTurnFinalizeRuntimeAdapter.ts | 190 ++++++++++++++++++ ...assistantDeepTurnFinalizeRuntimeAdapter.ts | 57 ++++++ .../backend/src/services/assistantService.ts | 128 +++--------- ...tAddressTurnFinalizeRuntimeAdapter.test.ts | 167 +++++++++++++++ ...tantDeepTurnFinalizeRuntimeAdapter.test.ts | 112 +++++++++++ 9 files changed, 794 insertions(+), 209 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantAddressTurnFinalizeRuntimeAdapter.js create mode 100644 llm_normalizer/backend/dist/services/assistantDeepTurnFinalizeRuntimeAdapter.js create mode 100644 llm_normalizer/backend/src/services/assistantAddressTurnFinalizeRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/src/services/assistantDeepTurnFinalizeRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/tests/assistantAddressTurnFinalizeRuntimeAdapter.test.ts create mode 100644 llm_normalizer/backend/tests/assistantDeepTurnFinalizeRuntimeAdapter.test.ts diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 2f4c917..3811deb 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -826,7 +826,76 @@ Validation: - `assistantMcpRuntimeBridge.test.ts` - `assistantAddressFollowupContext.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 completed)** +Implemented in current pass (Phase 2.24): +1. Extracted deep-lane finalization/response tail from `assistantService` into dedicated runtime adapter: + - `assistantDeepTurnFinalizeRuntimeAdapter.ts` + - introduced: + - `finalizeAssistantDeepTurn(...)` +2. Centralized finalization runtime sequence (behavior-preserving): + - assistant turn commit + processed-event logging; + - deep-turn API success response assembly from committed conversation state. +3. Rewired `assistantService` deep-lane to consume finalization adapter output (behavior-preserving). +4. Added focused unit tests: + - `assistantDeepTurnFinalizeRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted Stage 2 assembler/adapter pack passed: + - `assistantOrchestrationContracts.test.ts` + - `assistantOrchestrationRuntimeAdapter.test.ts` + - `assistantAnswerPackageBuilder.test.ts` + - `assistantCoverageGrounding.test.ts` + - `assistantQueryPlanning.test.ts` + - `assistantEvidenceBundleAssembler.test.ts` + - `assistantDebugPayloadAssembler.test.ts` + - `assistantMessageLogAssembler.test.ts` + - `assistantContractsBundleAssembler.test.ts` + - `assistantDeepResponseAssembler.test.ts` + - `assistantDeepTurnPackaging.test.ts` + - `assistantDeepTurnInputBuilder.test.ts` + - `assistantInvestigationStateRuntimeAdapter.test.ts` + - `assistantTurnCommitRuntimeAdapter.test.ts` + - `assistantDeepTurnPrePackagingContext.test.ts` + - `assistantDeepTurnResponseBuilder.test.ts` + - `assistantDeepTurnCompositionRuntimeAdapter.test.ts` + - `assistantDeepTurnGuardRuntimeAdapter.test.ts` + - `assistantDeepTurnRetrievalRuntimeAdapter.test.ts` + - `assistantDeepTurnPlanRuntimeAdapter.test.ts` + - `assistantDeepTurnContextRuntimeAdapter.test.ts` + - `assistantDeepTurnGroundingRuntimeAdapter.test.ts` + - `assistantDeepTurnPackagingRuntimeAdapter.test.ts` + - `assistantDeepTurnFinalizeRuntimeAdapter.test.ts` +3. Additional safety regressions passed: + - `assistantWave10SettlementCorrectiveRegression.test.ts` + - `assistantMcpRuntimeBridge.test.ts` + - `assistantAddressFollowupContext.test.ts` + +Implemented in current pass (Phase 2.25): +1. Extracted address-lane finalization/response tail from `assistantService` into dedicated runtime adapter: + - `assistantAddressTurnFinalizeRuntimeAdapter.ts` + - introduced: + - `finalizeAssistantAddressTurn(...)` +2. Centralized address finalization runtime sequence (behavior-preserving): + - assistant item creation for address lane; + - structured `assistant_message_address` processed-event payload build; + - turn commit/persist/log via shared commit runtime adapter; + - API success response assembly from committed conversation state. +3. Rewired `assistantService` address-lane finalize path to consume adapter output (behavior-preserving). +4. Added focused unit tests: + - `assistantAddressTurnFinalizeRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted Stage 2 adapter/finalization pack passed: + - `assistantAddressTurnFinalizeRuntimeAdapter.test.ts` + - `assistantDeepTurnFinalizeRuntimeAdapter.test.ts` + - `assistantTurnCommitRuntimeAdapter.test.ts` +3. Additional safety regressions passed: + - `assistantWave10SettlementCorrectiveRegression.test.ts` + - `assistantMcpRuntimeBridge.test.ts` + - `assistantAddressFollowupContext.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 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantAddressTurnFinalizeRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressTurnFinalizeRuntimeAdapter.js new file mode 100644 index 0000000..4a11e18 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantAddressTurnFinalizeRuntimeAdapter.js @@ -0,0 +1,118 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.finalizeAssistantAddressTurn = finalizeAssistantAddressTurn; +const nanoid_1 = require("nanoid"); +const assistantTurnCommitRuntimeAdapter_1 = require("./assistantTurnCommitRuntimeAdapter"); +function toTraceId(debug) { + const value = debug?.trace_id; + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} +function buildAddressProcessedLogDetails(input, assistantItem) { + const laneDebug = input.addressLaneDebug; + const llmMeta = input.llmPreDecomposeMeta; + const carryover = input.carryoverMeta; + return { + session_id: input.sessionId, + message_id: assistantItem.message_id, + user_message: input.userMessage, + effective_address_user_message: input.effectiveAddressUserMessage, + address_followup_context_applied: Boolean(carryover), + address_followup_context_previous_intent: carryover?.previousAddressIntent ?? null, + address_followup_context_previous_anchor: carryover?.previousAddressAnchor ?? null, + address_llm_predecompose_attempted: Boolean(llmMeta?.attempted), + address_llm_predecompose_applied: Boolean(llmMeta?.applied), + address_llm_predecompose_provider: llmMeta?.provider ?? null, + address_llm_predecompose_trace_id: llmMeta?.traceId ?? null, + address_llm_predecompose_reason: llmMeta?.reason ?? null, + address_fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null, + address_sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null, + address_tool_gate_decision: llmMeta?.toolGateDecision ?? null, + address_tool_gate_reason: llmMeta?.toolGateReason ?? null, + address_dialog_continuation_decision: llmMeta?.dialogContinuationContract?.decision ?? null, + address_dialog_continuation_target_intent: llmMeta?.dialogContinuationContract?.target_intent ?? null, + address_retry_attempted: Boolean(llmMeta?.addressRetryAudit?.attempted), + address_retry_reason: llmMeta?.addressRetryAudit?.reason ?? null, + address_retry_initial_limited_category: llmMeta?.addressRetryAudit?.initial_limited_category ?? null, + address_retry_result_category: llmMeta?.addressRetryAudit?.retry_result_category ?? null, + address_llm_predecompose_contract_intent: llmMeta?.predecomposeContract?.intent ?? null, + address_llm_predecompose_contract_aggregation_profile: llmMeta?.predecomposeContract?.aggregation_profile ?? null, + address_llm_predecompose_contract_period_scope: llmMeta?.predecomposeContract?.period?.scope ?? null, + detected_mode: laneDebug.detected_mode, + query_shape: laneDebug.query_shape, + detected_intent: laneDebug.detected_intent, + extracted_filters: laneDebug.extracted_filters, + selected_recipe: laneDebug.selected_recipe, + mcp_call_status_legacy: laneDebug.mcp_call_status_legacy, + account_scope_mode: laneDebug.account_scope_mode, + account_scope_fallback_applied: laneDebug.account_scope_fallback_applied, + anchor_type: laneDebug.anchor_type, + resolver_confidence: laneDebug.resolver_confidence, + match_failure_stage: laneDebug.match_failure_stage, + match_failure_reason: laneDebug.match_failure_reason, + mcp_call_status: laneDebug.mcp_call_status, + rows_fetched: laneDebug.rows_fetched, + raw_rows_received: laneDebug.raw_rows_received, + rows_after_account_scope: laneDebug.rows_after_account_scope, + rows_after_recipe_filter: laneDebug.rows_after_recipe_filter, + rows_materialized: laneDebug.rows_materialized, + rows_matched: laneDebug.rows_matched, + materialization_drop_reason: laneDebug.materialization_drop_reason, + account_token_raw: laneDebug.account_token_raw, + account_token_normalized: laneDebug.account_token_normalized, + account_scope_fields_checked: laneDebug.account_scope_fields_checked, + account_scope_match_strategy: laneDebug.account_scope_match_strategy, + account_scope_drop_reason: laneDebug.account_scope_drop_reason, + runtime_readiness: laneDebug.runtime_readiness, + limited_reason_category: laneDebug.limited_reason_category, + response_type: laneDebug.response_type, + limitations: laneDebug.limitations, + assistant_reply: assistantItem.text, + reply_type: assistantItem.reply_type, + trace_id: assistantItem.trace_id + }; +} +function finalizeAssistantAddressTurn(input) { + const nowIso = input.nowIso ?? (() => new Date().toISOString()); + const messageIdFactory = input.messageIdFactory ?? (() => `msg-${(0, nanoid_1.nanoid)(10)}`); + const commitSafe = input.commitFn ?? assistantTurnCommitRuntimeAdapter_1.commitAssistantTurnAndLog; + const assistantItem = { + message_id: messageIdFactory(), + session_id: input.sessionId, + role: "assistant", + text: input.assistantReply, + reply_type: input.replyType, + created_at: nowIso(), + trace_id: toTraceId(input.debug), + debug: input.debug + }; + const logDetails = buildAddressProcessedLogDetails(input, assistantItem); + const commitResult = commitSafe({ + sessionId: input.sessionId, + assistantItem, + eventType: "assistant_message_address", + logDetails, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent + }); + const response = { + ok: true, + session_id: input.sessionId, + assistant_reply: assistantItem.text, + reply_type: assistantItem.reply_type, + conversation_item: assistantItem, + debug: input.debug, + conversation: commitResult.conversation + }; + return { + assistantItem, + commitResult, + response + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantDeepTurnFinalizeRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantDeepTurnFinalizeRuntimeAdapter.js new file mode 100644 index 0000000..383b558 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantDeepTurnFinalizeRuntimeAdapter.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.finalizeAssistantDeepTurn = finalizeAssistantDeepTurn; +const assistantDeepTurnResponseBuilder_1 = require("./assistantDeepTurnResponseBuilder"); +const assistantTurnCommitRuntimeAdapter_1 = require("./assistantTurnCommitRuntimeAdapter"); +function finalizeAssistantDeepTurn(input) { + const commitSafe = input.commitFn ?? assistantTurnCommitRuntimeAdapter_1.commitAssistantTurnAndLog; + const buildResponseSafe = input.buildResponseFn ?? assistantDeepTurnResponseBuilder_1.buildAssistantDeepTurnSuccessResponse; + const commitResult = commitSafe({ + sessionId: input.sessionId, + assistantItem: input.assistantItem, + eventType: "assistant_message", + logDetails: input.deepAnalysisLogDetails, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent + }); + const response = buildResponseSafe({ + sessionId: input.sessionId, + assistantReply: input.assistantReply, + replyType: input.replyType, + conversationItem: input.assistantItem, + debug: input.debug, + conversation: commitResult.conversation + }); + return { + commitResult, + response + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 0037054..623c118 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -65,17 +65,17 @@ const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient")) const addressMcpClient_1 = __importStar(require("./addressMcpClient")); const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry")); const assistantCanon_1 = __importStar(require("./assistantCanon")); +const assistantAddressTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantAddressTurnFinalizeRuntimeAdapter")); const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding")); -const assistantDeepTurnResponseBuilder_1 = __importStar(require("./assistantDeepTurnResponseBuilder")); const assistantDeepTurnCompositionRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnCompositionRuntimeAdapter")); const assistantDeepTurnContextRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnContextRuntimeAdapter")); +const assistantDeepTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnFinalizeRuntimeAdapter")); const assistantDeepTurnGuardRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnGuardRuntimeAdapter")); const assistantDeepTurnGroundingRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnGroundingRuntimeAdapter")); const assistantDeepTurnPackagingRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPackagingRuntimeAdapter")); const assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPlanRuntimeAdapter")); const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter")); const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning")); -const assistantTurnCommitRuntimeAdapter_1 = __importStar(require("./assistantTurnCommitRuntimeAdapter")); const iconv_lite_1 = __importDefault(require("iconv-lite")); const DATA_SCOPE_CACHE_TTL_MS = 60_000; const dataScopeProbeCache = new Map(); @@ -4439,98 +4439,24 @@ class AssistantService { if (debugActiveOrganization) { debug.assistant_active_organization = debugActiveOrganization; } - const assistantItem = { - message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, - session_id: sessionId, - role: "assistant", - text: safeAddressReply, - reply_type: addressLane.reply_type, - created_at: new Date().toISOString(), - trace_id: debug.trace_id, - debug - }; - this.sessions.appendItem(sessionId, assistantItem); - const current = this.sessions.getSession(sessionId); - if (current) { - this.sessionLogger.persistSession(current); - } - const conversation = cloneItems(current?.items ?? []); - (0, log_1.logJson)({ - timestamp: new Date().toISOString(), - level: "info", - service: "assistant_loop", - message: "assistant_message_processed", + const finalization = (0, assistantAddressTurnFinalizeRuntimeAdapter_1.finalizeAssistantAddressTurn)({ sessionId, - eventType: "assistant_message_address", - details: { - session_id: sessionId, - message_id: assistantItem.message_id, - user_message: userMessage, - effective_address_user_message: effectiveAddressUserMessage, - address_followup_context_applied: Boolean(carryoverMeta), - address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null, - address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null, - address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted), - address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied), - address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null, - address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null, - address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null, - address_fallback_rule_hit: llmPreDecomposeMeta?.fallbackRuleHit ?? null, - address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null, - address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null, - address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null, - address_dialog_continuation_decision: llmPreDecomposeMeta?.dialogContinuationContract?.decision ?? null, - address_dialog_continuation_target_intent: llmPreDecomposeMeta?.dialogContinuationContract?.target_intent ?? null, - address_retry_attempted: Boolean(llmPreDecomposeMeta?.addressRetryAudit?.attempted), - address_retry_reason: llmPreDecomposeMeta?.addressRetryAudit?.reason ?? null, - address_retry_initial_limited_category: llmPreDecomposeMeta?.addressRetryAudit?.initial_limited_category ?? null, - address_retry_result_category: llmPreDecomposeMeta?.addressRetryAudit?.retry_result_category ?? null, - address_llm_predecompose_contract_intent: llmPreDecomposeMeta?.predecomposeContract?.intent ?? null, - address_llm_predecompose_contract_aggregation_profile: llmPreDecomposeMeta?.predecomposeContract?.aggregation_profile ?? null, - address_llm_predecompose_contract_period_scope: llmPreDecomposeMeta?.predecomposeContract?.period?.scope ?? null, - detected_mode: addressLane.debug.detected_mode, - query_shape: addressLane.debug.query_shape, - detected_intent: addressLane.debug.detected_intent, - extracted_filters: addressLane.debug.extracted_filters, - selected_recipe: addressLane.debug.selected_recipe, - mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, - account_scope_mode: addressLane.debug.account_scope_mode, - account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, - anchor_type: addressLane.debug.anchor_type, - resolver_confidence: addressLane.debug.resolver_confidence, - match_failure_stage: addressLane.debug.match_failure_stage, - match_failure_reason: addressLane.debug.match_failure_reason, - mcp_call_status: addressLane.debug.mcp_call_status, - rows_fetched: addressLane.debug.rows_fetched, - raw_rows_received: addressLane.debug.raw_rows_received, - rows_after_account_scope: addressLane.debug.rows_after_account_scope, - rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, - rows_materialized: addressLane.debug.rows_materialized, - rows_matched: addressLane.debug.rows_matched, - materialization_drop_reason: addressLane.debug.materialization_drop_reason, - account_token_raw: addressLane.debug.account_token_raw, - account_token_normalized: addressLane.debug.account_token_normalized, - account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, - account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, - account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, - runtime_readiness: addressLane.debug.runtime_readiness, - limited_reason_category: addressLane.debug.limited_reason_category, - response_type: addressLane.debug.response_type, - limitations: addressLane.debug.limitations, - assistant_reply: assistantItem.text, - reply_type: assistantItem.reply_type, - trace_id: assistantItem.trace_id - } - }); - return { - ok: true, - session_id: sessionId, - assistant_reply: assistantItem.text, - reply_type: assistantItem.reply_type, - conversation_item: assistantItem, + userMessage, + effectiveAddressUserMessage, + assistantReply: safeAddressReply, + replyType: addressLane.reply_type, + addressLaneDebug: addressLane.debug, debug, - conversation - }; + carryoverMeta, + llmPreDecomposeMeta, + 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; }; const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => { try { @@ -5098,26 +5024,20 @@ class AssistantService { const debug = packagingRuntime.debug; const assistantItem = packagingRuntime.assistantItem; const deepAnalysisLogDetails = packagingRuntime.deepAnalysisLogDetails; - const commitResult = (0, assistantTurnCommitRuntimeAdapter_1.commitAssistantTurnAndLog)({ + const finalization = (0, assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn)({ sessionId, + assistantReply: safeAssistantReply, + replyType: composition.reply_type, assistantItem, - eventType: "assistant_message", - logDetails: deepAnalysisLogDetails, + debug, + deepAnalysisLogDetails, 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) }); - const conversation = commitResult.conversation; - return (0, assistantDeepTurnResponseBuilder_1.buildAssistantDeepTurnSuccessResponse)({ - sessionId, - assistantReply: safeAssistantReply, - replyType: composition.reply_type, - conversationItem: assistantItem, - debug, - conversation - }); + return finalization.response; } } exports.AssistantService = AssistantService; diff --git a/llm_normalizer/backend/src/services/assistantAddressTurnFinalizeRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressTurnFinalizeRuntimeAdapter.ts new file mode 100644 index 0000000..f1f914e --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantAddressTurnFinalizeRuntimeAdapter.ts @@ -0,0 +1,190 @@ +import { nanoid } from "nanoid"; +import type { + AssistantConversationItem, + AssistantDebugPayload, + AssistantMessageResponsePayload, + AssistantReplyType +} from "../types/assistant"; +import type { AddressExecutionDebug } from "../types/addressQuery"; +import type { CommitAssistantTurnAndLogOutput } from "./assistantTurnCommitRuntimeAdapter"; +import { commitAssistantTurnAndLog } from "./assistantTurnCommitRuntimeAdapter"; + +export interface AddressCarryoverMetaLogInput { + previousAddressIntent?: string | null; + previousAddressAnchor?: string | null; +} + +export interface AddressLlmPreDecomposeMetaLogInput { + attempted?: boolean; + applied?: boolean; + provider?: string | null; + traceId?: string | null; + reason?: string | null; + fallbackRuleHit?: string | null; + sanitizedUserMessage?: string | null; + toolGateDecision?: string | null; + toolGateReason?: string | null; + dialogContinuationContract?: { + decision?: string | null; + target_intent?: string | null; + } | null; + addressRetryAudit?: { + attempted?: boolean; + reason?: string | null; + initial_limited_category?: string | null; + retry_result_category?: string | null; + } | null; + predecomposeContract?: { + intent?: string | null; + aggregation_profile?: string | null; + period?: { + scope?: string | null; + } | null; + } | null; +} + +export interface FinalizeAssistantAddressTurnInput { + sessionId: string; + userMessage: string; + effectiveAddressUserMessage: string; + assistantReply: string; + replyType: AssistantReplyType; + addressLaneDebug: AddressExecutionDebug; + debug: AssistantDebugPayload | Record; + carryoverMeta?: AddressCarryoverMetaLogInput | null; + llmPreDecomposeMeta?: AddressLlmPreDecomposeMetaLogInput | null; + appendItem: Parameters[0]["appendItem"]; + getSession: Parameters[0]["getSession"]; + persistSession: Parameters[0]["persistSession"]; + cloneConversation: Parameters[0]["cloneConversation"]; + logEvent: Parameters[0]["logEvent"]; + nowIso?: () => string; + messageIdFactory?: () => string; + commitFn?: typeof commitAssistantTurnAndLog; +} + +export interface FinalizeAssistantAddressTurnOutput { + assistantItem: AssistantConversationItem; + commitResult: CommitAssistantTurnAndLogOutput; + response: AssistantMessageResponsePayload; +} + +function toTraceId(debug: AssistantDebugPayload | Record): string | null { + const value = (debug as Record | null | undefined)?.trace_id; + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function buildAddressProcessedLogDetails(input: FinalizeAssistantAddressTurnInput, assistantItem: AssistantConversationItem) { + const laneDebug = input.addressLaneDebug; + const llmMeta = input.llmPreDecomposeMeta; + const carryover = input.carryoverMeta; + + return { + session_id: input.sessionId, + message_id: assistantItem.message_id, + user_message: input.userMessage, + effective_address_user_message: input.effectiveAddressUserMessage, + address_followup_context_applied: Boolean(carryover), + address_followup_context_previous_intent: carryover?.previousAddressIntent ?? null, + address_followup_context_previous_anchor: carryover?.previousAddressAnchor ?? null, + address_llm_predecompose_attempted: Boolean(llmMeta?.attempted), + address_llm_predecompose_applied: Boolean(llmMeta?.applied), + address_llm_predecompose_provider: llmMeta?.provider ?? null, + address_llm_predecompose_trace_id: llmMeta?.traceId ?? null, + address_llm_predecompose_reason: llmMeta?.reason ?? null, + address_fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null, + address_sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null, + address_tool_gate_decision: llmMeta?.toolGateDecision ?? null, + address_tool_gate_reason: llmMeta?.toolGateReason ?? null, + address_dialog_continuation_decision: llmMeta?.dialogContinuationContract?.decision ?? null, + address_dialog_continuation_target_intent: llmMeta?.dialogContinuationContract?.target_intent ?? null, + address_retry_attempted: Boolean(llmMeta?.addressRetryAudit?.attempted), + address_retry_reason: llmMeta?.addressRetryAudit?.reason ?? null, + address_retry_initial_limited_category: llmMeta?.addressRetryAudit?.initial_limited_category ?? null, + address_retry_result_category: llmMeta?.addressRetryAudit?.retry_result_category ?? null, + address_llm_predecompose_contract_intent: llmMeta?.predecomposeContract?.intent ?? null, + address_llm_predecompose_contract_aggregation_profile: llmMeta?.predecomposeContract?.aggregation_profile ?? null, + address_llm_predecompose_contract_period_scope: llmMeta?.predecomposeContract?.period?.scope ?? null, + detected_mode: laneDebug.detected_mode, + query_shape: laneDebug.query_shape, + detected_intent: laneDebug.detected_intent, + extracted_filters: laneDebug.extracted_filters, + selected_recipe: laneDebug.selected_recipe, + mcp_call_status_legacy: laneDebug.mcp_call_status_legacy, + account_scope_mode: laneDebug.account_scope_mode, + account_scope_fallback_applied: laneDebug.account_scope_fallback_applied, + anchor_type: laneDebug.anchor_type, + resolver_confidence: laneDebug.resolver_confidence, + match_failure_stage: laneDebug.match_failure_stage, + match_failure_reason: laneDebug.match_failure_reason, + mcp_call_status: laneDebug.mcp_call_status, + rows_fetched: laneDebug.rows_fetched, + raw_rows_received: laneDebug.raw_rows_received, + rows_after_account_scope: laneDebug.rows_after_account_scope, + rows_after_recipe_filter: laneDebug.rows_after_recipe_filter, + rows_materialized: laneDebug.rows_materialized, + rows_matched: laneDebug.rows_matched, + materialization_drop_reason: laneDebug.materialization_drop_reason, + account_token_raw: laneDebug.account_token_raw, + account_token_normalized: laneDebug.account_token_normalized, + account_scope_fields_checked: laneDebug.account_scope_fields_checked, + account_scope_match_strategy: laneDebug.account_scope_match_strategy, + account_scope_drop_reason: laneDebug.account_scope_drop_reason, + runtime_readiness: laneDebug.runtime_readiness, + limited_reason_category: laneDebug.limited_reason_category, + response_type: laneDebug.response_type, + limitations: laneDebug.limitations, + assistant_reply: assistantItem.text, + reply_type: assistantItem.reply_type, + trace_id: assistantItem.trace_id + }; +} + +export function finalizeAssistantAddressTurn( + input: FinalizeAssistantAddressTurnInput +): FinalizeAssistantAddressTurnOutput { + const nowIso = input.nowIso ?? (() => new Date().toISOString()); + const messageIdFactory = input.messageIdFactory ?? (() => `msg-${nanoid(10)}`); + const commitSafe = input.commitFn ?? commitAssistantTurnAndLog; + const assistantItem: AssistantConversationItem = { + message_id: messageIdFactory(), + session_id: input.sessionId, + role: "assistant", + text: input.assistantReply, + reply_type: input.replyType, + created_at: nowIso(), + trace_id: toTraceId(input.debug), + debug: input.debug as AssistantDebugPayload + }; + const logDetails = buildAddressProcessedLogDetails(input, assistantItem); + const commitResult = commitSafe({ + sessionId: input.sessionId, + assistantItem, + eventType: "assistant_message_address", + logDetails, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent + }); + const response: AssistantMessageResponsePayload = { + ok: true, + session_id: input.sessionId, + assistant_reply: assistantItem.text, + reply_type: assistantItem.reply_type as AssistantReplyType, + conversation_item: assistantItem, + debug: input.debug as AssistantDebugPayload, + conversation: commitResult.conversation + }; + + return { + assistantItem, + commitResult, + response + }; +} diff --git a/llm_normalizer/backend/src/services/assistantDeepTurnFinalizeRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantDeepTurnFinalizeRuntimeAdapter.ts new file mode 100644 index 0000000..f62e07be --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantDeepTurnFinalizeRuntimeAdapter.ts @@ -0,0 +1,57 @@ +import type { AssistantConversationItem, AssistantDebugPayload, AssistantMessageResponsePayload, AssistantReplyType } from "../types/assistant"; +import { buildAssistantDeepTurnSuccessResponse } from "./assistantDeepTurnResponseBuilder"; +import type { CommitAssistantTurnAndLogOutput } from "./assistantTurnCommitRuntimeAdapter"; +import { commitAssistantTurnAndLog } from "./assistantTurnCommitRuntimeAdapter"; + +export interface FinalizeAssistantDeepTurnInput { + sessionId: string; + assistantReply: string; + replyType: AssistantReplyType; + assistantItem: AssistantConversationItem; + debug: AssistantDebugPayload | Record; + deepAnalysisLogDetails: Record; + appendItem: Parameters[0]["appendItem"]; + getSession: Parameters[0]["getSession"]; + persistSession: Parameters[0]["persistSession"]; + cloneConversation: Parameters[0]["cloneConversation"]; + logEvent: Parameters[0]["logEvent"]; + commitFn?: typeof commitAssistantTurnAndLog; + buildResponseFn?: typeof buildAssistantDeepTurnSuccessResponse; +} + +export interface FinalizeAssistantDeepTurnOutput { + commitResult: CommitAssistantTurnAndLogOutput; + response: AssistantMessageResponsePayload; +} + +export function finalizeAssistantDeepTurn( + input: FinalizeAssistantDeepTurnInput +): FinalizeAssistantDeepTurnOutput { + const commitSafe = input.commitFn ?? commitAssistantTurnAndLog; + const buildResponseSafe = input.buildResponseFn ?? buildAssistantDeepTurnSuccessResponse; + + const commitResult = commitSafe({ + sessionId: input.sessionId, + assistantItem: input.assistantItem, + eventType: "assistant_message", + logDetails: input.deepAnalysisLogDetails, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent + }); + const response = buildResponseSafe({ + sessionId: input.sessionId, + assistantReply: input.assistantReply, + replyType: input.replyType, + conversationItem: input.assistantItem, + debug: input.debug, + conversation: commitResult.conversation + }); + + return { + commitResult, + response + }; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 8ad08a4..6fbc0ce 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -19,17 +19,17 @@ import * as openaiResponsesClient_1 from "./openaiResponsesClient"; import * as addressMcpClient_1 from "./addressMcpClient"; import * as capabilitiesRegistry_1 from "./capabilitiesRegistry"; import * as assistantCanon_1 from "./assistantCanon"; +import * as assistantAddressTurnFinalizeRuntimeAdapter_1 from "./assistantAddressTurnFinalizeRuntimeAdapter"; import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; -import * as assistantDeepTurnResponseBuilder_1 from "./assistantDeepTurnResponseBuilder"; import * as assistantDeepTurnCompositionRuntimeAdapter_1 from "./assistantDeepTurnCompositionRuntimeAdapter"; import * as assistantDeepTurnContextRuntimeAdapter_1 from "./assistantDeepTurnContextRuntimeAdapter"; +import * as assistantDeepTurnFinalizeRuntimeAdapter_1 from "./assistantDeepTurnFinalizeRuntimeAdapter"; import * as assistantDeepTurnGuardRuntimeAdapter_1 from "./assistantDeepTurnGuardRuntimeAdapter"; import * as assistantDeepTurnGroundingRuntimeAdapter_1 from "./assistantDeepTurnGroundingRuntimeAdapter"; import * as assistantDeepTurnPackagingRuntimeAdapter_1 from "./assistantDeepTurnPackagingRuntimeAdapter"; import * as assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanRuntimeAdapter"; import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter"; import * as assistantQueryPlanning_1 from "./assistantQueryPlanning"; -import * as assistantTurnCommitRuntimeAdapter_1 from "./assistantTurnCommitRuntimeAdapter"; import iconv from "iconv-lite"; const DATA_SCOPE_CACHE_TTL_MS = 60_000; const dataScopeProbeCache = new Map(); @@ -4394,98 +4394,24 @@ export class AssistantService { if (debugActiveOrganization) { debug.assistant_active_organization = debugActiveOrganization; } - const assistantItem = { - message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, - session_id: sessionId, - role: "assistant", - text: safeAddressReply, - reply_type: addressLane.reply_type, - created_at: new Date().toISOString(), - trace_id: debug.trace_id, - debug - }; - this.sessions.appendItem(sessionId, assistantItem); - const current = this.sessions.getSession(sessionId); - if (current) { - this.sessionLogger.persistSession(current); - } - const conversation = cloneItems(current?.items ?? []); - (0, log_1.logJson)({ - timestamp: new Date().toISOString(), - level: "info", - service: "assistant_loop", - message: "assistant_message_processed", + const finalization = (0, assistantAddressTurnFinalizeRuntimeAdapter_1.finalizeAssistantAddressTurn)({ sessionId, - eventType: "assistant_message_address", - details: { - session_id: sessionId, - message_id: assistantItem.message_id, - user_message: userMessage, - effective_address_user_message: effectiveAddressUserMessage, - address_followup_context_applied: Boolean(carryoverMeta), - address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null, - address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null, - address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted), - address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied), - address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null, - address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null, - address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null, - address_fallback_rule_hit: llmPreDecomposeMeta?.fallbackRuleHit ?? null, - address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null, - address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null, - address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null, - address_dialog_continuation_decision: llmPreDecomposeMeta?.dialogContinuationContract?.decision ?? null, - address_dialog_continuation_target_intent: llmPreDecomposeMeta?.dialogContinuationContract?.target_intent ?? null, - address_retry_attempted: Boolean(llmPreDecomposeMeta?.addressRetryAudit?.attempted), - address_retry_reason: llmPreDecomposeMeta?.addressRetryAudit?.reason ?? null, - address_retry_initial_limited_category: llmPreDecomposeMeta?.addressRetryAudit?.initial_limited_category ?? null, - address_retry_result_category: llmPreDecomposeMeta?.addressRetryAudit?.retry_result_category ?? null, - address_llm_predecompose_contract_intent: llmPreDecomposeMeta?.predecomposeContract?.intent ?? null, - address_llm_predecompose_contract_aggregation_profile: llmPreDecomposeMeta?.predecomposeContract?.aggregation_profile ?? null, - address_llm_predecompose_contract_period_scope: llmPreDecomposeMeta?.predecomposeContract?.period?.scope ?? null, - detected_mode: addressLane.debug.detected_mode, - query_shape: addressLane.debug.query_shape, - detected_intent: addressLane.debug.detected_intent, - extracted_filters: addressLane.debug.extracted_filters, - selected_recipe: addressLane.debug.selected_recipe, - mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, - account_scope_mode: addressLane.debug.account_scope_mode, - account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, - anchor_type: addressLane.debug.anchor_type, - resolver_confidence: addressLane.debug.resolver_confidence, - match_failure_stage: addressLane.debug.match_failure_stage, - match_failure_reason: addressLane.debug.match_failure_reason, - mcp_call_status: addressLane.debug.mcp_call_status, - rows_fetched: addressLane.debug.rows_fetched, - raw_rows_received: addressLane.debug.raw_rows_received, - rows_after_account_scope: addressLane.debug.rows_after_account_scope, - rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, - rows_materialized: addressLane.debug.rows_materialized, - rows_matched: addressLane.debug.rows_matched, - materialization_drop_reason: addressLane.debug.materialization_drop_reason, - account_token_raw: addressLane.debug.account_token_raw, - account_token_normalized: addressLane.debug.account_token_normalized, - account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, - account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, - account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, - runtime_readiness: addressLane.debug.runtime_readiness, - limited_reason_category: addressLane.debug.limited_reason_category, - response_type: addressLane.debug.response_type, - limitations: addressLane.debug.limitations, - assistant_reply: assistantItem.text, - reply_type: assistantItem.reply_type, - trace_id: assistantItem.trace_id - } - }); - return { - ok: true, - session_id: sessionId, - assistant_reply: assistantItem.text, - reply_type: assistantItem.reply_type, - conversation_item: assistantItem, + userMessage, + effectiveAddressUserMessage, + assistantReply: safeAddressReply, + replyType: addressLane.reply_type, + addressLaneDebug: addressLane.debug, debug, - conversation - }; + carryoverMeta, + llmPreDecomposeMeta, + 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; }; const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => { try { @@ -5053,26 +4979,20 @@ export class AssistantService { const debug = packagingRuntime.debug; const assistantItem = packagingRuntime.assistantItem; const deepAnalysisLogDetails = packagingRuntime.deepAnalysisLogDetails; - const commitResult = (0, assistantTurnCommitRuntimeAdapter_1.commitAssistantTurnAndLog)({ + const finalization = (0, assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn)({ sessionId, + assistantReply: safeAssistantReply, + replyType: composition.reply_type, assistantItem, - eventType: "assistant_message", - logDetails: deepAnalysisLogDetails, + debug, + deepAnalysisLogDetails, 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) }); - const conversation = commitResult.conversation; - return (0, assistantDeepTurnResponseBuilder_1.buildAssistantDeepTurnSuccessResponse)({ - sessionId, - assistantReply: safeAssistantReply, - replyType: composition.reply_type, - conversationItem: assistantItem, - debug, - conversation - }); + return finalization.response; } } diff --git a/llm_normalizer/backend/tests/assistantAddressTurnFinalizeRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressTurnFinalizeRuntimeAdapter.test.ts new file mode 100644 index 0000000..801787f --- /dev/null +++ b/llm_normalizer/backend/tests/assistantAddressTurnFinalizeRuntimeAdapter.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from "vitest"; +import { finalizeAssistantAddressTurn } from "../src/services/assistantAddressTurnFinalizeRuntimeAdapter"; + +function buildLaneDebug() { + return { + detected_mode: "address_query", + detected_mode_confidence: "high", + query_shape: "DOCUMENT_LIST", + query_shape_confidence: "high", + detected_intent: "list_documents_by_counterparty", + detected_intent_confidence: "high", + extracted_filters: { counterparty: "свк", limit: 20 }, + missing_required_filters: [], + selected_recipe: "address_documents_by_counterparty_v1", + mcp_call_status_legacy: "matched_non_empty", + account_scope_mode: "preferred", + account_scope_fallback_applied: false, + anchor_type: "counterparty", + anchor_value_raw: "свк", + anchor_value_resolved: "Группа СВК", + resolver_confidence: "medium", + ambiguity_count: 0, + match_failure_stage: "none", + match_failure_reason: null, + mcp_call_status: "matched_non_empty", + rows_fetched: 20, + raw_rows_received: 20, + rows_after_account_scope: 5, + rows_after_recipe_filter: 3, + rows_materialized: 5, + rows_matched: 3, + raw_row_keys_sample: [], + materialization_drop_reason: "none", + account_token_raw: null, + account_token_normalized: null, + account_scope_fields_checked: ["account_dt", "account_kt", "registrator", "analytics"], + account_scope_match_strategy: "account_code_regex_plus_alias_map_v1", + account_scope_drop_reason: "not_applicable", + runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", + limited_reason_category: null, + response_type: "FACTUAL_LIST", + limitations: [], + reasons: [] + } as any; +} + +describe("assistant address turn finalize runtime adapter", () => { + it("builds assistant item and passes expected log details into commit runtime", () => { + const laneDebug = buildLaneDebug(); + const commitCalls: Array> = []; + const output = finalizeAssistantAddressTurn({ + sessionId: "asst-1", + userMessage: "покажи документы по свк", + effectiveAddressUserMessage: "документы по контрагенту свк", + assistantReply: "Собран список документов.", + replyType: "factual", + addressLaneDebug: laneDebug, + debug: { trace_id: "address-trace-1", x: 1 } as any, + carryoverMeta: { + previousAddressIntent: "list_documents_by_counterparty", + previousAddressAnchor: "Группа СВК" + }, + llmPreDecomposeMeta: { + attempted: true, + applied: true, + provider: "openai", + traceId: "predec-1", + reason: "llm_contract_ok", + fallbackRuleHit: null, + sanitizedUserMessage: "покажи документы по свк", + toolGateDecision: "run_address_lane", + toolGateReason: "address_mode_classifier_detected", + dialogContinuationContract: { + decision: "new_topic", + target_intent: null + }, + addressRetryAudit: { + attempted: false, + reason: null, + initial_limited_category: null, + retry_result_category: null + }, + predecomposeContract: { + intent: "list_documents_by_counterparty", + aggregation_profile: "list_lookup", + period: { + scope: "year" + } + } + }, + appendItem: () => {}, + getSession: () => null, + persistSession: () => {}, + cloneConversation: () => [], + logEvent: () => {}, + nowIso: () => "2026-04-10T12:00:00.000Z", + messageIdFactory: () => "msg-fixed-1", + commitFn: ((input: Record) => { + commitCalls.push(input); + return { + currentSession: null, + conversation: [] + }; + }) as any + }); + + expect(output.assistantItem.message_id).toBe("msg-fixed-1"); + expect(output.assistantItem.created_at).toBe("2026-04-10T12:00:00.000Z"); + expect(output.assistantItem.trace_id).toBe("address-trace-1"); + expect(commitCalls).toHaveLength(1); + expect(commitCalls[0]?.["eventType"]).toBe("assistant_message_address"); + const logDetails = commitCalls[0]?.["logDetails"] as Record; + expect(logDetails?.["session_id"]).toBe("asst-1"); + expect(logDetails?.["effective_address_user_message"]).toBe("документы по контрагенту свк"); + expect(logDetails?.["address_followup_context_applied"]).toBe(true); + expect(logDetails?.["address_llm_predecompose_attempted"]).toBe(true); + expect(logDetails?.["address_dialog_continuation_decision"]).toBe("new_topic"); + expect(logDetails?.["assistant_reply"]).toBe("Собран список документов."); + expect(output.response.assistant_reply).toBe("Собран список документов."); + expect(output.response.reply_type).toBe("factual"); + }); + + it("uses default commit runtime and returns conversation from stored session", () => { + const laneDebug = buildLaneDebug(); + let appendCalls = 0; + let persistCalls = 0; + let logCalls = 0; + let storedSession: any = null; + const output = finalizeAssistantAddressTurn({ + sessionId: "asst-2", + userMessage: "покажи документы", + effectiveAddressUserMessage: "документы", + assistantReply: "Готово", + replyType: "partial_coverage", + addressLaneDebug: laneDebug, + debug: { trace_id: "address-trace-2" } as any, + appendItem: (_sessionId, item) => { + appendCalls += 1; + storedSession = { + session_id: "asst-2", + updated_at: "2026-04-10T12:00:00.000Z", + items: [item], + investigation_state: null + }; + }, + getSession: () => storedSession, + persistSession: () => { + persistCalls += 1; + }, + cloneConversation: (items) => items.map((item) => ({ ...item })), + logEvent: () => { + logCalls += 1; + }, + messageIdFactory: () => "msg-fixed-2", + nowIso: () => "2026-04-10T12:10:00.000Z" + }); + + expect(appendCalls).toBe(1); + expect(persistCalls).toBe(1); + expect(logCalls).toBe(1); + expect(output.response.ok).toBe(true); + expect(output.response.session_id).toBe("asst-2"); + expect(output.response.reply_type).toBe("partial_coverage"); + expect(output.response.conversation).toHaveLength(1); + expect(output.response.conversation_item.message_id).toBe("msg-fixed-2"); + }); +}); diff --git a/llm_normalizer/backend/tests/assistantDeepTurnFinalizeRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantDeepTurnFinalizeRuntimeAdapter.test.ts new file mode 100644 index 0000000..06e24c5 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantDeepTurnFinalizeRuntimeAdapter.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import { finalizeAssistantDeepTurn } from "../src/services/assistantDeepTurnFinalizeRuntimeAdapter"; + +describe("assistant deep turn finalize runtime adapter", () => { + it("commits assistant turn and builds response with committed conversation", () => { + const callOrder: string[] = []; + const conversation = [ + { + message_id: "msg-1", + session_id: "asst-1", + role: "assistant", + text: "ok", + reply_type: "factual", + created_at: "2026-04-10T10:00:00.000Z", + trace_id: "trace-1", + debug: null + } + ] as any; + + const output = finalizeAssistantDeepTurn({ + sessionId: "asst-1", + assistantReply: "safe-reply", + replyType: "factual", + assistantItem: conversation[0], + debug: { d: 1 }, + deepAnalysisLogDetails: { stage: "deep" }, + appendItem: () => {}, + getSession: () => null, + persistSession: () => {}, + cloneConversation: () => [], + logEvent: () => {}, + commitFn: ((input: Record) => { + callOrder.push("commit"); + expect(input.eventType).toBe("assistant_message"); + expect(input.sessionId).toBe("asst-1"); + expect(input.logDetails).toEqual({ stage: "deep" }); + return { + currentSession: null, + conversation + }; + }) as any, + buildResponseFn: ((input: Record) => { + callOrder.push("response"); + expect(input.conversation).toBe(conversation); + return { + ok: true, + session_id: "asst-1", + assistant_reply: "safe-reply", + reply_type: "factual", + conversation_item: conversation[0], + debug: { d: 1 }, + conversation + }; + }) as any + }); + + expect(callOrder).toEqual(["commit", "response"]); + expect(output.commitResult.conversation).toBe(conversation); + expect(output.response.assistant_reply).toBe("safe-reply"); + expect(output.response.conversation).toBe(conversation as any); + }); + + it("uses default commit/response functions when custom hooks are not provided", () => { + const assistantItem = { + message_id: "msg-1", + session_id: "asst-1", + role: "assistant", + text: "ok", + reply_type: "factual", + created_at: "2026-04-10T10:00:00.000Z", + trace_id: "trace-1", + debug: null + } as any; + const storedSession = { + session_id: "asst-1", + updated_at: "2026-04-10T10:00:00.000Z", + items: [assistantItem], + investigation_state: null + }; + let appendCalled = 0; + let persistCalled = 0; + let logCalled = 0; + + const output = finalizeAssistantDeepTurn({ + sessionId: "asst-1", + assistantReply: "safe-reply", + replyType: "factual", + assistantItem, + debug: { debug: true }, + deepAnalysisLogDetails: { info: "x" }, + appendItem: () => { + appendCalled += 1; + }, + getSession: () => storedSession as any, + persistSession: () => { + persistCalled += 1; + }, + cloneConversation: (items) => items.map((item) => ({ ...item })), + logEvent: () => { + logCalled += 1; + } + }); + + expect(appendCalled).toBe(1); + expect(persistCalled).toBe(1); + expect(logCalled).toBe(1); + expect(output.response.ok).toBe(true); + expect(output.response.session_id).toBe("asst-1"); + expect(output.response.reply_type).toBe("factual"); + expect(output.response.conversation.length).toBe(1); + }); +});