From 5acb0165738b3776be4607ed3cad5d8588bc803f Mon Sep 17 00:00:00 2001 From: dctouch Date: Thu, 23 Apr 2026 20:50:09 +0300 Subject: [PATCH] =?UTF-8?q?ARCH:=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20documents=20pivot=20=D0=BE=D1=82=20inventory=20s?= =?UTF-8?q?elected-object=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...hase78_payments_to_contracts_all_time.json | 65 ++++++++++++++++ ...to_contracts_to_documents_year_switch.json | 77 +++++++++++++++++++ .../address_runtime/decomposeStage.js | 10 ++- .../address_runtime/decomposeStage.ts | 11 ++- .../tests/addressQueryRuntimeM23.test.ts | 16 ++++ 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 docs/orchestration/address_truth_harness_phase78_payments_to_contracts_all_time.json create mode 100644 docs/orchestration/address_truth_harness_phase79_payments_to_contracts_to_documents_year_switch.json diff --git a/docs/orchestration/address_truth_harness_phase78_payments_to_contracts_all_time.json b/docs/orchestration/address_truth_harness_phase78_payments_to_contracts_all_time.json new file mode 100644 index 0000000..6956a53 --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase78_payments_to_contracts_all_time.json @@ -0,0 +1,65 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase78_payments_to_contracts_all_time", + "domain": "address_phase78_payments_to_contracts_all_time", + "title": "Phase 78 payments to contracts all-time continuity", + "description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then pivots again to contracts via pronoun follow-up, and finally requests the same contracts for all available time without renaming the counterparty.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_documents_by_counterparty", + "title": "Open documents for the counterparty", + "question": "Покажи документы по Жуковке 51.", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк", + "(?i)документ|сч[её]т|акт|накладн|строк" + ], + "criticality": "critical", + "semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"] + }, + { + "step_id": "step_02_payments_by_pronoun_followup", + "title": "Pivot to payments", + "question": "А по нему платежи?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)платеж|операц|банк|поступлен|списан" + ], + "criticality": "critical", + "semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"] + }, + { + "step_id": "step_03_contracts_after_payments_pivot", + "title": "Pivot again to contracts", + "question": "А по нему договоры?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)договор|контракт|соглаш" + ], + "criticality": "critical", + "semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"] + }, + { + "step_id": "step_04_all_time_after_second_pivot", + "title": "Request all available time after the second pivot", + "question": "А за все время?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)договор|контракт|соглаш" + ], + "forbidden_answer_patterns": [ + "(?i)уточните .* контрагент", + "(?i)уточните .* период", + "(?i)метадан", + "(?i)схем", + "(?i)объект[а-я]* 1с" + ], + "criticality": "critical", + "semantic_tags": ["all_time_after_second_pivot", "contracts_followup", "integrity_guard"] + } + ] +} diff --git a/docs/orchestration/address_truth_harness_phase79_payments_to_contracts_to_documents_year_switch.json b/docs/orchestration/address_truth_harness_phase79_payments_to_contracts_to_documents_year_switch.json new file mode 100644 index 0000000..64ea5cf --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase79_payments_to_contracts_to_documents_year_switch.json @@ -0,0 +1,77 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase79_payments_to_contracts_to_documents_year_switch", + "domain": "address_phase79_payments_to_contracts_to_documents_year_switch", + "title": "Phase 79 payments to contracts to documents year-switch continuity", + "description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then to contracts, then back to documents by pronoun follow-up, and finally narrows the same document contour to 2021 without renaming the counterparty.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_documents_by_counterparty", + "title": "Open documents for the counterparty", + "question": "Покажи документы по Жуковке 51.", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк", + "(?i)документ|сч[её]т|акт|накладн|строк" + ], + "criticality": "critical", + "semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"] + }, + { + "step_id": "step_02_payments_by_pronoun_followup", + "title": "Pivot to payments", + "question": "А по нему платежи?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)платеж|операц|банк|поступлен|списан" + ], + "criticality": "critical", + "semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"] + }, + { + "step_id": "step_03_contracts_after_payments_pivot", + "title": "Pivot to contracts", + "question": "А по нему договоры?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)договор|контракт|соглаш" + ], + "criticality": "critical", + "semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"] + }, + { + "step_id": "step_04_documents_after_contracts_pivot", + "title": "Pivot back to documents", + "question": "А по нему документы?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)документ|сч[её]т|акт|накладн|строк" + ], + "criticality": "critical", + "semantic_tags": ["documents_followup", "third_pivot", "integrity_guard"] + }, + { + "step_id": "step_05_year_switch_after_third_pivot", + "title": "Switch the year after the third pivot", + "question": "А за 2021?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)2021", + "(?i)жуковк|контрагент", + "(?i)документ|сч[её]т|акт|накладн|строк" + ], + "forbidden_answer_patterns": [ + "(?i)уточните .* контрагент", + "(?i)метадан", + "(?i)схем", + "(?i)объект[а-я]* 1с" + ], + "criticality": "critical", + "semantic_tags": ["year_switch_after_third_pivot", "documents_followup", "integrity_guard"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js index c34a9ac..6a02e81 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js @@ -1152,7 +1152,15 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty; const isVatFollowup = hasVatCue(normalizedMessage); const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined); - const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal); + const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined); + const inventoryLineageActive = previousIsInventoryFamily || + rootIsInventoryFamily || + followupContext.previous_anchor_type === "item" || + followupContext.root_anchor_type === "item" || + followupContext.current_frame_kind === "inventory_root" || + followupContext.current_frame_kind === "inventory_drilldown"; + const inventorySelectedObjectFollowup = inventoryLineageActive && + (hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal)); const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage); if (inventoryPurchaseDateVatBridge && (detectedIntent.intent === "unknown" || diff --git a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts index dfaa1d1..198a1fc 100644 --- a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts @@ -1440,8 +1440,17 @@ function deriveIntentWithFollowupContext( const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty; const isVatFollowup = hasVatCue(normalizedMessage); const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined); + const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined); + const inventoryLineageActive = + previousIsInventoryFamily || + rootIsInventoryFamily || + followupContext.previous_anchor_type === "item" || + followupContext.root_anchor_type === "item" || + followupContext.current_frame_kind === "inventory_root" || + followupContext.current_frame_kind === "inventory_drilldown"; const inventorySelectedObjectFollowup = - hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal); + inventoryLineageActive && + (hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal)); const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage); diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index e617692..b875350 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -4632,6 +4632,22 @@ describe("address decompose stage follow-up carryover", () => { ).toBe(true); }); + it("does not drift into inventory selected-object documents on a counterparty contracts follow-up", () => { + const result = runAddressDecomposeStage("а по нему документы?", { + previous_intent: "list_contracts_by_counterparty", + target_intent: "list_documents_by_counterparty", + previous_filters: { + counterparty: "ТСЖ \\Жуковка 51\\" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "ТСЖ \\Жуковка 51\\" + }); + expect(result).not.toBeNull(); + expect(result?.intent.intent).toBe("list_documents_by_counterparty"); + expect(result?.filters.extracted_filters.counterparty).toBe("ТСЖ \\Жуковка 51\\"); + expect(result?.baseReasons).not.toContain("intent_adjusted_to_inventory_followup_context"); + }); + it("replaces 'кроме этого документа...' pseudo-anchor with previous counterparty from follow-up context", () => { const result = runAddressDecomposeStage("кроме этого документа есть еще чтото?", { previous_intent: "list_documents_by_counterparty",