From 7dd6607ded9bd8617f377ebaa3f1a7d13dd9836a Mon Sep 17 00:00:00 2001 From: dctouch Date: Thu, 2 Apr 2026 00:30:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=94=D0=A0=D0=95=D0=A1=D0=9D=D0=AB?= =?UTF-8?q?=D0=99=20=D0=A0=D0=95=D0=96=D0=98=D0=9C=20-=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?LLM=20fallback=20=D1=8F=D0=BA=D0=BE=D1=80=D0=B5=D0=B9=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../batch_results.csv | 25 + .../batch_results.json | 554 ++++++++++++++++++ .../batch_results.csv | 25 + .../batch_results.json | 338 +++++++++++ .../batch_results.csv | 25 + .../batch_results.json | 290 +++++++++ .../batch_results.csv | 25 + .../batch_results.json | 290 +++++++++ .../src/services/addressFilterExtractor.ts | 48 +- .../src/services/addressIntentResolver.ts | 142 +++++ .../src/services/addressQueryService.ts | 222 ++++++- .../src/services/addressRecipeCatalog.ts | 38 +- .../services/address_runtime/composeStage.ts | 24 + .../address_runtime/decomposeStage.ts | 14 +- .../services/address_runtime/resolveStage.ts | 13 +- .../backend/src/services/assistantService.ts | 186 +++++- .../backend/src/types/addressQuery.ts | 2 + .../tests/addressQueryRuntimeM23.test.ts | 128 +++- .../assistantAddressLlmPredecompose.test.ts | 387 ++++++++++++ 19 files changed, 2727 insertions(+), 49 deletions(-) create mode 100644 docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.csv create mode 100644 docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.json create mode 100644 docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.csv create mode 100644 docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.json create mode 100644 docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.csv create mode 100644 docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.json create mode 100644 docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.csv create mode 100644 docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.json diff --git a/docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.csv b/docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.csv new file mode 100644 index 0000000..bd79285 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.csv @@ -0,0 +1,25 @@ +"idx","question","reply_type","trace_id","detected_intent","query_shape","mcp_call_status","rows_matched","limited_reason_category","llm_decomposition_applied","llm_decomposition_reason","fallback_rule_hit","tool_gate_decision","runtime_readiness","period_from","period_to","as_of_date","anchor_type","anchor_value_raw","anchor_value_resolved","assistant_reply_head" +"1","СЃРІРє РґРѕРєРё Р·Р° 20РіРѕРґ покеж","factual","address-sugd3wlUri","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","3","","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"2","СЃРІРє 20 РіРѕРґ - покажи РґРѕРєРё плс","factual","address--4sSq56ihd","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","3","","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"3","что РїРѕ СЃРІРє Р·Р° 2020 РіРѕРґ выведи РІСЃРµ РґРѕРєРё плиз что есть","partial_coverage","address-MUkQr3-8cm","list_documents_by_counterparty","DOCUMENT_LIST","materialized_but_not_anchor_matched","0","missing_anchor","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","что","что","Для точного адресного поиска не хватает обязательного якоря." +"4","какие документы РїРѕ контрагенту СЃРІРє Р·Р° РІСЃРµ время","factual","address-vfIW3ugGJ3","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","26","","True","fallback_rule_applied_after_llm_error","documents_counterparty_all_time_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","","","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"5","РґРѕРєРё РїРѕ СЃРІРє СЃ 01.07.2020 РїРѕ 31.07.2020","factual","address-OheS6P-gDn","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","2","","True","fallback_rule_applied_after_llm_error","documents_counterparty_month_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-07-01","2020-07-31","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"6","СЃРІРє июль 2020 какие РґРѕРєРё есть","factual","address-DCuYSZOqzh","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","2","","True","fallback_rule_applied_after_llm_error","documents_counterparty_month_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-07-01","2020-07-31","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"7","покажи сальдо РїРѕ 60.01 РЅР° 31.07.20","factual","address-aW8e72ada6","account_balance_snapshot","AGGREGATE_LOOKUP","matched_non_empty","200","","True","fallback_rule_applied_after_llm_error","balance_account_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","","","2026-04-01","account","60.01","60.01","Адресный срез по счету собран (по движениям live MCP)." +"8","остаток 60 на 2020.05","factual","address-o-EiCUq_sW","account_balance_snapshot","AGGREGATE_LOOKUP","matched_non_empty","6","","True","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","2020-05-31","account","60","60","Адресный срез по счету собран (по движениям live MCP)." +"9","какой остаток по счету 60 на 2020 май","factual","address-jjAFfbUg3l","account_balance_snapshot","AGGREGATE_LOOKUP","matched_non_empty","6","","True","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","2020-05-31","account","60","60","Адресный срез по счету собран (по движениям live MCP)." +"10","60 счет остаток РЅР° май 2020 покажи","factual","address-zp57Rg1ax2","account_balance_snapshot","AGGREGATE_LOOKUP","matched_non_empty","6","","True","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","2020-05-31","account","60","60","Адресный срез по счету собран (по движениям live MCP)." +"11","какой остаток по счету 60 на 2020-05-31","factual","address-UdMlO0oihD","account_balance_snapshot","AGGREGATE_LOOKUP","matched_non_empty","6","","True","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","2020-05-31","account","60","60","Адресный срез по счету собран (по движениям live MCP)." +"12","остаток по 60 на май 2020, не за весь 2020","factual","address-C9kv5RSpvX","account_balance_snapshot","AGGREGATE_LOOKUP","matched_non_empty","6","","True","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","2020-05-31","account","60","60","Адресный срез по счету собран (по движениям live MCP)." +"13","покаж РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 РґРѕРєРё Р·Р° 2020","factual","address-nAM6RcOew7","list_documents_by_contract","DOCUMENT_LIST","matched_non_empty","233","","True","fallback_rule_applied_after_llm_error","documents_contract_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","contract","1-","1-","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"14","какие документы РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 Р·Р° РІСЃРµ время","factual","address-0KPk8HnFoG","list_documents_by_contract","DOCUMENT_LIST","matched_non_empty","120","","True","fallback_rule_applied_after_llm_error","documents_contract_all_time_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","","","","contract","1-","1-","Собран список документов по договору (live address lane)." +"15","есть ли долг РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 РЅР° 2020-07-31","partial_coverage","address-NAspBEx-B2","account_balance_snapshot","AGGREGATE_LOOKUP","no_raw_rows","0","empty_match","True","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-07-01","2020-07-31","2020-07-31","account","07","07","В live-данных по текущему фильтру записи не найдены." +"16","какие хвосты РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 РЅР° дату 31.07.2020","partial_coverage","address-9UG7ycCWej","account_balance_snapshot","UNKNOWN","no_raw_rows","0","empty_match","False","error:Failed to extract output_text from /responses payload.","","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-07-01","2020-07-31","2020-07-31","account","07","07","В live-данных по текущему фильтру записи не найдены." +"17","какие платежи были РїРѕ СЃРІРє РІ 2020","partial_coverage","address-db6MWzmaK3","list_documents_by_counterparty","DOCUMENT_LIST","materialized_but_not_anchor_matched","0","missing_anchor","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","были","были","Для точного адресного поиска не хватает обязательного якоря." +"18","были ли поступления РѕС‚ СЃРІРє Р·Р° июль 2020","partial_coverage","address-kj2wxmasp5","list_documents_by_counterparty","DOCUMENT_LIST","materialized_but_not_anchor_matched","0","missing_anchor","True","fallback_rule_applied_after_llm_error","documents_counterparty_month_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-07-01","2020-07-31","","counterparty","были","были","Для точного адресного поиска не хватает обязательного якоря." +"19","покажи списания СЃ расчетного счета РїРѕ СЃРІРє Р·Р° 2020","partial_coverage","address-5ZApJI8tq_","bank_operations_by_counterparty","DOCUMENT_LIST","materialized_but_not_anchor_matched","0","missing_anchor","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","списания","списания","Для точного адресного поиска не хватает обязательного якоря." +"20","СЃРІРє Р·Р° 2020 покаж РІСЃРµ поступления","factual","address-b8vcHTo3Mb","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","3","","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"21","покажи документы РїРѕ СЃРІРє Р·Р° 2020","factual","address-3plzT4WuiP","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","3","","True","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-01-01","2020-12-31","","counterparty","свк","Группа СВК","Собран список документов по контрагенту (live address lane)." +"22","а теперь только за май 2020","factual","address-ZAbNn9yMro","list_documents_by_counterparty","UNKNOWN","matched_non_empty","26","","False","not_address_like","","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","","counterparty","свк","Группа СВК","По окну 2020-05-01..2020-05-31 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." +"23","а по счету 60.01 на ту же дату","factual","address-8AhrNbM9OG","account_balance_snapshot","UNKNOWN","matched_non_empty","5","","False","error:Failed to extract output_text from /responses payload.","","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2020-05-01","2020-05-31","2020-05-31","account","60.01","60.01","Адресный срез по счету собран (по движениям live MCP)." +"24","бля епт покажи РґРѕРєРё РїРѕ СЃРІРє Р·Р° 20Р№","factual","address-t3_otb_RVf","list_documents_by_counterparty","DOCUMENT_LIST","matched_non_empty","26","","True","fallback_rule_applied_after_llm_error","documents_counterparty_rewrite","run_address_lane","LIVE_QUERYABLE_WITH_LIMITS","2026-01-01","2026-04-01","","counterparty","свк","Группа СВК","По окну 2026-01-01..2026-04-01 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." diff --git a/docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.json b/docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.json new file mode 100644 index 0000000..d5494ab --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-04-32_live_batch_from_1txt/batch_results.json @@ -0,0 +1,554 @@ +[ + { + "idx": 1, + "question": "СЃРІРє РґРѕРєРё Р·Р° 20РіРѕРґ покеж", + "reply_type": "factual", + "trace_id": "address-sugd3wlUri", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 3, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 2, + "question": "СЃРІРє 20 РіРѕРґ - покажи РґРѕРєРё плс", + "reply_type": "factual", + "trace_id": "address--4sSq56ihd", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 3, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 3, + "question": "что РїРѕ СЃРІРє Р·Р° 2020 РіРѕРґ выведи РІСЃРµ РґРѕРєРё плиз что есть", + "reply_type": "partial_coverage", + "trace_id": "address-MUkQr3-8cm", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "materialized_but_not_anchor_matched", + "rows_matched": 0, + "limited_reason_category": "missing_anchor", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "что", + "anchor_value_resolved": "что", + "assistant_reply_head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 4, + "question": "какие документы РїРѕ контрагенту СЃРІРє Р·Р° РІСЃРµ время", + "reply_type": "factual", + "trace_id": "address-vfIW3ugGJ3", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 26, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_all_time_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "", + "period_to": "", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 5, + "question": "РґРѕРєРё РїРѕ СЃРІРє СЃ 01.07.2020 РїРѕ 31.07.2020", + "reply_type": "factual", + "trace_id": "address-OheS6P-gDn", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 2, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_month_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-07-01", + "period_to": "2020-07-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 6, + "question": "СЃРІРє июль 2020 какие РґРѕРєРё есть", + "reply_type": "factual", + "trace_id": "address-DCuYSZOqzh", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 2, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_month_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-07-01", + "period_to": "2020-07-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 7, + "question": "покажи сальдо РїРѕ 60.01 РЅР° 31.07.20", + "reply_type": "factual", + "trace_id": "address-aW8e72ada6", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "matched_non_empty", + "rows_matched": 200, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_account_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "", + "period_to": "", + "as_of_date": "2026-04-01", + "anchor_type": "account", + "anchor_value_raw": "60.01", + "anchor_value_resolved": "60.01", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 8, + "question": "остаток 60 на 2020.05", + "reply_type": "factual", + "trace_id": "address-o-EiCUq_sW", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "matched_non_empty", + "rows_matched": 6, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "2020-05-31", + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 9, + "question": "какой остаток по счету 60 на 2020 май", + "reply_type": "factual", + "trace_id": "address-jjAFfbUg3l", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "matched_non_empty", + "rows_matched": 6, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "2020-05-31", + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 10, + "question": "60 счет остаток РЅР° май 2020 покажи", + "reply_type": "factual", + "trace_id": "address-zp57Rg1ax2", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "matched_non_empty", + "rows_matched": 6, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "2020-05-31", + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 11, + "question": "какой остаток по счету 60 на 2020-05-31", + "reply_type": "factual", + "trace_id": "address-UdMlO0oihD", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "matched_non_empty", + "rows_matched": 6, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "2020-05-31", + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 12, + "question": "остаток по 60 на май 2020, не за весь 2020", + "reply_type": "factual", + "trace_id": "address-C9kv5RSpvX", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "matched_non_empty", + "rows_matched": 6, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "2020-05-31", + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 13, + "question": "покаж РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 РґРѕРєРё Р·Р° 2020", + "reply_type": "factual", + "trace_id": "address-nAM6RcOew7", + "detected_intent": "list_documents_by_contract", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 233, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_contract_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "contract", + "anchor_value_raw": "1-", + "anchor_value_resolved": "1-", + "assistant_reply_head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 14, + "question": "какие документы РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 Р·Р° РІСЃРµ время", + "reply_type": "factual", + "trace_id": "address-0KPk8HnFoG", + "detected_intent": "list_documents_by_contract", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 120, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_contract_all_time_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "", + "period_to": "", + "as_of_date": "", + "anchor_type": "contract", + "anchor_value_raw": "1-", + "anchor_value_resolved": "1-", + "assistant_reply_head": "Собран список документов по договору (live address lane)." + }, + { + "idx": 15, + "question": "есть ли долг РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 РЅР° 2020-07-31", + "reply_type": "partial_coverage", + "trace_id": "address-NAspBEx-B2", + "detected_intent": "account_balance_snapshot", + "query_shape": "AGGREGATE_LOOKUP", + "mcp_call_status": "no_raw_rows", + "rows_matched": 0, + "limited_reason_category": "empty_match", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-07-01", + "period_to": "2020-07-31", + "as_of_date": "2020-07-31", + "anchor_type": "account", + "anchor_value_raw": "07", + "anchor_value_resolved": "07", + "assistant_reply_head": "В live-данных по текущему фильтру записи не найдены." + }, + { + "idx": 16, + "question": "какие хвосты РїРѕ РґРѕРіРѕРІРѕСЂСѓ 1-ПМ/2020 РЅР° дату 31.07.2020", + "reply_type": "partial_coverage", + "trace_id": "address-9UG7ycCWej", + "detected_intent": "account_balance_snapshot", + "query_shape": "UNKNOWN", + "mcp_call_status": "no_raw_rows", + "rows_matched": 0, + "limited_reason_category": "empty_match", + "llm_decomposition_applied": false, + "llm_decomposition_reason": "error:Failed to extract output_text from /responses payload.", + "fallback_rule_hit": "", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-07-01", + "period_to": "2020-07-31", + "as_of_date": "2020-07-31", + "anchor_type": "account", + "anchor_value_raw": "07", + "anchor_value_resolved": "07", + "assistant_reply_head": "В live-данных по текущему фильтру записи не найдены." + }, + { + "idx": 17, + "question": "какие платежи были РїРѕ СЃРІРє РІ 2020", + "reply_type": "partial_coverage", + "trace_id": "address-db6MWzmaK3", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "materialized_but_not_anchor_matched", + "rows_matched": 0, + "limited_reason_category": "missing_anchor", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "были", + "anchor_value_resolved": "были", + "assistant_reply_head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 18, + "question": "были ли поступления РѕС‚ СЃРІРє Р·Р° июль 2020", + "reply_type": "partial_coverage", + "trace_id": "address-kj2wxmasp5", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "materialized_but_not_anchor_matched", + "rows_matched": 0, + "limited_reason_category": "missing_anchor", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_month_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-07-01", + "period_to": "2020-07-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "были", + "anchor_value_resolved": "были", + "assistant_reply_head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 19, + "question": "покажи списания СЃ расчетного счета РїРѕ СЃРІРє Р·Р° 2020", + "reply_type": "partial_coverage", + "trace_id": "address-5ZApJI8tq_", + "detected_intent": "bank_operations_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "materialized_but_not_anchor_matched", + "rows_matched": 0, + "limited_reason_category": "missing_anchor", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "списания", + "anchor_value_resolved": "списания", + "assistant_reply_head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 20, + "question": "СЃРІРє Р·Р° 2020 покаж РІСЃРµ поступления", + "reply_type": "factual", + "trace_id": "address-b8vcHTo3Mb", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 3, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 21, + "question": "покажи документы РїРѕ СЃРІРє Р·Р° 2020", + "reply_type": "factual", + "trace_id": "address-3plzT4WuiP", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 3, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 22, + "question": "а теперь только за май 2020", + "reply_type": "factual", + "trace_id": "address-ZAbNn9yMro", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "UNKNOWN", + "mcp_call_status": "matched_non_empty", + "rows_matched": 26, + "limited_reason_category": "", + "llm_decomposition_applied": false, + "llm_decomposition_reason": "not_address_like", + "fallback_rule_hit": "", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "По окну 2020-05-01..2020-05-31 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." + }, + { + "idx": 23, + "question": "а по счету 60.01 на ту же дату", + "reply_type": "factual", + "trace_id": "address-8AhrNbM9OG", + "detected_intent": "account_balance_snapshot", + "query_shape": "UNKNOWN", + "mcp_call_status": "matched_non_empty", + "rows_matched": 5, + "limited_reason_category": "", + "llm_decomposition_applied": false, + "llm_decomposition_reason": "error:Failed to extract output_text from /responses payload.", + "fallback_rule_hit": "", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2020-05-01", + "period_to": "2020-05-31", + "as_of_date": "2020-05-31", + "anchor_type": "account", + "anchor_value_raw": "60.01", + "anchor_value_resolved": "60.01", + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 24, + "question": "бля епт покажи РґРѕРєРё РїРѕ СЃРІРє Р·Р° 20Р№", + "reply_type": "factual", + "trace_id": "address-t3_otb_RVf", + "detected_intent": "list_documents_by_counterparty", + "query_shape": "DOCUMENT_LIST", + "mcp_call_status": "matched_non_empty", + "rows_matched": 26, + "limited_reason_category": "", + "llm_decomposition_applied": true, + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_rewrite", + "tool_gate_decision": "run_address_lane", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "period_from": "2026-01-01", + "period_to": "2026-04-01", + "as_of_date": "", + "anchor_type": "counterparty", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "assistant_reply_head": "По окну 2026-01-01..2026-04-01 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." + } +] diff --git a/docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.csv b/docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.csv new file mode 100644 index 0000000..adcb503 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.csv @@ -0,0 +1,25 @@ +"idx","question","reply_type","mcp_call_status","limited_reason_category","detected_intent","llm_decomposition_reason","fallback_rule_hit","anchor_value_raw","anchor_value_resolved","rows_matched","assistant_reply_head" +"1","свк доки за 20год покеж","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","свк","Группа СВК","3","Собран список документов по контрагенту (live address lane)." +"2","свк 20 год - покажи доки плс","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","свк","Группа СВК","3","Собран список документов по контрагенту (live address lane)." +"3","что по свк за 2020 год выведи все доки плиз что есть","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","свк","Группа СВК","3","Собран список документов по контрагенту (live address lane)." +"4","какие документы по контрагенту свк за все время","partial_coverage","error","execution_error","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_all_time_rewrite","свк","свк","0","Не удалось выполнить адресный live-запрос в V1." +"5","доки по свк с 01.07.2020 по 31.07.2020","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_month_rewrite","свк","Группа СВК","2","Собран список документов по контрагенту (live address lane)." +"6","свк июль 2020 какие доки есть","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_month_rewrite","свк","Группа СВК","2","Собран список документов по контрагенту (live address lane)." +"7","покажи сальдо по 60.01 на 31.07.20","factual","matched_non_empty","","account_balance_snapshot","fallback_rule_applied_after_llm_error","balance_account_rewrite","60.01","60.01","200","Адресный срез по счету собран (по движениям live MCP)." +"8","остаток 60 на 2020.05","factual","matched_non_empty","","account_balance_snapshot","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","60","60","6","Адресный срез по счету собран (по движениям live MCP)." +"9","какой остаток по счету 60 на 2020 май","factual","matched_non_empty","","account_balance_snapshot","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","60","60","6","Адресный срез по счету собран (по движениям live MCP)." +"10","60 счет остаток на май 2020 покажи","factual","matched_non_empty","","account_balance_snapshot","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","60","60","6","Адресный срез по счету собран (по движениям live MCP)." +"11","какой остаток по счету 60 на 2020-05-31","factual","matched_non_empty","","account_balance_snapshot","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","60","60","6","Адресный срез по счету собран (по движениям live MCP)." +"12","остаток по 60 на май 2020, не за весь 2020","factual","matched_non_empty","","account_balance_snapshot","fallback_rule_applied_after_llm_error","balance_month_period_rewrite","60","60","6","Адресный срез по счету собран (по движениям live MCP)." +"13","покаж по договору 1-��/2020 доки за 2020","factual","matched_non_empty","","list_documents_by_contract","fallback_rule_applied_after_llm_error","documents_contract_year_rewrite","1-","1-","233","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"14","какие документы по договору 1-��/2020 за все время","factual","matched_non_empty","","list_documents_by_contract","fallback_rule_applied_after_llm_error","documents_contract_all_time_rewrite","1-","1-","120","Собран список документов по договору (live address lane)." +"15","есть ли долг по договору 1-��/2020 на 2020-07-31","partial_coverage","materialized_but_filtered_out_by_recipe","recipe_visibility_gap","list_documents_by_contract","error:Failed to extract output_text from /responses payload.","","1-��/2020 на 2020-07-31","1-��/2020 на 2020-07-31","0","Текущий live recipe не дает нужную видимость данных для этого сценария." +"16","какие хвосты по договору 1-��/2020 на дату 31.07.2020","partial_coverage","materialized_but_not_anchor_matched","missing_anchor","list_documents_by_contract","error:Failed to extract output_text from /responses payload.","","1-��/2020 на дату 31","1-��/2020 на дату 31","0","Для точного адресного поиска не хватает обязательного якоря." +"17","какие платежи были по свк в 2020","factual","matched_non_empty","","bank_operations_by_counterparty","fallback_rule_applied_after_llm_error","bank_operations_counterparty_year_rewrite","свк","Группа СВК","3","Собран список банковских операций по контрагенту (live address lane)." +"18","были ли поступления от свк за июль 2020","factual","matched_non_empty","","bank_operations_by_counterparty","fallback_rule_applied_after_llm_error","bank_operations_counterparty_month_rewrite","свк","Группа СВК","2","Собран список банковских операций по контрагенту (live address lane)." +"19","покажи списания с расчетного счета по свк за 2020","factual","matched_non_empty","","bank_operations_by_counterparty","fallback_rule_applied_after_llm_error","bank_operations_counterparty_year_rewrite","свк","Группа СВК","3","Собран список банковских операций по контрагенту (live address lane)." +"20","свк за 2020 покаж все поступления","factual","matched_non_empty","","bank_operations_by_counterparty","fallback_rule_applied_after_llm_error","bank_operations_counterparty_year_rewrite","свк","Группа СВК","3","Собран список банковских операций по контрагенту (live address lane)." +"21","покажи документы по свк за 2020","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_year_rewrite","свк","Группа СВК","3","Собран список документов по контрагенту (live address lane)." +"22","а теперь только за май 2020","partial_coverage","materialized_but_not_anchor_matched","missing_anchor","list_documents_by_counterparty","not_address_like","","свк","свк","0","Для точного адресного поиска не хватает обязательного якоря." +"23","а по счету 60.01 на ту же дату","factual","matched_non_empty","","account_balance_snapshot","error:Failed to extract output_text from /responses payload.","","60.01","60.01","5","Адресный срез по счету собран (по движениям live MCP)." +"24","бля епт покажи доки по свк за 20й","factual","matched_non_empty","","list_documents_by_counterparty","fallback_rule_applied_after_llm_error","documents_counterparty_rewrite","свк","Группа СВК","26","По окну 2026-01-01..2026-04-01 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." diff --git a/docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.json b/docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.json new file mode 100644 index 0000000..0061b5c --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-12-38_live_batch_from_1txt_after_fix/batch_results.json @@ -0,0 +1,338 @@ +[ + { + "idx": 1, + "question": "свк доки за 20год покеж", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 2, + "question": "свк 20 год - покажи доки плс", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 3, + "question": "что по свк за 2020 год выведи все доки плиз что есть", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 4, + "question": "какие документы по контрагенту свк за все время", + "reply_type": "partial_coverage", + "mcp_call_status": "error", + "limited_reason_category": "execution_error", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_all_time_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "свк", + "rows_matched": 0, + "assistant_reply_head": "Не удалось выполнить адресный live-запрос в V1." + }, + { + "idx": 5, + "question": "доки по свк с 01.07.2020 по 31.07.2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_month_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 2, + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 6, + "question": "свк июль 2020 какие доки есть", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_month_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 2, + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 7, + "question": "покажи сальдо по 60.01 на 31.07.20", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_account_rewrite", + "anchor_value_raw": "60.01", + "anchor_value_resolved": "60.01", + "rows_matched": 200, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 8, + "question": "остаток 60 на 2020.05", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "rows_matched": 6, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 9, + "question": "какой остаток по счету 60 на 2020 май", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "rows_matched": 6, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 10, + "question": "60 счет остаток на май 2020 покажи", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "rows_matched": 6, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 11, + "question": "какой остаток по счету 60 на 2020-05-31", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "rows_matched": 6, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 12, + "question": "остаток по 60 на май 2020, не за весь 2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "balance_month_period_rewrite", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "rows_matched": 6, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 13, + "question": "покаж по договору 1-��/2020 доки за 2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_contract", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_contract_year_rewrite", + "anchor_value_raw": "1-", + "anchor_value_resolved": "1-", + "rows_matched": 233, + "assistant_reply_head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 14, + "question": "какие документы по договору 1-��/2020 за все время", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_contract", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_contract_all_time_rewrite", + "anchor_value_raw": "1-", + "anchor_value_resolved": "1-", + "rows_matched": 120, + "assistant_reply_head": "Собран список документов по договору (live address lane)." + }, + { + "idx": 15, + "question": "есть ли долг по договору 1-��/2020 на 2020-07-31", + "reply_type": "partial_coverage", + "mcp_call_status": "materialized_but_filtered_out_by_recipe", + "limited_reason_category": "recipe_visibility_gap", + "detected_intent": "list_documents_by_contract", + "llm_decomposition_reason": "error:Failed to extract output_text from /responses payload.", + "fallback_rule_hit": "", + "anchor_value_raw": "1-��/2020 на 2020-07-31", + "anchor_value_resolved": "1-��/2020 на 2020-07-31", + "rows_matched": 0, + "assistant_reply_head": "Текущий live recipe не дает нужную видимость данных для этого сценария." + }, + { + "idx": 16, + "question": "какие хвосты по договору 1-��/2020 на дату 31.07.2020", + "reply_type": "partial_coverage", + "mcp_call_status": "materialized_but_not_anchor_matched", + "limited_reason_category": "missing_anchor", + "detected_intent": "list_documents_by_contract", + "llm_decomposition_reason": "error:Failed to extract output_text from /responses payload.", + "fallback_rule_hit": "", + "anchor_value_raw": "1-��/2020 на дату 31", + "anchor_value_resolved": "1-��/2020 на дату 31", + "rows_matched": 0, + "assistant_reply_head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 17, + "question": "какие платежи были по свк в 2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "bank_operations_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "bank_operations_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 18, + "question": "были ли поступления от свк за июль 2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "bank_operations_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "bank_operations_counterparty_month_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 2, + "assistant_reply_head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 19, + "question": "покажи списания с расчетного счета по свк за 2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "bank_operations_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "bank_operations_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 20, + "question": "свк за 2020 покаж все поступления", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "bank_operations_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "bank_operations_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 21, + "question": "покажи документы по свк за 2020", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_year_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 3, + "assistant_reply_head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 22, + "question": "а теперь только за май 2020", + "reply_type": "partial_coverage", + "mcp_call_status": "materialized_but_not_anchor_matched", + "limited_reason_category": "missing_anchor", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "not_address_like", + "fallback_rule_hit": "", + "anchor_value_raw": "свк", + "anchor_value_resolved": "свк", + "rows_matched": 0, + "assistant_reply_head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 23, + "question": "а по счету 60.01 на ту же дату", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "account_balance_snapshot", + "llm_decomposition_reason": "error:Failed to extract output_text from /responses payload.", + "fallback_rule_hit": "", + "anchor_value_raw": "60.01", + "anchor_value_resolved": "60.01", + "rows_matched": 5, + "assistant_reply_head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 24, + "question": "бля епт покажи доки по свк за 20й", + "reply_type": "factual", + "mcp_call_status": "matched_non_empty", + "limited_reason_category": "", + "detected_intent": "list_documents_by_counterparty", + "llm_decomposition_reason": "fallback_rule_applied_after_llm_error", + "fallback_rule_hit": "documents_counterparty_rewrite", + "anchor_value_raw": "свк", + "anchor_value_resolved": "Группа СВК", + "rows_matched": 26, + "assistant_reply_head": "По окну 2026-01-01..2026-04-01 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." + } +] diff --git a/docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.csv b/docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.csv new file mode 100644 index 0000000..6b62561 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.csv @@ -0,0 +1,25 @@ +"idx","question","reply_type","mcp","limited","intent","anchor_raw","anchor_resolved","fallback","head" +"1","свк доки за 20год покеж","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"2","свк 20 год - покажи доки плс","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"3","что по свк за 2020 год выведи все доки плиз что есть","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"4","какие документы по контрагенту свк за все время","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_all_time_rewrite","Собран список документов по контрагенту (live address lane)." +"5","доки по свк с 01.07.2020 по 31.07.2020","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_month_rewrite","Собран список документов по контрагенту (live address lane)." +"6","свк июль 2020 какие доки есть","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_month_rewrite","Собран список документов по контрагенту (live address lane)." +"7","покажи сальдо по 60.01 на 31.07.20","factual","matched_non_empty","","account_balance_snapshot","60.01","60.01","balance_account_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"8","остаток 60 на 2020.05","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"9","какой остаток по счету 60 на 2020 май","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"10","60 счет остаток на май 2020 покажи","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"11","какой остаток по счету 60 на 2020-05-31","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"12","остаток по 60 на май 2020, не за весь 2020","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"13","покаж по договору 1-��/2020 доки за 2020","factual","matched_non_empty","","list_documents_by_contract","1-","1-","documents_contract_year_rewrite","По окну 2020-01-01..2020-12-31 строк не найдено; показаны ближайшие доступные данные 2020-09-30..2022-09-30." +"14","какие документы по договору 1-��/2020 за все время","factual","matched_non_empty","","list_documents_by_contract","1-","1-","documents_contract_all_time_rewrite","Собран список документов по договору (live address lane)." +"15","есть ли долг по договору 1-��/2020 на 2020-07-31","factual","matched_non_empty","","list_documents_by_contract","1-��/2020","1-��/2020","","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"16","какие хвосты по договору 1-��/2020 на дату 31.07.2020","factual","matched_non_empty","","list_documents_by_contract","1-��/2020","1-��/2020","","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"17","какие платежи были по свк в 2020","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_year_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"18","были ли поступления от свк за июль 2020","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_month_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"19","покажи списания с расчетного счета по свк за 2020","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_year_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"20","свк за 2020 покаж все поступления","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_year_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"21","покажи документы по свк за 2020","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"22","а теперь только за май 2020","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","","По окну 2020-05-01..2020-05-31 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." +"23","а по счету 60.01 на ту же дату","factual","matched_non_empty","","account_balance_snapshot","60.01","60.01","","Адресный срез по счету собран (по движениям live MCP)." +"24","бля епт покажи доки по свк за 20й","partial_coverage","no_raw_rows","empty_match","list_documents_by_counterparty","свк","свк","documents_counterparty_rewrite","В live-данных по текущему фильтру записи не найдены." diff --git a/docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.json b/docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.json new file mode 100644 index 0000000..50857e5 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-18-01_live_batch_from_1txt_final/batch_results.json @@ -0,0 +1,290 @@ +[ + { + "idx": 1, + "question": "свк доки за 20год покеж", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 2, + "question": "свк 20 год - покажи доки плс", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 3, + "question": "что по свк за 2020 год выведи все доки плиз что есть", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 4, + "question": "какие документы по контрагенту свк за все время", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_all_time_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 5, + "question": "доки по свк с 01.07.2020 по 31.07.2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_month_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 6, + "question": "свк июль 2020 какие доки есть", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_month_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 7, + "question": "покажи сальдо по 60.01 на 31.07.20", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60.01", + "anchor_resolved": "60.01", + "fallback": "balance_account_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 8, + "question": "остаток 60 на 2020.05", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 9, + "question": "какой остаток по счету 60 на 2020 май", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 10, + "question": "60 счет остаток на май 2020 покажи", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 11, + "question": "какой остаток по счету 60 на 2020-05-31", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 12, + "question": "остаток по 60 на май 2020, не за весь 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 13, + "question": "покаж по договору 1-��/2020 доки за 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-", + "anchor_resolved": "1-", + "fallback": "documents_contract_year_rewrite", + "head": "По окну 2020-01-01..2020-12-31 строк не найдено; показаны ближайшие доступные данные 2020-09-30..2022-09-30." + }, + { + "idx": 14, + "question": "какие документы по договору 1-��/2020 за все время", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-", + "anchor_resolved": "1-", + "fallback": "documents_contract_all_time_rewrite", + "head": "Собран список документов по договору (live address lane)." + }, + { + "idx": 15, + "question": "есть ли долг по договору 1-��/2020 на 2020-07-31", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-��/2020", + "anchor_resolved": "1-��/2020", + "fallback": "", + "head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 16, + "question": "какие хвосты по договору 1-��/2020 на дату 31.07.2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-��/2020", + "anchor_resolved": "1-��/2020", + "fallback": "", + "head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 17, + "question": "какие платежи были по свк в 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_year_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 18, + "question": "были ли поступления от свк за июль 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_month_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 19, + "question": "покажи списания с расчетного счета по свк за 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_year_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 20, + "question": "свк за 2020 покаж все поступления", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_year_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 21, + "question": "покажи документы по свк за 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 22, + "question": "а теперь только за май 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "", + "head": "По окну 2020-05-01..2020-05-31 строк не найдено; показаны ближайшие доступные данные 2020-07-27..2021-11-10." + }, + { + "idx": 23, + "question": "а по счету 60.01 на ту же дату", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60.01", + "anchor_resolved": "60.01", + "fallback": "", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 24, + "question": "бля епт покажи доки по свк за 20й", + "reply_type": "partial_coverage", + "mcp": "no_raw_rows", + "limited": "empty_match", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "свк", + "fallback": "documents_counterparty_rewrite", + "head": "В live-данных по текущему фильтру записи не найдены." + } +] diff --git a/docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.csv b/docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.csv new file mode 100644 index 0000000..abe3241 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.csv @@ -0,0 +1,25 @@ +"idx","question","reply_type","mcp","limited","intent","anchor_raw","anchor_resolved","fallback","head" +"1","свк доки за 20год покеж","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"2","свк 20 год - покажи доки плс","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"3","что по свк за 2020 год выведи все доки плиз что есть","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"4","какие документы по контрагенту свк за все время","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_all_time_rewrite","Собран список документов по контрагенту (live address lane)." +"5","доки по свк с 01.07.2020 по 31.07.2020","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_month_rewrite","Собран список документов по контрагенту (live address lane)." +"6","свк июль 2020 какие доки есть","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_month_rewrite","Собран список документов по контрагенту (live address lane)." +"7","покажи сальдо по 60.01 на 31.07.20","factual","matched_non_empty","","account_balance_snapshot","60.01","60.01","balance_account_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"8","остаток 60 на 2020.05","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"9","какой остаток по счету 60 на 2020 май","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"10","60 счет остаток на май 2020 покажи","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"11","какой остаток по счету 60 на 2020-05-31","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"12","остаток по 60 на май 2020, не за весь 2020","factual","matched_non_empty","","account_balance_snapshot","60","60","balance_month_period_rewrite","Адресный срез по счету собран (по движениям live MCP)." +"13","покаж по договору 1-��/2020 доки за 2020","factual","matched_non_empty","","list_documents_by_contract","1-","1-","documents_contract_year_rewrite","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"14","какие документы по договору 1-��/2020 за все время","factual","matched_non_empty","","list_documents_by_contract","1-","1-","documents_contract_all_time_rewrite","Собран список документов по договору (live address lane)." +"15","есть ли долг по договору 1-��/2020 на 2020-07-31","factual","matched_non_empty","","list_documents_by_contract","1-��/2020","1-��/2020","","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"16","какие хвосты по договору 1-��/2020 на дату 31.07.2020","factual","matched_non_empty","","list_documents_by_contract","1-��/2020","1-��/2020","","Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." +"17","какие платежи были по свк в 2020","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_year_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"18","были ли поступления от свк за июль 2020","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_month_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"19","покажи списания с расчетного счета по свк за 2020","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_year_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"20","свк за 2020 покаж все поступления","factual","matched_non_empty","","bank_operations_by_counterparty","свк","Группа СВК","bank_operations_counterparty_year_rewrite","Собран список банковских операций по контрагенту (live address lane)." +"21","покажи документы по свк за 2020","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." +"22","а теперь только за май 2020","partial_coverage","materialized_but_not_anchor_matched","missing_anchor","list_documents_by_counterparty","свк","свк","","Для точного адресного поиска не хватает обязательного якоря." +"23","а по счету 60.01 на ту же дату","factual","matched_non_empty","","account_balance_snapshot","60.01","60.01","","Адресный срез по счету собран (по движениям live MCP)." +"24","бля епт покажи доки по свк за 20й","factual","matched_non_empty","","list_documents_by_counterparty","свк","Группа СВК","documents_counterparty_year_rewrite","Собран список документов по контрагенту (live address lane)." diff --git a/docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.json b/docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.json new file mode 100644 index 0000000..749b717 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-02_00-21-14_live_batch_from_1txt_final2/batch_results.json @@ -0,0 +1,290 @@ +[ + { + "idx": 1, + "question": "свк доки за 20год покеж", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 2, + "question": "свк 20 год - покажи доки плс", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 3, + "question": "что по свк за 2020 год выведи все доки плиз что есть", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 4, + "question": "какие документы по контрагенту свк за все время", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_all_time_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 5, + "question": "доки по свк с 01.07.2020 по 31.07.2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_month_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 6, + "question": "свк июль 2020 какие доки есть", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_month_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 7, + "question": "покажи сальдо по 60.01 на 31.07.20", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60.01", + "anchor_resolved": "60.01", + "fallback": "balance_account_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 8, + "question": "остаток 60 на 2020.05", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 9, + "question": "какой остаток по счету 60 на 2020 май", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 10, + "question": "60 счет остаток на май 2020 покажи", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 11, + "question": "какой остаток по счету 60 на 2020-05-31", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 12, + "question": "остаток по 60 на май 2020, не за весь 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60", + "anchor_resolved": "60", + "fallback": "balance_month_period_rewrite", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 13, + "question": "покаж по договору 1-��/2020 доки за 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-", + "anchor_resolved": "1-", + "fallback": "documents_contract_year_rewrite", + "head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 14, + "question": "какие документы по договору 1-��/2020 за все время", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-", + "anchor_resolved": "1-", + "fallback": "documents_contract_all_time_rewrite", + "head": "Собран список документов по договору (live address lane)." + }, + { + "idx": 15, + "question": "есть ли долг по договору 1-��/2020 на 2020-07-31", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-��/2020", + "anchor_resolved": "1-��/2020", + "fallback": "", + "head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 16, + "question": "какие хвосты по договору 1-��/2020 на дату 31.07.2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_contract", + "anchor_raw": "1-��/2020", + "anchor_resolved": "1-��/2020", + "fallback": "", + "head": "Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк." + }, + { + "idx": 17, + "question": "какие платежи были по свк в 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_year_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 18, + "question": "были ли поступления от свк за июль 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_month_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 19, + "question": "покажи списания с расчетного счета по свк за 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_year_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 20, + "question": "свк за 2020 покаж все поступления", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "bank_operations_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "bank_operations_counterparty_year_rewrite", + "head": "Собран список банковских операций по контрагенту (live address lane)." + }, + { + "idx": 21, + "question": "покажи документы по свк за 2020", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + }, + { + "idx": 22, + "question": "а теперь только за май 2020", + "reply_type": "partial_coverage", + "mcp": "materialized_but_not_anchor_matched", + "limited": "missing_anchor", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "свк", + "fallback": "", + "head": "Для точного адресного поиска не хватает обязательного якоря." + }, + { + "idx": 23, + "question": "а по счету 60.01 на ту же дату", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "account_balance_snapshot", + "anchor_raw": "60.01", + "anchor_resolved": "60.01", + "fallback": "", + "head": "Адресный срез по счету собран (по движениям live MCP)." + }, + { + "idx": 24, + "question": "бля епт покажи доки по свк за 20й", + "reply_type": "factual", + "mcp": "matched_non_empty", + "limited": "", + "intent": "list_documents_by_counterparty", + "anchor_raw": "свк", + "anchor_resolved": "Группа СВК", + "fallback": "documents_counterparty_year_rewrite", + "head": "Собран список документов по контрагенту (live address lane)." + } +] diff --git a/llm_normalizer/backend/src/services/addressFilterExtractor.ts b/llm_normalizer/backend/src/services/addressFilterExtractor.ts index bb794bb..9283b1d 100644 --- a/llm_normalizer/backend/src/services/addressFilterExtractor.ts +++ b/llm_normalizer/backend/src/services/addressFilterExtractor.ts @@ -312,6 +312,18 @@ function cleanupAnchorValue(value: string): string { return ""; } + // Remove trailing as-of qualifiers often captured by broad contract/counterparty regexes: + // " на 2020-07-31", " на дату 31.07.2020", " as of 2020-07-31". + const asOfTailPattern = + /\s+(?:на\s+(?:дат[ауеы]\s+)?\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|as\s+of\s+\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?)(?:\s+|$)[\s\S]*$/iu; + if (asOfTailPattern.test(normalized)) { + return normalized.replace(asOfTailPattern, "").trim(); + } + const asOfTruncatedTailPattern = /\s+на\s+дат[ауеы]\s+\d{1,2}(?:\s+|$)[\s\S]*$/iu; + if (asOfTruncatedTailPattern.test(normalized)) { + return normalized.replace(asOfTruncatedTailPattern, "").trim(); + } + // Remove trailing period qualifiers that can be swallowed by broad anchor regexes: // " с 2020-07-01 по 2020-07-31", " from 2020-07-01 to 2020-07-31" const periodTailPattern = @@ -386,6 +398,29 @@ function extractLooseByAnchorValue(text: string): string | undefined { return token; } +function extractContractTokenHeuristic(text: string): string | undefined { + const source = String(text ?? ""); + const explicit = source.match(/(?:№|#|n)\s*([a-zа-яё0-9][a-zа-яё0-9./_-]{1,})/iu); + if (explicit) { + const token = String(explicit[1] ?? "").trim(); + if (token.length >= 2) { + return token; + } + } + const slashLike = source.match(/\b(\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?)\b/iu); + if (!slashLike) { + return undefined; + } + const token = String(slashLike[1] ?? "").trim(); + if (!token) { + return undefined; + } + if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])$/.test(token)) { + return undefined; + } + return token; +} + function isLikelyCounterpartyToken(rawToken: string): boolean { const token = String(rawToken ?? "").trim(); const lowered = token.toLowerCase(); @@ -577,6 +612,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array text.includes(item)); @@ -198,6 +238,72 @@ function hasPartyAnchorMention(text: string): boolean { ); } +function hasContractAnchorMention(text: string): boolean { + return ( + text.includes("договор") || + text.includes("дог.") || + text.includes("contract") || + text.includes("dogovor") + ); +} + +function hasContractNumberLikeToken(text: string): boolean { + if (/(?:^|[\s([{])(?:№|#|n)\s*[a-zа-яё0-9][a-zа-яё0-9./_-]{1,}(?=$|[\s,.;:!?)\]}])/iu.test(text)) { + return true; + } + + const rawTokens = text + .split(/[\s,;:!?()[\]{}"«»]+/u) + .map((token) => token.replace(/^[^\p{L}\p{N}#№]+|[^\p{L}\p{N}./_-]+$/gu, "").trim()) + .filter((token) => token.length > 0); + + for (const rawToken of rawTokens) { + const token = String(rawToken ?? "").trim(); + if (!/^\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?$/u.test(token)) { + continue; + } + if (!token) { + continue; + } + const parts = token.split(/[./_-]+/u).map((part) => Number(part)); + if (!parts.every((part) => Number.isFinite(part))) { + return true; + } + + if (parts.length === 2) { + const [a, b] = parts; + const yearFirst = a >= 1900 && a <= 2099 && b >= 1 && b <= 12; + const yearSecond = b >= 1900 && b <= 2099 && a >= 1 && a <= 12; + if (yearFirst || yearSecond) { + continue; + } + return true; + } + + if (parts.length === 3) { + const [a, b, c] = parts; + const ymd = a >= 1900 && a <= 2099 && b >= 1 && b <= 12 && c >= 1 && c <= 31; + const dmy = c >= 1900 && c <= 2099 && a >= 1 && a <= 31 && b >= 1 && b <= 12; + if (ymd || dmy) { + continue; + } + return true; + } + + return true; + } + + return false; +} + +function hasContractAnchorSignal(text: string): boolean { + if (hasContractAnchorMention(text)) { + return true; + } + // Allow short forms like "15/24" for follow-up prompts if document/bank signal exists. + return hasContractNumberLikeToken(text) && hasDocsOrBankSignal(text); +} + function hasLooseByAnchorMention(text: string): boolean { const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); if (!match) { @@ -260,6 +366,20 @@ function hasDocsOrBankSignal(text: string): boolean { ); } +function hasBankOperationSignal(text: string): boolean { + return hasAny(text, BANK_OPERATION_CORE_HINTS) || hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) || hasAny(text, BANK_OPERATIONS_BY_CONTRACT_HINTS); +} + +function hasDocumentSignal(text: string): boolean { + return ( + text.includes("док") || + text.includes("доки") || + text.includes("документ") || + text.includes("docs") || + text.includes("documents") + ); +} + function hasHeuristicCounterpartyAnchor(text: string): boolean { if (!hasDocsOrBankSignal(text)) { return false; @@ -329,6 +449,28 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti }; } + if ( + hasContractAnchorSignal(text) && + hasBankOperationSignal(text) + ) { + return { + intent: "bank_operations_by_contract", + confidence: "medium", + reasons: ["bank_ops_by_contract_signal_detected"] + }; + } + + if ( + hasContractAnchorSignal(text) && + (hasAny(text, DOCUMENTS_BY_CONTRACT_HINTS) || hasDocumentSignal(text)) + ) { + return { + intent: "list_documents_by_contract", + confidence: "medium", + reasons: ["documents_by_contract_signal_detected"] + }; + } + if ( hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) && (hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text)) diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index f5f22c5..3e52eaf 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -34,6 +34,7 @@ interface AddressTryHandleOptions { const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const; const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1" as const; +const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000; const PARTY_ANCHOR_STOPWORDS = new Set([ "ооо", "ао", @@ -48,6 +49,35 @@ const PARTY_ANCHOR_STOPWORDS = new Set([ "по", "by" ]); +const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([ + "что", + "чо", + "были", + "был", + "была", + "было", + "ли", + "какие", + "какой", + "покажи", + "показать", + "выведи", + "списания", + "списание", + "поступления", + "поступление", + "доки", + "документ", + "документы", + "документов", + "банковские", + "операции", + "платежи", + "платеж", + "платежи", + "плс", + "please" +]); const ACCOUNT_ALIAS_MAP: Record = { "51": ["расчетный счет", "расчетные счета", "bank account"], "52": ["валютный счет", "валютные счета", "currency account"], @@ -152,6 +182,30 @@ function matchesAnchorText(searchable: string, anchor: string): boolean { }); } +function isLikelyLowQualityPartyAnchor(value: string | null | undefined): boolean { + const normalized = normalizeSearchText(String(value ?? "")); + if (!normalized) { + return true; + } + const tokens = normalized.split(" ").filter(Boolean); + if (tokens.length === 0) { + return true; + } + const meaningfulTokens = tokens.filter((token) => { + if (token.length < 2) { + return false; + } + if (PARTY_ANCHOR_STOPWORDS.has(token) || LOW_QUALITY_PARTY_ANCHOR_TOKENS.has(token)) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(token)) { + return false; + } + return true; + }); + return meaningfulTokens.length === 0; +} + function normalizeAccountToken(value: string): string { const source = String(value ?? "").trim().replace(",", "."); const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/); @@ -370,13 +424,13 @@ function applyAddressFilters(rows: NormalizedAddressRow[], filters: AddressFilte } function applyIntentSpecificFilter(intent: AddressIntent, rows: NormalizedAddressRow[]): NormalizedAddressRow[] { - if (intent === "bank_operations_by_counterparty") { + if (intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_contract") { const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i; return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase())); } - if (intent === "list_documents_by_counterparty") { + if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") { const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i; return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0); @@ -402,7 +456,21 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte if (!hasExplicitPeriodWindow(filters)) { return false; } - return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty"; + return ( + intent === "list_documents_by_counterparty" || + intent === "bank_operations_by_counterparty" || + intent === "list_documents_by_contract" || + intent === "bank_operations_by_contract" + ); +} + +function isDocumentOrBankIntent(intent: AddressIntent): boolean { + return ( + intent === "list_documents_by_counterparty" || + intent === "bank_operations_by_counterparty" || + intent === "list_documents_by_contract" || + intent === "bank_operations_by_contract" + ); } function toIsoDatePrefix(value: string | null): string | null { @@ -959,6 +1027,124 @@ export class AddressQueryService { }); } + if ( + filteredRows.length === 0 && + isDocumentOrBankIntent(intent.intent) && + (stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe") + ) { + const currentLimit = + typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit) + ? Math.max(1, Math.trunc(filters.extracted_filters.limit)) + : plan.limit; + if (currentLimit < ADDRESS_ANCHOR_RECOVERY_LIMIT) { + const expandedLimitFilters: AddressFilterSet = { + ...filters.extracted_filters, + limit: ADDRESS_ANCHOR_RECOVERY_LIMIT + }; + const expandedSelection = selectAddressRecipe(intent.intent, expandedLimitFilters); + if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) { + const expandedPlan = buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters); + if (expandedPlan.limit > currentLimit) { + const expandedMcp = await executeAddressMcpQuery({ + query: expandedPlan.query, + limit: expandedPlan.limit + }); + if (!expandedMcp.error) { + const expandedRawRows = toNormalizedRows(expandedMcp.raw_rows); + const expandedScopedRows = applyAccountScopeFilter(expandedRawRows, expandedPlan.account_scope); + const expandedAccountScopeFallbackApplied = + expandedPlan.account_scope_mode === "preferred" && + expandedPlan.account_scope.length > 0 && + expandedRawRows.length > 0 && + expandedScopedRows.length === 0; + const expandedNormalizedRows = expandedAccountScopeFallbackApplied ? expandedRawRows : expandedScopedRows; + let expandedAnchor = resolvePrimaryAnchor(intent.intent, expandedLimitFilters); + expandedAnchor = refineAnchorFromRows(expandedAnchor, expandedNormalizedRows); + const expandedFiltersForMatching: AddressFilterSet = + expandedAnchor.anchor_type === "counterparty" && expandedAnchor.anchor_value_resolved + ? { ...expandedLimitFilters, counterparty: expandedAnchor.anchor_value_resolved } + : expandedAnchor.anchor_type === "contract" && expandedAnchor.anchor_value_resolved + ? { ...expandedLimitFilters, contract: expandedAnchor.anchor_value_resolved } + : expandedLimitFilters; + const expandedAccountScopeAudit = buildAccountScopeAudit({ + intent: intent.intent, + filters: expandedFiltersForMatching, + accountScope: expandedPlan.account_scope, + rowsBeforeScope: expandedRawRows.length, + rowsAfterScope: expandedNormalizedRows.length + }); + const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching); + const expandedRowsByAnchor = expandedAnchorFilter.rows; + const expandedFilteredRows = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor); + if (expandedFilteredRows.length > 0) { + const expandedRowDiagnostics = deriveRowStageDiagnostics( + expandedMcp.raw_rows, + expandedNormalizedRows.length, + expandedNormalizedRows.length + ); + const expandedStageStatus = deriveMcpStageStatus({ + rawRowsReceived: expandedMcp.raw_rows.length, + rowsMaterialized: expandedNormalizedRows.length, + rowsAnchorMatched: expandedRowsByAnchor.length, + rowsMatched: expandedFilteredRows.length + }); + const expandedFactual = composeFactualReply(intent.intent, expandedFilteredRows); + const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`; + const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"]; + const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"]; + return { + handled: true, + reply_text: `${expandedPrefix}\n${expandedFactual.text}`, + reply_type: inferReplyType(expandedFactual.responseType), + response_type: expandedFactual.responseType, + debug: { + detected_mode: mode.mode, + detected_mode_confidence: mode.confidence, + query_shape: shape.shape, + query_shape_confidence: shape.confidence, + detected_intent: intent.intent, + detected_intent_confidence: intent.confidence, + extracted_filters: filters.extracted_filters, + missing_required_filters: [], + selected_recipe: expandedSelection.selected_recipe.recipe_id, + mcp_call_status_legacy: toLegacyMcpStatus(expandedStageStatus), + account_scope_mode: expandedPlan.account_scope_mode, + account_scope_fallback_applied: expandedAccountScopeFallbackApplied, + anchor_type: expandedAnchor.anchor_type, + anchor_value_raw: expandedAnchor.anchor_value_raw, + anchor_value_resolved: expandedAnchor.anchor_value_resolved, + resolver_confidence: expandedAnchor.resolver_confidence, + ambiguity_count: expandedAnchor.ambiguity_count, + match_failure_stage: "none", + match_failure_reason: null, + mcp_call_status: expandedStageStatus, + rows_fetched: expandedMcp.fetched_rows, + raw_rows_received: expandedMcp.raw_rows.length, + rows_after_account_scope: expandedNormalizedRows.length, + rows_after_recipe_filter: expandedRowsByAnchor.length, + rows_materialized: expandedNormalizedRows.length, + rows_matched: expandedFilteredRows.length, + raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample, + materialization_drop_reason: expandedRowDiagnostics.materializationDropReason, + account_token_raw: expandedAccountScopeAudit.accountTokenRaw, + account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason, + runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", + limited_reason_category: null, + response_type: expandedFactual.responseType, + limitations: expandedLimitations, + reasons: expandedReasons + } + }; + } + } + } + } + } + } + if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { const autoBroadenedFilters: AddressFilterSet = { ...filters.extracted_filters }; delete autoBroadenedFilters.period_from; @@ -1071,32 +1257,52 @@ export class AddressQueryService { const isVisibilityGapCandidate = hadBaseRows && hadAnchorMatchedRows && - (intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty"); + (intent.intent === "list_documents_by_counterparty" || + intent.intent === "bank_operations_by_counterparty" || + intent.intent === "list_documents_by_contract" || + intent.intent === "bank_operations_by_contract"); const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched"; const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe"; + const isFollowupAnchorCarryover = + Array.isArray(filters.warnings) && + (filters.warnings.includes("counterparty_from_followup_context") || + filters.warnings.includes("contract_from_followup_context")); + const isLowQualityPartyAnchor = + (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") && + isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw); + const anchorMismatchCategory: AddressLimitedReasonCategory = + isFollowupAnchorCarryover || !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor"; const category: AddressLimitedReasonCategory = isAnchorMismatch - ? "missing_anchor" + ? anchorMismatchCategory : isRecipeFilteredOut ? "recipe_visibility_gap" : isVisibilityGapCandidate ? "recipe_visibility_gap" : "empty_match"; const reasonText = isAnchorMismatch - ? "якорь контрагента/договора не найден в материализованных live-строках" + ? anchorMismatchCategory === "missing_anchor" + ? "якорь контрагента/договора не найден в материализованных live-строках" + : "по указанному якорю и фильтрам в live-выборке нет строк" : isRecipeFilteredOut ? "строки по якорю найдены, но отфильтрованы intent-specific recipe" : isVisibilityGapCandidate ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" : "по выбранным фильтрам в live-выборке нет строк"; const nextStep = isAnchorMismatch - ? "уточните контрагента точным именем или добавьте ИНН/договор" + ? anchorMismatchCategory === "missing_anchor" + ? "уточните контрагента точным именем или добавьте ИНН/договор" + : "уточните период или снимите часть фильтров" : isRecipeFilteredOut ? "сузьте период, уточните контрагента или документный тип" : isVisibilityGapCandidate ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" : "уточните период, контрагента, договор или снимите часть фильтров"; const limitations = isAnchorMismatch - ? ["anchor_not_matched_after_materialization"] + ? [ + anchorMismatchCategory === "missing_anchor" + ? "anchor_not_matched_after_materialization" + : "no_rows_for_anchor_after_materialization" + ] : isRecipeFilteredOut ? ["rows_filtered_out_by_recipe_after_anchor_match"] : [ diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index 8454ed8..212b5af 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -108,6 +108,26 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [ account_scope_mode: "preferred", query_template: "bank_docs" }, + { + recipe_id: "address_documents_by_contract_v1", + intent: "list_documents_by_contract", + purpose: "Collect contract-related document lines from movements", + required_filters: ["contract"], + optional_filters: ["period_from", "period_to", "as_of_date", "organization", "counterparty", "limit", "sort"], + default_limit: 100, + account_scope: ["60", "62", "76", "51", "52"], + account_scope_mode: "preferred" + }, + { + recipe_id: "address_bank_operations_by_contract_v1", + intent: "bank_operations_by_contract", + purpose: "Collect bank operation candidates by contract from movement lines", + required_filters: ["contract"], + optional_filters: ["period_from", "period_to", "as_of_date", "organization", "counterparty", "limit", "sort"], + default_limit: 100, + account_scope: ["51", "52"], + account_scope_mode: "preferred" + }, { recipe_id: "address_documents_forming_balance_v1", intent: "documents_forming_balance", @@ -241,8 +261,10 @@ function buildMovementAccountCondition(filters: AddressFilterSet): string | null } function shouldBoostLimitForAllTimeCounterparty(filters: AddressFilterSet): boolean { - const hasCounterparty = typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0; - if (!hasCounterparty) { + const hasAnchor = + (typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0) || + (typeof filters.contract === "string" && filters.contract.trim().length > 0); + if (!hasAnchor) { return false; } const hasPeriod = Boolean( @@ -254,7 +276,12 @@ function shouldBoostLimitForAllTimeCounterparty(filters: AddressFilterSet): bool } function maxLimitForIntent(intent: AddressIntent): number { - if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { + if ( + intent === "list_documents_by_counterparty" || + intent === "bank_operations_by_counterparty" || + intent === "list_documents_by_contract" || + intent === "bank_operations_by_contract" + ) { return ADDRESS_MAX_LIMIT_EXTENDED; } return ADDRESS_MAX_LIMIT_DEFAULT; @@ -292,7 +319,10 @@ export function buildAddressRecipePlan( ? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit))) : recipe.default_limit; const boostedLimit = - (recipe.intent === "list_documents_by_counterparty" || recipe.intent === "bank_operations_by_counterparty") && + (recipe.intent === "list_documents_by_counterparty" || + recipe.intent === "bank_operations_by_counterparty" || + recipe.intent === "list_documents_by_contract" || + recipe.intent === "bank_operations_by_contract") && shouldBoostLimitForAllTimeCounterparty(filters) ? Math.max(baseLimit, maxLimit) : (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts index fb3962e..4a80591 100644 --- a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -116,6 +116,18 @@ export function composeFactualReply( }; } + if (intent === "list_documents_by_contract") { + const lines = [ + "Собран список документов по договору (live address lane).", + `Строк отобрано: ${rows.length}.`, + ...formatTopRows(rows, rows.length) + ]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + if (intent === "bank_operations_by_counterparty") { const lines = [ "Собран список банковских операций по контрагенту (live address lane).", @@ -128,6 +140,18 @@ export function composeFactualReply( }; } + if (intent === "bank_operations_by_contract") { + const lines = [ + "Собран список банковских операций по договору (live address lane).", + `Строк отобрано: ${rows.length}.`, + ...formatTopRows(rows, rows.length) + ]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + const title = intent === "list_payables_counterparties" ? "Срез обязательств (payables) собран по движениям с account scope 60/76." diff --git a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts index 5b9e6a8..13586e9 100644 --- a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts @@ -119,6 +119,16 @@ function mergeFollowupFilters( } } + if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") { + if (!toNonEmptyString(merged.contract)) { + const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null); + if (inheritedContract) { + merged.contract = inheritedContract; + reasons.push("contract_from_followup_context"); + } + } + } + if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { if (!toNonEmptyString(merged.account)) { const inheritedAccount = previousAccount ?? (followupContext.previous_anchor_type === "account" ? previousAnchorValue : null); @@ -184,7 +194,9 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi account_balance_snapshot: ["account", "as_of_date"], documents_forming_balance: ["account", "as_of_date"], list_documents_by_counterparty: ["counterparty"], - bank_operations_by_counterparty: ["counterparty"] + bank_operations_by_counterparty: ["counterparty"], + list_documents_by_contract: ["contract"], + bank_operations_by_contract: ["contract"] }; const required = requiredByIntent[intent] ?? []; return required.filter((key) => { diff --git a/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts b/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts index 7e0cee4..7fa9648 100644 --- a/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts @@ -134,6 +134,18 @@ export function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilt } } + if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") { + if (contract) { + return { + anchor_type: "contract", + anchor_value_raw: contract, + anchor_value_resolved: contract, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + } + if (counterparty) { return { anchor_type: "counterparty", @@ -208,4 +220,3 @@ export function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: Resolv ambiguity_count: candidates.length - 1 }; } - diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index cc7616a..aea9bbf 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -1828,9 +1828,24 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([ "show", "list", "выведи", + "что", + "чо", "которые", "какие", "какой", + "были", + "был", + "была", + "было", + "ли", + "списания", + "списание", + "поступления", + "поступление", + "расчетного", + "расчётного", + "счета", + "счёта", "есть", "est", "kakie", @@ -1910,6 +1925,8 @@ const ADDRESS_MONTH_ALIAS_MAP = { dec: "12" }; const ADDRESS_DOCS_SIGNAL_PATTERN = /(?:док|доки|документ|документы|документов|docs?|documents?|bank|выписк|плат[её]ж|оплат|поступлен|списан|операц)/i; +const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|плат[её]ж|оплат|поступлен|списан|операц|расчетн)/i; +const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|\bcontract\b)/iu; const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i; const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu; function normalizeAddressMonthAliasToken(token) { @@ -1975,14 +1992,21 @@ function extractAddressFallbackYear(text) { return fullYearMatch[1]; } const shortYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/iu); - if (!shortYearMatch) { + if (shortYearMatch) { + const shortYear = Number(shortYearMatch[1]); + if (Number.isFinite(shortYear) && shortYear >= 0 && shortYear <= 99) { + return String(2000 + shortYear); + } + } + const shortOrdinalYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:[-\s]?(?:й|ый|ой|th))(?=$|[^a-zа-яё0-9])/iu); + if (!shortOrdinalYearMatch) { return null; } - const shortYear = Number(shortYearMatch[1]); - if (!Number.isFinite(shortYear) || shortYear < 0 || shortYear > 99) { + const shortOrdinalYear = Number(shortOrdinalYearMatch[1]); + if (!Number.isFinite(shortOrdinalYear) || shortOrdinalYear < 0 || shortOrdinalYear > 99) { return null; } - return String(2000 + shortYear); + return String(2000 + shortOrdinalYear); } function extractAddressFallbackMonthYear(text) { const source = String(text ?? ""); @@ -1998,14 +2022,14 @@ function extractAddressFallbackMonthYear(text) { const year = numericMonthYear[2]; return `${year}-${month}`; } - const namedMonthYear = source.match(/\b([a-zа-яё]+)\s+(20\d{2})\b/iu); + const namedMonthYear = source.match(/(?:^|[^a-zа-яё0-9])([a-zа-яё]+)\s+(20\d{2})(?=$|[^a-zа-яё0-9])/iu); if (namedMonthYear) { const month = normalizeAddressMonthAliasToken(namedMonthYear[1]); if (month) { return `${namedMonthYear[2]}-${month}`; } } - const yearNamedMonth = source.match(/\b(20\d{2})\s+([a-zа-яё]+)\b/iu); + const yearNamedMonth = source.match(/(?:^|[^a-zа-яё0-9])(20\d{2})\s+([a-zа-яё]+)(?=$|[^a-zа-яё0-9])/iu); if (yearNamedMonth) { const month = normalizeAddressMonthAliasToken(yearNamedMonth[2]); if (month) { @@ -2014,7 +2038,42 @@ function extractAddressFallbackMonthYear(text) { } return null; } +function extractAddressFallbackAccountToken(text) { + const source = String(text ?? ""); + const explicitMatch = source.match(/(?:сч[её]т(?:а|у|ом|е)?|account)\D{0,12}(\d{2}(?:[.,]\d{1,2})?)/iu); + if (explicitMatch && explicitMatch[1]) { + return String(explicitMatch[1]).replace(",", "."); + } + const tokenPattern = /\b(\d{2}(?:[.,]\d{1,2})?)\b/giu; + let match = tokenPattern.exec(source); + while (match) { + const raw = String(match[1] ?? ""); + const start = match.index; + const end = start + raw.length; + const prev = start > 0 ? source[start - 1] : " "; + const next = end < source.length ? source[end] : " "; + if (!/[./-]/.test(prev) && !/[./-]/.test(next) && !/\d/.test(prev) && !/\d/.test(next)) { + return raw.replace(",", "."); + } + match = tokenPattern.exec(source); + } + return null; +} function pickAddressFallbackCounterpartyToken(text) { + const source = String(text ?? ""); + const byAnchor = source.match(/(?:^|[\s,.;:!?()\-])(?:по|от)\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=$|[\s,.;:!?()\-])/iu); + if (byAnchor && byAnchor[1]) { + const byToken = String(byAnchor[1]).trim(); + const normalizedByToken = byToken.toLowerCase(); + if (byToken && + !ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(normalizedByToken) && + !/^\d{2}(?:\.\d{1,2})?$/.test(normalizedByToken) && + !/^(?:19|20)\d{2}$/.test(normalizedByToken) && + !/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalizedByToken) && + !/^(?:договор|договора|договору|договором|договоре|contract)$/.test(normalizedByToken)) { + return byToken; + } + } const candidates = extractAddressAnchorTokens(text); for (const token of candidates) { const normalized = String(token ?? "").toLowerCase(); @@ -2027,10 +2086,44 @@ function pickAddressFallbackCounterpartyToken(text) { if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalized)) { continue; } + if (/^(?:договор|договора|договору|договором|договоре|contract)$/.test(normalized)) { + continue; + } return token; } return null; } +function extractAddressFallbackContractToken(text) { + const source = String(text ?? ""); + const patterns = [ + /(?:договор(?:а|у|ом|е)?|\bcontract\b)\s*(?:№|#|n|no\.?)?\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})/iu, + /(?:№|#|n|no\.?)\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})\s*(?:договор(?:а|у|ом|е)?|\bcontract\b)/iu + ]; + for (const pattern of patterns) { + const match = pattern.exec(source); + if (!match || !match[1]) { + continue; + } + const candidate = String(match[1]).replace(/^[^a-zа-я0-9]+|[^a-zа-я0-9/_-]+$/giu, ""); + if (!candidate || candidate.length < 2) { + continue; + } + if (/^(?:19|20)\d{2}$/.test(candidate)) { + continue; + } + if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])(?:[./-](?:0?[1-9]|[12]\d|3[01]))?$/.test(candidate)) { + continue; + } + return candidate; + } + if (ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source)) { + const generic = source.match(/\b([a-zа-я0-9]{1,10}[/-][a-zа-я0-9]{1,10}(?:[/-][a-zа-я0-9]{1,10})?)\b/iu); + if (generic && generic[1]) { + return generic[1]; + } + } + return null; +} function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) { const sourceRaw = compactWhitespace(repairAddressMojibake(String(userMessage ?? ""))); const source = compactWhitespace(String(sanitizedUserMessage ?? sourceRaw).toLowerCase()); @@ -2040,9 +2133,10 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) const monthYear = extractAddressFallbackMonthYear(source); const year = extractAddressFallbackYear(source); const allTime = ADDRESS_ALL_TIME_PATTERN.test(source); - const accountMatch = source.match(/\b(\d{2}(?:[.,]\d{1,2})?)\b/); - const account = accountMatch ? String(accountMatch[1]).replace(",", ".") : null; + const account = extractAddressFallbackAccountToken(source); const docsSignal = ADDRESS_DOCS_SIGNAL_PATTERN.test(source); + const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source); + const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source); const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source); if (balanceSignal && account) { let periodClause = ""; @@ -2064,28 +2158,59 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) } } if (docsSignal) { - const counterparty = pickAddressFallbackCounterpartyToken(source); - if (counterparty) { - let periodClause = ""; - let rule = "documents_counterparty_rewrite"; - if (allTime) { - periodClause = " за все время"; - rule = "documents_counterparty_all_time_rewrite"; + if (contractSignal) { + const contract = extractAddressFallbackContractToken(sourceRaw || source); + if (contract) { + let periodClause = ""; + let rule = bankSignal ? "bank_operations_contract_rewrite" : "documents_contract_rewrite"; + if (allTime) { + periodClause = " за все время"; + rule = bankSignal ? "bank_operations_contract_all_time_rewrite" : "documents_contract_all_time_rewrite"; + } + else if (monthYear) { + periodClause = ` за ${monthYear}`; + rule = bankSignal ? "bank_operations_contract_month_rewrite" : "documents_contract_month_rewrite"; + } + else if (year) { + periodClause = ` за ${year} год`; + rule = bankSignal ? "bank_operations_contract_year_rewrite" : "documents_contract_year_rewrite"; + } + const subject = bankSignal ? "банковские операции" : "документы"; + const candidate = compactWhitespace(`${subject} по договору ${contract}${periodClause}`); + if (candidate && candidate !== sourceRaw.toLowerCase()) { + return { + candidate, + rule + }; + } } - else if (monthYear) { - periodClause = ` за ${monthYear}`; - rule = "documents_counterparty_month_rewrite"; - } - else if (year) { - periodClause = ` за ${year} год`; - rule = "documents_counterparty_year_rewrite"; - } - const candidate = compactWhitespace(`документы по контрагенту ${counterparty}${periodClause}`); - if (candidate && candidate !== sourceRaw.toLowerCase()) { - return { - candidate, - rule - }; + } + else { + const counterparty = pickAddressFallbackCounterpartyToken(source); + if (counterparty) { + let periodClause = ""; + const subject = bankSignal ? "банковские операции" : "документы"; + const rulePrefix = bankSignal ? "bank_operations_counterparty" : "documents_counterparty"; + let rule = `${rulePrefix}_rewrite`; + if (allTime) { + periodClause = " за все время"; + rule = `${rulePrefix}_all_time_rewrite`; + } + else if (monthYear) { + periodClause = ` за ${monthYear}`; + rule = `${rulePrefix}_month_rewrite`; + } + else if (year) { + periodClause = ` за ${year} год`; + rule = `${rulePrefix}_year_rewrite`; + } + const candidate = compactWhitespace(`${subject} по контрагенту ${counterparty}${periodClause}`); + if (candidate && candidate !== sourceRaw.toLowerCase()) { + return { + candidate, + rule + }; + } } } } @@ -2620,12 +2745,13 @@ export class AssistantService { }; this.sessions.appendItem(sessionId, userItem); const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { + const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? ""); const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta); const assistantItem = { message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, session_id: sessionId, role: "assistant", - text: addressLane.reply_text, + text: safeAddressReply, reply_type: addressLane.reply_type, created_at: new Date().toISOString(), trace_id: debug.trace_id, diff --git a/llm_normalizer/backend/src/types/addressQuery.ts b/llm_normalizer/backend/src/types/addressQuery.ts index c24ad00..41abd5d 100644 --- a/llm_normalizer/backend/src/types/addressQuery.ts +++ b/llm_normalizer/backend/src/types/addressQuery.ts @@ -8,6 +8,8 @@ export type AddressIntent = | "open_items_by_counterparty_or_contract" | "list_documents_by_counterparty" | "bank_operations_by_counterparty" + | "list_documents_by_contract" + | "bank_operations_by_contract" | "documents_forming_balance" | "unknown"; diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index d426f93..f9b2c48 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -6,6 +6,7 @@ import { extractAddressFilters } from "../src/services/addressFilterExtractor"; import { AddressQueryService } from "../src/services/addressQueryService"; import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog"; import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage"; +import { composeFactualReply } from "../src/services/address_runtime/composeStage"; describe("address query shape classifier", () => { it("classifies explain question as deep-shape", () => { @@ -35,6 +36,36 @@ describe("address query shape classifier", () => { }); }); +describe("address compose stage utf8 headers", () => { + it("renders readable russian header for contract document list", () => { + const reply = composeFactualReply("list_documents_by_contract", [ + { + period: "2020-10-15T13:34:53Z", + registrator: "Списание с расчетного счета 00000000246", + account_dt: "66.02", + account_kt: "51", + amount: 30819.47, + analytics: [] + } + ]); + expect(reply.text).toContain("Собран список документов по договору (live address lane)."); + }); + + it("renders readable russian header for contract bank operations", () => { + const reply = composeFactualReply("bank_operations_by_contract", [ + { + period: "2020-10-15T13:34:53Z", + registrator: "Списание с расчетного счета 00000000246", + account_dt: "66.02", + account_kt: "51", + amount: 30819.47, + analytics: [] + } + ]); + expect(reply.text).toContain("Собран список банковских операций по договору (live address lane)."); + }); +}); + describe("address intent resolver expansion (M2.3a)", () => { it("resolves documents by counterparty intent", () => { const result = resolveAddressIntent("show documents by counterparty Alfa from 2020-07-01 to 2020-07-31"); @@ -66,6 +97,23 @@ describe("address intent resolver expansion (M2.3a)", () => { expect(result.intent).toBe("bank_operations_by_counterparty"); }); + it("resolves documents by contract intent", () => { + const result = resolveAddressIntent("Покажи документы по договору 15/24 за 2020"); + expect(result.intent).toBe("list_documents_by_contract"); + }); + + it("resolves bank operations by contract intent", () => { + const result = resolveAddressIntent("Покажи банковские операции по договору 15/24"); + expect(result.intent).toBe("bank_operations_by_contract"); + }); + + it("resolves bank operations by contract for normalized phrase with linked contract wording", () => { + const result = resolveAddressIntent( + "Показать банковские операции (счета 51, 60, 62) связанные с договором 15/24." + ); + expect(result.intent).toBe("bank_operations_by_contract"); + }); + it("keeps bank_operations_by_counterparty even when account hints are present", () => { const result = resolveAddressIntent("Показать банковские операции (счета 51, 62) для контрагента СВК за 2020 год"); expect(result.intent).toBe("bank_operations_by_counterparty"); @@ -287,6 +335,49 @@ describe("address filter extraction for balance drilldown", () => { expect(result.warnings).toContain("period_derived_from_year_range_phrase"); }); + it("extracts contract and year period for contract document list", () => { + const result = extractAddressFilters( + "Покажи документы по договору 15/24 за 2020 год", + "list_documents_by_contract" + ); + expect(result.extracted_filters.contract).toBe("15/24"); + expect(result.extracted_filters.period_from).toBe("2020-01-01"); + expect(result.extracted_filters.period_to).toBe("2020-12-31"); + expect(result.warnings).toContain("period_derived_from_year_phrase"); + }); + + it("cuts trailing as-of date from contract anchor", () => { + const result = extractAddressFilters( + "Покажи документы по договору 1-ПМ/2020 на дату 31.07.2020", + "list_documents_by_contract" + ); + expect(result.extracted_filters.contract).toBe("1-ПМ/2020"); + expect(result.extracted_filters.period_from).toBe("2020-07-01"); + expect(result.extracted_filters.period_to).toBe("2020-07-31"); + }); + + it("does not force 90-day default window for by-contract query without explicit period", () => { + const result = extractAddressFilters( + "Покажи документы по договору 15/24", + "list_documents_by_contract" + ); + expect(result.extracted_filters.contract).toBe("15/24"); + expect(result.extracted_filters.period_from).toBeUndefined(); + expect(result.extracted_filters.period_to).toBeUndefined(); + expect(result.warnings).not.toContain("period_defaulted_last_90_days"); + }); + + it("extracts heuristic contract token for noisy contract phrase", () => { + const result = extractAddressFilters( + "доки 15/24 за 2020", + "list_documents_by_contract" + ); + expect(result.extracted_filters.contract).toBe("15/24"); + expect(result.warnings).toContain("contract_anchor_derived_from_heuristic_token"); + expect(result.extracted_filters.period_from).toBe("2020-01-01"); + expect(result.extracted_filters.period_to).toBe("2020-12-31"); + }); + it("extracts multiline year range period from phrase", () => { const result = extractAddressFilters( "Какие документы по СВК за 2000 - 2025\n год?", @@ -334,13 +425,24 @@ describe("address query limited taxonomy and stage diagnostics", () => { expect(result?.debug.mcp_call_status).toBe("skipped"); }); - it("returns unsupported for not-implemented contract document list intent", async () => { + it("routes contract document list intent into address recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("show documents by contract 15/24"); expect(result?.handled).toBe(true); - expect(result?.response_type).toBe("LIMITED_WITH_REASON"); - expect(result?.debug.limited_reason_category).toBe("unsupported"); - expect(result?.debug.mcp_call_status).toBe("skipped"); + expect(result?.debug.detected_intent).toBe("list_documents_by_contract"); + expect(result?.debug.selected_recipe).toBe("address_documents_by_contract_v1"); + expect(result?.debug.limited_reason_category).not.toBe("unsupported"); + expect(result?.debug.mcp_call_status).not.toBe("skipped"); + }); + + it("routes bank operations by contract intent into address recipe", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("Покажи банковские операции по договору 15/24"); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_intent).toBe("bank_operations_by_contract"); + expect(result?.debug.selected_recipe).toBe("address_bank_operations_by_contract_v1"); + expect(result?.debug.limited_reason_category).not.toBe("unsupported"); + expect(result?.debug.mcp_call_status).not.toBe("skipped"); }); it("includes resolver and row-stage diagnostics", async () => { @@ -461,6 +563,24 @@ describe("address decompose stage follow-up carryover", () => { expect(result?.baseReasons).toContain("as_of_date_from_followup_context"); expect(result?.baseReasons).toContain("address_followup_context_applied"); }); + + it("does not downgrade inherited follow-up anchor to missing_anchor when period has no rows", async () => { + const service = new AddressQueryService(); + const seed = await service.tryHandle("покажи документы по свк за 2020"); + expect(seed?.handled).toBe(true); + const followup = await service.tryHandle("а теперь только за май 2020", { + followupContext: { + previous_intent: (seed?.debug.detected_intent as any) ?? "list_documents_by_counterparty", + previous_filters: seed?.debug.extracted_filters, + previous_anchor_type: (seed?.debug.anchor_type as any) ?? "counterparty", + previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null + } + }); + expect(followup?.handled).toBe(true); + if (followup?.reply_type === "partial_coverage") { + expect(followup?.debug.limited_reason_category).not.toBe("missing_anchor"); + } + }); }); describe("address recipe catalog counterparty filtering", () => { diff --git a/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts b/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts index a258ad1..f247467 100644 --- a/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts @@ -215,4 +215,391 @@ describe("assistant address llm pre-decompose candidate preference", () => { expect(response.debug?.sanitized_user_message).toContain("свк"); expect(response.debug?.tool_gate_decision).toBe("run_address_lane"); }); + + it("keeps contract anchor in deterministic fallback when llm output is unusable", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-contract-docs", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "Покажи документы по договору 15/24 за 2020", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-contract-docs-${Date.now()}`, + user_message: "Покажи документы по договору 15/24 за 2020", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).toBe("документы по договору 15/24 за 2020 год"); + expect(response.debug?.llm_decomposition_applied).toBe(true); + expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm"); + expect(response.debug?.fallback_rule_hit).toBe("documents_contract_year_rewrite"); + }); + + it("keeps bank-by-contract intent in deterministic fallback when llm output is unusable", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-contract-bank", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "Покажи банковские операции по договору 15/24", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-contract-bank-${Date.now()}`, + user_message: "Покажи банковские операции по договору 15/24", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).toBe("банковские операции по договору 15/24"); + expect(response.debug?.llm_decomposition_applied).toBe(true); + expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm"); + expect(response.debug?.fallback_rule_hit).toBe("bank_operations_contract_rewrite"); + }); + + it("keeps month scope for balance fallback in 'year month' phrasing", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-balance-year-month", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "Какой остаток по счету 60 на 2020 май", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-balance-year-month-${Date.now()}`, + user_message: "Какой остаток по счету 60 на 2020 май", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).toBe("остаток по счету 60 на 2020-05"); + expect(response.debug?.llm_decomposition_applied).toBe(true); + expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm"); + expect(response.debug?.fallback_rule_hit).toBe("balance_month_period_rewrite"); + }); + + it("does not pick service words as counterparty anchor in noisy docs query", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-noisy-counterparty", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "что по свк за 2020 год выведи все доки плиз что есть", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-noisy-counterparty-${Date.now()}`, + user_message: "что по свк за 2020 год выведи все доки плиз что есть", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).toBe("документы по контрагенту свк за 2020 год"); + expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm"); + expect(response.debug?.fallback_rule_hit).toBe("documents_counterparty_year_rewrite"); + }); + + it("rewrites payment-style counterparty phrasing to bank operations", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-bank-counterparty", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "какие платежи были по свк в 2020", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-bank-counterparty-${Date.now()}`, + user_message: "какие платежи были по свк в 2020", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).toBe("банковские операции по контрагенту свк за 2020 год"); + expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm"); + expect(response.debug?.fallback_rule_hit).toBe("bank_operations_counterparty_year_rewrite"); + }); + + it("normalizes short ordinal year like '20й' in noisy docs phrasing", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-short-ordinal-year", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "бля епт покажи доки по свк за 20й", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-short-ordinal-year-${Date.now()}`, + user_message: "бля епт покажи доки по свк за 20й", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).toBe("документы по контрагенту свк за 2020 год"); + expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm"); + expect(response.debug?.fallback_rule_hit).toBe("documents_counterparty_year_rewrite"); + }); + + it("does not treat date fragments as account in balance fallback", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-date-not-account", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "есть ли долг по договору 1-ПМ/2020 на 2020-07-31", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [] + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-date-not-account-${Date.now()}`, + user_message: "есть ли долг по договору 1-ПМ/2020 на 2020-07-31", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual"); + expect(calls).toHaveLength(1); + expect(calls[0].message).not.toContain("остаток по счету 07"); + expect(calls[0].message.toLowerCase()).toContain("договору 1-пм/2020"); + }); });