diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 3bffbb2..b349fd7 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -1501,7 +1501,43 @@ Validation: - `assistantDeepTurnPackagingRuntimeAdapter.test.ts` - `assistantWave10SettlementCorrectiveRegression.test.ts` -Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 + 2.43 + 2.44 + 2.45 + 2.46 + 2.47 completed)** +Implemented in current pass (Phase 2.48): +1. Extracted turn-level runtime input assembly from `assistantService` into dedicated builder module: + - `assistantTurnRuntimeInputBuilder.ts` + - introduced: + - `buildAssistantUserTurnBootstrapRuntimeInput(...)` + - `buildAssistantAddressAttemptRuntimeInput(...)` + - `buildAssistantDeepTurnAttemptRuntimeInput(...)` +2. Rewired `assistantService.handleMessage` to consume builder outputs (behavior-preserving): + - moved bulky dependency mapping for bootstrap/address/deep attempts out of service body; + - preserved existing runtime adapters and route behavior. +3. Added focused unit tests: + - `assistantTurnRuntimeInputBuilder.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `assistantTurnRuntimeInputBuilder.test.ts` + - `assistantTurnAttemptRuntimeAdapter.test.ts` + - `assistantAddressAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseAttemptRuntimeAdapter.test.ts` + - `assistantLivingChatAttemptRuntimeAdapter.test.ts` + - `assistantAddressLaneAttemptRuntimeAdapter.test.ts` + - `assistantUserTurnBootstrapRuntimeAdapter.test.ts` + - `assistantLivingChatLlmRuntimeAdapter.test.ts` + - `assistantLivingChatHandlerRuntimeAdapter.test.ts` + - `assistantLivingChatRuntimeAdapter.test.ts` + - `assistantAddressRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnPackagingRuntimeAdapter.test.ts` + - `assistantWave10SettlementCorrectiveRegression.test.ts` + +Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 + 2.43 + 2.44 + 2.45 + 2.46 + 2.47 + 2.48 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 03dd03d..75ad4b2 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -66,6 +66,7 @@ const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistan const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding")); const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter")); const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter")); +const assistantTurnRuntimeInputBuilder_1 = __importStar(require("./assistantTurnRuntimeInputBuilder")); const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter")); const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning")); const iconv_lite_1 = __importDefault(require("iconv-lite")); @@ -4370,123 +4371,90 @@ class AssistantService { return this.sessions.getSession(sessionId); } async handleMessage(payload) { + const turnRuntimeDeps = { + ensureSession: (targetSessionId) => this.sessions.ensureSession(targetSessionId), + appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), + getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), + persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), + setInvestigationState: (targetSessionId, snapshot) => this.sessions.setInvestigationState(targetSessionId, snapshot), + normalize: (normalizePayload) => this.normalizerService.normalize(normalizePayload), + executeRouteRuntime: (route, fragmentText, options) => this.dataLayer.executeRouteRuntime(route, fragmentText, options), + tryAddressQueryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options), + chatClient: this.chatClient, + messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, + nowIso: () => new Date().toISOString(), + defaultApiKey: process.env.OPENAI_API_KEY ?? "", + logEvent: (runtimePayload) => (0, log_1.logJson)(runtimePayload), + featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, + featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, + featureInvestigationStateV1: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1, + featureStateFollowupBindingV1: config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1, + featureContractsV11: config_1.FEATURE_ASSISTANT_CONTRACTS_V11, + featureAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11, + featureProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, + featureLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, + defaultModel: config_1.DEFAULT_MODEL, + defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL, + compactWhitespace, + repairAddressMojibake, + resolveRuntimeAnalysisContext, + runAddressLlmPreDecompose: async (runtimePayload, runtimeUserMessage) => runAddressLlmPreDecompose(this.normalizerService, runtimePayload, runtimeUserMessage), + buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback, + toNonEmptyString, + resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2, + mergeFollowupContextWithOrganizationScope, + isRetryableAddressLimitedResult, + mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), + applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), + applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), + buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply, + loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt, + sanitizeOutgoingAssistantText, + buildAddressDebugPayload, + buildAddressFollowupOffer, + buildFollowupStateBinding, + resolveBusinessScopeAlignment, + inferP0DomainFromMessage, + resolveBusinessScopeFromLiveContext, + extractRequirements, + toExecutionPlan, + enforceRbpLiveRoutePlan, + enforceFaLiveRoutePlan, + mapNoRouteReason, + buildSkippedResult, + evaluateCoverage, + checkGrounding, + collectRbpLiveRouteAudit, + collectFaLiveRouteAudit, + hasExplicitPeriodAnchorFromNormalized, + extractDroppedIntentSegments: (normalizedPayload) => extractDiscardedIntentSegments(normalizedPayload), + toDebugRoutes: (routeSummary) => toDebugRoutes(routeSummary), + extractExecutionState + }; const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({ payload, - runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)({ - payload: runtimePayload, - ensureSession: (targetSessionId) => this.sessions.ensureSession(targetSessionId), - appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), - getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), - persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), - compactWhitespace, - repairAddressMojibake, - resolveRuntimeAnalysisContext, - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, - nowIso: () => new Date().toISOString() - }), + runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)), resolveSessionOrganizationScopeContext: (runtimeUserMessage, sessionItems) => resolveSessionOrganizationScopeContext(runtimeUserMessage, sessionItems), - runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)({ - featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, - sessionId: runtimeInput.sessionId, - userMessage: runtimeInput.userMessage, - sessionItems: runtimeInput.sessionItems, - payload: runtimeInput.payload, - sessionScope: { - knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations, - selectedOrganization: runtimeInput.sessionOrganizationScope.selectedOrganization, - activeOrganization: runtimeInput.sessionOrganizationScope.activeOrganization - }, - featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, - runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, runtimeInput.payload, runtimeInput.userMessage), - buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, - sanitizeAddressMessageForFallback, - toNonEmptyString, - resolveAddressFollowupCarryoverContext, - resolveAssistantOrchestrationDecision, - buildAddressDialogContinuationContractV2, - runtimeAnalysisContextAsOfDate: runtimeInput.runtimeAnalysisContext.as_of_date, - compactWhitespace, - mergeFollowupContextWithOrganizationScope, - runAddressQueryTryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options), - isRetryableAddressLimitedResult, - mergeKnownOrganizations, - hasAssistantDataScopeMetaQuestionSignal, - shouldHandleAsAssistantCapabilityMetaQuery, - hasDestructiveDataActionSignal, - hasDangerOrCoercionSignal, - hasOperationalAdminActionRequestSignal, - hasOrganizationFactLookupSignal, - hasOrganizationFactFollowupSignal, - shouldEmitOrganizationSelectionReply, - hasAssistantCapabilityQuestionSignal, - resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), - applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), - applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), - buildAssistantSafetyRefusalReply, - buildAssistantDataScopeContractReply, - buildAssistantOrganizationFactBoundaryReply, - buildAssistantDataScopeSelectionReply, - buildAssistantOperationalBoundaryReply, - buildAssistantCapabilityContractReply, - chatClient: this.chatClient, - loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt, - sanitizeOutgoingAssistantText, - defaultModel: config_1.DEFAULT_MODEL, - defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL, - defaultApiKey: process.env.OPENAI_API_KEY ?? "", - buildAddressDebugPayload, - buildAddressFollowupOffer, - 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: (runtimePayload) => (0, log_1.logJson)(runtimePayload), - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, - nowIso: () => new Date().toISOString() - }), - runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)({ - sessionId: runtimeInput.sessionId, - questionId: runtimeInput.questionId, - userMessage: runtimeInput.userMessage, - payload: runtimeInput.payload, - runtimeAnalysisContext: runtimeInput.runtimeAnalysisContext, - sessionInvestigationState: runtimeInput.sessionInvestigationState, - addressRuntimeMetaForDeep: runtimeInput.addressRuntimeMetaForDeep, - featureInvestigationStateV1: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1, - featureStateFollowupBindingV1: config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1, - featureContractsV11: config_1.FEATURE_ASSISTANT_CONTRACTS_V11, - featureAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11, - featureProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, - featureLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, - buildFollowupStateBinding, - normalize: (normalizePayload) => this.normalizerService.normalize(normalizePayload), - resolveBusinessScopeAlignment, - inferP0DomainFromMessage, - resolveBusinessScopeFromLiveContext, - extractRequirements, - toExecutionPlan, - enforceRbpLiveRoutePlan, - enforceFaLiveRoutePlan, - executeRouteRuntime: (route, fragmentText, options) => this.dataLayer.executeRouteRuntime(route, fragmentText, options), - mapNoRouteReason, - buildSkippedResult, - evaluateCoverage, - checkGrounding, - collectRbpLiveRouteAudit, - collectFaLiveRouteAudit, - hasExplicitPeriodAnchor: (normalizedPayload) => hasExplicitPeriodAnchorFromNormalized(normalizedPayload), - extractDroppedIntentSegments: (normalizedPayload) => extractDiscardedIntentSegments(normalizedPayload), - buildDebugRoutes: (routeSummary) => toDebugRoutes(routeSummary), - extractExecutionState: (normalizedPayload) => extractExecutionState(normalizedPayload), - sanitizeReply: (value, fallback) => sanitizeOutgoingAssistantText(value, fallback), - persistInvestigationState: (targetSessionId, snapshot) => this.sessions.setInvestigationState(targetSessionId, snapshot), - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, - 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: (runtimePayload) => (0, log_1.logJson)(runtimePayload) - }) + runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantAddressAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)), + runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantDeepTurnAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)) }); return turnRuntime.response; } diff --git a/llm_normalizer/backend/dist/services/assistantTurnRuntimeInputBuilder.js b/llm_normalizer/backend/dist/services/assistantTurnRuntimeInputBuilder.js new file mode 100644 index 0000000..4b5ce9e --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantTurnRuntimeInputBuilder.js @@ -0,0 +1,125 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildAssistantUserTurnBootstrapRuntimeInput = buildAssistantUserTurnBootstrapRuntimeInput; +exports.buildAssistantAddressAttemptRuntimeInput = buildAssistantAddressAttemptRuntimeInput; +exports.buildAssistantDeepTurnAttemptRuntimeInput = buildAssistantDeepTurnAttemptRuntimeInput; +function buildAssistantUserTurnBootstrapRuntimeInput(payload, deps) { + return { + payload, + ensureSession: deps.ensureSession, + appendItem: deps.appendItem, + getSession: deps.getSession, + persistSession: deps.persistSession, + compactWhitespace: deps.compactWhitespace, + repairAddressMojibake: deps.repairAddressMojibake, + resolveRuntimeAnalysisContext: deps.resolveRuntimeAnalysisContext, + messageIdFactory: deps.messageIdFactory, + nowIso: deps.nowIso + }; +} +function buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps) { + return { + featureAssistantAddressQueryV1: deps.featureAssistantAddressQueryV1, + sessionId: runtimeInput.sessionId, + userMessage: runtimeInput.userMessage, + sessionItems: runtimeInput.sessionItems, + payload: runtimeInput.payload, + sessionScope: { + knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations, + selectedOrganization: runtimeInput.sessionOrganizationScope.selectedOrganization, + activeOrganization: runtimeInput.sessionOrganizationScope.activeOrganization + }, + featureAddressLlmPredecomposeV1: deps.featureAddressLlmPredecomposeV1, + runAddressLlmPreDecompose: async () => deps.runAddressLlmPreDecompose(runtimeInput.payload, runtimeInput.userMessage), + buildAddressLlmPredecomposeContractV1: deps.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback: deps.sanitizeAddressMessageForFallback, + toNonEmptyString: deps.toNonEmptyString, + resolveAddressFollowupCarryoverContext: deps.resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision: deps.resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2: deps.buildAddressDialogContinuationContractV2, + runtimeAnalysisContextAsOfDate: runtimeInput.runtimeAnalysisContext.as_of_date, + compactWhitespace: deps.compactWhitespace, + mergeFollowupContextWithOrganizationScope: deps.mergeFollowupContextWithOrganizationScope, + runAddressQueryTryHandle: deps.tryAddressQueryHandle, + isRetryableAddressLimitedResult: deps.isRetryableAddressLimitedResult, + mergeKnownOrganizations: deps.mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal: deps.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: deps.shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal: deps.hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal: deps.hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal: deps.hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal: deps.hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal: deps.hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: deps.resolveDataScopeProbe, + applyScriptGuard: deps.applyScriptGuard, + applyGroundingGuard: deps.applyGroundingGuard, + buildAssistantSafetyRefusalReply: deps.buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply: deps.buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply: deps.buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply: deps.buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply: deps.buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply: deps.buildAssistantCapabilityContractReply, + chatClient: deps.chatClient, + loadAssistantCanonExcerpt: deps.loadAssistantCanonExcerpt, + sanitizeOutgoingAssistantText: deps.sanitizeOutgoingAssistantText, + defaultModel: deps.defaultModel, + defaultBaseUrl: deps.defaultBaseUrl, + defaultApiKey: deps.defaultApiKey, + buildAddressDebugPayload: deps.buildAddressDebugPayload, + buildAddressFollowupOffer: deps.buildAddressFollowupOffer, + appendItem: deps.appendItem, + getSession: deps.getSession, + persistSession: deps.persistSession, + cloneConversation: (items) => items.map((item) => ({ ...item })), + logEvent: deps.logEvent, + messageIdFactory: deps.messageIdFactory, + nowIso: deps.nowIso + }; +} +function buildAssistantDeepTurnAttemptRuntimeInput(runtimeInput, deps) { + return { + sessionId: runtimeInput.sessionId, + questionId: runtimeInput.questionId, + userMessage: runtimeInput.userMessage, + payload: runtimeInput.payload, + runtimeAnalysisContext: runtimeInput.runtimeAnalysisContext, + sessionInvestigationState: runtimeInput.sessionInvestigationState, + addressRuntimeMetaForDeep: runtimeInput.addressRuntimeMetaForDeep, + featureInvestigationStateV1: deps.featureInvestigationStateV1, + featureStateFollowupBindingV1: deps.featureStateFollowupBindingV1, + featureContractsV11: deps.featureContractsV11, + featureAnswerPolicyV11: deps.featureAnswerPolicyV11, + featureProblemCentricAnswerV1: deps.featureProblemCentricAnswerV1, + featureLifecycleAnswerV1: deps.featureLifecycleAnswerV1, + buildFollowupStateBinding: deps.buildFollowupStateBinding, + normalize: deps.normalize, + resolveBusinessScopeAlignment: deps.resolveBusinessScopeAlignment, + inferP0DomainFromMessage: deps.inferP0DomainFromMessage, + resolveBusinessScopeFromLiveContext: deps.resolveBusinessScopeFromLiveContext, + extractRequirements: deps.extractRequirements, + toExecutionPlan: deps.toExecutionPlan, + enforceRbpLiveRoutePlan: deps.enforceRbpLiveRoutePlan, + enforceFaLiveRoutePlan: deps.enforceFaLiveRoutePlan, + executeRouteRuntime: deps.executeRouteRuntime, + mapNoRouteReason: deps.mapNoRouteReason, + buildSkippedResult: deps.buildSkippedResult, + evaluateCoverage: deps.evaluateCoverage, + checkGrounding: deps.checkGrounding, + collectRbpLiveRouteAudit: deps.collectRbpLiveRouteAudit, + collectFaLiveRouteAudit: deps.collectFaLiveRouteAudit, + hasExplicitPeriodAnchor: deps.hasExplicitPeriodAnchorFromNormalized, + extractDroppedIntentSegments: deps.extractDroppedIntentSegments, + buildDebugRoutes: deps.toDebugRoutes, + extractExecutionState: deps.extractExecutionState, + sanitizeReply: deps.sanitizeOutgoingAssistantText, + persistInvestigationState: deps.setInvestigationState, + messageIdFactory: deps.messageIdFactory, + appendItem: deps.appendItem, + getSession: deps.getSession, + persistSession: deps.persistSession, + cloneConversation: (items) => items.map((item) => ({ ...item })), + logEvent: deps.logEvent + }; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 75b0f36..b36099d 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -20,6 +20,7 @@ import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAtte import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter"; import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter"; +import * as assistantTurnRuntimeInputBuilder_1 from "./assistantTurnRuntimeInputBuilder"; import * as assistantUserTurnBootstrapRuntimeAdapter_1 from "./assistantUserTurnBootstrapRuntimeAdapter"; import * as assistantQueryPlanning_1 from "./assistantQueryPlanning"; import iconv from "iconv-lite"; @@ -4325,123 +4326,90 @@ export class AssistantService { return this.sessions.getSession(sessionId); } async handleMessage(payload) { + const turnRuntimeDeps = { + ensureSession: (targetSessionId) => this.sessions.ensureSession(targetSessionId), + appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), + getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), + persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), + setInvestigationState: (targetSessionId, snapshot) => this.sessions.setInvestigationState(targetSessionId, snapshot), + normalize: (normalizePayload) => this.normalizerService.normalize(normalizePayload), + executeRouteRuntime: (route, fragmentText, options) => this.dataLayer.executeRouteRuntime(route, fragmentText, options), + tryAddressQueryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options), + chatClient: this.chatClient, + messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, + nowIso: () => new Date().toISOString(), + defaultApiKey: process.env.OPENAI_API_KEY ?? "", + logEvent: (runtimePayload) => (0, log_1.logJson)(runtimePayload), + featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, + featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, + featureInvestigationStateV1: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1, + featureStateFollowupBindingV1: config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1, + featureContractsV11: config_1.FEATURE_ASSISTANT_CONTRACTS_V11, + featureAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11, + featureProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, + featureLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, + defaultModel: config_1.DEFAULT_MODEL, + defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL, + compactWhitespace, + repairAddressMojibake, + resolveRuntimeAnalysisContext, + runAddressLlmPreDecompose: async (runtimePayload, runtimeUserMessage) => runAddressLlmPreDecompose(this.normalizerService, runtimePayload, runtimeUserMessage), + buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, + sanitizeAddressMessageForFallback, + toNonEmptyString, + resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision, + buildAddressDialogContinuationContractV2, + mergeFollowupContextWithOrganizationScope, + isRetryableAddressLimitedResult, + mergeKnownOrganizations, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasDestructiveDataActionSignal, + hasDangerOrCoercionSignal, + hasOperationalAdminActionRequestSignal, + hasOrganizationFactLookupSignal, + hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply, + hasAssistantCapabilityQuestionSignal, + resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), + applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), + applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), + buildAssistantSafetyRefusalReply, + buildAssistantDataScopeContractReply, + buildAssistantOrganizationFactBoundaryReply, + buildAssistantDataScopeSelectionReply, + buildAssistantOperationalBoundaryReply, + buildAssistantCapabilityContractReply, + loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt, + sanitizeOutgoingAssistantText, + buildAddressDebugPayload, + buildAddressFollowupOffer, + buildFollowupStateBinding, + resolveBusinessScopeAlignment, + inferP0DomainFromMessage, + resolveBusinessScopeFromLiveContext, + extractRequirements, + toExecutionPlan, + enforceRbpLiveRoutePlan, + enforceFaLiveRoutePlan, + mapNoRouteReason, + buildSkippedResult, + evaluateCoverage, + checkGrounding, + collectRbpLiveRouteAudit, + collectFaLiveRouteAudit, + hasExplicitPeriodAnchorFromNormalized, + extractDroppedIntentSegments: (normalizedPayload) => extractDiscardedIntentSegments(normalizedPayload), + toDebugRoutes: (routeSummary) => toDebugRoutes(routeSummary), + extractExecutionState + }; const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({ payload, - runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)({ - payload: runtimePayload, - ensureSession: (targetSessionId) => this.sessions.ensureSession(targetSessionId), - appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), - getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), - persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), - compactWhitespace, - repairAddressMojibake, - resolveRuntimeAnalysisContext, - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, - nowIso: () => new Date().toISOString() - }), + runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)), resolveSessionOrganizationScopeContext: (runtimeUserMessage, sessionItems) => resolveSessionOrganizationScopeContext(runtimeUserMessage, sessionItems), - runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)({ - featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, - sessionId: runtimeInput.sessionId, - userMessage: runtimeInput.userMessage, - sessionItems: runtimeInput.sessionItems, - payload: runtimeInput.payload, - sessionScope: { - knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations, - selectedOrganization: runtimeInput.sessionOrganizationScope.selectedOrganization, - activeOrganization: runtimeInput.sessionOrganizationScope.activeOrganization - }, - featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, - runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, runtimeInput.payload, runtimeInput.userMessage), - buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1, - sanitizeAddressMessageForFallback, - toNonEmptyString, - resolveAddressFollowupCarryoverContext, - resolveAssistantOrchestrationDecision, - buildAddressDialogContinuationContractV2, - runtimeAnalysisContextAsOfDate: runtimeInput.runtimeAnalysisContext.as_of_date, - compactWhitespace, - mergeFollowupContextWithOrganizationScope, - runAddressQueryTryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options), - isRetryableAddressLimitedResult, - mergeKnownOrganizations, - hasAssistantDataScopeMetaQuestionSignal, - shouldHandleAsAssistantCapabilityMetaQuery, - hasDestructiveDataActionSignal, - hasDangerOrCoercionSignal, - hasOperationalAdminActionRequestSignal, - hasOrganizationFactLookupSignal, - hasOrganizationFactFollowupSignal, - shouldEmitOrganizationSelectionReply, - hasAssistantCapabilityQuestionSignal, - resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(), - applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage), - applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput), - buildAssistantSafetyRefusalReply, - buildAssistantDataScopeContractReply, - buildAssistantOrganizationFactBoundaryReply, - buildAssistantDataScopeSelectionReply, - buildAssistantOperationalBoundaryReply, - buildAssistantCapabilityContractReply, - chatClient: this.chatClient, - loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt, - sanitizeOutgoingAssistantText, - defaultModel: config_1.DEFAULT_MODEL, - defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL, - defaultApiKey: process.env.OPENAI_API_KEY ?? "", - buildAddressDebugPayload, - buildAddressFollowupOffer, - 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: (runtimePayload) => (0, log_1.logJson)(runtimePayload), - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, - nowIso: () => new Date().toISOString() - }), - runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)({ - sessionId: runtimeInput.sessionId, - questionId: runtimeInput.questionId, - userMessage: runtimeInput.userMessage, - payload: runtimeInput.payload, - runtimeAnalysisContext: runtimeInput.runtimeAnalysisContext, - sessionInvestigationState: runtimeInput.sessionInvestigationState, - addressRuntimeMetaForDeep: runtimeInput.addressRuntimeMetaForDeep, - featureInvestigationStateV1: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1, - featureStateFollowupBindingV1: config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1, - featureContractsV11: config_1.FEATURE_ASSISTANT_CONTRACTS_V11, - featureAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11, - featureProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, - featureLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, - buildFollowupStateBinding, - normalize: (normalizePayload) => this.normalizerService.normalize(normalizePayload), - resolveBusinessScopeAlignment, - inferP0DomainFromMessage, - resolveBusinessScopeFromLiveContext, - extractRequirements, - toExecutionPlan, - enforceRbpLiveRoutePlan, - enforceFaLiveRoutePlan, - executeRouteRuntime: (route, fragmentText, options) => this.dataLayer.executeRouteRuntime(route, fragmentText, options), - mapNoRouteReason, - buildSkippedResult, - evaluateCoverage, - checkGrounding, - collectRbpLiveRouteAudit, - collectFaLiveRouteAudit, - hasExplicitPeriodAnchor: (normalizedPayload) => hasExplicitPeriodAnchorFromNormalized(normalizedPayload), - extractDroppedIntentSegments: (normalizedPayload) => extractDiscardedIntentSegments(normalizedPayload), - buildDebugRoutes: (routeSummary) => toDebugRoutes(routeSummary), - extractExecutionState: (normalizedPayload) => extractExecutionState(normalizedPayload), - sanitizeReply: (value, fallback) => sanitizeOutgoingAssistantText(value, fallback), - persistInvestigationState: (targetSessionId, snapshot) => this.sessions.setInvestigationState(targetSessionId, snapshot), - messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, - 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: (runtimePayload) => (0, log_1.logJson)(runtimePayload) - }) + runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantAddressAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)), + runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantDeepTurnAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)) }); return turnRuntime.response; } diff --git a/llm_normalizer/backend/src/services/assistantTurnRuntimeInputBuilder.ts b/llm_normalizer/backend/src/services/assistantTurnRuntimeInputBuilder.ts new file mode 100644 index 0000000..fef9732 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantTurnRuntimeInputBuilder.ts @@ -0,0 +1,221 @@ +import type { RunAssistantAddressAttemptRuntimeInput } from "./assistantAddressAttemptRuntimeAdapter"; +import type { RunAssistantDeepTurnAttemptRuntimeInput } from "./assistantDeepTurnAttemptRuntimeAdapter"; +import type { + RunAssistantTurnAttemptRuntimeAddressInput, + RunAssistantTurnAttemptRuntimeDeepInput +} from "./assistantTurnAttemptRuntimeAdapter"; +import type { + AssistantUserTurnBootstrapPayloadLike, + RunAssistantUserTurnBootstrapRuntimeInput +} from "./assistantUserTurnBootstrapRuntimeAdapter"; + +export interface AssistantTurnRuntimeBuilderDeps { + ensureSession: RunAssistantUserTurnBootstrapRuntimeInput["ensureSession"]; + appendItem: RunAssistantUserTurnBootstrapRuntimeInput["appendItem"]; + getSession: RunAssistantUserTurnBootstrapRuntimeInput["getSession"]; + persistSession: RunAssistantUserTurnBootstrapRuntimeInput["persistSession"]; + setInvestigationState: (sessionId: string, snapshot: unknown) => void; + normalize: (payload: unknown) => Promise; + executeRouteRuntime: (route: string, fragmentText: string, options?: unknown) => Promise; + tryAddressQueryHandle: (messageUsed: string, options?: unknown) => Promise; + chatClient: unknown; + messageIdFactory: () => string; + nowIso: () => string; + defaultApiKey: string; + logEvent: (payload: Record) => void; + featureAssistantAddressQueryV1: boolean; + featureAddressLlmPredecomposeV1: boolean; + featureInvestigationStateV1: boolean; + featureStateFollowupBindingV1: boolean; + featureContractsV11: boolean; + featureAnswerPolicyV11: boolean; + featureProblemCentricAnswerV1: boolean; + featureLifecycleAnswerV1: boolean; + defaultModel: string; + defaultBaseUrl: string; + compactWhitespace: RunAssistantUserTurnBootstrapRuntimeInput["compactWhitespace"]; + repairAddressMojibake: RunAssistantUserTurnBootstrapRuntimeInput["repairAddressMojibake"]; + resolveRuntimeAnalysisContext: RunAssistantUserTurnBootstrapRuntimeInput["resolveRuntimeAnalysisContext"]; + runAddressLlmPreDecompose: (payload: unknown, userMessage: string) => Promise>; + buildAddressLlmPredecomposeContractV1: (...args: any[]) => unknown; + sanitizeAddressMessageForFallback: (...args: any[]) => unknown; + toNonEmptyString: (...args: any[]) => unknown; + resolveAddressFollowupCarryoverContext: (...args: any[]) => unknown; + resolveAssistantOrchestrationDecision: (...args: any[]) => unknown; + buildAddressDialogContinuationContractV2: (...args: any[]) => unknown; + mergeFollowupContextWithOrganizationScope: (...args: any[]) => unknown; + isRetryableAddressLimitedResult: (...args: any[]) => unknown; + mergeKnownOrganizations: (...args: any[]) => unknown; + hasAssistantDataScopeMetaQuestionSignal: (...args: any[]) => unknown; + shouldHandleAsAssistantCapabilityMetaQuery: (...args: any[]) => unknown; + hasDestructiveDataActionSignal: (...args: any[]) => unknown; + hasDangerOrCoercionSignal: (...args: any[]) => unknown; + hasOperationalAdminActionRequestSignal: (...args: any[]) => unknown; + hasOrganizationFactLookupSignal: (...args: any[]) => unknown; + hasOrganizationFactFollowupSignal: (...args: any[]) => unknown; + shouldEmitOrganizationSelectionReply: (...args: any[]) => unknown; + hasAssistantCapabilityQuestionSignal: (...args: any[]) => unknown; + resolveDataScopeProbe: () => unknown; + applyScriptGuard: (...args: any[]) => unknown; + applyGroundingGuard: (...args: any[]) => unknown; + buildAssistantSafetyRefusalReply: (...args: any[]) => unknown; + buildAssistantDataScopeContractReply: (...args: any[]) => unknown; + buildAssistantOrganizationFactBoundaryReply: (...args: any[]) => unknown; + buildAssistantDataScopeSelectionReply: (...args: any[]) => unknown; + buildAssistantOperationalBoundaryReply: (...args: any[]) => unknown; + buildAssistantCapabilityContractReply: (...args: any[]) => unknown; + loadAssistantCanonExcerpt: (...args: any[]) => unknown; + sanitizeOutgoingAssistantText: (value: unknown, fallback?: string) => string; + buildAddressDebugPayload: (...args: any[]) => unknown; + buildAddressFollowupOffer: (...args: any[]) => unknown; + buildFollowupStateBinding: (...args: any[]) => unknown; + resolveBusinessScopeAlignment: (...args: any[]) => unknown; + inferP0DomainFromMessage: (...args: any[]) => unknown; + resolveBusinessScopeFromLiveContext: (...args: any[]) => unknown; + extractRequirements: (...args: any[]) => unknown; + toExecutionPlan: (...args: any[]) => unknown; + enforceRbpLiveRoutePlan: (...args: any[]) => unknown; + enforceFaLiveRoutePlan: (...args: any[]) => unknown; + mapNoRouteReason: (...args: any[]) => unknown; + buildSkippedResult: (...args: any[]) => unknown; + evaluateCoverage: (...args: any[]) => unknown; + checkGrounding: (...args: any[]) => unknown; + collectRbpLiveRouteAudit: (...args: any[]) => unknown; + collectFaLiveRouteAudit: (...args: any[]) => unknown; + hasExplicitPeriodAnchorFromNormalized: (...args: any[]) => unknown; + extractDroppedIntentSegments: (...args: any[]) => unknown; + toDebugRoutes: (...args: any[]) => unknown; + extractExecutionState: (...args: any[]) => unknown; +} + +export function buildAssistantUserTurnBootstrapRuntimeInput( + payload: AssistantUserTurnBootstrapPayloadLike, + deps: AssistantTurnRuntimeBuilderDeps +): RunAssistantUserTurnBootstrapRuntimeInput { + return { + payload, + ensureSession: deps.ensureSession, + appendItem: deps.appendItem, + getSession: deps.getSession, + persistSession: deps.persistSession, + compactWhitespace: deps.compactWhitespace, + repairAddressMojibake: deps.repairAddressMojibake, + resolveRuntimeAnalysisContext: deps.resolveRuntimeAnalysisContext, + messageIdFactory: deps.messageIdFactory, + nowIso: deps.nowIso + }; +} + +export function buildAssistantAddressAttemptRuntimeInput( + runtimeInput: RunAssistantTurnAttemptRuntimeAddressInput, + deps: AssistantTurnRuntimeBuilderDeps +): RunAssistantAddressAttemptRuntimeInput { + return { + featureAssistantAddressQueryV1: deps.featureAssistantAddressQueryV1, + sessionId: runtimeInput.sessionId, + userMessage: runtimeInput.userMessage, + sessionItems: runtimeInput.sessionItems, + payload: runtimeInput.payload as any, + sessionScope: { + knownOrganizations: runtimeInput.sessionOrganizationScope.knownOrganizations, + selectedOrganization: runtimeInput.sessionOrganizationScope.selectedOrganization, + activeOrganization: runtimeInput.sessionOrganizationScope.activeOrganization + }, + featureAddressLlmPredecomposeV1: deps.featureAddressLlmPredecomposeV1, + runAddressLlmPreDecompose: async () => deps.runAddressLlmPreDecompose(runtimeInput.payload, runtimeInput.userMessage), + buildAddressLlmPredecomposeContractV1: deps.buildAddressLlmPredecomposeContractV1 as any, + sanitizeAddressMessageForFallback: deps.sanitizeAddressMessageForFallback as any, + toNonEmptyString: deps.toNonEmptyString as any, + resolveAddressFollowupCarryoverContext: deps.resolveAddressFollowupCarryoverContext as any, + resolveAssistantOrchestrationDecision: deps.resolveAssistantOrchestrationDecision as any, + buildAddressDialogContinuationContractV2: deps.buildAddressDialogContinuationContractV2 as any, + runtimeAnalysisContextAsOfDate: runtimeInput.runtimeAnalysisContext.as_of_date, + compactWhitespace: deps.compactWhitespace, + mergeFollowupContextWithOrganizationScope: deps.mergeFollowupContextWithOrganizationScope as any, + runAddressQueryTryHandle: deps.tryAddressQueryHandle as any, + isRetryableAddressLimitedResult: deps.isRetryableAddressLimitedResult as any, + mergeKnownOrganizations: deps.mergeKnownOrganizations as any, + hasAssistantDataScopeMetaQuestionSignal: deps.hasAssistantDataScopeMetaQuestionSignal as any, + shouldHandleAsAssistantCapabilityMetaQuery: deps.shouldHandleAsAssistantCapabilityMetaQuery as any, + hasDestructiveDataActionSignal: deps.hasDestructiveDataActionSignal as any, + hasDangerOrCoercionSignal: deps.hasDangerOrCoercionSignal as any, + hasOperationalAdminActionRequestSignal: deps.hasOperationalAdminActionRequestSignal as any, + hasOrganizationFactLookupSignal: deps.hasOrganizationFactLookupSignal as any, + hasOrganizationFactFollowupSignal: deps.hasOrganizationFactFollowupSignal as any, + shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply as any, + hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal as any, + resolveDataScopeProbe: deps.resolveDataScopeProbe as any, + applyScriptGuard: deps.applyScriptGuard as any, + applyGroundingGuard: deps.applyGroundingGuard as any, + buildAssistantSafetyRefusalReply: deps.buildAssistantSafetyRefusalReply as any, + buildAssistantDataScopeContractReply: deps.buildAssistantDataScopeContractReply as any, + buildAssistantOrganizationFactBoundaryReply: deps.buildAssistantOrganizationFactBoundaryReply as any, + buildAssistantDataScopeSelectionReply: deps.buildAssistantDataScopeSelectionReply as any, + buildAssistantOperationalBoundaryReply: deps.buildAssistantOperationalBoundaryReply as any, + buildAssistantCapabilityContractReply: deps.buildAssistantCapabilityContractReply as any, + chatClient: deps.chatClient as any, + loadAssistantCanonExcerpt: deps.loadAssistantCanonExcerpt as any, + sanitizeOutgoingAssistantText: deps.sanitizeOutgoingAssistantText, + defaultModel: deps.defaultModel, + defaultBaseUrl: deps.defaultBaseUrl, + defaultApiKey: deps.defaultApiKey, + buildAddressDebugPayload: deps.buildAddressDebugPayload as any, + buildAddressFollowupOffer: deps.buildAddressFollowupOffer as any, + appendItem: deps.appendItem as any, + getSession: deps.getSession as any, + persistSession: deps.persistSession as any, + cloneConversation: (items) => items.map((item) => ({ ...item })), + logEvent: deps.logEvent as any, + messageIdFactory: deps.messageIdFactory, + nowIso: deps.nowIso + }; +} + +export function buildAssistantDeepTurnAttemptRuntimeInput( + runtimeInput: RunAssistantTurnAttemptRuntimeDeepInput, + deps: AssistantTurnRuntimeBuilderDeps +): RunAssistantDeepTurnAttemptRuntimeInput { + return { + sessionId: runtimeInput.sessionId, + questionId: runtimeInput.questionId, + userMessage: runtimeInput.userMessage, + payload: runtimeInput.payload as any, + runtimeAnalysisContext: runtimeInput.runtimeAnalysisContext as any, + sessionInvestigationState: runtimeInput.sessionInvestigationState as any, + addressRuntimeMetaForDeep: runtimeInput.addressRuntimeMetaForDeep, + featureInvestigationStateV1: deps.featureInvestigationStateV1, + featureStateFollowupBindingV1: deps.featureStateFollowupBindingV1, + featureContractsV11: deps.featureContractsV11, + featureAnswerPolicyV11: deps.featureAnswerPolicyV11, + featureProblemCentricAnswerV1: deps.featureProblemCentricAnswerV1, + featureLifecycleAnswerV1: deps.featureLifecycleAnswerV1, + buildFollowupStateBinding: deps.buildFollowupStateBinding as any, + normalize: deps.normalize as any, + resolveBusinessScopeAlignment: deps.resolveBusinessScopeAlignment as any, + inferP0DomainFromMessage: deps.inferP0DomainFromMessage as any, + resolveBusinessScopeFromLiveContext: deps.resolveBusinessScopeFromLiveContext as any, + extractRequirements: deps.extractRequirements as any, + toExecutionPlan: deps.toExecutionPlan as any, + enforceRbpLiveRoutePlan: deps.enforceRbpLiveRoutePlan as any, + enforceFaLiveRoutePlan: deps.enforceFaLiveRoutePlan as any, + executeRouteRuntime: deps.executeRouteRuntime as any, + mapNoRouteReason: deps.mapNoRouteReason as any, + buildSkippedResult: deps.buildSkippedResult as any, + evaluateCoverage: deps.evaluateCoverage as any, + checkGrounding: deps.checkGrounding as any, + collectRbpLiveRouteAudit: deps.collectRbpLiveRouteAudit as any, + collectFaLiveRouteAudit: deps.collectFaLiveRouteAudit as any, + hasExplicitPeriodAnchor: deps.hasExplicitPeriodAnchorFromNormalized as any, + extractDroppedIntentSegments: deps.extractDroppedIntentSegments as any, + buildDebugRoutes: deps.toDebugRoutes as any, + extractExecutionState: deps.extractExecutionState as any, + sanitizeReply: deps.sanitizeOutgoingAssistantText as any, + persistInvestigationState: deps.setInvestigationState as any, + messageIdFactory: deps.messageIdFactory as any, + appendItem: deps.appendItem as any, + getSession: deps.getSession as any, + persistSession: deps.persistSession as any, + cloneConversation: (items) => items.map((item) => ({ ...item })), + logEvent: deps.logEvent as any + }; +} diff --git a/llm_normalizer/backend/tests/assistantTurnRuntimeInputBuilder.test.ts b/llm_normalizer/backend/tests/assistantTurnRuntimeInputBuilder.test.ts new file mode 100644 index 0000000..2c71b74 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantTurnRuntimeInputBuilder.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it, vi } from "vitest"; +import { + buildAssistantAddressAttemptRuntimeInput, + buildAssistantDeepTurnAttemptRuntimeInput, + buildAssistantUserTurnBootstrapRuntimeInput +} from "../src/services/assistantTurnRuntimeInputBuilder"; + +function buildDeps(overrides: Record = {}) { + const noop = vi.fn(() => null); + return { + ensureSession: vi.fn(() => ({ session_id: "asst-1", items: [], investigation_state: null })), + appendItem: vi.fn(), + getSession: vi.fn(() => ({ session_id: "asst-1", items: [], investigation_state: null })), + persistSession: vi.fn(), + setInvestigationState: vi.fn(), + normalize: vi.fn(async () => ({})), + executeRouteRuntime: vi.fn(async () => ({})), + tryAddressQueryHandle: vi.fn(async () => ({ response_type: "READY" })), + chatClient: {}, + messageIdFactory: vi.fn(() => "msg-1"), + nowIso: vi.fn(() => "2026-04-11T00:00:00.000Z"), + defaultApiKey: "key", + logEvent: vi.fn(), + featureAssistantAddressQueryV1: true, + featureAddressLlmPredecomposeV1: true, + featureInvestigationStateV1: true, + featureStateFollowupBindingV1: true, + featureContractsV11: true, + featureAnswerPolicyV11: true, + featureProblemCentricAnswerV1: true, + featureLifecycleAnswerV1: true, + defaultModel: "gpt-5", + defaultBaseUrl: "http://localhost", + compactWhitespace: vi.fn((value: unknown) => String(value ?? "").trim()), + repairAddressMojibake: vi.fn((value: unknown) => String(value ?? "")), + resolveRuntimeAnalysisContext: vi.fn(() => ({ as_of_date: "2020-07-31" })), + runAddressLlmPreDecompose: vi.fn(async () => ({})), + buildAddressLlmPredecomposeContractV1: noop, + sanitizeAddressMessageForFallback: noop, + toNonEmptyString: vi.fn((value: unknown) => + typeof value === "string" && value.trim().length > 0 ? value.trim() : null + ), + resolveAddressFollowupCarryoverContext: noop, + resolveAssistantOrchestrationDecision: noop, + buildAddressDialogContinuationContractV2: noop, + mergeFollowupContextWithOrganizationScope: noop, + isRetryableAddressLimitedResult: noop, + mergeKnownOrganizations: noop, + hasAssistantDataScopeMetaQuestionSignal: noop, + shouldHandleAsAssistantCapabilityMetaQuery: noop, + hasDestructiveDataActionSignal: noop, + hasDangerOrCoercionSignal: noop, + hasOperationalAdminActionRequestSignal: noop, + hasOrganizationFactLookupSignal: noop, + hasOrganizationFactFollowupSignal: noop, + shouldEmitOrganizationSelectionReply: noop, + hasAssistantCapabilityQuestionSignal: noop, + resolveDataScopeProbe: vi.fn(() => null), + applyScriptGuard: noop, + applyGroundingGuard: noop, + buildAssistantSafetyRefusalReply: noop, + buildAssistantDataScopeContractReply: noop, + buildAssistantOrganizationFactBoundaryReply: noop, + buildAssistantDataScopeSelectionReply: noop, + buildAssistantOperationalBoundaryReply: noop, + buildAssistantCapabilityContractReply: noop, + loadAssistantCanonExcerpt: noop, + sanitizeOutgoingAssistantText: vi.fn((value: unknown, fallback = "") => { + const text = typeof value === "string" ? value.trim() : ""; + return text || fallback; + }), + buildAddressDebugPayload: noop, + buildAddressFollowupOffer: noop, + buildFollowupStateBinding: noop, + resolveBusinessScopeAlignment: noop, + inferP0DomainFromMessage: noop, + resolveBusinessScopeFromLiveContext: noop, + extractRequirements: noop, + toExecutionPlan: noop, + enforceRbpLiveRoutePlan: noop, + enforceFaLiveRoutePlan: noop, + mapNoRouteReason: noop, + buildSkippedResult: noop, + evaluateCoverage: noop, + checkGrounding: noop, + collectRbpLiveRouteAudit: noop, + collectFaLiveRouteAudit: noop, + hasExplicitPeriodAnchorFromNormalized: noop, + extractDroppedIntentSegments: noop, + toDebugRoutes: noop, + extractExecutionState: noop, + ...overrides + } as any; +} + +describe("assistant turn runtime input builder", () => { + it("builds bootstrap runtime input from shared deps", () => { + const deps = buildDeps(); + const payload = { + session_id: "asst-1", + user_message: "hello" + }; + + const runtimeInput = buildAssistantUserTurnBootstrapRuntimeInput(payload, deps); + + expect(runtimeInput.payload).toBe(payload); + expect(runtimeInput.ensureSession).toBe(deps.ensureSession); + expect(runtimeInput.messageIdFactory?.()).toBe("msg-1"); + expect(runtimeInput.nowIso?.()).toBe("2026-04-11T00:00:00.000Z"); + }); + + it("builds address attempt input and preserves address context mapping", async () => { + const runAddressLlmPreDecompose = vi.fn(async () => ({ mode: "supported" })); + const deps = buildDeps({ runAddressLlmPreDecompose }); + const runtimeInput = { + payload: { context: { period_hint: "2020-07-31" } }, + sessionId: "asst-1", + userMessage: "где хвост", + sessionItems: [], + runtimeAnalysisContext: { as_of_date: "2020-07-31" }, + sessionOrganizationScope: { + knownOrganizations: ["Org A"], + selectedOrganization: "Org A", + activeOrganization: "Org A" + } + } as any; + + const built = buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps); + const predecompose = await built.runAddressLlmPreDecompose(); + await built.runAddressQueryTryHandle("message", { analysisDateHint: "2020-07-31" }); + + expect(predecompose).toEqual({ mode: "supported" }); + expect(runAddressLlmPreDecompose).toHaveBeenCalledWith(runtimeInput.payload, "где хвост"); + expect(built.runtimeAnalysisContextAsOfDate).toBe("2020-07-31"); + expect(built.sessionScope).toEqual(runtimeInput.sessionOrganizationScope); + expect(deps.tryAddressQueryHandle).toHaveBeenCalledWith("message", { analysisDateHint: "2020-07-31" }); + }); + + it("builds deep attempt input with shared guards and state hooks", () => { + const setInvestigationState = vi.fn(); + const sanitizeOutgoingAssistantText = vi.fn(() => "safe"); + const deps = buildDeps({ setInvestigationState, sanitizeOutgoingAssistantText }); + const runtimeInput = { + payload: { useMock: true }, + sessionId: "asst-1", + questionId: "msg-q1", + userMessage: "почему долг не закрыт", + runtimeAnalysisContext: { as_of_date: "2020-07-31" }, + sessionInvestigationState: { scope: "settlements_60_62" }, + addressRuntimeMetaForDeep: { attempted: true } + } as any; + + const built = buildAssistantDeepTurnAttemptRuntimeInput(runtimeInput, deps); + const sanitized = built.sanitizeReply(" raw ", "fallback"); + built.persistInvestigationState("asst-1", { scope: "next" }); + + expect(sanitized).toBe("safe"); + expect(sanitizeOutgoingAssistantText).toHaveBeenCalledWith(" raw ", "fallback"); + expect(setInvestigationState).toHaveBeenCalledWith("asst-1", { scope: "next" }); + expect(built.sessionId).toBe("asst-1"); + expect(built.questionId).toBe("msg-q1"); + expect(built.addressRuntimeMetaForDeep).toEqual({ attempted: true }); + expect(built.featureContractsV11).toBe(true); + }); +});