From dca49ef4e1d7c45836460d47c8ecf6764aab8aed Mon Sep 17 00:00:00 2001 From: dctouch Date: Wed, 22 Apr 2026 17:49:09 +0300 Subject: [PATCH] =?UTF-8?q?ARCH:=20=D0=BF=D1=80=D0=B8=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?discovery-period=20=D0=B2=20planner=20follow-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...e32_planner_selected_chain_end_to_end.json | 115 ++++++++++++++++++ .../services/assistantContinuityPolicy.js | 13 +- .../src/services/assistantContinuityPolicy.ts | 18 ++- .../tests/assistantContinuityPolicy.test.ts | 38 ++++++ 4 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 docs/orchestration/address_truth_harness_phase32_planner_selected_chain_end_to_end.json diff --git a/docs/orchestration/address_truth_harness_phase32_planner_selected_chain_end_to_end.json b/docs/orchestration/address_truth_harness_phase32_planner_selected_chain_end_to_end.json new file mode 100644 index 0000000..ee30d45 --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase32_planner_selected_chain_end_to_end.json @@ -0,0 +1,115 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase32_planner_selected_chain_end_to_end", + "domain": "address_phase32_planner_selected_chain_end_to_end", + "title": "Phase 32 planner-selected chain end-to-end replay", + "description": "Targeted AGENT replay for closing Big Block C: a grounded 1C counterparty must survive planner-selected pivots across incoming value-flow, outgoing payouts, net flow, document evidence, and movement 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_net_after_payout", + "title": "Net-flow follow-up reuses the same grounded counterparty and checked year after payout", + "question": "а какое нетто?", + "allowed_reply_types": ["factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)2020", "(?i)нетто|сальдо", "(?i)руб"], + "required_answer_patterns_any": ["(?i)получ", "(?i)заплат", "(?i)группа\\s+свк", "(?i)свк"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "net_value_flow", "followup_reuse"] + }, + { + "step_id": "step_05_documents_after_net", + "title": "Document evidence follow-up keeps the grounded counterparty after the net answer", + "question": "а по документам?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)документ|счет|накладн|акт"], + "required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2020"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту", + "(?i)сколько получили", + "(?i)сколько заплатили", + "(?i)нетто" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "document_evidence", "value_flow_pivot", "followup_reuse"] + }, + { + "step_id": "step_06_movements_after_documents", + "title": "Movement evidence follow-up keeps the grounded counterparty after the document answer", + "question": "а по движениям?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": ["(?i)движени|операц|платеж|списан|поступ"], + "required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2020"], + "forbidden_answer_patterns": [ + "(?i)не найден контрагент", + "(?i)уточните, какого контрагента", + "(?i)по какому контрагенту", + "(?i)сколько получили", + "(?i)сколько заплатили", + "(?i)нетто" + ], + "criticality": "critical", + "semantic_tags": ["entity_resolution", "movement_evidence", "document_pivot", "followup_reuse"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js index 16487f1..bcfd044 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -425,6 +425,8 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT const extractedFilters = readAddressDebugFilters(debug); const nextFilters = extractedFilters ? { ...extractedFilters } : {}; const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString); + const preferGroundedDiscoveryDateScope = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && + Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo); const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString); const organization = readAddressDebugOrganization(debug, toNonEmptyString); if (counterparty && !toNonEmptyString(nextFilters.counterparty)) { @@ -433,14 +435,19 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT if (organization && !toNonEmptyString(nextFilters.organization)) { nextFilters.organization = organization; } - if (discoveryDateScope.asOfDate && !toNonEmptyString(nextFilters.as_of_date)) { + if (discoveryDateScope.asOfDate && (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.as_of_date))) { nextFilters.as_of_date = discoveryDateScope.asOfDate; + delete nextFilters.period_from; + delete nextFilters.period_to; } - if (discoveryDateScope.periodFrom && !toNonEmptyString(nextFilters.period_from)) { + if (discoveryDateScope.periodFrom && + (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_from))) { nextFilters.period_from = discoveryDateScope.periodFrom; } - if (discoveryDateScope.periodTo && !toNonEmptyString(nextFilters.period_to)) { + if (discoveryDateScope.periodTo && + (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_to))) { nextFilters.period_to = discoveryDateScope.periodTo; + delete nextFilters.as_of_date; } const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString); const rootFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index de6b2aa..cc72090 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -622,6 +622,9 @@ export function resolveAddressDebugCarryoverFilters( const extractedFilters = readAddressDebugFilters(debug); const nextFilters = extractedFilters ? { ...extractedFilters } : {}; const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString); + const preferGroundedDiscoveryDateScope = + hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && + Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo); const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString); const organization = readAddressDebugOrganization(debug, toNonEmptyString); if (counterparty && !toNonEmptyString(nextFilters.counterparty)) { @@ -630,14 +633,23 @@ export function resolveAddressDebugCarryoverFilters( if (organization && !toNonEmptyString(nextFilters.organization)) { nextFilters.organization = organization; } - if (discoveryDateScope.asOfDate && !toNonEmptyString(nextFilters.as_of_date)) { + if (discoveryDateScope.asOfDate && (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.as_of_date))) { nextFilters.as_of_date = discoveryDateScope.asOfDate; + delete nextFilters.period_from; + delete nextFilters.period_to; } - if (discoveryDateScope.periodFrom && !toNonEmptyString(nextFilters.period_from)) { + if ( + discoveryDateScope.periodFrom && + (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_from)) + ) { nextFilters.period_from = discoveryDateScope.periodFrom; } - if (discoveryDateScope.periodTo && !toNonEmptyString(nextFilters.period_to)) { + if ( + discoveryDateScope.periodTo && + (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_to)) + ) { nextFilters.period_to = discoveryDateScope.periodTo; + delete nextFilters.as_of_date; } const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString); const rootFilters = diff --git a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts index deb5a4d..c2ee728 100644 --- a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts @@ -192,6 +192,44 @@ describe("assistantContinuityPolicy organization authority", () => { }); }); + it("prefers grounded discovery date scope over stale exact-route date filters in carryover", () => { + const debug = { + execution_lane: "address_query", + extracted_filters: { + counterparty: "Группа СВК", + period_to: "2026-04-22" + }, + 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_action_family: "payout", + explicit_entity_candidates: ["Группа СВК"], + explicit_date_scope: "2020" + } + }, + bridge: { + bridge_status: "answer_draft_ready", + business_fact_answer_allowed: true, + pilot: { + pilot_scope: "counterparty_supplier_payout_query_movements_v1" + }, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference" + } + } + } + }; + + expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({ + counterparty: "Группа СВК", + period_from: "2020-01-01", + period_to: "2020-12-31" + }); + }); + it("prefers the resolved entity from grounded entity-resolution discovery for counterparty carryover", () => { const debug = { execution_lane: "living_chat",