From c328c52c9bee8c8bdfe642fa1efcc0c8cf7a8ffb Mon Sep 17 00:00:00 2001 From: dctouch Date: Wed, 22 Apr 2026 09:45:49 +0300 Subject: [PATCH] =?UTF-8?q?ARCH:=20=D1=83=D1=81=D0=B8=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20metadata=20ambiguity=20choice=20sets=20=D0=B2=20lane?= =?UTF-8?q?=20arbitration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assistantMcpDiscoveryTurnInputAdapter.js | 75 +++++++++++++--- .../src/services/assistantContinuityPolicy.ts | 11 +++ .../assistantMcpDiscoveryTurnInputAdapter.ts | 62 ++++++++++++- .../src/services/assistantTransitionPolicy.ts | 7 ++ ...istantMcpDiscoveryTurnInputAdapter.test.ts | 86 +++++++++++++++++++ .../tests/assistantTransitionPolicy.test.ts | 58 +++++++++++++ 6 files changed, 282 insertions(+), 17 deletions(-) diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 9ddc9e6..1d77c4e 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -228,9 +228,22 @@ function collectFollowupDiscoverySeed(followupContext) { dateScope, metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family), metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set), - metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true + metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true, + metadataAmbiguityEntitySets: collectEntityCandidates(followupContext?.previous_discovery_metadata_ambiguity_entity_sets) }; } +function metadataEntitySetsSuggestDocumentLane(values) { + return values.some((value) => /(?:документ|document|invoice|waybill|накладн|счет[- ]?фактур|акт)/iu.test(value)); +} +function metadataEntitySetsSuggestMovementLane(values) { + return values.some((value) => /(?:регистр|register|movement|движени|операц|проводк|bank)/iu.test(value)); +} +function metadataAmbiguityCollapsesToDocumentLane(values) { + return values.length > 0 && metadataEntitySetsSuggestDocumentLane(values) && !metadataEntitySetsSuggestMovementLane(values); +} +function metadataAmbiguityCollapsesToMovementLane(values) { + return values.length > 0 && metadataEntitySetsSuggestMovementLane(values) && !metadataEntitySetsSuggestDocumentLane(values); +} function hasLifecycleSignal(text) { return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text); } @@ -257,10 +270,10 @@ function hasMetadataObjectHint(text) { return /(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|registers?|documents?|catalogs?|fields?)/iu.test(text); } function hasDocumentEvidenceFollowupSignal(text) { - return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test(text); + return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043a\u0430\u043a\u0438\u0435|\u0441\u043f\u0438\u0441\u043e\u043a|\u0434\u0430\u0439|\u0438\u0449\u0438)\s+(?:\u0441\u0447(?:[еe]т|\u0435\u0442)[-\u2011 ]?\u0444\u0430\u043a\u0442\u0443\u0440(?:\u044b|\u0430)?|\u043d\u0430\u043a\u043b\u0430\u0434\u043d(?:\u044b\u0435|\u0430\u044f)?|\u0430\u043a\u0442(?:\u044b)?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044e)|invoice(?:s)?|bill(?:s)?|waybill(?:s)?)|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test(text); } function hasMovementEvidenceFollowupSignal(text) { - return /(?:\u043f\u043e\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f\u043c|\u0438\u044f)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0438\u0449\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a(?:\u0438\u0435|\u0438\u0439)\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|movement(?:s)?\s+(?:then|next)?|(?:then|next)\s+movements?|go\s+to\s+movements?)/iu.test(text); + return /(?:\u043f\u043e\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f\u043c|\u0438\u044f)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0438\u0449\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a(?:\u0438\u0435|\u0438\u0439)\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043a\u0430\u043a\u0438\u0435|\u0441\u043f\u0438\u0441\u043e\u043a|\u0434\u0430\u0439|\u0438\u0449\u0438)\s+(?:\u043f\u043b\u0430\u0442[еe]\u0436(?:\u0438|\u0438)?|\u043e\u043f\u0435\u0440\u0430\u0446(?:\u0438\u0438|\u0438\u044e)|\u043f\u0440\u043e\u0432\u043e\u0434\u043a(?:\u0438|\u0430)|\u0441\u043f\u0438\u0441\u0430\u043d(?:\u0438\u044f|\u0438\u0435)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|payment(?:s)?|transaction(?:s)?|operation(?:s)?|posting(?:s)?|bank\s+operation(?:s)?)|movement(?:s)?\s+(?:then|next)?|(?:then|next)\s+movements?|go\s+to\s+movements?)/iu.test(text); } function hasMetadataDownstreamContinuationSignal(text) { return /(?:\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0434(?:\u0435|\u0451)\u043c\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u0448\u043b(?:\u0438|\u0451\u043c)\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439|\u0438\u0449\u0438\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0449\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0441\u0442\u0440\u043e\u043a\u0438|\u0433\u043b\u0443\u0431\u0436\u0435|\u0447\u0442\u043e\s+\u0434\u0430\u043b\u044c\u0448\u0435|continue|go\s+ahead|go\s+deeper|look\s+deeper|drill\s+down|show\s+(?:data|rows))/iu.test(text); @@ -351,6 +364,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText); const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText); const rawDateScope = collectDateScopeFromRawText(rawText); + const metadataDocumentHintSignal = hasDocumentEvidenceFollowupSignal(rawText); + const metadataMovementHintSignal = hasMovementEvidenceFollowupSignal(rawText); const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); @@ -373,26 +388,28 @@ function buildAssistantMcpDiscoveryTurnInput(input) { followupSeed.counterparty && !rawLifecycleSignal && !rawValueFlowSignal && - hasDocumentEvidenceFollowupSignal(rawText)); + metadataDocumentHintSignal); const metadataAmbiguityResolvedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataAmbiguityDetected && + (followupSeed.metadataAmbiguityEntitySets.length === 0 || + metadataEntitySetsSuggestDocumentLane(followupSeed.metadataAmbiguityEntitySets)) && followupSeed.counterparty && !rawLifecycleSignal && !rawValueFlowSignal && - hasDocumentEvidenceFollowupSignal(rawText)); + metadataDocumentHintSignal); const metadataGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataRouteFamily === "movement_evidence" && !followupSeed.metadataAmbiguityDetected && followupSeed.counterparty && !rawLifecycleSignal && - !rawValueFlowSignal && - hasMovementEvidenceFollowupSignal(rawText)); + metadataMovementHintSignal); const metadataAmbiguityResolvedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataAmbiguityDetected && + (followupSeed.metadataAmbiguityEntitySets.length === 0 || + metadataEntitySetsSuggestMovementLane(followupSeed.metadataAmbiguityEntitySets)) && followupSeed.counterparty && !rawLifecycleSignal && - !rawValueFlowSignal && - hasMovementEvidenceFollowupSignal(rawText)); + metadataMovementHintSignal); const metadataGroundedLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && (followupSeed.metadataRouteFamily === "document_evidence" || followupSeed.metadataRouteFamily === "movement_evidence") && @@ -401,15 +418,37 @@ function buildAssistantMcpDiscoveryTurnInput(input) { !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && - !hasDocumentEvidenceFollowupSignal(rawText) && - !hasMovementEvidenceFollowupSignal(rawText) && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && + hasMetadataDownstreamContinuationSignal(rawText)); + const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataAmbiguityDetected && + metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && + hasMetadataDownstreamContinuationSignal(rawText)); + const metadataAmbiguityCollapsedMovementLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataAmbiguityDetected && + metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && hasMetadataDownstreamContinuationSignal(rawText)); const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable || metadataAmbiguityResolvedDocumentFollowupApplicable || - (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence"); + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || + metadataAmbiguityCollapsedDocumentLaneContinuationApplicable; const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable || metadataAmbiguityResolvedMovementFollowupApplicable || - (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence"); + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || + metadataAmbiguityCollapsedMovementLaneContinuationApplicable; const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && !metadataGroundedDocumentLaneApplicable && !metadataGroundedMovementLaneApplicable; @@ -437,7 +476,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; const bidirectionalValueFlowSignal = !lifecycleSignal && (rawBidirectionalValueFlowSignal || seededAction === "net_value_flow"); - const valueFlowSignal = !lifecycleSignal && (rawValueFlowSignal || seededDomain === "counterparty_value"); + const valueFlowSignal = !lifecycleSignal && + !metadataGroundedMovementLaneApplicable && + (rawValueFlowSignal || seededDomain === "counterparty_value"); const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && (rawPayoutSignal || seededAction === "payout"); @@ -616,6 +657,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (metadataGroundedLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); } + if (metadataAmbiguityCollapsedDocumentLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_document_lane"); + } + if (metadataAmbiguityCollapsedMovementLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane"); + } if (unsupported) { pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); } diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index 01971e1..939b7ed 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -192,6 +192,17 @@ export function readAssistantMcpDiscoveryMetadataAmbiguityDetected( return readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true; } +export function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets( + debug: Record | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): string[] { + const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets; + if (!Array.isArray(values)) { + return []; + } + return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item)); +} + function mapAssistantMcpDiscoveryPilotScopeToAddressIntent( pilotScope: string | null, actionFamily: string | null diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index 9b3b611..936d31b 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -265,6 +265,7 @@ function collectFollowupDiscoverySeed(followupContext: Record | metadataRouteFamily: string | null; metadataSelectedEntitySet: string | null; metadataAmbiguityDetected: boolean; + metadataAmbiguityEntitySets: string[]; } { const previousFilters = toRecordObject(followupContext?.previous_filters); const rootFilters = toRecordObject(followupContext?.root_filters); @@ -302,10 +303,29 @@ function collectFollowupDiscoverySeed(followupContext: Record | dateScope, metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family), metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set), - metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true + metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true, + metadataAmbiguityEntitySets: collectEntityCandidates(followupContext?.previous_discovery_metadata_ambiguity_entity_sets) }; } +function metadataEntitySetsSuggestDocumentLane(values: string[]): boolean { + return values.some((value) => /(?:документ|document|invoice|waybill|накладн|счет[- ]?фактур|акт)/iu.test(value)); +} + +function metadataEntitySetsSuggestMovementLane(values: string[]): boolean { + return values.some((value) => + /(?:регистр|register|movement|движени|операц|проводк|bank)/iu.test(value) + ); +} + +function metadataAmbiguityCollapsesToDocumentLane(values: string[]): boolean { + return values.length > 0 && metadataEntitySetsSuggestDocumentLane(values) && !metadataEntitySetsSuggestMovementLane(values); +} + +function metadataAmbiguityCollapsesToMovementLane(values: string[]): boolean { + return values.length > 0 && metadataEntitySetsSuggestMovementLane(values) && !metadataEntitySetsSuggestDocumentLane(values); +} + function hasLifecycleSignal(text: string): boolean { return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test( text @@ -520,6 +540,8 @@ export function buildAssistantMcpDiscoveryTurnInput( const metadataAmbiguityResolvedDocumentFollowupApplicable = Boolean( followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataAmbiguityDetected && + (followupSeed.metadataAmbiguityEntitySets.length === 0 || + metadataEntitySetsSuggestDocumentLane(followupSeed.metadataAmbiguityEntitySets)) && followupSeed.counterparty && !rawLifecycleSignal && !rawValueFlowSignal && @@ -536,6 +558,8 @@ export function buildAssistantMcpDiscoveryTurnInput( const metadataAmbiguityResolvedMovementFollowupApplicable = Boolean( followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataAmbiguityDetected && + (followupSeed.metadataAmbiguityEntitySets.length === 0 || + metadataEntitySetsSuggestMovementLane(followupSeed.metadataAmbiguityEntitySets)) && followupSeed.counterparty && !rawLifecycleSignal && metadataMovementHintSignal @@ -553,14 +577,40 @@ export function buildAssistantMcpDiscoveryTurnInput( !metadataMovementHintSignal && hasMetadataDownstreamContinuationSignal(rawText) ); + const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean( + followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataAmbiguityDetected && + metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && + hasMetadataDownstreamContinuationSignal(rawText) + ); + const metadataAmbiguityCollapsedMovementLaneContinuationApplicable = Boolean( + followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataAmbiguityDetected && + metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && + hasMetadataDownstreamContinuationSignal(rawText) + ); const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable || metadataAmbiguityResolvedDocumentFollowupApplicable || - (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence"); + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || + metadataAmbiguityCollapsedDocumentLaneContinuationApplicable; const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable || metadataAmbiguityResolvedMovementFollowupApplicable || - (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence"); + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || + metadataAmbiguityCollapsedMovementLaneContinuationApplicable; const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && !metadataGroundedDocumentLaneApplicable && @@ -784,6 +834,12 @@ export function buildAssistantMcpDiscoveryTurnInput( if (metadataGroundedLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); } + if (metadataAmbiguityCollapsedDocumentLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_document_lane"); + } + if (metadataAmbiguityCollapsedMovementLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane"); + } if (unsupported) { pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); } diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 3d3c03b..b3f4fad 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -11,6 +11,7 @@ import { readAddressDebugFilters, readAddressDebugItem, readAssistantMcpDiscoveryMetadataAmbiguityDetected, + readAssistantMcpDiscoveryMetadataAmbiguityEntitySets, readAssistantMcpDiscoveryMetadataRouteFamily, readAssistantMcpDiscoveryMetadataSelectedEntitySet, readAddressDebugTemporalScope, @@ -621,6 +622,10 @@ export function createAssistantTransitionPolicy(deps) { const sourceDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected( carryoverSourceDebug ); + const sourceDiscoveryMetadataAmbiguityEntitySets = readAssistantMcpDiscoveryMetadataAmbiguityEntitySets( + carryoverSourceDebug, + deps.toNonEmptyString + ); const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; @@ -956,6 +961,8 @@ export function createAssistantTransitionPolicy(deps) { previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined, previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined, previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined, + previous_discovery_metadata_ambiguity_entity_sets: + sourceDiscoveryMetadataAmbiguityEntitySets.length > 0 ? sourceDiscoveryMetadataAmbiguityEntitySets : undefined, resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined, root_context_only: rootScopedPivot || undefined, root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined, diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 7b14b2f..eae42fc 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -367,6 +367,7 @@ describe("assistant MCP discovery turn input adapter", () => { followupContext: { previous_discovery_pilot_scope: "metadata_inspection_v1", previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"], previous_filters: { counterparty: "SVK", period_from: "2020-01-01", @@ -398,6 +399,7 @@ describe("assistant MCP discovery turn input adapter", () => { followupContext: { previous_discovery_pilot_scope: "metadata_inspection_v1", previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"], previous_filters: { counterparty: "SVK", period_from: "2020-01-01", @@ -429,6 +431,7 @@ describe("assistant MCP discovery turn input adapter", () => { followupContext: { previous_discovery_pilot_scope: "metadata_inspection_v1", previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"], previous_filters: { counterparty: "SVK", period_from: "2020-01-01", @@ -459,6 +462,7 @@ describe("assistant MCP discovery turn input adapter", () => { followupContext: { previous_discovery_pilot_scope: "metadata_inspection_v1", previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"], previous_filters: { counterparty: "SVK", period_from: "2020-01-01", @@ -484,6 +488,88 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected"); }); + it("does not resolve metadata ambiguity into movement lane when confirmed ambiguity sets contain documents only", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "по движениям", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["Документ"], + previous_filters: { + counterparty: "SVK", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "SVK" + } + }); + + expect(result.reason_codes).not.toContain("mcp_discovery_metadata_ambiguity_resolved_to_movement_lane"); + }); + + it("continues from ambiguous metadata into document lane when ambiguity choice set collapses to documents on a generic downstream follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "continue with data", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "invoice"], + previous_filters: { + counterparty: "SVK", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "SVK" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("document evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "document_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_collapsed_to_document_lane"); + }); + + it("continues from ambiguous metadata into movement lane when ambiguity choice set collapses to movements on a generic downstream follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "continue with data", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_ambiguity_detected: true, + previous_discovery_metadata_ambiguity_entity_sets: ["РегистрНакопления", "movement"], + previous_filters: { + counterparty: "SVK", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "SVK" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("movement evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "movement_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane"); + }); + it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "а теперь за 2021?", diff --git a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts index 0be5645..c0e62a0 100644 --- a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts @@ -1169,6 +1169,64 @@ describe("assistantTransitionPolicy", () => { expect(carryover?.followupContext?.previous_discovery_metadata_selected_entity_set).toBe("Документ"); expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBeUndefined(); }); + it("carries metadata ambiguity entity sets into follow-up context for downstream lane arbitration", () => { + const policy = buildPolicy({ + findLastAddressAssistantItem: () => ({ + text: "metadata ambiguity", + debug: { + execution_lane: "living_chat", + mcp_discovery_response_applied: true, + assistant_mcp_discovery_entry_point_v1: { + schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", + entry_status: "bridge_executed", + turn_input: { + turn_meaning_ref: { + asked_domain_family: "metadata", + asked_action_family: "inspect_documents", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020" + } + }, + bridge: { + bridge_status: "answer_draft_ready", + business_fact_answer_allowed: true, + pilot: { + pilot_scope: "metadata_inspection_v1", + derived_metadata_surface: { + selected_entity_set: null, + downstream_route_family: null, + ambiguity_detected: true, + ambiguity_entity_sets: ["Документ", "РегистрНакопления"] + } + }, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference" + } + } + } + } + }), + hasAddressFollowupContextSignal: () => true, + hasReferentialPointer: () => false, + resolveAddressIntent: () => ({ intent: "unknown" }), + resolveAddressIntentFamily: () => null, + resolveAssistantTurnMeaning: () => null + }); + + const carryover = policy.resolveAddressFollowupCarryoverContext( + "по документам", + [{ kind: "assistant", text: "metadata ambiguity" }], + "по документам", + { predecomposeContract: { intent: "unknown" } }, + null + ); + + expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true); + expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([ + "Документ", + "РегистрНакопления" + ]); + }); it("switches to VAT tax-period intent while preserving carried period filters", () => { const policy = buildPolicy({ findLastAddressAssistantItem: () => ({