From bc54cd962848f4109249fcecc7c0a1ed06a3c21f Mon Sep 17 00:00:00 2001 From: dctouch Date: Wed, 22 Apr 2026 15:41:49 +0300 Subject: [PATCH] =?UTF-8?q?ARCH:=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20grounded=20value-flow=20follow-up=20=D1=81=20document?= =?UTF-8?q?=20evidence=20lane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...phase29_value_flow_to_documents_chain.json | 97 ++++++++++++ .../assistantMcpDiscoveryTurnInputAdapter.js | 21 +++ .../assistantMcpDiscoveryTurnInputAdapter.ts | 27 ++++ ...istantMcpDiscoveryTurnInputAdapter.test.ts | 148 ++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 docs/orchestration/address_truth_harness_phase29_value_flow_to_documents_chain.json diff --git a/docs/orchestration/address_truth_harness_phase29_value_flow_to_documents_chain.json b/docs/orchestration/address_truth_harness_phase29_value_flow_to_documents_chain.json new file mode 100644 index 0000000..a50bf88 --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase29_value_flow_to_documents_chain.json @@ -0,0 +1,97 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase29_value_flow_to_documents_chain", + "domain": "address_phase29_value_flow_to_documents_chain", + "title": "Phase 29 grounded value-flow to document evidence replay", + "description": "Targeted AGENT replay for Big Block C where a grounded counterparty and carried period must survive a pivot from value-flow answers into document evidence without forcing the user to restate the resolved name.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_resolve_counterparty_alias", + "title": "Entity resolution grounds the checked 1C counterparty from a loose alias", + "question": "найди в 1С контрагента СВК", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)свк", "(?i)контрагент"], + "required_answer_patterns_any": [ + "(?i)группа\\s+свк", + "(?i)каталог", + "(?i)найден", + "(?i)наиболее вероятн" + ], + "forbidden_answer_patterns": [ + "(?i)получили", + "(?i)заплатили", + "(?i)нетто", + "(?i)оборот", + "(?i)выручк", + "(?i)сумм(а|ы)" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "alias_grounding", "followup_anchor"] + }, + { + "step_id": "step_02_incoming_by_resolved_entity", + "title": "Incoming value-flow follow-up reuses the resolved counterparty anchor", + "question": "сколько получили по нему за 2020 год", + "allowed_reply_types": ["factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)2020", "(?i)получил|входящ|поступ", "(?i)руб"], + "required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "incoming_value_flow", "followup_reuse"] + }, + { + "step_id": "step_03_payout_switch_by_resolved_entity", + "title": "Outgoing payment follow-up keeps the same grounded counterparty and checked year", + "question": "а теперь сколько заплатили?", + "allowed_reply_types": ["factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)2020", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"], + "required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту", + "(?i)за какой год" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "payout_switch", "followup_reuse", "date_carryover"] + }, + { + "step_id": "step_04_year_switch_on_payout", + "title": "Short year switch keeps the payout contour and grounded counterparty", + "question": "а за 2021?", + "allowed_reply_types": ["factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)2021", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"], + "required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "payout_year_switch", "followup_reuse"] + }, + { + "step_id": "step_05_documents_after_value_flow", + "title": "Document evidence follow-up keeps the same grounded counterparty after the money answer", + "question": "а по документам?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)документ|счет|накладн|акт"], + "required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2021"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту", + "(?i)сколько получили", + "(?i)сколько заплатили", + "(?i)нетто" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "document_evidence", "value_flow_pivot", "followup_reuse"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 80cad55..97697c3 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -537,6 +537,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) { followupSeed.pilotScope === "counterparty_value_flow_query_movements_v1" || followupSeed.pilotScope === "counterparty_supplier_payout_query_movements_v1" || followupSeed.pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1")); + const groundedValueFlowEvidenceSourceApplicable = Boolean((followupSeed.counterparty || followupSeed.discoveryEntity) && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + (followupSeed.domain === "counterparty_value" || + followupSeed.action === "turnover" || + followupSeed.action === "payout" || + followupSeed.action === "net_value_flow" || + followupSeed.pilotScope === "counterparty_value_flow_query_movements_v1" || + followupSeed.pilotScope === "counterparty_supplier_payout_query_movements_v1" || + followupSeed.pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1")); + const valueFlowGroundedDocumentFollowupApplicable = Boolean(groundedValueFlowEvidenceSourceApplicable && metadataDocumentHintSignal); + const valueFlowGroundedMovementFollowupApplicable = Boolean(groundedValueFlowEvidenceSourceApplicable && metadataMovementHintSignal); const documentEvidenceGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" && (followupSeed.counterparty || followupSeed.discoveryEntity) && !rawLifecycleSignal && @@ -594,12 +607,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable || metadataAmbiguityResolvedDocumentFollowupApplicable || entityResolutionGroundedDocumentFollowupApplicable || + valueFlowGroundedDocumentFollowupApplicable || movementEvidenceGroundedDocumentFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || metadataAmbiguityCollapsedDocumentLaneContinuationApplicable; const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable || metadataAmbiguityResolvedMovementFollowupApplicable || entityResolutionGroundedMovementFollowupApplicable || + valueFlowGroundedMovementFollowupApplicable || documentEvidenceGroundedMovementFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || metadataAmbiguityCollapsedMovementLaneContinuationApplicable; @@ -880,6 +895,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (entityResolutionGroundedMovementFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup"); } + if (valueFlowGroundedDocumentFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup"); + } + if (valueFlowGroundedMovementFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_movement_followup"); + } if (groundedValueFlowFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_grounded_value_flow_followup"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index 4828738..734561c 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -732,6 +732,25 @@ export function buildAssistantMcpDiscoveryTurnInput( followupSeed.pilotScope === "counterparty_supplier_payout_query_movements_v1" || followupSeed.pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") ); + const groundedValueFlowEvidenceSourceApplicable = Boolean( + (followupSeed.counterparty || followupSeed.discoveryEntity) && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + (followupSeed.domain === "counterparty_value" || + followupSeed.action === "turnover" || + followupSeed.action === "payout" || + followupSeed.action === "net_value_flow" || + followupSeed.pilotScope === "counterparty_value_flow_query_movements_v1" || + followupSeed.pilotScope === "counterparty_supplier_payout_query_movements_v1" || + followupSeed.pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") + ); + const valueFlowGroundedDocumentFollowupApplicable = Boolean( + groundedValueFlowEvidenceSourceApplicable && metadataDocumentHintSignal + ); + const valueFlowGroundedMovementFollowupApplicable = Boolean( + groundedValueFlowEvidenceSourceApplicable && metadataMovementHintSignal + ); const documentEvidenceGroundedMovementFollowupApplicable = Boolean( followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" && (followupSeed.counterparty || followupSeed.discoveryEntity) && @@ -802,6 +821,7 @@ export function buildAssistantMcpDiscoveryTurnInput( metadataGroundedDocumentFollowupApplicable || metadataAmbiguityResolvedDocumentFollowupApplicable || entityResolutionGroundedDocumentFollowupApplicable || + valueFlowGroundedDocumentFollowupApplicable || movementEvidenceGroundedDocumentFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || metadataAmbiguityCollapsedDocumentLaneContinuationApplicable; @@ -809,6 +829,7 @@ export function buildAssistantMcpDiscoveryTurnInput( metadataGroundedMovementFollowupApplicable || metadataAmbiguityResolvedMovementFollowupApplicable || entityResolutionGroundedMovementFollowupApplicable || + valueFlowGroundedMovementFollowupApplicable || documentEvidenceGroundedMovementFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || metadataAmbiguityCollapsedMovementLaneContinuationApplicable; @@ -1105,6 +1126,12 @@ export function buildAssistantMcpDiscoveryTurnInput( if (entityResolutionGroundedMovementFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup"); } + if (valueFlowGroundedDocumentFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup"); + } + if (valueFlowGroundedMovementFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_movement_followup"); + } if (groundedValueFlowFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_grounded_value_flow_followup"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 0ba9d6c..1307514 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -422,6 +422,154 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); }); + it.skip("switches from a grounded exact value-flow answer into document evidence without restating the counterparty", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "Р° РїРѕ документам?", + assistantTurnMeaning: { + asked_domain_family: "counterparty", + asked_action_family: "turnover", + explicit_intent_candidate: "customer_revenue_and_payments" + }, + followupContext: { + previous_intent: "customer_revenue_and_payments", + previous_filters: { + counterparty: "Группа РЎР’Рљ", + period_from: "2021-01-01", + period_to: "2021-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "Группа РЎР’Рљ" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("assistant_turn_meaning"); + 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: ["Группа РЎР’Рљ"], + explicit_date_scope: "2021", + unsupported_but_understood_family: "document_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_value_flow_grounded_document_followup"); + expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); + }); + + it.skip("switches from a grounded exact value-flow answer into movement evidence without restating the counterparty", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "Р° РїРѕ движениям?", + assistantTurnMeaning: { + asked_domain_family: "counterparty", + asked_action_family: "turnover", + explicit_intent_candidate: "customer_revenue_and_payments" + }, + followupContext: { + previous_intent: "customer_revenue_and_payments", + previous_filters: { + counterparty: "Группа РЎР’Рљ", + organization: "РћРћРћ Альтернатива Плюс", + period_from: "2021-01-01", + period_to: "2021-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "Группа РЎР’Рљ" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("assistant_turn_meaning"); + 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: ["Группа РЎР’Рљ"], + explicit_organization_scope: "РћРћРћ Альтернатива Плюс", + explicit_date_scope: "2021", + unsupported_but_understood_family: "movement_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_value_flow_grounded_movement_followup"); + expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); + }); + + it("switches from a grounded exact value-flow answer into document evidence with a clean UTF-8 follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "а по документам?", + assistantTurnMeaning: { + asked_domain_family: "counterparty", + asked_action_family: "turnover", + explicit_intent_candidate: "customer_revenue_and_payments" + }, + followupContext: { + previous_intent: "customer_revenue_and_payments", + previous_filters: { + counterparty: "Группа СВК", + period_from: "2021-01-01", + period_to: "2021-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "Группа СВК" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("assistant_turn_meaning"); + expect(result.semantic_data_need).toBe("counterparty value-flow evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_entity_candidates: ["Группа СВК"], + explicit_date_scope: "2021", + unsupported_but_understood_family: "document_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_value_flow_grounded_document_followup"); + expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); + }); + + it("switches from a grounded exact value-flow answer into movement evidence with a clean UTF-8 follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "а по движениям?", + assistantTurnMeaning: { + asked_domain_family: "counterparty", + asked_action_family: "turnover", + explicit_intent_candidate: "customer_revenue_and_payments" + }, + followupContext: { + previous_intent: "customer_revenue_and_payments", + previous_filters: { + counterparty: "Группа СВК", + organization: "ООО Альтернатива Плюс", + period_from: "2021-01-01", + period_to: "2021-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "Группа СВК" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("assistant_turn_meaning"); + expect(result.semantic_data_need).toBe("counterparty value-flow evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["Группа СВК"], + explicit_organization_scope: "ООО Альтернатива Плюс", + explicit_date_scope: "2021", + unsupported_but_understood_family: "movement_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_value_flow_grounded_movement_followup"); + expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); + }); + it("seeds short monthly follow-up from prior bidirectional discovery context", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "а по месяцам?",