From 9c22460e8bf261c0cb858c1a000bb43ee4c7fde8 Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 10 Apr 2026 22:26:05 +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.34=20=20=D0=B2=D1=8B=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=20address-=D0=B2=D0=B5=D1=82=D0=BA=D1=83=20(orchestration=20+?= =?UTF-8?q?=20lane=20+=20finalize)=20=D0=B2=20=D0=B5=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20runtime-=D0=BE=D1=80=D0=BA=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D0=BE=D1=80,=20=D1=87=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=20handleMessage=20=D1=81=D1=82=D0=B0=D0=BB=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=87=D1=82=D0=B8=20=D0=BF=D0=BB=D0=BE=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TECH/1CLLMARCH-FACT.md | 60 +++++- ...istantAddressLaneResponseRuntimeAdapter.js | 46 ++++ .../assistantAddressRuntimeAdapter.js | 87 ++++++++ .../backend/dist/services/assistantService.js | 142 +++++-------- ...istantAddressLaneResponseRuntimeAdapter.ts | 89 ++++++++ .../assistantAddressRuntimeAdapter.ts | 189 +++++++++++++++++ .../backend/src/services/assistantService.ts | 142 +++++-------- ...tAddressLaneResponseRuntimeAdapter.test.ts | 103 +++++++++ .../assistantAddressRuntimeAdapter.test.ts | 196 ++++++++++++++++++ 9 files changed, 869 insertions(+), 185 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js create mode 100644 llm_normalizer/backend/dist/services/assistantAddressRuntimeAdapter.js create mode 100644 llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/src/services/assistantAddressRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts create mode 100644 llm_normalizer/backend/tests/assistantAddressRuntimeAdapter.test.ts diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 125ca5f..f2216d9 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -1098,7 +1098,65 @@ 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 completed)** +Implemented in current pass (Phase 2.34): +1. Extracted top-level address branch orchestration from `assistantService` into dedicated runtime adapter: + - `assistantAddressRuntimeAdapter.ts` + - introduced: + - `runAssistantAddressRuntime(...)` +2. Centralized full address-branch control flow (behavior-preserving): + - address bootstrap orchestration stage; + - tool-gate skip/chat fallback stage; + - lane execution/retry stage with analysis-date hint propagation; + - address finalize stage projection with retry audit merge. +3. Rewired `assistantService` address branch to a single runtime adapter invocation and preserved `addressRuntimeMetaForDeep` propagation contract. +4. Added focused unit tests: + - `assistantAddressRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `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` + +Implemented in current pass (Phase 2.35): +1. Extracted address-lane response-tail (debug enrichment + finalize projection) from `assistantService` into dedicated runtime adapter: + - `assistantAddressLaneResponseRuntimeAdapter.ts` + - introduced: + - `runAssistantAddressLaneResponseRuntime(...)` +2. Centralized address response-tail sequence (behavior-preserving): + - reply sanitization and structured address debug payload assembly; + - followup-offer projection + known/active organization debug enrichment; + - address turn finalization through existing finalize adapter contract. +3. Rewired `assistantService` `finalizeAddressLaneResponse(...)` closure to consume response runtime adapter output. +4. Added focused unit tests: + - `assistantAddressLaneResponseRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `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 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js new file mode 100644 index 0000000..69edee9 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js @@ -0,0 +1,46 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponseRuntime; +const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter"); +function runAssistantAddressLaneResponseRuntime(input) { + const finalizeAddressTurnSafe = input.finalizeAddressTurn ?? assistantAddressTurnFinalizeRuntimeAdapter_1.finalizeAssistantAddressTurn; + const safeAddressReply = input.sanitizeOutgoingAssistantText(input.addressLane.reply_text); + const debug = input.buildAddressDebugPayload(input.addressLane.debug, input.llmPreDecomposeMeta); + const followupOffer = input.buildAddressFollowupOffer(debug); + if (followupOffer) { + debug.address_followup_offer = followupOffer; + } + const debugKnownOrganizations = input.mergeKnownOrganizations(input.knownOrganizations); + const debugFilters = debug?.extracted_filters && typeof debug.extracted_filters === "object" + ? debug.extracted_filters + : null; + const debugActiveOrganization = input.toNonEmptyString(debugFilters?.organization) ?? + input.toNonEmptyString(input.activeOrganization); + if (debugKnownOrganizations.length > 0) { + debug.assistant_known_organizations = debugKnownOrganizations; + } + if (debugActiveOrganization) { + debug.assistant_active_organization = debugActiveOrganization; + } + const finalization = finalizeAddressTurnSafe({ + sessionId: input.sessionId, + userMessage: input.userMessage, + effectiveAddressUserMessage: input.effectiveAddressUserMessage, + assistantReply: safeAddressReply, + replyType: input.addressLane.reply_type, + addressLaneDebug: (input.addressLane.debug ?? null), + debug, + carryoverMeta: (input.carryoverMeta ?? null), + llmPreDecomposeMeta: (input.llmPreDecomposeMeta ?? null), + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent, + messageIdFactory: input.messageIdFactory + }); + return { + response: finalization.response, + debug + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantAddressRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressRuntimeAdapter.js new file mode 100644 index 0000000..492e0d4 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantAddressRuntimeAdapter.js @@ -0,0 +1,87 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runAssistantAddressRuntime = runAssistantAddressRuntime; +const assistantAddressOrchestrationRuntimeAdapter_1 = require("./assistantAddressOrchestrationRuntimeAdapter"); +const assistantAddressLaneRuntimeAdapter_1 = require("./assistantAddressLaneRuntimeAdapter"); +const assistantAddressToolGateRuntimeAdapter_1 = require("./assistantAddressToolGateRuntimeAdapter"); +async function runAssistantAddressRuntime(input) { + if (!input.featureAssistantAddressQueryV1) { + return { + handled: false, + response: null, + addressRuntimeMetaForDeep: null + }; + } + const runAddressOrchestrationRuntimeSafe = input.runAddressOrchestrationRuntime ?? assistantAddressOrchestrationRuntimeAdapter_1.buildAssistantAddressOrchestrationRuntime; + const runAddressToolGateRuntimeSafe = input.runAddressToolGateRuntime ?? assistantAddressToolGateRuntimeAdapter_1.runAssistantAddressToolGateRuntime; + const runAddressLaneRuntimeSafe = input.runAddressLaneRuntime ?? assistantAddressLaneRuntimeAdapter_1.runAssistantAddressLaneRuntime; + const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({ + userMessage: input.userMessage, + sessionItems: input.sessionItems, + llmProvider: input.llmProvider, + useMock: input.useMock, + featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, + runAddressLlmPreDecompose: input.runAddressLlmPreDecompose, + buildAddressLlmPredecomposeContractV1: input.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback: input.sanitizeAddressMessageForFallback, + toNonEmptyString: input.toNonEmptyString, + resolveAddressFollowupCarryoverContext: input.resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2: input.buildAddressDialogContinuationContractV2 + }); + const addressInputMessage = addressOrchestrationRuntime.addressInputMessage; + const carryover = addressOrchestrationRuntime.carryover; + const orchestrationDecision = addressOrchestrationRuntime.orchestrationDecision; + const addressRuntimeMeta = addressOrchestrationRuntime.addressRuntimeMeta; + const livingModeDecision = addressOrchestrationRuntime.livingModeDecision; + const addressRuntimeMetaForDeep = addressRuntimeMeta; + const toolGateRuntime = await runAddressToolGateRuntimeSafe({ + sessionId: input.sessionId, + userMessage: input.userMessage, + addressInputMessage, + orchestrationDecision, + livingModeDecision, + addressRuntimeMeta, + logEvent: input.logEvent, + tryHandleLivingChat: input.tryHandleLivingChat, + nowIso: input.nowIso + }); + if (toolGateRuntime.handled && toolGateRuntime.response) { + return { + handled: true, + response: toolGateRuntime.response, + addressRuntimeMetaForDeep + }; + } + if (Boolean(orchestrationDecision.runAddressLane)) { + const shouldPreferContextualLane = Boolean(carryover?.followupContext); + const analysisDateHint = input.runtimeAnalysisContextAsOfDate ?? input.toNonEmptyString(input.payloadContextPeriodHint); + const canRetryWithRawUserMessage = input.compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !== + input.compactWhitespace(String(input.userMessage ?? "").toLowerCase()); + const addressLaneRuntime = await runAddressLaneRuntimeSafe({ + userMessage: input.userMessage, + addressInputMessage, + carryover, + shouldPreferContextualLane, + canRetryWithRawUserMessage, + runAddressLaneAttempt: (messageUsed, carryMeta) => input.runAddressLaneAttempt(messageUsed, carryMeta, analysisDateHint), + isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult + }); + if (addressLaneRuntime.handled && addressLaneRuntime.selection) { + const response = input.finalizeAddressLaneResponse(addressLaneRuntime.selection.addressLane, addressLaneRuntime.selection.messageUsed, addressLaneRuntime.selection.carryMeta, { + ...addressRuntimeMeta, + addressRetryAudit: { ...addressLaneRuntime.retryAudit } + }); + return { + handled: true, + response, + addressRuntimeMetaForDeep + }; + } + } + return { + handled: false, + response: null, + addressRuntimeMetaForDeep + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 6c4dbfc..4bdbda4 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -65,7 +65,7 @@ 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 assistantAddressLaneResponseRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneResponseRuntimeAdapter")); const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding")); const assistantDeepTurnAnalysisRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAnalysisRuntimeAdapter")); const assistantDeepTurnCompositionRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnCompositionRuntimeAdapter")); @@ -78,9 +78,7 @@ const assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantD const assistantDeepTurnNormalizationRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnNormalizationRuntimeAdapter")); const assistantDeepTurnResponseRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnResponseRuntimeAdapter")); const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter")); -const assistantAddressLaneRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneRuntimeAdapter")); -const assistantAddressOrchestrationRuntimeAdapter_1 = __importStar(require("./assistantAddressOrchestrationRuntimeAdapter")); -const assistantAddressToolGateRuntimeAdapter_1 = __importStar(require("./assistantAddressToolGateRuntimeAdapter")); +const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter")); const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter")); const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter")); const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning")); @@ -4432,31 +4430,20 @@ class AssistantService { } const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items); const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { - const safeAddressReply = sanitizeOutgoingAssistantText(addressLane.reply_text); - const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta); - const followupOffer = buildAddressFollowupOffer(debug); - if (followupOffer) { - debug.address_followup_offer = followupOffer; - } - const debugKnownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations); - const debugActiveOrganization = toNonEmptyString(debug?.extracted_filters?.organization) ?? - toNonEmptyString(sessionOrganizationScope.activeOrganization); - if (debugKnownOrganizations.length > 0) { - debug.assistant_known_organizations = debugKnownOrganizations; - } - if (debugActiveOrganization) { - debug.assistant_active_organization = debugActiveOrganization; - } - const finalization = (0, assistantAddressTurnFinalizeRuntimeAdapter_1.finalizeAssistantAddressTurn)({ + const runtime = (0, assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime)({ sessionId, userMessage, effectiveAddressUserMessage, - assistantReply: safeAddressReply, - replyType: addressLane.reply_type, - addressLaneDebug: addressLane.debug, - debug, + addressLane, carryoverMeta, llmPreDecomposeMeta, + knownOrganizations: sessionOrganizationScope.knownOrganizations, + activeOrganization: sessionOrganizationScope.activeOrganization, + sanitizeOutgoingAssistantText, + buildAddressDebugPayload, + buildAddressFollowupOffer, + mergeKnownOrganizations, + toNonEmptyString, appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), @@ -4464,7 +4451,7 @@ class AssistantService { logEvent: (payload) => (0, log_1.logJson)(payload), messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}` }); - return finalization.response; + return runtime.response; }; const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => { try { @@ -4565,75 +4552,46 @@ class AssistantService { } }; let addressRuntimeMetaForDeep = null; - if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { - const addressOrchestrationRuntime = await (0, assistantAddressOrchestrationRuntimeAdapter_1.buildAssistantAddressOrchestrationRuntime)({ - userMessage, - sessionItems: session.items, - llmProvider: payload?.llmProvider, - useMock: Boolean(payload.useMock), - featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, - runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage), - buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, - sanitizeAddressMessageForFallback, - toNonEmptyString, - resolveAddressFollowupCarryoverContext, - resolveAssistantOrchestrationDecision, - buildAddressDialogContinuationContractV2 - }); - const addressPreDecompose = addressOrchestrationRuntime.addressPreDecompose; - const addressInputMessage = addressOrchestrationRuntime.addressInputMessage; - const carryover = addressOrchestrationRuntime.carryover; - const orchestrationDecision = addressOrchestrationRuntime.orchestrationDecision; - const addressRuntimeMeta = addressOrchestrationRuntime.addressRuntimeMeta; - addressRuntimeMetaForDeep = addressRuntimeMeta; - const livingModeDecision = addressOrchestrationRuntime.livingModeDecision; - const toolGateRuntime = await (0, assistantAddressToolGateRuntimeAdapter_1.runAssistantAddressToolGateRuntime)({ - sessionId, - userMessage, - addressInputMessage, - orchestrationDecision, - livingModeDecision, - addressRuntimeMeta, - logEvent: (payload) => (0, log_1.logJson)(payload), - tryHandleLivingChat: (modeDecision, runtimeMeta) => tryHandleLivingChat(modeDecision, runtimeMeta), - nowIso: () => new Date().toISOString() - }); - if (toolGateRuntime.handled && toolGateRuntime.response) { - return toolGateRuntime.response; - } - if (orchestrationDecision.runAddressLane) { - const shouldPreferContextualLane = Boolean(carryover?.followupContext); - const analysisDateHint = runtimeAnalysisContext.as_of_date ?? toNonEmptyString(payload?.context?.period_hint); - const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !== - compactWhitespace(String(userMessage ?? "").toLowerCase()); - const runAddressLaneAttempt = async (messageUsed, carryMeta) => { - const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization); - if (scopedFollowupContext) { - return this.addressQueryService.tryHandle(messageUsed, { - followupContext: scopedFollowupContext, - analysisDateHint - }); - } - return this.addressQueryService.tryHandle(messageUsed, { - analysisDateHint - }); - }; - const addressLaneRuntime = await (0, assistantAddressLaneRuntimeAdapter_1.runAssistantAddressLaneRuntime)({ - userMessage, - addressInputMessage, - carryover, - shouldPreferContextualLane, - canRetryWithRawUserMessage, - runAddressLaneAttempt, - isRetryableAddressLimitedResult + const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => { + const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization); + if (scopedFollowupContext) { + return this.addressQueryService.tryHandle(messageUsed, { + followupContext: scopedFollowupContext, + analysisDateHint }); - if (addressLaneRuntime.handled && addressLaneRuntime.selection) { - return finalizeAddressLaneResponse(addressLaneRuntime.selection.addressLane, addressLaneRuntime.selection.messageUsed, addressLaneRuntime.selection.carryMeta, { - ...addressRuntimeMeta, - addressRetryAudit: { ...addressLaneRuntime.retryAudit } - }); - } } + return this.addressQueryService.tryHandle(messageUsed, { + analysisDateHint + }); + }; + const addressRuntime = await (0, assistantAddressRuntimeAdapter_1.runAssistantAddressRuntime)({ + featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, + sessionId, + userMessage, + sessionItems: session.items, + llmProvider: payload?.llmProvider, + useMock: Boolean(payload.useMock), + featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, + runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage), + buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback, + toNonEmptyString, + resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2, + runtimeAnalysisContextAsOfDate: runtimeAnalysisContext.as_of_date, + payloadContextPeriodHint: payload?.context?.period_hint, + compactWhitespace, + runAddressLaneAttempt, + isRetryableAddressLimitedResult, + finalizeAddressLaneResponse, + tryHandleLivingChat: (modeDecision, runtimeMeta) => tryHandleLivingChat(modeDecision, runtimeMeta), + logEvent: (payload) => (0, log_1.logJson)(payload), + nowIso: () => new Date().toISOString() + }); + addressRuntimeMetaForDeep = addressRuntime.addressRuntimeMetaForDeep; + if (addressRuntime.handled && addressRuntime.response) { + return addressRuntime.response; } const normalizationRuntime = await (0, assistantDeepTurnNormalizationRuntimeAdapter_1.buildAssistantDeepTurnNormalizationRuntime)({ userMessage, diff --git a/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts new file mode 100644 index 0000000..bcba12e --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts @@ -0,0 +1,89 @@ +import type { AssistantAddressLaneLike, AssistantAddressFollowupCarryoverLike } from "./assistantAddressLaneRuntimeAdapter"; +import type { AssistantMessageResponsePayload } from "../types/assistant"; +import { + finalizeAssistantAddressTurn, + type FinalizeAssistantAddressTurnInput +} from "./assistantAddressTurnFinalizeRuntimeAdapter"; + +export interface RunAssistantAddressLaneResponseRuntimeInput { + sessionId: string; + userMessage: string; + effectiveAddressUserMessage: string; + addressLane: AssistantAddressLaneLike; + carryoverMeta?: AssistantAddressFollowupCarryoverLike | null; + llmPreDecomposeMeta?: Record | null; + knownOrganizations: string[]; + activeOrganization: string | null; + sanitizeOutgoingAssistantText: (text: unknown, fallback?: string) => string; + buildAddressDebugPayload: ( + addressDebug: unknown, + llmPreDecomposeMeta?: Record | null + ) => Record; + buildAddressFollowupOffer: (addressDebug: Record) => unknown; + mergeKnownOrganizations: (organizations: string[]) => string[]; + toNonEmptyString: (value: unknown) => string | null; + appendItem: FinalizeAssistantAddressTurnInput["appendItem"]; + getSession: FinalizeAssistantAddressTurnInput["getSession"]; + persistSession: FinalizeAssistantAddressTurnInput["persistSession"]; + cloneConversation: FinalizeAssistantAddressTurnInput["cloneConversation"]; + logEvent: FinalizeAssistantAddressTurnInput["logEvent"]; + messageIdFactory: FinalizeAssistantAddressTurnInput["messageIdFactory"]; + finalizeAddressTurn?: ( + input: FinalizeAssistantAddressTurnInput + ) => { + response: ResponseType; + }; +} + +export interface RunAssistantAddressLaneResponseRuntimeOutput { + response: ResponseType; + debug: Record; +} + +export function runAssistantAddressLaneResponseRuntime( + input: RunAssistantAddressLaneResponseRuntimeInput +): RunAssistantAddressLaneResponseRuntimeOutput { + const finalizeAddressTurnSafe = input.finalizeAddressTurn ?? finalizeAssistantAddressTurn; + const safeAddressReply = input.sanitizeOutgoingAssistantText(input.addressLane.reply_text); + const debug = input.buildAddressDebugPayload(input.addressLane.debug, input.llmPreDecomposeMeta); + const followupOffer = input.buildAddressFollowupOffer(debug); + if (followupOffer) { + debug.address_followup_offer = followupOffer; + } + const debugKnownOrganizations = input.mergeKnownOrganizations(input.knownOrganizations); + const debugFilters = + debug?.extracted_filters && typeof debug.extracted_filters === "object" + ? (debug.extracted_filters as Record) + : null; + const debugActiveOrganization = + input.toNonEmptyString(debugFilters?.organization) ?? + input.toNonEmptyString(input.activeOrganization); + if (debugKnownOrganizations.length > 0) { + debug.assistant_known_organizations = debugKnownOrganizations; + } + if (debugActiveOrganization) { + debug.assistant_active_organization = debugActiveOrganization; + } + const finalization = finalizeAddressTurnSafe({ + sessionId: input.sessionId, + userMessage: input.userMessage, + effectiveAddressUserMessage: input.effectiveAddressUserMessage, + assistantReply: safeAddressReply, + replyType: input.addressLane.reply_type as any, + addressLaneDebug: (input.addressLane.debug ?? null) as any, + debug, + carryoverMeta: (input.carryoverMeta ?? null) as any, + llmPreDecomposeMeta: (input.llmPreDecomposeMeta ?? null) as any, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent, + messageIdFactory: input.messageIdFactory + }); + + return { + response: finalization.response as ResponseType, + debug + }; +} diff --git a/llm_normalizer/backend/src/services/assistantAddressRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressRuntimeAdapter.ts new file mode 100644 index 0000000..6bd59f5 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantAddressRuntimeAdapter.ts @@ -0,0 +1,189 @@ +import { + buildAssistantAddressOrchestrationRuntime, + type AssistantAddressCarryoverLike, + type BuildAssistantAddressOrchestrationRuntimeInput, + type BuildAssistantAddressOrchestrationRuntimeOutput +} from "./assistantAddressOrchestrationRuntimeAdapter"; +import { + runAssistantAddressLaneRuntime, + type AssistantAddressLaneLike, + type RunAssistantAddressLaneRuntimeOutput +} from "./assistantAddressLaneRuntimeAdapter"; +import { + runAssistantAddressToolGateRuntime, + type AssistantAddressToolGateRuntimeOutput +} from "./assistantAddressToolGateRuntimeAdapter"; + +export interface RunAssistantAddressRuntimeInput { + featureAssistantAddressQueryV1: boolean; + sessionId: string; + userMessage: string; + sessionItems: unknown[]; + llmProvider: unknown; + useMock: boolean; + featureAddressLlmPredecomposeV1: boolean; + runAddressLlmPreDecompose: () => Promise>; + buildAddressLlmPredecomposeContractV1: BuildAssistantAddressOrchestrationRuntimeInput["buildAddressLlmPredecomposeContractV1"]; + sanitizeAddressMessageForFallback: BuildAssistantAddressOrchestrationRuntimeInput["sanitizeAddressMessageForFallback"]; + toNonEmptyString: (value: unknown) => string | null; + resolveAddressFollowupCarryoverContext: BuildAssistantAddressOrchestrationRuntimeInput["resolveAddressFollowupCarryoverContext"]; + resolveAssistantOrchestrationDecision: BuildAssistantAddressOrchestrationRuntimeInput["resolveAssistantOrchestrationDecision"]; + buildAddressDialogContinuationContractV2: BuildAssistantAddressOrchestrationRuntimeInput["buildAddressDialogContinuationContractV2"]; + runtimeAnalysisContextAsOfDate: string | null; + payloadContextPeriodHint: unknown; + compactWhitespace: (value: string) => string; + runAddressLaneAttempt: ( + messageUsed: string, + carryMeta: AssistantAddressCarryoverLike | null, + analysisDateHint: string | null + ) => Promise; + isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean; + finalizeAddressLaneResponse: ( + addressLane: AssistantAddressLaneLike, + effectiveAddressUserMessage: string, + carryoverMeta?: AssistantAddressCarryoverLike | null, + llmPreDecomposeMeta?: Record | null + ) => ResponseType; + tryHandleLivingChat: ( + modeDecision: { mode?: unknown; reason?: unknown }, + addressRuntimeMeta: Record | null + ) => Promise; + logEvent: (payload: Record) => void; + nowIso: () => string; + runAddressOrchestrationRuntime?: ( + input: BuildAssistantAddressOrchestrationRuntimeInput + ) => Promise; + runAddressToolGateRuntime?: ( + input: { + sessionId: string; + userMessage: string; + addressInputMessage: string; + orchestrationDecision: BuildAssistantAddressOrchestrationRuntimeOutput["orchestrationDecision"]; + livingModeDecision: BuildAssistantAddressOrchestrationRuntimeOutput["livingModeDecision"]; + addressRuntimeMeta: BuildAssistantAddressOrchestrationRuntimeOutput["addressRuntimeMeta"]; + logEvent: (payload: Record) => void; + tryHandleLivingChat: ( + modeDecision: { mode?: unknown; reason?: unknown }, + addressRuntimeMeta: Record | null + ) => Promise; + nowIso: () => string; + } + ) => Promise>; + runAddressLaneRuntime?: ( + input: { + userMessage: string; + addressInputMessage: string; + carryover: AssistantAddressCarryoverLike | null; + shouldPreferContextualLane: boolean; + canRetryWithRawUserMessage: boolean; + runAddressLaneAttempt: ( + messageUsed: string, + carryMeta: AssistantAddressCarryoverLike | null + ) => Promise; + isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean; + } + ) => Promise; +} + +export interface RunAssistantAddressRuntimeOutput { + handled: boolean; + response: ResponseType | null; + addressRuntimeMetaForDeep: Record | null; +} + +export async function runAssistantAddressRuntime( + input: RunAssistantAddressRuntimeInput +): Promise> { + if (!input.featureAssistantAddressQueryV1) { + return { + handled: false, + response: null, + addressRuntimeMetaForDeep: null + }; + } + + const runAddressOrchestrationRuntimeSafe = + input.runAddressOrchestrationRuntime ?? buildAssistantAddressOrchestrationRuntime; + const runAddressToolGateRuntimeSafe = input.runAddressToolGateRuntime ?? runAssistantAddressToolGateRuntime; + const runAddressLaneRuntimeSafe = input.runAddressLaneRuntime ?? runAssistantAddressLaneRuntime; + + const addressOrchestrationRuntime = await runAddressOrchestrationRuntimeSafe({ + userMessage: input.userMessage, + sessionItems: input.sessionItems, + llmProvider: input.llmProvider, + useMock: input.useMock, + featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1, + runAddressLlmPreDecompose: input.runAddressLlmPreDecompose, + buildAddressLlmPredecomposeContractV1: input.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback: input.sanitizeAddressMessageForFallback, + toNonEmptyString: input.toNonEmptyString, + resolveAddressFollowupCarryoverContext: input.resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2: input.buildAddressDialogContinuationContractV2 + }); + const addressInputMessage = addressOrchestrationRuntime.addressInputMessage; + const carryover = addressOrchestrationRuntime.carryover; + const orchestrationDecision = addressOrchestrationRuntime.orchestrationDecision; + const addressRuntimeMeta = addressOrchestrationRuntime.addressRuntimeMeta; + const livingModeDecision = addressOrchestrationRuntime.livingModeDecision; + const addressRuntimeMetaForDeep = addressRuntimeMeta; + + const toolGateRuntime = await runAddressToolGateRuntimeSafe({ + sessionId: input.sessionId, + userMessage: input.userMessage, + addressInputMessage, + orchestrationDecision, + livingModeDecision, + addressRuntimeMeta, + logEvent: input.logEvent, + tryHandleLivingChat: input.tryHandleLivingChat, + nowIso: input.nowIso + }); + if (toolGateRuntime.handled && toolGateRuntime.response) { + return { + handled: true, + response: toolGateRuntime.response, + addressRuntimeMetaForDeep + }; + } + + if (Boolean(orchestrationDecision.runAddressLane)) { + const shouldPreferContextualLane = Boolean(carryover?.followupContext); + const analysisDateHint = input.runtimeAnalysisContextAsOfDate ?? input.toNonEmptyString(input.payloadContextPeriodHint); + const canRetryWithRawUserMessage = + input.compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !== + input.compactWhitespace(String(input.userMessage ?? "").toLowerCase()); + const addressLaneRuntime = await runAddressLaneRuntimeSafe({ + userMessage: input.userMessage, + addressInputMessage, + carryover, + shouldPreferContextualLane, + canRetryWithRawUserMessage, + runAddressLaneAttempt: (messageUsed, carryMeta) => + input.runAddressLaneAttempt(messageUsed, carryMeta, analysisDateHint), + isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult + }); + if (addressLaneRuntime.handled && addressLaneRuntime.selection) { + const response = input.finalizeAddressLaneResponse( + addressLaneRuntime.selection.addressLane, + addressLaneRuntime.selection.messageUsed, + addressLaneRuntime.selection.carryMeta, + { + ...addressRuntimeMeta, + addressRetryAudit: { ...addressLaneRuntime.retryAudit } + } + ); + return { + handled: true, + response, + addressRuntimeMetaForDeep + }; + } + } + + return { + handled: false, + response: null, + addressRuntimeMetaForDeep + }; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 1d90317..563cde5 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -19,7 +19,7 @@ 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 assistantAddressLaneResponseRuntimeAdapter_1 from "./assistantAddressLaneResponseRuntimeAdapter"; import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; import * as assistantDeepTurnAnalysisRuntimeAdapter_1 from "./assistantDeepTurnAnalysisRuntimeAdapter"; import * as assistantDeepTurnCompositionRuntimeAdapter_1 from "./assistantDeepTurnCompositionRuntimeAdapter"; @@ -32,9 +32,7 @@ import * as assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanR import * as assistantDeepTurnNormalizationRuntimeAdapter_1 from "./assistantDeepTurnNormalizationRuntimeAdapter"; import * as assistantDeepTurnResponseRuntimeAdapter_1 from "./assistantDeepTurnResponseRuntimeAdapter"; import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter"; -import * as assistantAddressLaneRuntimeAdapter_1 from "./assistantAddressLaneRuntimeAdapter"; -import * as assistantAddressOrchestrationRuntimeAdapter_1 from "./assistantAddressOrchestrationRuntimeAdapter"; -import * as assistantAddressToolGateRuntimeAdapter_1 from "./assistantAddressToolGateRuntimeAdapter"; +import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter"; import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter"; import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter"; import * as assistantQueryPlanning_1 from "./assistantQueryPlanning"; @@ -4387,31 +4385,20 @@ export class AssistantService { } const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items); const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { - const safeAddressReply = sanitizeOutgoingAssistantText(addressLane.reply_text); - const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta); - const followupOffer = buildAddressFollowupOffer(debug); - if (followupOffer) { - debug.address_followup_offer = followupOffer; - } - const debugKnownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations); - const debugActiveOrganization = toNonEmptyString(debug?.extracted_filters?.organization) ?? - toNonEmptyString(sessionOrganizationScope.activeOrganization); - if (debugKnownOrganizations.length > 0) { - debug.assistant_known_organizations = debugKnownOrganizations; - } - if (debugActiveOrganization) { - debug.assistant_active_organization = debugActiveOrganization; - } - const finalization = (0, assistantAddressTurnFinalizeRuntimeAdapter_1.finalizeAssistantAddressTurn)({ + const runtime = (0, assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime)({ sessionId, userMessage, effectiveAddressUserMessage, - assistantReply: safeAddressReply, - replyType: addressLane.reply_type, - addressLaneDebug: addressLane.debug, - debug, + addressLane, carryoverMeta, llmPreDecomposeMeta, + knownOrganizations: sessionOrganizationScope.knownOrganizations, + activeOrganization: sessionOrganizationScope.activeOrganization, + sanitizeOutgoingAssistantText, + buildAddressDebugPayload, + buildAddressFollowupOffer, + mergeKnownOrganizations, + toNonEmptyString, appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), @@ -4419,7 +4406,7 @@ export class AssistantService { logEvent: (payload) => (0, log_1.logJson)(payload), messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}` }); - return finalization.response; + return runtime.response; }; const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => { try { @@ -4520,75 +4507,46 @@ export class AssistantService { } }; let addressRuntimeMetaForDeep = null; - if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { - const addressOrchestrationRuntime = await (0, assistantAddressOrchestrationRuntimeAdapter_1.buildAssistantAddressOrchestrationRuntime)({ - userMessage, - sessionItems: session.items, - llmProvider: payload?.llmProvider, - useMock: Boolean(payload.useMock), - featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, - runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage), - buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, - sanitizeAddressMessageForFallback, - toNonEmptyString, - resolveAddressFollowupCarryoverContext, - resolveAssistantOrchestrationDecision, - buildAddressDialogContinuationContractV2 - }); - const addressPreDecompose = addressOrchestrationRuntime.addressPreDecompose; - const addressInputMessage = addressOrchestrationRuntime.addressInputMessage; - const carryover = addressOrchestrationRuntime.carryover; - const orchestrationDecision = addressOrchestrationRuntime.orchestrationDecision; - const addressRuntimeMeta = addressOrchestrationRuntime.addressRuntimeMeta; - addressRuntimeMetaForDeep = addressRuntimeMeta; - const livingModeDecision = addressOrchestrationRuntime.livingModeDecision; - const toolGateRuntime = await (0, assistantAddressToolGateRuntimeAdapter_1.runAssistantAddressToolGateRuntime)({ - sessionId, - userMessage, - addressInputMessage, - orchestrationDecision, - livingModeDecision, - addressRuntimeMeta, - logEvent: (payload) => (0, log_1.logJson)(payload), - tryHandleLivingChat: (modeDecision, runtimeMeta) => tryHandleLivingChat(modeDecision, runtimeMeta), - nowIso: () => new Date().toISOString() - }); - if (toolGateRuntime.handled && toolGateRuntime.response) { - return toolGateRuntime.response; - } - if (orchestrationDecision.runAddressLane) { - const shouldPreferContextualLane = Boolean(carryover?.followupContext); - const analysisDateHint = runtimeAnalysisContext.as_of_date ?? toNonEmptyString(payload?.context?.period_hint); - const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !== - compactWhitespace(String(userMessage ?? "").toLowerCase()); - const runAddressLaneAttempt = async (messageUsed, carryMeta) => { - const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization); - if (scopedFollowupContext) { - return this.addressQueryService.tryHandle(messageUsed, { - followupContext: scopedFollowupContext, - analysisDateHint - }); - } - return this.addressQueryService.tryHandle(messageUsed, { - analysisDateHint - }); - }; - const addressLaneRuntime = await (0, assistantAddressLaneRuntimeAdapter_1.runAssistantAddressLaneRuntime)({ - userMessage, - addressInputMessage, - carryover, - shouldPreferContextualLane, - canRetryWithRawUserMessage, - runAddressLaneAttempt, - isRetryableAddressLimitedResult + const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => { + const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization); + if (scopedFollowupContext) { + return this.addressQueryService.tryHandle(messageUsed, { + followupContext: scopedFollowupContext, + analysisDateHint }); - if (addressLaneRuntime.handled && addressLaneRuntime.selection) { - return finalizeAddressLaneResponse(addressLaneRuntime.selection.addressLane, addressLaneRuntime.selection.messageUsed, addressLaneRuntime.selection.carryMeta, { - ...addressRuntimeMeta, - addressRetryAudit: { ...addressLaneRuntime.retryAudit } - }); - } } + return this.addressQueryService.tryHandle(messageUsed, { + analysisDateHint + }); + }; + const addressRuntime = await (0, assistantAddressRuntimeAdapter_1.runAssistantAddressRuntime)({ + featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, + sessionId, + userMessage, + sessionItems: session.items, + llmProvider: payload?.llmProvider, + useMock: Boolean(payload.useMock), + featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, + runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage), + buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback, + toNonEmptyString, + resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2, + runtimeAnalysisContextAsOfDate: runtimeAnalysisContext.as_of_date, + payloadContextPeriodHint: payload?.context?.period_hint, + compactWhitespace, + runAddressLaneAttempt, + isRetryableAddressLimitedResult, + finalizeAddressLaneResponse, + tryHandleLivingChat: (modeDecision, runtimeMeta) => tryHandleLivingChat(modeDecision, runtimeMeta), + logEvent: (payload) => (0, log_1.logJson)(payload), + nowIso: () => new Date().toISOString() + }); + addressRuntimeMetaForDeep = addressRuntime.addressRuntimeMetaForDeep; + if (addressRuntime.handled && addressRuntime.response) { + return addressRuntime.response; } const normalizationRuntime = await (0, assistantDeepTurnNormalizationRuntimeAdapter_1.buildAssistantDeepTurnNormalizationRuntime)({ userMessage, diff --git a/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts new file mode 100644 index 0000000..334a1b1 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it, vi } from "vitest"; +import { runAssistantAddressLaneResponseRuntime } from "../src/services/assistantAddressLaneResponseRuntimeAdapter"; + +describe("assistant address lane response runtime adapter", () => { + it("builds debug payload and finalizes address turn", () => { + const finalizeAddressTurn = vi.fn(() => ({ + response: { + ok: true + } + })); + + const runtime = runAssistantAddressLaneResponseRuntime({ + sessionId: "asst-1", + userMessage: "raw", + effectiveAddressUserMessage: "canon", + addressLane: { + handled: true, + reply_text: "answer", + reply_type: "factual", + debug: { + extracted_filters: { + organization: "ООО Ромашка" + } + } + }, + carryoverMeta: { + followupContext: { + previous_intent: "list_documents" + } + }, + llmPreDecomposeMeta: { + attempted: true + }, + knownOrganizations: ["ООО Ромашка", "ООО Лютик"], + activeOrganization: "ООО Ромашка", + sanitizeOutgoingAssistantText: (text) => String(text ?? "").trim(), + buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record) }), + buildAddressFollowupOffer: () => ({ suggestion: "continue_previous" }), + mergeKnownOrganizations: (items) => Array.from(new Set(items)), + toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null), + appendItem: () => {}, + getSession: () => ({ session_id: "asst-1", updated_at: "", items: [], investigation_state: null } as any), + persistSession: () => {}, + cloneConversation: (items) => items, + logEvent: () => {}, + messageIdFactory: () => "msg-1", + finalizeAddressTurn + }); + + expect(finalizeAddressTurn).toHaveBeenCalledWith( + expect.objectContaining({ + assistantReply: "answer", + replyType: "factual", + llmPreDecomposeMeta: { + attempted: true + } + }) + ); + expect(runtime.response).toEqual({ ok: true }); + expect(runtime.debug).toEqual( + expect.objectContaining({ + assistant_known_organizations: ["ООО Ромашка", "ООО Лютик"], + assistant_active_organization: "ООО Ромашка", + address_followup_offer: { suggestion: "continue_previous" } + }) + ); + }); + + it("keeps debug minimal when optional enrichment is absent", () => { + const runtime = runAssistantAddressLaneResponseRuntime({ + sessionId: "asst-2", + userMessage: "raw", + effectiveAddressUserMessage: "raw", + addressLane: { + handled: true, + reply_text: "answer", + reply_type: "partial_coverage", + debug: {} + }, + knownOrganizations: [], + activeOrganization: null, + sanitizeOutgoingAssistantText: (text) => String(text ?? ""), + buildAddressDebugPayload: () => ({}), + buildAddressFollowupOffer: () => null, + mergeKnownOrganizations: (items) => items, + toNonEmptyString: () => null, + appendItem: () => {}, + getSession: () => ({ session_id: "asst-2", updated_at: "", items: [], investigation_state: null } as any), + persistSession: () => {}, + cloneConversation: (items) => items, + logEvent: () => {}, + messageIdFactory: () => "msg-2", + finalizeAddressTurn: () => ({ + response: { + ok: true + } + }) + }); + + expect(runtime.debug).toEqual({}); + expect(runtime.response).toEqual({ ok: true }); + }); +}); diff --git a/llm_normalizer/backend/tests/assistantAddressRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressRuntimeAdapter.test.ts new file mode 100644 index 0000000..45e45a1 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantAddressRuntimeAdapter.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, it, vi } from "vitest"; +import { runAssistantAddressRuntime } from "../src/services/assistantAddressRuntimeAdapter"; + +describe("assistant address runtime adapter", () => { + it("returns unhandled when address feature is disabled", async () => { + const result = await runAssistantAddressRuntime({ + featureAssistantAddressQueryV1: false, + sessionId: "asst-1", + userMessage: "question", + sessionItems: [], + llmProvider: "openai", + useMock: false, + featureAddressLlmPredecomposeV1: true, + runAddressLlmPreDecompose: async () => ({}), + buildAddressLlmPredecomposeContractV1: () => ({}), + sanitizeAddressMessageForFallback: (value) => value, + toNonEmptyString: () => null, + resolveAddressFollowupCarryoverContext: () => null, + resolveAssistantOrchestrationDecision: () => ({}), + buildAddressDialogContinuationContractV2: () => ({}), + runtimeAnalysisContextAsOfDate: null, + payloadContextPeriodHint: null, + compactWhitespace: (value) => value.trim(), + runAddressLaneAttempt: async () => null, + isRetryableAddressLimitedResult: () => false, + finalizeAddressLaneResponse: () => ({ ok: true }), + tryHandleLivingChat: async () => null, + logEvent: () => {}, + nowIso: () => "2026-04-10T00:00:00.000Z" + }); + + expect(result).toEqual({ + handled: false, + response: null, + addressRuntimeMetaForDeep: null + }); + }); + + it("returns early when tool-gate chat fallback handles", async () => { + const runAddressOrchestrationRuntime = vi.fn(async () => ({ + addressPreDecompose: {}, + addressInputMessage: "canon", + carryover: null, + orchestrationDecision: { runAddressLane: false }, + addressRuntimeMeta: { attempted: true }, + livingModeDecision: { mode: "chat", reason: "x" } + })); + const runAddressToolGateRuntime = vi.fn(async () => ({ + handled: true, + response: { ok: "chat" } + })); + const runAddressLaneRuntime = vi.fn(async () => ({ + handled: false, + selection: null, + retryAudit: { + attempted: false, + reason: null, + initial_limited_category: null, + retry_message: null, + retry_used_followup_context: false, + retry_result_category: null + } + })); + + const result = await runAssistantAddressRuntime({ + featureAssistantAddressQueryV1: true, + sessionId: "asst-2", + userMessage: "question", + sessionItems: [], + llmProvider: "openai", + useMock: false, + featureAddressLlmPredecomposeV1: true, + runAddressLlmPreDecompose: async () => ({}), + buildAddressLlmPredecomposeContractV1: () => ({}), + sanitizeAddressMessageForFallback: (value) => value, + toNonEmptyString: () => null, + resolveAddressFollowupCarryoverContext: () => null, + resolveAssistantOrchestrationDecision: () => ({}), + buildAddressDialogContinuationContractV2: () => ({}), + runtimeAnalysisContextAsOfDate: null, + payloadContextPeriodHint: null, + compactWhitespace: (value) => value.trim(), + runAddressLaneAttempt: async () => null, + isRetryableAddressLimitedResult: () => false, + finalizeAddressLaneResponse: () => ({ ok: true }), + tryHandleLivingChat: async () => ({ ok: true }), + logEvent: () => {}, + nowIso: () => "2026-04-10T00:00:00.000Z", + runAddressOrchestrationRuntime, + runAddressToolGateRuntime, + runAddressLaneRuntime + }); + + expect(result.handled).toBe(true); + expect(result.response).toEqual({ ok: "chat" }); + expect(result.addressRuntimeMetaForDeep).toEqual({ attempted: true }); + expect(runAddressLaneRuntime).not.toHaveBeenCalled(); + }); + + it("finalizes address lane when lane runtime resolves handled selection", async () => { + const runAddressLaneAttempt = vi.fn(async () => ({ + handled: true + })); + const finalizeAddressLaneResponse = vi.fn(() => ({ ok: "address" })); + const runAddressLaneRuntime = vi.fn(async (input) => { + await input.runAddressLaneAttempt("canon", null); + return { + handled: true, + selection: { + addressLane: { + handled: true + }, + messageUsed: "canon", + carryMeta: null + }, + retryAudit: { + attempted: true, + reason: "limited_result_retry_with_raw_message", + initial_limited_category: "missing_anchor", + retry_message: "raw", + retry_used_followup_context: false, + retry_result_category: null + } + }; + }); + + const result = await runAssistantAddressRuntime({ + featureAssistantAddressQueryV1: true, + sessionId: "asst-3", + userMessage: "raw question", + sessionItems: [], + llmProvider: "openai", + useMock: false, + featureAddressLlmPredecomposeV1: true, + runAddressLlmPreDecompose: async () => ({}), + buildAddressLlmPredecomposeContractV1: () => ({}), + sanitizeAddressMessageForFallback: (value) => value, + toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null), + resolveAddressFollowupCarryoverContext: () => ({ + followupContext: { + intent: "x" + } + }), + resolveAssistantOrchestrationDecision: () => ({}), + buildAddressDialogContinuationContractV2: () => ({}), + runtimeAnalysisContextAsOfDate: null, + payloadContextPeriodHint: "2020-07-31", + compactWhitespace: (value) => value.replace(/\s+/g, " ").trim(), + runAddressLaneAttempt, + isRetryableAddressLimitedResult: () => false, + finalizeAddressLaneResponse, + tryHandleLivingChat: async () => null, + logEvent: () => {}, + nowIso: () => "2026-04-10T00:00:00.000Z", + runAddressOrchestrationRuntime: async () => ({ + addressPreDecompose: {}, + addressInputMessage: "canon", + carryover: { + followupContext: { + intent: "x" + } + }, + orchestrationDecision: { runAddressLane: true }, + addressRuntimeMeta: { + attempted: true + }, + livingModeDecision: { mode: "deep_analysis", reason: "x" } + }), + runAddressToolGateRuntime: async () => ({ + handled: false, + response: null + }), + runAddressLaneRuntime + }); + + expect(runAddressLaneAttempt).toHaveBeenCalledWith("canon", null, "2020-07-31"); + expect(finalizeAddressLaneResponse).toHaveBeenCalledWith( + { handled: true }, + "canon", + null, + expect.objectContaining({ + attempted: true, + addressRetryAudit: expect.objectContaining({ + attempted: true + }) + }) + ); + expect(result).toEqual({ + handled: true, + response: { ok: "address" }, + addressRuntimeMetaForDeep: { + attempted: true + } + }); + }); +});