diff --git a/docs/orchestration/address_truth_harness_phase76_contracts_to_payments_all_time.json b/docs/orchestration/address_truth_harness_phase76_contracts_to_payments_all_time.json new file mode 100644 index 0000000..ad4c3f1 --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase76_contracts_to_payments_all_time.json @@ -0,0 +1,64 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase76_contracts_to_payments_all_time", + "domain": "address_phase76_contracts_to_payments_all_time", + "title": "Phase 76 contracts to payments all-time continuity", + "description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts, then pivots again to payments via pronoun follow-up, and finally switches to all-time scope 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_contracts_by_pronoun_followup", + "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", "counterparty_pronoun_resolution", "integrity_guard"] + }, + { + "step_id": "step_03_payments_after_contracts_pivot", + "title": "Pivot again to payments", + "question": "А по нему платежи?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)жуковк|контрагент", + "(?i)платеж|операц|банк|поступлен|списан" + ], + "criticality": "critical", + "semantic_tags": ["payments_followup", "second_pivot", "integrity_guard"] + }, + { + "step_id": "step_04_all_time_after_second_pivot", + "title": "Switch to all-time scope 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)объект[а-я]* 1с" + ], + "criticality": "critical", + "semantic_tags": ["all_time_after_second_pivot", "payments_followup", "integrity_guard"] + } + ] +} diff --git a/docs/orchestration/address_truth_harness_phase77_payments_to_contracts_year_switch.json b/docs/orchestration/address_truth_harness_phase77_payments_to_contracts_year_switch.json new file mode 100644 index 0000000..a4437c0 --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase77_payments_to_contracts_year_switch.json @@ -0,0 +1,65 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase77_payments_to_contracts_year_switch", + "domain": "address_phase77_payments_to_contracts_year_switch", + "title": "Phase 77 payments to contracts year-switch 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 switches the year 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_year_switch_after_second_pivot", + "title": "Switch the year after the second 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_second_pivot", "contracts_followup", "integrity_guard"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index 0705db8..2313587 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -554,6 +554,7 @@ function createAssistantTransitionPolicy(deps) { !shortValueFlowRetargetPrimary && !shortValueFlowRetargetAlternate && !hasImplicitContinuationSignal && + !hasSuggestedIntentPivotSignal && !hasOrganizationClarificationContinuation && !hasIndexReferenceSignal) { return null; @@ -567,6 +568,7 @@ function createAssistantTransitionPolicy(deps) { !shortValueFlowRetargetPrimary && !shortValueFlowRetargetAlternate && !hasImplicitContinuationSignal && + !hasSuggestedIntentPivotSignal && !hasOrganizationClarificationContinuation && !hasIndexReferenceSignal) { return null; @@ -683,6 +685,7 @@ function createAssistantTransitionPolicy(deps) { } hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) || + hasSuggestedIntentPivotSignal || Boolean(debtRoleSwapPrimary) || shortValueFlowRetargetPrimary || inventoryShortFollowupPrimary || @@ -690,6 +693,7 @@ function createAssistantTransitionPolicy(deps) { hasInventoryRootTemporalFollowupPrimary; hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage) ? deps.hasAddressFollowupContextSignal(alternateMessage) || + hasSuggestedIntentPivotSignal || Boolean(debtRoleSwapAlternate) || shortValueFlowRetargetAlternate || inventoryShortFollowupAlternate || @@ -700,6 +704,7 @@ function createAssistantTransitionPolicy(deps) { hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal || hasOrganizationClarificationContinuation || + hasSuggestedIntentPivotSignal || hasImplicitContinuationSignal || inventoryShortFollowupPrimary || inventoryShortFollowupAlternate || diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index ebf4286..1711523 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -751,6 +751,7 @@ export function createAssistantTransitionPolicy(deps) { !shortValueFlowRetargetPrimary && !shortValueFlowRetargetAlternate && !hasImplicitContinuationSignal && + !hasSuggestedIntentPivotSignal && !hasOrganizationClarificationContinuation && !hasIndexReferenceSignal ) { @@ -766,6 +767,7 @@ export function createAssistantTransitionPolicy(deps) { !shortValueFlowRetargetPrimary && !shortValueFlowRetargetAlternate && !hasImplicitContinuationSignal && + !hasSuggestedIntentPivotSignal && !hasOrganizationClarificationContinuation && !hasIndexReferenceSignal ) { @@ -944,6 +946,7 @@ export function createAssistantTransitionPolicy(deps) { } hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) || + hasSuggestedIntentPivotSignal || Boolean(debtRoleSwapPrimary) || shortValueFlowRetargetPrimary || inventoryShortFollowupPrimary || @@ -951,6 +954,7 @@ export function createAssistantTransitionPolicy(deps) { hasInventoryRootTemporalFollowupPrimary; hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage) ? deps.hasAddressFollowupContextSignal(alternateMessage) || + hasSuggestedIntentPivotSignal || Boolean(debtRoleSwapAlternate) || shortValueFlowRetargetAlternate || inventoryShortFollowupAlternate || @@ -961,6 +965,7 @@ export function createAssistantTransitionPolicy(deps) { hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal || hasOrganizationClarificationContinuation || + hasSuggestedIntentPivotSignal || hasImplicitContinuationSignal || inventoryShortFollowupPrimary || inventoryShortFollowupAlternate || diff --git a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts index b5ba1dd..8add1cb 100644 --- a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts @@ -737,6 +737,52 @@ describe("assistantTransitionPolicy", () => { expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot"); }); + it("switches from payments to suggested contracts on a pronoun follow-up even when generic follow-up signal is weak", () => { + const policy = buildPolicy({ + findLastAddressAssistantItem: () => ({ + text: "Собран список банковских операций по контрагенту ТСЖ \\Жуковка 51\\.", + debug: { + detected_intent: "bank_operations_by_counterparty", + extracted_filters: { + counterparty: "ТСЖ \\Жуковка 51\\" + }, + anchor_type: "counterparty", + anchor_value_resolved: "ТСЖ \\Жуковка 51\\" + } + }), + buildAddressFollowupOffer: () => ({ + enabled: true, + source_intent: "bank_operations_by_counterparty", + suggested_intents: ["list_documents_by_counterparty", "list_contracts_by_counterparty"] + }), + hasAddressFollowupContextSignal: () => false, + hasReferentialPointer: () => true + }); + + const carryover = policy.resolveAddressFollowupCarryoverContext("А по нему договоры?", [], null, null, null); + + expect(carryover?.followupContext?.previous_intent).toBe("list_contracts_by_counterparty"); + expect(carryover?.followupContext?.target_intent).toBe("list_contracts_by_counterparty"); + expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty"); + expect(carryover?.hasSuggestedIntentPivotSignal).toBe(true); + expect(carryover?.followupSelectionMode).toBe("switch_to_suggested_intent"); + + const contract = policy.buildAddressDialogContinuationContractV2( + "А по нему договоры?", + "Покажи договоры, связанные с указанным объектом", + carryover, + { + predecomposeContract: { + intent: "unknown" + } + } + ); + + expect(contract.decision).toBe("switch_to_suggested"); + expect(contract.target_intent).toBe("list_contracts_by_counterparty"); + expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot"); + }); + it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => { const policy = buildPolicy({ findLastAddressAssistantItem: () => ({