From ac3757bc59dabe581706e3193e6a592614fa6cc3 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sun, 19 Apr 2026 13:42:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B0:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20base=20address=20tool=20gate=20?= =?UTF-8?q?=D0=B8=D0=B7=20assistantService=20=D0=B2=20assistantRoutePolicy?= =?UTF-8?q?=20=D0=B8=20=D0=B2=D1=8B=D1=80=D0=BE=D0=B2=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20top-level=20orchestration=20owner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ontinuity_stabilization_plan_2026-04-17.md | 11 ++ .../dist/services/assistantRoutePolicy.js | 148 ++++++++++++++++- .../backend/dist/services/assistantService.js | 138 +--------------- .../src/services/assistantRoutePolicy.ts | 149 +++++++++++++++++- .../backend/src/services/assistantService.ts | 139 +--------------- .../tests/assistantRoutePolicy.test.ts | 23 +++ 6 files changed, 329 insertions(+), 279 deletions(-) diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index 308ba01..ca1590e 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -466,6 +466,17 @@ Still open after the accepted phase12 replay: - continuity now also owns `resolveFollowupTargetIntent(...)`, so `carryover target intent` precedence for purchase-date VAT bridge, selected-object retarget, root-context carryover, same-date pivot, displayed-entity retarget, and plain previous-intent fallback is expressed in one shared helper instead of an inline ternary tower; - this matters because root-pivot semantics and target-intent precedence are among the heaviest remaining orchestration decisions in the follow-up path, and keeping them under one shared continuity layer reduces another chance that future domain expansion reintroduces drift between carryover state and target route selection; - targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, and a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` is accepted `20/20`, which is the critical proof that the flagship mixed replay still survives after the decision-block extraction. +- the next top-level orchestration pass now removes one more heavy owner from `assistantService` and makes route arbitration more self-contained: + - the base `address tool gate` decision no longer lives as a service-local block in `assistantService` and is no longer injected into route arbitration as an external callback-only source of truth; + - `assistantRoutePolicy` now owns the default `resolveBaseAddressToolGateDecision(...)` path itself, including: + - meta/capability skip logic; + - classifier/intent/LLM-canonical exact-route signals; + - lexical/address-data fallback signals; + - unsupported predecompose semantic guard; + - `followup_context_detected` fallback when no direct message signal exists; + - this matters because the top-level `run address lane vs keep chat` gate is now structurally closer to the same route-policy owner that already arbitrates memory/meta/follow-up/deep transitions, instead of remaining split across `assistantService` glue and route policy heuristics; + - the route policy still accepts an override in tests, so regression coverage remains narrow and controllable, but the production runtime no longer depends on a duplicate service-local decision block; + - targeted `assistantRoutePolicy`, `assistantContinuityPolicy`, and `assistantTransitionPolicy` suites are green after the move, and a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` remains accepted `20/20`, which is the proof that the flagship mixed path survives after lifting the gate out of `assistantService`. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js index 8d32a9c..fb45119 100644 --- a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js @@ -47,7 +47,149 @@ function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) { return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent)); } function createAssistantRoutePolicy(deps) { - const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, isAnswerInspectionFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps; + const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, isAnswerInspectionFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision: resolveAddressToolGateDecisionOverride, hasAddressLlmPreDecomposeCandidate, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps; + function resolveBaseAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) { + const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? "")); + const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? ""); + const repairedRawMessageForGate = repairAddressMojibake(rawMessageForGate); + const rawMetaSignals = resolveMetaSignalSet({ + rawUserMessage: rawMessageForGate, + repairedRawUserMessage: repairedRawMessageForGate, + effectiveAddressUserMessage: rawMessageForGate, + repairedEffectiveAddressUserMessage: repairedRawMessageForGate + }); + const effectiveMetaSignals = resolveMetaSignalSet({ + rawUserMessage: rawMessageForGate, + repairedRawUserMessage: repairedRawMessageForGate, + effectiveAddressUserMessage: String(addressInputMessage ?? ""), + repairedEffectiveAddressUserMessage: repairedInputMessage + }); + const dataScopeMetaQuery = Boolean(rawMetaSignals.dataScopeMetaQuery || effectiveMetaSignals.dataScopeMetaQuery); + const rawCapabilityMetaQuery = Boolean(rawMetaSignals.capabilityMetaQuery); + const capabilityMetaQuery = Boolean(rawCapabilityMetaQuery || effectiveMetaSignals.capabilityMetaQuery); + const rawDataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) || + hasDataRetrievalRequestSignal(repairedRawMessageForGate); + const dataRetrievalSignal = rawDataRetrievalSignal || hasDataRetrievalRequestSignal(repairedInputMessage); + const rawCapabilityMetaOverride = rawCapabilityMetaQuery && !rawDataRetrievalSignal; + if (dataScopeMetaQuery || rawCapabilityMetaOverride || (capabilityMetaQuery && !dataRetrievalSignal)) { + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected" + }; + } + const modeDetection = detectAddressQuestionMode(repairedInputMessage || addressInputMessage); + const modeDetectionRaw = detectAddressQuestionMode(String(addressInputMessage ?? "")); + const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query"; + const intentResolution = resolveAddressIntent(repairedInputMessage || addressInputMessage); + const intentResolutionRaw = resolveAddressIntent(String(addressInputMessage ?? "")); + const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown"; + const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); + const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence); + const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); + const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && + typeof llmPreDecomposeMeta.semanticExtractionContract === "object" + ? llmPreDecomposeMeta.semanticExtractionContract + : null; + const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; + const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" && + /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") && + semanticCanonicalRecommended; + const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); + const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis"; + const hasLlmCanonicalSignal = semanticCanonicalRecommended && + Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && + ((llmContractMode === "address_query" && llmContractModeConfidence !== "low") || + (llmCanonicalAppliedSignal && + (hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal))); + const hasLlmCanonicalDataSignal = semanticCanonicalRecommended && + Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && + Boolean(llmPreDecomposeMeta?.applied) && + (llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) && + hasStrongDataIntentSignal(repairedInputMessage); + const hasBusinessRankingAddressSignal = /(?:кто\s+(?:нам\s+)?(?:больше(?:\s+всего)?\s+принес|принес\s+больше(?:\s+всего)?).*(?:денег)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); + const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) || + hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage); + const hasLexicalAddressSignal = hasAddressLlmPreDecomposeCandidate(addressInputMessage) || + hasAddressLlmPreDecomposeCandidate(repairedInputMessage) || + hasAccountingSignal(addressInputMessage) || + hasAccountingSignal(repairedInputMessage) || + hasShortDebtMirrorFollowupSignal(rawMessageForGate) || + hasShortDebtMirrorFollowupSignal(repairedInputMessage) || + hasBusinessRankingAddressSignal || + sameDateAccountFollowupSignal; + const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" && + (llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") && + llmContractIntent === "unknown"; + const hasAnyAddressSignal = hasClassifierSignal || + hasIntentSignal || + hasLlmCanonicalSignal || + hasLlmCanonicalDataSignal || + hasLexicalAddressSignal || + llmSupportedDeepAddressIntentSignal; + const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) || + hasDataRetrievalRequestSignal(rawMessageForGate) || + hasAccountingSignal(rawMessageForGate) || + hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate); + const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) || + hasAccountingSignal(repairedInputMessage) || + hasDataRetrievalRequestSignal(repairedInputMessage); + if (!semanticCanonicalRecommended && + llmContractIntent === "unknown" && + !followupContext && + !hasClassifierSignal && + !hasIntentSignal && + !hasLexicalAddressSignal && + !llmSupportedDeepAddressIntentSignal && + !strongDataSignalFromRawMessage && + !strongDataSignalFromEffectiveMessage) { + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: "llm_predecompose_semantic_guard_rejected" + }; + } + if (hasUnsupportedLowConfidencePredecomposeSignal && + !followupContext && + !hasAnyAddressSignal && + !llmSupportedDeepAddressIntentSignal && + !strongDataSignalFromRawMessage && + !strongDataSignalFromEffectiveMessage) { + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: "llm_predecompose_unsupported_mode" + }; + } + const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage; + if (hasMessageSignal) { + return { + runAddressLane: true, + decision: "run_address_lane", + reason: hasClassifierSignal + ? "address_mode_classifier_detected" + : hasIntentSignal + ? "address_intent_resolver_detected" + : hasLlmCanonicalSignal + ? "llm_canonical_candidate_detected" + : hasLlmCanonicalDataSignal + ? "llm_canonical_data_signal_detected" + : "address_signal_detected" + }; + } + if (followupContext) { + return { + runAddressLane: true, + decision: "run_address_lane", + reason: "followup_context_detected" + }; + } + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: "no_address_signal_after_l0" + }; + } function hasInventoryRootRestatementFollowupSignal(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е"); if (!normalized) { @@ -210,7 +352,9 @@ function createAssistantRoutePolicy(deps) { !capabilityMetaQuery && !dataRetrievalSignal); const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; - const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); + const baseToolGate = typeof resolveAddressToolGateDecisionOverride === "function" + ? resolveAddressToolGateDecisionOverride(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage) + : resolveBaseAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && !capabilityMetaQuery && !dataRetrievalSignal && diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index a6fa93a..fadb45e 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -3693,142 +3693,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage }, userMessage); } } -function resolveAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) { - const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? "")); - const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? ""); - const repairedRawMessageForGate = repairAddressMojibake(rawMessageForGate); - const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawMessageForGate) || - hasAssistantDataScopeMetaQuestionSignal(repairedRawMessageForGate) || - hasAssistantDataScopeMetaQuestionSignal(repairedInputMessage); - const rawCapabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawMessageForGate) || - shouldHandleAsAssistantCapabilityMetaQuery(repairedRawMessageForGate); - const capabilityMetaQuery = rawCapabilityMetaQuery || - shouldHandleAsAssistantCapabilityMetaQuery(repairedInputMessage); - const rawDataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) || - hasDataRetrievalRequestSignal(repairedRawMessageForGate); - const dataRetrievalSignal = rawDataRetrievalSignal || - hasDataRetrievalRequestSignal(repairedInputMessage); - const rawCapabilityMetaOverride = rawCapabilityMetaQuery && !rawDataRetrievalSignal; - if (dataScopeMetaQuery || rawCapabilityMetaOverride || (capabilityMetaQuery && !dataRetrievalSignal)) { - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected" - }; - } - const directDeepAnalysisSignal = hasDirectDeepAnalysisSignal(rawMessageForGate) || - hasDirectDeepAnalysisSignal(repairedInputMessage); - const deepAnalysisPreferenceSignal = directDeepAnalysisSignal || - hasDeepAnalysisPreferenceSignal(rawMessageForGate) || - hasDeepAnalysisPreferenceSignal(repairedInputMessage); - const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage); - const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(String(addressInputMessage ?? "")); - const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query"; - const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedInputMessage || addressInputMessage); - const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(String(addressInputMessage ?? "")); - const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown"; - const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); - const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence); - const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); - const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && - typeof llmPreDecomposeMeta.semanticExtractionContract === "object" - ? llmPreDecomposeMeta.semanticExtractionContract - : null; - const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; - const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" && - /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") && - semanticCanonicalRecommended; - const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); - const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis"; - const hasLlmCanonicalSignal = semanticCanonicalRecommended && - Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && - ((llmContractMode === "address_query" && llmContractModeConfidence !== "low") || - (llmCanonicalAppliedSignal && - (hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal))); - const hasLlmCanonicalDataSignal = semanticCanonicalRecommended && - Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && - Boolean(llmPreDecomposeMeta?.applied) && - (llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) && - hasStrongDataIntentSignal(repairedInputMessage); - const hasBusinessRankingAddressSignal = /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?).*(?:\u0434\u0435\u043d\u0435\u0433)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); - const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) || - hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage); - const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) || - isAddressLlmPreDecomposeCandidate(repairedInputMessage) || - hasAccountingSignal(addressInputMessage) || - hasAccountingSignal(repairedInputMessage) || - hasShortDebtMirrorFollowupSignal(rawMessageForGate) || - hasShortDebtMirrorFollowupSignal(repairedInputMessage) || - hasBusinessRankingAddressSignal || - sameDateAccountFollowupSignal; - const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" && - (llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") && - llmContractIntent === "unknown"; - const hasAnyAddressSignal = hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal || llmSupportedDeepAddressIntentSignal; - const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) || - hasDataRetrievalRequestSignal(rawMessageForGate) || - hasAccountingSignal(rawMessageForGate) || - hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate); - const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) || - hasAccountingSignal(repairedInputMessage) || - hasDataRetrievalRequestSignal(repairedInputMessage); - if (!semanticCanonicalRecommended && - llmContractIntent === "unknown" && - !followupContext && - !hasClassifierSignal && - !hasIntentSignal && - !hasLexicalAddressSignal && - !llmSupportedDeepAddressIntentSignal && - !strongDataSignalFromRawMessage && - !strongDataSignalFromEffectiveMessage) { - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: "llm_predecompose_semantic_guard_rejected" - }; - } - if (hasUnsupportedLowConfidencePredecomposeSignal && !followupContext && - !hasAnyAddressSignal && - !llmSupportedDeepAddressIntentSignal && - !strongDataSignalFromRawMessage && - !strongDataSignalFromEffectiveMessage) { - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: "llm_predecompose_unsupported_mode" - }; - } - const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage; - if (hasMessageSignal) { - return { - runAddressLane: true, - decision: "run_address_lane", - reason: hasClassifierSignal - ? "address_mode_classifier_detected" - : hasIntentSignal - ? "address_intent_resolver_detected" - : hasLlmCanonicalSignal - ? "llm_canonical_candidate_detected" - : hasLlmCanonicalDataSignal - ? "llm_canonical_data_signal_detected" - : llmSupportedDeepAddressIntentSignal - ? "address_signal_detected" - : "address_signal_detected" - }; - } - if (followupContext) { - return { - runAddressLane: true, - decision: "run_address_lane", - reason: "followup_context_detected" - }; - } - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: "no_address_signal_after_l0" - }; -} function hasLooseAllTimeAddressLookupSignal(text) { const repaired = repairAddressMojibake(String(text ?? "")); const normalized = compactWhitespace(repaired.toLowerCase()); @@ -4223,7 +4087,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli hasShortInventoryObjectFollowupSignal, resolveRouteMemorySignals: assistantMemoryRecapPolicy.resolveRouteMemorySignals, findLastAddressAssistantItem, - resolveAddressToolGateDecision, + hasAddressLlmPreDecomposeCandidate: isAddressLlmPreDecomposeCandidate, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, diff --git a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts index b8288ad..31a0110 100644 --- a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts @@ -78,7 +78,8 @@ export function createAssistantRoutePolicy(deps) { isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, - resolveAddressToolGateDecision, + resolveAddressToolGateDecision: resolveAddressToolGateDecisionOverride, + hasAddressLlmPreDecomposeCandidate, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, @@ -88,6 +89,148 @@ export function createAssistantRoutePolicy(deps) { resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps; + function resolveBaseAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) { + const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? "")); + const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? ""); + const repairedRawMessageForGate = repairAddressMojibake(rawMessageForGate); + const rawMetaSignals = resolveMetaSignalSet({ + rawUserMessage: rawMessageForGate, + repairedRawUserMessage: repairedRawMessageForGate, + effectiveAddressUserMessage: rawMessageForGate, + repairedEffectiveAddressUserMessage: repairedRawMessageForGate + }); + const effectiveMetaSignals = resolveMetaSignalSet({ + rawUserMessage: rawMessageForGate, + repairedRawUserMessage: repairedRawMessageForGate, + effectiveAddressUserMessage: String(addressInputMessage ?? ""), + repairedEffectiveAddressUserMessage: repairedInputMessage + }); + const dataScopeMetaQuery = Boolean(rawMetaSignals.dataScopeMetaQuery || effectiveMetaSignals.dataScopeMetaQuery); + const rawCapabilityMetaQuery = Boolean(rawMetaSignals.capabilityMetaQuery); + const capabilityMetaQuery = Boolean(rawCapabilityMetaQuery || effectiveMetaSignals.capabilityMetaQuery); + const rawDataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) || + hasDataRetrievalRequestSignal(repairedRawMessageForGate); + const dataRetrievalSignal = rawDataRetrievalSignal || hasDataRetrievalRequestSignal(repairedInputMessage); + const rawCapabilityMetaOverride = rawCapabilityMetaQuery && !rawDataRetrievalSignal; + if (dataScopeMetaQuery || rawCapabilityMetaOverride || (capabilityMetaQuery && !dataRetrievalSignal)) { + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected" + }; + } + const modeDetection = detectAddressQuestionMode(repairedInputMessage || addressInputMessage); + const modeDetectionRaw = detectAddressQuestionMode(String(addressInputMessage ?? "")); + const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query"; + const intentResolution = resolveAddressIntent(repairedInputMessage || addressInputMessage); + const intentResolutionRaw = resolveAddressIntent(String(addressInputMessage ?? "")); + const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown"; + const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); + const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence); + const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); + const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && + typeof llmPreDecomposeMeta.semanticExtractionContract === "object" + ? llmPreDecomposeMeta.semanticExtractionContract + : null; + const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; + const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" && + /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") && + semanticCanonicalRecommended; + const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); + const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis"; + const hasLlmCanonicalSignal = semanticCanonicalRecommended && + Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && + ((llmContractMode === "address_query" && llmContractModeConfidence !== "low") || + (llmCanonicalAppliedSignal && + (hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal))); + const hasLlmCanonicalDataSignal = semanticCanonicalRecommended && + Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && + Boolean(llmPreDecomposeMeta?.applied) && + (llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) && + hasStrongDataIntentSignal(repairedInputMessage); + const hasBusinessRankingAddressSignal = /(?:кто\s+(?:нам\s+)?(?:больше(?:\s+всего)?\s+принес|принес\s+больше(?:\s+всего)?).*(?:денег)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); + const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) || + hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage); + const hasLexicalAddressSignal = hasAddressLlmPreDecomposeCandidate(addressInputMessage) || + hasAddressLlmPreDecomposeCandidate(repairedInputMessage) || + hasAccountingSignal(addressInputMessage) || + hasAccountingSignal(repairedInputMessage) || + hasShortDebtMirrorFollowupSignal(rawMessageForGate) || + hasShortDebtMirrorFollowupSignal(repairedInputMessage) || + hasBusinessRankingAddressSignal || + sameDateAccountFollowupSignal; + const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" && + (llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") && + llmContractIntent === "unknown"; + const hasAnyAddressSignal = hasClassifierSignal || + hasIntentSignal || + hasLlmCanonicalSignal || + hasLlmCanonicalDataSignal || + hasLexicalAddressSignal || + llmSupportedDeepAddressIntentSignal; + const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) || + hasDataRetrievalRequestSignal(rawMessageForGate) || + hasAccountingSignal(rawMessageForGate) || + hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate); + const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) || + hasAccountingSignal(repairedInputMessage) || + hasDataRetrievalRequestSignal(repairedInputMessage); + if (!semanticCanonicalRecommended && + llmContractIntent === "unknown" && + !followupContext && + !hasClassifierSignal && + !hasIntentSignal && + !hasLexicalAddressSignal && + !llmSupportedDeepAddressIntentSignal && + !strongDataSignalFromRawMessage && + !strongDataSignalFromEffectiveMessage) { + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: "llm_predecompose_semantic_guard_rejected" + }; + } + if (hasUnsupportedLowConfidencePredecomposeSignal && + !followupContext && + !hasAnyAddressSignal && + !llmSupportedDeepAddressIntentSignal && + !strongDataSignalFromRawMessage && + !strongDataSignalFromEffectiveMessage) { + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: "llm_predecompose_unsupported_mode" + }; + } + const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage; + if (hasMessageSignal) { + return { + runAddressLane: true, + decision: "run_address_lane", + reason: hasClassifierSignal + ? "address_mode_classifier_detected" + : hasIntentSignal + ? "address_intent_resolver_detected" + : hasLlmCanonicalSignal + ? "llm_canonical_candidate_detected" + : hasLlmCanonicalDataSignal + ? "llm_canonical_data_signal_detected" + : "address_signal_detected" + }; + } + if (followupContext) { + return { + runAddressLane: true, + decision: "run_address_lane", + reason: "followup_context_detected" + }; + } + return { + runAddressLane: false, + decision: "skip_address_lane", + reason: "no_address_signal_after_l0" + }; + } function hasInventoryRootRestatementFollowupSignal(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е"); if (!normalized) { @@ -250,7 +393,9 @@ export function createAssistantRoutePolicy(deps) { !capabilityMetaQuery && !dataRetrievalSignal); const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; - const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); + const baseToolGate = typeof resolveAddressToolGateDecisionOverride === "function" + ? resolveAddressToolGateDecisionOverride(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage) + : resolveBaseAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && !capabilityMetaQuery && !dataRetrievalSignal && diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index caf5bc9..7331c7f 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -3649,143 +3649,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage }, userMessage); } } -function resolveAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) { - const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? "")); - const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? ""); - const repairedRawMessageForGate = repairAddressMojibake(rawMessageForGate); - const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawMessageForGate) || - hasAssistantDataScopeMetaQuestionSignal(repairedRawMessageForGate) || - hasAssistantDataScopeMetaQuestionSignal(repairedInputMessage); - const rawCapabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawMessageForGate) || - shouldHandleAsAssistantCapabilityMetaQuery(repairedRawMessageForGate); - const capabilityMetaQuery = rawCapabilityMetaQuery || - shouldHandleAsAssistantCapabilityMetaQuery(repairedInputMessage); - const rawDataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) || - hasDataRetrievalRequestSignal(repairedRawMessageForGate); - const dataRetrievalSignal = rawDataRetrievalSignal || - hasDataRetrievalRequestSignal(repairedInputMessage); - const rawCapabilityMetaOverride = rawCapabilityMetaQuery && !rawDataRetrievalSignal; - if (dataScopeMetaQuery || rawCapabilityMetaOverride || (capabilityMetaQuery && !dataRetrievalSignal)) { - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected" - }; - } - const directDeepAnalysisSignal = hasDirectDeepAnalysisSignal(rawMessageForGate) || - hasDirectDeepAnalysisSignal(repairedInputMessage); - const deepAnalysisPreferenceSignal = directDeepAnalysisSignal || - hasDeepAnalysisPreferenceSignal(rawMessageForGate) || - hasDeepAnalysisPreferenceSignal(repairedInputMessage); - const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage); - const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(String(addressInputMessage ?? "")); - const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query"; - const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedInputMessage || addressInputMessage); - const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(String(addressInputMessage ?? "")); - const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown"; - const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); - const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence); - const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); - const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && - typeof llmPreDecomposeMeta.semanticExtractionContract === "object" - ? llmPreDecomposeMeta.semanticExtractionContract - : null; - const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; - const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" && - /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") && - semanticCanonicalRecommended; - const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); - const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis"; - const hasLlmCanonicalSignal = semanticCanonicalRecommended && - Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && - ((llmContractMode === "address_query" && llmContractModeConfidence !== "low") || - (llmCanonicalAppliedSignal && - (hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal))); - const hasLlmCanonicalDataSignal = semanticCanonicalRecommended && - Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) && - Boolean(llmPreDecomposeMeta?.applied) && - (llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) && - hasStrongDataIntentSignal(repairedInputMessage); - const hasBusinessRankingAddressSignal = /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?).*(?:\u0434\u0435\u043d\u0435\u0433)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase())); - const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) || - hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage); - const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) || - isAddressLlmPreDecomposeCandidate(repairedInputMessage) || - hasAccountingSignal(addressInputMessage) || - hasAccountingSignal(repairedInputMessage) || - hasShortDebtMirrorFollowupSignal(rawMessageForGate) || - hasShortDebtMirrorFollowupSignal(repairedInputMessage) || - hasBusinessRankingAddressSignal || - sameDateAccountFollowupSignal; - const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" && - (llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") && - llmContractIntent === "unknown"; - const hasAnyAddressSignal = - hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal || llmSupportedDeepAddressIntentSignal; - const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) || - hasDataRetrievalRequestSignal(rawMessageForGate) || - hasAccountingSignal(rawMessageForGate) || - hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate); - const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) || - hasAccountingSignal(repairedInputMessage) || - hasDataRetrievalRequestSignal(repairedInputMessage); - if (!semanticCanonicalRecommended && - llmContractIntent === "unknown" && - !followupContext && - !hasClassifierSignal && - !hasIntentSignal && - !hasLexicalAddressSignal && - !llmSupportedDeepAddressIntentSignal && - !strongDataSignalFromRawMessage && - !strongDataSignalFromEffectiveMessage) { - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: "llm_predecompose_semantic_guard_rejected" - }; - } - if (hasUnsupportedLowConfidencePredecomposeSignal && !followupContext && - !hasAnyAddressSignal && - !llmSupportedDeepAddressIntentSignal && - !strongDataSignalFromRawMessage && - !strongDataSignalFromEffectiveMessage) { - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: "llm_predecompose_unsupported_mode" - }; - } - const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage; - if (hasMessageSignal) { - return { - runAddressLane: true, - decision: "run_address_lane", - reason: hasClassifierSignal - ? "address_mode_classifier_detected" - : hasIntentSignal - ? "address_intent_resolver_detected" - : hasLlmCanonicalSignal - ? "llm_canonical_candidate_detected" - : hasLlmCanonicalDataSignal - ? "llm_canonical_data_signal_detected" - : llmSupportedDeepAddressIntentSignal - ? "address_signal_detected" - : "address_signal_detected" - }; - } - if (followupContext) { - return { - runAddressLane: true, - decision: "run_address_lane", - reason: "followup_context_detected" - }; - } - return { - runAddressLane: false, - decision: "skip_address_lane", - reason: "no_address_signal_after_l0" - }; -} function hasLooseAllTimeAddressLookupSignal(text) { const repaired = repairAddressMojibake(String(text ?? "")); const normalized = compactWhitespace(repaired.toLowerCase()); @@ -4181,7 +4044,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli hasShortInventoryObjectFollowupSignal, resolveRouteMemorySignals: assistantMemoryRecapPolicy.resolveRouteMemorySignals, findLastAddressAssistantItem, - resolveAddressToolGateDecision, + hasAddressLlmPreDecomposeCandidate: isAddressLlmPreDecomposeCandidate, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, diff --git a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts index bcbd38a..3e27739 100644 --- a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts @@ -85,6 +85,7 @@ function buildPolicy(overrides: Record = {}) { contextualMemoryRecapFollowupDetected: false }), findLastAddressAssistantItem: () => null, + hasAddressLlmPreDecomposeCandidate: () => false, resolveAddressToolGateDecision: () => ({ runAddressLane: false, decision: "skip_address_lane", @@ -159,6 +160,28 @@ describe("assistantRoutePolicy", () => { expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true); }); + it("uses internal route-policy tool gate when no external override is provided", () => { + const policy = buildPolicy({ + resolveAddressToolGateDecision: undefined, + detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }), + resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }), + hasAddressLlmPreDecomposeCandidate: () => true + }); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "какие остатки на складе", + effectiveAddressUserMessage: "какие остатки на складе", + followupContext: null, + llmPreDecomposeMeta: null, + useMock: false + }); + + expect(decision.runAddressLane).toBe(true); + expect(decision.toolGateDecision).toBe("run_address_lane"); + expect(decision.toolGateReason).toBe("address_mode_classifier_detected"); + expect(decision.livingMode).toBe("address_data"); + }); + it("does not let deep session continuation override an exact VAT period route", () => { const policy = buildPolicy({ detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),