From b612615219c46abc3a2654718b3871dbbd0728e8 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 11 Apr 2026 00:57:52 +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=20=202.51=20-=20=D0=B2=D1=8B=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=20living-chat=20guard=20chain=20(script/grounding/meta?= =?UTF-8?q?=20boundary=20wiring)=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=20input-builder=20adapter,=20=D1=87?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=8B=20=20=D1=83=D0=B6=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?assistantService.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TECH/1CLLMARCH-FACT.md | 37 ++++++- .../assistantAddressAttemptRuntimeAdapter.js | 6 +- .../assistantLivingChatAttemptInputBuilder.js | 52 +++++++++ .../assistantAddressAttemptRuntimeAdapter.ts | 96 +++++++++-------- .../assistantLivingChatAttemptInputBuilder.ts | 102 ++++++++++++++++++ ...stantLivingChatAttemptInputBuilder.test.ts | 96 +++++++++++++++++ 6 files changed, 338 insertions(+), 51 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantLivingChatAttemptInputBuilder.js create mode 100644 llm_normalizer/backend/src/services/assistantLivingChatAttemptInputBuilder.ts create mode 100644 llm_normalizer/backend/tests/assistantLivingChatAttemptInputBuilder.test.ts diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index f78ce41..66c969b 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -1610,7 +1610,42 @@ Validation: - `assistantWave10SettlementCorrectiveRegression.test.ts` - `assistantLivingChatMode.test.ts` -Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 + 2.43 + 2.44 + 2.45 + 2.46 + 2.47 + 2.48 + 2.49 + 2.50 completed)** +Implemented in current pass (Phase 2.51): +1. Extracted living-chat attempt input assembly from `assistantAddressAttemptRuntimeAdapter` into dedicated builder: + - `assistantLivingChatAttemptInputBuilder.ts` + - introduced: + - `buildAssistantLivingChatAttemptRuntimeInput(...)` +2. Rewired `assistantAddressAttemptRuntimeAdapter` to consume the new builder (behavior-preserving): + - moved inline living-chat payload mapping (including `traceIdFactory` derivation and scope/meta wiring) behind a single input-builder boundary. +3. Added focused unit tests: + - `assistantLivingChatAttemptInputBuilder.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `assistantLivingChatAttemptInputBuilder.test.ts` + - `assistantAddressAttemptRuntimeAdapter.test.ts` + - `assistantLivingChatAttemptRuntimeAdapter.test.ts` + - `assistantLivingChatHandlerRuntimeAdapter.test.ts` + - `assistantLivingChatRuntimeAdapter.test.ts` + - `assistantTurnRuntimeDepsAdapter.test.ts` + - `assistantTurnRuntimeInputBuilder.test.ts` + - `assistantTurnAttemptRuntimeAdapter.test.ts` + - `assistantOrganizationScopeRuntimeAdapter.test.ts` + - `assistantAddressLaneAttemptRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseAttemptRuntimeAdapter.test.ts` + - `assistantAddressRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnPackagingRuntimeAdapter.test.ts` + - `assistantWave10SettlementCorrectiveRegression.test.ts` + - `assistantLivingChatMode.test.ts` + +Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 + 2.43 + 2.44 + 2.45 + 2.46 + 2.47 + 2.48 + 2.49 + 2.50 + 2.51 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantAddressAttemptRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressAttemptRuntimeAdapter.js index faf038f..3947586 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressAttemptRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressAttemptRuntimeAdapter.js @@ -5,6 +5,7 @@ const assistantAddressRuntimeAdapter_1 = require("./assistantAddressRuntimeAdapt const assistantAddressLaneAttemptRuntimeAdapter_1 = require("./assistantAddressLaneAttemptRuntimeAdapter"); const assistantAddressLaneResponseAttemptRuntimeAdapter_1 = require("./assistantAddressLaneResponseAttemptRuntimeAdapter"); const assistantLivingChatAttemptRuntimeAdapter_1 = require("./assistantLivingChatAttemptRuntimeAdapter"); +const assistantLivingChatAttemptInputBuilder_1 = require("./assistantLivingChatAttemptInputBuilder"); async function runAssistantAddressAttemptRuntime(input) { const runAddressRuntimeSafe = input.runAddressRuntime ?? assistantAddressRuntimeAdapter_1.runAssistantAddressRuntime; const runAddressLaneAttemptRuntimeSafe = input.runAddressLaneAttemptRuntime ?? assistantAddressLaneAttemptRuntimeAdapter_1.runAssistantAddressLaneAttemptRuntime; @@ -31,7 +32,7 @@ async function runAssistantAddressAttemptRuntime(input) { logEvent: input.logEvent, messageIdFactory: input.messageIdFactory }); - const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => runLivingChatAttemptRuntimeSafe({ + const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => runLivingChatAttemptRuntimeSafe((0, assistantLivingChatAttemptInputBuilder_1.buildAssistantLivingChatAttemptRuntimeInput)({ sessionId: input.sessionId, userMessage: input.userMessage, sessionItems: input.sessionItems, @@ -42,7 +43,6 @@ async function runAssistantAddressAttemptRuntime(input) { activeOrganization: input.sessionScope.activeOrganization }, addressRuntimeMeta, - traceIdFactory: () => `chat-${input.messageIdFactory().replace(/^msg-/, "")}`, toNonEmptyString: input.toNonEmptyString, mergeKnownOrganizations: input.mergeKnownOrganizations, hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, @@ -77,7 +77,7 @@ async function runAssistantAddressAttemptRuntime(input) { defaultModel: input.defaultModel, defaultBaseUrl: input.defaultBaseUrl, defaultApiKey: input.defaultApiKey - }); + })); const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => runAddressLaneAttemptRuntimeSafe({ messageUsed, carryMeta, diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatAttemptInputBuilder.js b/llm_normalizer/backend/dist/services/assistantLivingChatAttemptInputBuilder.js new file mode 100644 index 0000000..22ce9eb --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantLivingChatAttemptInputBuilder.js @@ -0,0 +1,52 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildAssistantLivingChatAttemptRuntimeInput = buildAssistantLivingChatAttemptRuntimeInput; +function buildAssistantLivingChatAttemptRuntimeInput(input) { + return { + sessionId: input.sessionId, + userMessage: input.userMessage, + sessionItems: input.sessionItems, + modeDecision: input.modeDecision, + sessionScope: { + knownOrganizations: input.sessionScope.knownOrganizations, + selectedOrganization: input.sessionScope.selectedOrganization, + activeOrganization: input.sessionScope.activeOrganization + }, + addressRuntimeMeta: input.addressRuntimeMeta ?? null, + traceIdFactory: () => `chat-${input.messageIdFactory().replace(/^msg-/, "")}`, + toNonEmptyString: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: input.resolveDataScopeProbe, + applyScriptGuard: input.applyScriptGuard, + applyGroundingGuard: input.applyGroundingGuard, + buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent, + messageIdFactory: input.messageIdFactory, + nowIso: input.nowIso, + payload: input.payload, + chatClient: input.chatClient, + loadAssistantCanonExcerpt: input.loadAssistantCanonExcerpt, + sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText, + defaultModel: input.defaultModel, + defaultBaseUrl: input.defaultBaseUrl, + defaultApiKey: input.defaultApiKey + }; +} diff --git a/llm_normalizer/backend/src/services/assistantAddressAttemptRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressAttemptRuntimeAdapter.ts index 7d511d8..080f27b 100644 --- a/llm_normalizer/backend/src/services/assistantAddressAttemptRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressAttemptRuntimeAdapter.ts @@ -15,6 +15,7 @@ import { runAssistantLivingChatAttemptRuntime, type RunAssistantLivingChatAttemptRuntimeInput } from "./assistantLivingChatAttemptRuntimeAdapter"; +import { buildAssistantLivingChatAttemptRuntimeInput } from "./assistantLivingChatAttemptInputBuilder"; interface AddressAttemptPayload { llmProvider?: unknown; @@ -135,53 +136,54 @@ export async function runAssistantAddressAttemptRuntime( modeDecision, addressRuntimeMeta = null ) => - runLivingChatAttemptRuntimeSafe({ - sessionId: input.sessionId, - userMessage: input.userMessage, - sessionItems: input.sessionItems, - modeDecision, - sessionScope: { - knownOrganizations: input.sessionScope.knownOrganizations, - selectedOrganization: input.sessionScope.selectedOrganization, - activeOrganization: input.sessionScope.activeOrganization - }, - addressRuntimeMeta, - traceIdFactory: () => `chat-${input.messageIdFactory().replace(/^msg-/, "")}`, - toNonEmptyString: input.toNonEmptyString, - mergeKnownOrganizations: input.mergeKnownOrganizations as any, - hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, - shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery, - hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal, - hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal, - hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal, - hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal, - hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal, - shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply, - hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal, - resolveDataScopeProbe: input.resolveDataScopeProbe, - applyScriptGuard: input.applyScriptGuard, - applyGroundingGuard: input.applyGroundingGuard, - buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply, - buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply, - buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply, - buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply, - buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply, - buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply, - appendItem: input.appendItem, - getSession: input.getSession, - persistSession: input.persistSession, - cloneConversation: input.cloneConversation, - logEvent: input.logEvent, - messageIdFactory: input.messageIdFactory, - nowIso: input.nowIso, - payload: input.payload, - chatClient: input.chatClient, - loadAssistantCanonExcerpt: input.loadAssistantCanonExcerpt, - sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText, - defaultModel: input.defaultModel, - defaultBaseUrl: input.defaultBaseUrl, - defaultApiKey: input.defaultApiKey - } as RunAssistantLivingChatAttemptRuntimeInput); + runLivingChatAttemptRuntimeSafe( + buildAssistantLivingChatAttemptRuntimeInput({ + sessionId: input.sessionId, + userMessage: input.userMessage, + sessionItems: input.sessionItems, + modeDecision, + sessionScope: { + knownOrganizations: input.sessionScope.knownOrganizations, + selectedOrganization: input.sessionScope.selectedOrganization, + activeOrganization: input.sessionScope.activeOrganization + }, + addressRuntimeMeta, + toNonEmptyString: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations as any, + hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: input.resolveDataScopeProbe, + applyScriptGuard: input.applyScriptGuard, + applyGroundingGuard: input.applyGroundingGuard, + buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent, + messageIdFactory: input.messageIdFactory, + nowIso: input.nowIso, + payload: input.payload, + chatClient: input.chatClient, + loadAssistantCanonExcerpt: input.loadAssistantCanonExcerpt, + sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText, + defaultModel: input.defaultModel, + defaultBaseUrl: input.defaultBaseUrl, + defaultApiKey: input.defaultApiKey + } as any) as RunAssistantLivingChatAttemptRuntimeInput + ); const runAddressLaneAttempt: RunAssistantAddressRuntimeInput["runAddressLaneAttempt"] = async ( messageUsed, diff --git a/llm_normalizer/backend/src/services/assistantLivingChatAttemptInputBuilder.ts b/llm_normalizer/backend/src/services/assistantLivingChatAttemptInputBuilder.ts new file mode 100644 index 0000000..f59643e --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantLivingChatAttemptInputBuilder.ts @@ -0,0 +1,102 @@ +import type { RunAssistantLivingChatAttemptRuntimeInput } from "./assistantLivingChatAttemptRuntimeAdapter"; + +interface AssistantLivingChatAttemptSessionScope { + knownOrganizations: string[]; + selectedOrganization: string | null; + activeOrganization: string | null; +} + +export interface BuildAssistantLivingChatAttemptRuntimeInputInput { + sessionId: string; + userMessage: string; + sessionItems: RunAssistantLivingChatAttemptRuntimeInput["sessionItems"]; + modeDecision: RunAssistantLivingChatAttemptRuntimeInput["modeDecision"]; + sessionScope: AssistantLivingChatAttemptSessionScope; + addressRuntimeMeta?: Record | null; + toNonEmptyString: RunAssistantLivingChatAttemptRuntimeInput["toNonEmptyString"]; + mergeKnownOrganizations: RunAssistantLivingChatAttemptRuntimeInput["mergeKnownOrganizations"]; + hasAssistantDataScopeMetaQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput["hasAssistantDataScopeMetaQuestionSignal"]; + shouldHandleAsAssistantCapabilityMetaQuery: RunAssistantLivingChatAttemptRuntimeInput["shouldHandleAsAssistantCapabilityMetaQuery"]; + hasDestructiveDataActionSignal: RunAssistantLivingChatAttemptRuntimeInput["hasDestructiveDataActionSignal"]; + hasDangerOrCoercionSignal: RunAssistantLivingChatAttemptRuntimeInput["hasDangerOrCoercionSignal"]; + hasOperationalAdminActionRequestSignal: RunAssistantLivingChatAttemptRuntimeInput["hasOperationalAdminActionRequestSignal"]; + hasOrganizationFactLookupSignal: RunAssistantLivingChatAttemptRuntimeInput["hasOrganizationFactLookupSignal"]; + hasOrganizationFactFollowupSignal: RunAssistantLivingChatAttemptRuntimeInput["hasOrganizationFactFollowupSignal"]; + shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput["shouldEmitOrganizationSelectionReply"]; + hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput["hasAssistantCapabilityQuestionSignal"]; + resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput["resolveDataScopeProbe"]; + applyScriptGuard: RunAssistantLivingChatAttemptRuntimeInput["applyScriptGuard"]; + applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput["applyGroundingGuard"]; + buildAssistantSafetyRefusalReply: RunAssistantLivingChatAttemptRuntimeInput["buildAssistantSafetyRefusalReply"]; + buildAssistantDataScopeContractReply: RunAssistantLivingChatAttemptRuntimeInput["buildAssistantDataScopeContractReply"]; + buildAssistantOrganizationFactBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput["buildAssistantOrganizationFactBoundaryReply"]; + buildAssistantDataScopeSelectionReply: RunAssistantLivingChatAttemptRuntimeInput["buildAssistantDataScopeSelectionReply"]; + buildAssistantOperationalBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput["buildAssistantOperationalBoundaryReply"]; + buildAssistantCapabilityContractReply: RunAssistantLivingChatAttemptRuntimeInput["buildAssistantCapabilityContractReply"]; + appendItem: RunAssistantLivingChatAttemptRuntimeInput["appendItem"]; + getSession: RunAssistantLivingChatAttemptRuntimeInput["getSession"]; + persistSession: RunAssistantLivingChatAttemptRuntimeInput["persistSession"]; + cloneConversation: RunAssistantLivingChatAttemptRuntimeInput["cloneConversation"]; + logEvent: RunAssistantLivingChatAttemptRuntimeInput["logEvent"]; + messageIdFactory: NonNullable["messageIdFactory"]>; + nowIso: RunAssistantLivingChatAttemptRuntimeInput["nowIso"]; + payload: RunAssistantLivingChatAttemptRuntimeInput["payload"]; + chatClient: RunAssistantLivingChatAttemptRuntimeInput["chatClient"]; + loadAssistantCanonExcerpt: RunAssistantLivingChatAttemptRuntimeInput["loadAssistantCanonExcerpt"]; + sanitizeOutgoingAssistantText: RunAssistantLivingChatAttemptRuntimeInput["sanitizeOutgoingAssistantText"]; + defaultModel: RunAssistantLivingChatAttemptRuntimeInput["defaultModel"]; + defaultBaseUrl: RunAssistantLivingChatAttemptRuntimeInput["defaultBaseUrl"]; + defaultApiKey?: RunAssistantLivingChatAttemptRuntimeInput["defaultApiKey"]; +} + +export function buildAssistantLivingChatAttemptRuntimeInput( + input: BuildAssistantLivingChatAttemptRuntimeInputInput +): RunAssistantLivingChatAttemptRuntimeInput { + return { + sessionId: input.sessionId, + userMessage: input.userMessage, + sessionItems: input.sessionItems, + modeDecision: input.modeDecision, + sessionScope: { + knownOrganizations: input.sessionScope.knownOrganizations, + selectedOrganization: input.sessionScope.selectedOrganization, + activeOrganization: input.sessionScope.activeOrganization + }, + addressRuntimeMeta: input.addressRuntimeMeta ?? null, + traceIdFactory: () => `chat-${input.messageIdFactory().replace(/^msg-/, "")}`, + toNonEmptyString: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: input.resolveDataScopeProbe, + applyScriptGuard: input.applyScriptGuard, + applyGroundingGuard: input.applyGroundingGuard, + buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply, + appendItem: input.appendItem, + getSession: input.getSession, + persistSession: input.persistSession, + cloneConversation: input.cloneConversation, + logEvent: input.logEvent, + messageIdFactory: input.messageIdFactory, + nowIso: input.nowIso, + payload: input.payload, + chatClient: input.chatClient, + loadAssistantCanonExcerpt: input.loadAssistantCanonExcerpt, + sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText, + defaultModel: input.defaultModel, + defaultBaseUrl: input.defaultBaseUrl, + defaultApiKey: input.defaultApiKey + }; +} diff --git a/llm_normalizer/backend/tests/assistantLivingChatAttemptInputBuilder.test.ts b/llm_normalizer/backend/tests/assistantLivingChatAttemptInputBuilder.test.ts new file mode 100644 index 0000000..ebadd7f --- /dev/null +++ b/llm_normalizer/backend/tests/assistantLivingChatAttemptInputBuilder.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi } from "vitest"; +import { buildAssistantLivingChatAttemptRuntimeInput } from "../src/services/assistantLivingChatAttemptInputBuilder"; + +function buildInput(overrides: Record = {}) { + return { + sessionId: "asst-1", + userMessage: "че там", + sessionItems: [], + modeDecision: { + mode: "chat", + reason: "living_chat_signal_detected" + }, + sessionScope: { + knownOrganizations: ["Org A"], + selectedOrganization: "Org A", + activeOrganization: "Org A" + }, + addressRuntimeMeta: { + source: "address_runtime" + }, + toNonEmptyString: (value: unknown) => + typeof value === "string" && value.trim().length > 0 ? value.trim() : null, + mergeKnownOrganizations: (values: string[]) => values, + hasAssistantDataScopeMetaQuestionSignal: () => false, + shouldHandleAsAssistantCapabilityMetaQuery: () => false, + hasDestructiveDataActionSignal: () => false, + hasDangerOrCoercionSignal: () => false, + hasOperationalAdminActionRequestSignal: () => false, + hasOrganizationFactLookupSignal: () => false, + hasOrganizationFactFollowupSignal: () => false, + shouldEmitOrganizationSelectionReply: () => false, + hasAssistantCapabilityQuestionSignal: () => false, + resolveDataScopeProbe: () => null, + applyScriptGuard: (chatText: string) => chatText, + applyGroundingGuard: (guardInput: Record) => guardInput, + buildAssistantSafetyRefusalReply: () => "safety", + buildAssistantDataScopeContractReply: () => "scope", + buildAssistantOrganizationFactBoundaryReply: () => "boundary", + buildAssistantDataScopeSelectionReply: () => "selection", + buildAssistantOperationalBoundaryReply: () => "operational", + buildAssistantCapabilityContractReply: () => "capability", + appendItem: () => {}, + getSession: () => null, + persistSession: () => {}, + cloneConversation: (items: unknown[]) => items, + logEvent: () => {}, + messageIdFactory: vi.fn(() => "msg-abc123"), + nowIso: () => "2026-04-11T00:00:00.000Z", + payload: { + llmProvider: "openai" + }, + chatClient: {}, + loadAssistantCanonExcerpt: () => "", + sanitizeOutgoingAssistantText: (value: unknown, fallback = "") => { + const text = typeof value === "string" ? value.trim() : ""; + return text || fallback; + }, + defaultModel: "gpt-5", + defaultBaseUrl: "http://localhost", + defaultApiKey: "key", + ...overrides + } as any; +} + +describe("assistant living chat attempt input builder", () => { + it("builds living-chat runtime input with derived trace id and session scope", () => { + const runtimeInput = buildAssistantLivingChatAttemptRuntimeInput(buildInput()); + + expect(runtimeInput.sessionId).toBe("asst-1"); + expect(runtimeInput.userMessage).toBe("че там"); + expect(runtimeInput.traceIdFactory()).toBe("chat-abc123"); + expect(runtimeInput.sessionScope).toEqual({ + knownOrganizations: ["Org A"], + selectedOrganization: "Org A", + activeOrganization: "Org A" + }); + expect(runtimeInput.modeDecision).toEqual({ + mode: "chat", + reason: "living_chat_signal_detected" + }); + }); + + it("normalizes absent address runtime meta to null and preserves optional api key", () => { + const runtimeInput = buildAssistantLivingChatAttemptRuntimeInput( + buildInput({ + addressRuntimeMeta: undefined, + defaultApiKey: undefined + }) + ); + + expect(runtimeInput.addressRuntimeMeta).toBeNull(); + expect(runtimeInput.defaultApiKey).toBeUndefined(); + expect(runtimeInput.defaultBaseUrl).toBe("http://localhost"); + expect(runtimeInput.defaultModel).toBe("gpt-5"); + }); +});