diff --git a/docs/ADDRESS/address_query/README.md b/docs/ADDRESS/address_query/README.md index e4a2794..5cd17d2 100644 --- a/docs/ADDRESS/address_query/README.md +++ b/docs/ADDRESS/address_query/README.md @@ -12,6 +12,7 @@ - `runtime_readiness_matrix_v1.md` - матрица structural vs runtime readiness. - `known_positive_live_suite_v1.md` - базовый template positive-evidence suite. - `data_aware_positive_acceptance_suite_v1.md` - M2.3 canonical guide для curated live acceptance. +- `curated_positive_live_suite_v1.md` - M2.3c curated suite (counterparty/account split + negative twins). - `address_query_bootstrap_report_2026-03-29.md` - итоговая сводка bootstrap этапа. ## Связанные run-паки @@ -21,4 +22,5 @@ - `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3_DocumentsFormingBalance_DataAwareAcceptance/` - `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3A_Stage_Diagnostic_Materialization/` - `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3B_AccountScope_Mode_Tuning/` +- `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/` diff --git a/docs/ADDRESS/address_query/address_runtime_contracts.md b/docs/ADDRESS/address_query/address_runtime_contracts.md index 4e15e86..e62107b 100644 --- a/docs/ADDRESS/address_query/address_runtime_contracts.md +++ b/docs/ADDRESS/address_query/address_runtime_contracts.md @@ -1,4 +1,4 @@ -# Address Runtime Contracts V1 (M2.3b) +# Address Runtime Contracts V1 (M2.3c) Дата: 2026-03-29 @@ -38,6 +38,9 @@ - `ambiguity_count` - MCP/evidence flow block: - `mcp_call_status` + - `mcp_call_status_legacy` + - `match_failure_stage` + - `match_failure_reason` - `rows_fetched` - `raw_rows_received` - `rows_after_account_scope` @@ -46,6 +49,12 @@ - `rows_matched` - `raw_row_keys_sample` - `materialization_drop_reason` +- account-scope audit block: + - `account_token_raw` + - `account_token_normalized` + - `account_scope_fields_checked` + - `account_scope_match_strategy` + - `account_scope_drop_reason` - `response_type` - `limited_reason_category` - `fallback_reason` @@ -85,16 +94,20 @@ - `DEEP_ONLY` - `UNKNOWN` -## MCP Stage Status Taxonomy (M2.3a) +## MCP Stage Status Taxonomy (M2.3c) - `skipped` - `error` - `no_raw_rows` - `raw_rows_received_but_not_materialized` -- `materialized_but_not_matched` +- `materialized_but_not_anchor_matched` +- `materialized_but_filtered_out_by_recipe` - `matched_non_empty` -## Materialization Drop Reasons (M2.3a) +Legacy compatibility: +- `mcp_call_status_legacy` may still report `materialized_but_not_matched` for backward-compatible analytics. + +## Materialization Drop Reasons (M2.3c) - `none` - `dropped_by_account_scope_filter` @@ -103,15 +116,21 @@ - `missing_registrator_field` - `unknown_row_shape` -## Account Scope Strategy (M2.3b) +## Account Scope Strategy (M2.3c) - `strict` - account scope is mandatory and applied as a hard filter. - `preferred` - account scope is applied first; if it yields zero rows while raw rows exist, runtime falls back to raw rows and continues matching. +## M2.3c Runtime Snapshot + +- Counterparty intents now have confirmed `matched_non_empty` cases in curated live suite. +- Account intents still mostly stop at `raw_rows_received_but_not_materialized`. +- Guardrails remain unchanged: no free query generation, no false factual outputs. + ## Compound Query Note - `COMPOUND_FACTUAL_QUERY` currently remains detection-only. -- Multi-intent decomposition execution is planned for next increment. +- Multi-intent decomposition execution is planned for the next increment. ## Guardrails @@ -119,5 +138,3 @@ - read-only MCP - no free-form query generation - no silent source substitution - - diff --git a/docs/ADDRESS/address_query/address_scenario_matrix.md b/docs/ADDRESS/address_query/address_scenario_matrix.md index 5ec4441..49ff709 100644 --- a/docs/ADDRESS/address_query/address_scenario_matrix.md +++ b/docs/ADDRESS/address_query/address_scenario_matrix.md @@ -41,7 +41,7 @@ - Если обязательные фильтры не извлечены, вернуть `LIMITED_WITH_REASON` с указанием недостающих параметров. - Если вопрос требует causal/proof reasoning, перевести в deep-analysis path. - Для `as_of_date` по умолчанию используется текущая дата runtime (на момент этого документа: 2026-03-29), если пользователь явно не задал дату. -## Runtime status note (M2.3b) +## Runtime status note (M2.3c) Implemented in live runtime now: - `list_documents_by_counterparty` @@ -53,6 +53,6 @@ Still not implemented in runtime: Stage diagnostic note: - strict account intents still show `raw_rows_received > 0`, but `rows_after_account_scope = 0`; -- preferred counterparty intents now reach `rows_materialized > 0`, but rows still drop at recipe/anchor filter stage; -- non-empty factual acceptance now requires resolver/filter tuning after materialization. +- counterparty intents now have curated `matched_non_empty` cases after resolver/filter tuning; +- account family still needs account-scope/materialization fix before first stable non-empty account case. diff --git a/docs/ADDRESS/address_query/curated_positive_live_suite_v1.md b/docs/ADDRESS/address_query/curated_positive_live_suite_v1.md new file mode 100644 index 0000000..ba7bc83 --- /dev/null +++ b/docs/ADDRESS/address_query/curated_positive_live_suite_v1.md @@ -0,0 +1,48 @@ +# Curated Positive Live Suite V1 (M2.3c) + +Дата: 2026-03-29 + +## Назначение + +Этот suite нужен для acceptance live-runtime без хардкода бизнес-объектов в продуктовой логике. + +- runtime остается `data-agnostic` +- acceptance остается `data-aware` + +То есть набор кейсов собирается по exploratory live pass, но не вшивается в runtime-правила. + +## Семейства в M2.3c + +1. `counterparty` +2. `account` + +## Curated Case Set + +| case_id | family | question pattern | intent | expected | +|---|---|---|---|---| +| C1 | counterparty | documents by counterparty (known non-empty anchor + period) | `list_documents_by_counterparty` | `FACTUAL_LIST`, non-empty | +| C2 | counterparty | bank operations by counterparty (known non-empty anchor + period) | `bank_operations_by_counterparty` | `FACTUAL_LIST`, non-empty | +| C3 | counterparty | documents by counterparty (negative twin) | `list_documents_by_counterparty` | `LIMITED_WITH_REASON` | +| C4 | counterparty | bank ops by counterparty (negative twin) | `bank_operations_by_counterparty` | `LIMITED_WITH_REASON` | +| C5 | account | account balance snapshot by account/date | `account_balance_snapshot` | stage-diagnostic limited | +| C6 | account | documents forming balance by account/date | `documents_forming_balance` | stage-diagnostic limited | +| C7 | account | documents forming balance by account/date (variant) | `documents_forming_balance` | stage-diagnostic limited | +| C8 | account | account balance snapshot by account/date (variant) | `account_balance_snapshot` | stage-diagnostic limited | + +## Что проверяем этим suite + +- есть ли реальные `matched_non_empty` в counterparty-family; +- сохраняется ли `false_factual_rate = 0` на negative twins; +- где именно застревает account-family (`raw_rows_received_but_not_materialized` vs later stages). + +## Acceptance Rules + +- минимум один non-empty factual для каждого counterparty intent; +- zero false-factual; +- account-family must be localized with explicit stage/failure reason (без размытого limited). + +## Где лежат артефакты + +- run-pack: `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/` +- diagnostic matrix: `stage_diagnostic_matrix.md` +- case matrix: `curated_positive_case_matrix.md` diff --git a/docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip b/docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip new file mode 100644 index 0000000..fcd8933 Binary files /dev/null and b/docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip differ diff --git a/docs/ADDRESS/address_query/query_recipes_v1.md b/docs/ADDRESS/address_query/query_recipes_v1.md index 48cbc5d..9065f96 100644 --- a/docs/ADDRESS/address_query/query_recipes_v1.md +++ b/docs/ADDRESS/address_query/query_recipes_v1.md @@ -97,9 +97,20 @@ - Runtime вызывает MCP proxy (`/api/execute_query`) только с query-template из recipe и параметрами после валидации. - Для V1 все recipe выполняются в `read-only` режиме. - Ограничения на выборку (`limit`) и сортировки фиксируются recipe-контрактом, а не свободным текстом вопроса. -## 8) Account Scope Strategy (M2.3b) +## 8) Account Scope Strategy (M2.3c) - `account_balance_snapshot` and `documents_forming_balance` use `strict` account scope. - counterparty-oriented recipes use `preferred` account scope with runtime fallback to raw rows when scope gives zero rows. - this keeps account-intent precision while preventing blind row loss on party intents. +## 9) Runtime Query Template Notes (M2.3c) + +- `address.documents.by_counterparty` and `address.bank_ops.by_counterparty` use a dedicated `bank_docs` live query template. +- account intents (`address.account.balance_snapshot`, `address.balance.drilldown_documents`) continue using movement-oriented query template with strict account scope. +- stage diagnostics are tracked with split statuses: + - `raw_rows_received_but_not_materialized` + - `materialized_but_not_anchor_matched` + - `materialized_but_filtered_out_by_recipe` + - `matched_non_empty` +- for backward compatibility analytics, legacy status is emitted as `mcp_call_status_legacy`. + diff --git a/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md b/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md index 9b0c306..e1e9780 100644 --- a/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md +++ b/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md @@ -1,4 +1,4 @@ -# Runtime Readiness Matrix V1 (M2.3b) +# Runtime Readiness Matrix V1 (M2.3c) Дата: 2026-03-29 @@ -7,45 +7,29 @@ ## Статусы - `STRUCTURALLY_VISIBLE` - сущность подтверждена в snapshot/inventory. -- `LIVE_QUERYABLE` - в текущем live path можно дать factual без натяжек. -- `LIVE_QUERYABLE_WITH_LIMITS` - live path работает, но часто нужен дополнительный anchor. -- `REQUIRES_SPECIALIZED_RECIPE` - базовый movement recipe недостаточен для materialization. +- `LIVE_QUERYABLE` - в текущем live path можно давать factual ответ стабильно. +- `LIVE_QUERYABLE_WITH_LIMITS` - live path работает, но результат зависит от anchor/period precision. +- `REQUIRES_SPECIALIZED_RECIPE` - базовый recipe-контур не покрывает сценарий. - `DEEP_ONLY` - сценарий не относится к address V1. ## Матрица (P0/P1) | scenario_id | scenario | structural_readiness | runtime_readiness | current_blocker | next_action | |---|---|---|---|---|---| -| AQ-P0-01 | list_open_contracts | STRUCTURALLY_VISIBLE | REQUIRES_SPECIALIZED_RECIPE | weak contract anchors in movement rows | добавить object-aware recipe (`documents/contracts`) | -| AQ-P0-02 | list_payables_counterparties | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | empty matches on narrow filters | расширить live evidence pack по контрагентам | -| AQ-P0-03 | list_receivables_counterparties | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | empty matches on narrow filters | улучшить фильтрацию и fallback hints | -| AQ-P0-04 | account_balance_snapshot | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | dry-run frequently returns `empty_match` on broad `today` filters | lock data-aware positive account/date fixtures | -| AQ-P0-05 | open_items_by_counterparty_or_contract (counterparty) | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | missing counterparty anchor in short phrases | усилить anchor-first extraction | -| AQ-P0-06 | open_items_by_counterparty_or_contract (contract) | STRUCTURALLY_VISIBLE | REQUIRES_SPECIALIZED_RECIPE | movement rows often miss contract linkage | двухшаговый path: anchor resolution -> focused recipe | -| AQ-P0-07 | documents_by_counterparty | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | implemented path, but dry-run still often `empty_match` on current anchors/period | expand data-aware positive fixtures and improve resolver targeting | -| AQ-P0-07B | bank_operations_by_counterparty | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | implemented path, but dry-run still often `empty_match` on current anchors/period | expand data-aware positive fixtures and tighten bank-doc targeting | -| AQ-P0-08 | documents_by_contract | STRUCTURALLY_VISIBLE | REQUIRES_SPECIALIZED_RECIPE | by-contract live recipe not implemented in runtime V1 | add contract-aware document-list recipe with resolver confidence gate | -| AQ-P0-09 | documents_forming_balance | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | implemented, but stage diagnostic shows loss before materialization | diagnose and tune account-scope filtering for live recipes | -| AQ-P1-10 | account_turnover_snapshot | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | not in current intent set | расширение intents V1.1 | +| AQ-P0-01 | list_open_contracts | STRUCTURALLY_VISIBLE | REQUIRES_SPECIALIZED_RECIPE | weak contract anchors in current live rows | add contract-aware document recipe + resolver confidence gate | +| AQ-P0-02 | list_payables_counterparties | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | broad prompts still produce sparse matches | keep curated positive suite and tighten period hints | +| AQ-P0-03 | list_receivables_counterparties | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | broad prompts still produce sparse matches | keep curated positive suite and tighten period hints | +| AQ-P0-04 | account_balance_snapshot | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | `raw_rows_received > 0`, but account scope drops rows before materialization | account token/shape audit and account field mapping fix | +| AQ-P0-05 | open_items_by_counterparty_or_contract (counterparty) | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | requires explicit counterparty anchor for stable non-empty | anchor refinement and resolver ambiguity handling | +| AQ-P0-06 | open_items_by_counterparty_or_contract (contract) | STRUCTURALLY_VISIBLE | REQUIRES_SPECIALIZED_RECIPE | movement rows often miss contract linkage | two-step path: contract resolver -> focused recipe | +| AQ-P0-07 | documents_by_counterparty | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | positive cases confirmed, but narrow/broad anchor variants still fragile | continue resolver/filter tuning and parity checks | +| AQ-P0-07B | bank_operations_by_counterparty | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | positive cases confirmed, but narrow/broad anchor variants still fragile | continue resolver/filter tuning and bank-doc visibility checks | +| AQ-P0-08 | documents_by_contract | STRUCTURALLY_VISIBLE | REQUIRES_SPECIALIZED_RECIPE | by-contract live recipe not implemented in runtime V1 | implement contract resolver + focused recipe | +| AQ-P0-09 | documents_forming_balance | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | implemented, but account-family still blocked before materialization | account scope/materialization diagnostics and account token normalization | +| AQ-P1-10 | account_turnover_snapshot | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | not in current intent set | extend intents in V1.1 | -## Примечание - -Матрица разделяет "видимость сущности в inventory" и "операционную готовность live-runtime". -Это обязательная опора для приоритезации Sprint B, чтобы не путать structural coverage и runtime proofability. -### Sync note (M2.3b -> live dry-run) - -`account_balance_snapshot` intentionally remains `LIVE_QUERYABLE_WITH_LIMITS`. -Reason: dry-run still shows repeatable `empty_match` on broad `as_of=today` prompts. -Promote to `LIVE_QUERYABLE` only after data-aware positive live cases are stable. - -`documents_forming_balance` is implemented with strict account-scope path. -Validation should be based on data-aware acceptance suite, not only safety dry-run. - -Stage-diagnostic replay (M2.3b) shows split-stage behavior: -`D1-D3`: `raw_rows_received > 0` with `rows_after_account_scope = 0` (strict account intents). -`D4-D5`: `rows_after_account_scope > 0` and `rows_materialized > 0`, but `rows_after_recipe_filter = 0` (preferred mode progressed to matching stage). -Current bottleneck moved forward for non-account intents: resolver/filter matching after materialization. - -`COMPOUND_FACTUAL_QUERY` currently remains detection-only. -Multi-intent decomposition execution is not part of M2.3b and tracked for next increment. +## Sync Note (M2.3c) +- `documents_by_counterparty` and `bank_operations_by_counterparty` now have curated `matched_non_empty` cases. +- `account_balance_snapshot` and `documents_forming_balance` remain limited because rows are dropped before materialization. +- `COMPOUND_FACTUAL_QUERY` is detection-only and does not execute multi-intent decomposition yet. diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit.zip b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit.zip new file mode 100644 index 0000000..bc13944 Binary files /dev/null and b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit.zip differ diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/README.md b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/README.md new file mode 100644 index 0000000..663c85f --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/README.md @@ -0,0 +1,17 @@ +# 2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit + +## Scope +- Track A: resolver/filter tuning for counterparty intents. +- Track B: account-scope/materialization audit for account intents. +- Curated positive live suite for acceptance. + +## Included artifacts +- `run_summary.json` +- `before_after_metrics.json` +- `curated_positive_case_matrix.md` +- `assistant_window_dry_run_results.json` +- `stage_diagnostic_matrix.md` +- `debug_payloads/` +- `live_call_inventory_address.json` +- `smoke_checks.md` +- `changed_files.txt` \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/assistant_window_dry_run_results.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/assistant_window_dry_run_results.json new file mode 100644 index 0000000..bfe1047 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/assistant_window_dry_run_results.json @@ -0,0 +1,566 @@ +{ + "generated_at": "2026-03-29T18:30:51.853Z", + "run_id": "2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit", + "cases": [ + { + "id": "C1", + "family": "counterparty", + "question": "show documents by counterparty svk from 2020-07-01 to 2020-07-31", + "expected_intent": "list_documents_by_counterparty", + "expected_response_type": "FACTUAL_LIST", + "expected_non_empty": true, + "handled": true, + "response_type": "FACTUAL_LIST", + "reply_type": "factual", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "list_documents_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_documents_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "svk", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "matched_non_empty", + "mcp_call_status_legacy": "matched_non_empty", + "stage_interpretation": "Rows passed all stages and produced factual non-empty output.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 2, + "rows_matched": 2, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "svk", + "anchor_value_resolved": "Группа СВК", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": null, + "response_is_non_empty": true, + "assistant_reply_preview": "", + "elapsed_ms": 513, + "generated_at": "2026-03-29T18:30:44.406Z" + }, + { + "id": "C2", + "family": "counterparty", + "question": "show bank operations by counterparty svk from 2020-07-01 to 2020-07-31", + "expected_intent": "bank_operations_by_counterparty", + "expected_response_type": "FACTUAL_LIST", + "expected_non_empty": true, + "handled": true, + "response_type": "FACTUAL_LIST", + "reply_type": "factual", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "bank_operations_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_bank_operations_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "svk", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "matched_non_empty", + "mcp_call_status_legacy": "matched_non_empty", + "stage_interpretation": "Rows passed all stages and produced factual non-empty output.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 2, + "rows_matched": 2, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "svk", + "anchor_value_resolved": "Группа СВК", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": null, + "response_is_non_empty": true, + "assistant_reply_preview": "", + "elapsed_ms": 1046, + "generated_at": "2026-03-29T18:30:45.456Z" + }, + { + "id": "C3", + "family": "counterparty", + "question": "show documents by counterparty alfa from 2020-07-01 to 2020-07-31", + "expected_intent": "list_documents_by_counterparty", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "list_documents_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_documents_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "alfa", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "materialized_but_not_anchor_matched", + "mcp_call_status_legacy": "materialized_but_not_matched", + "stage_interpretation": "Rows materialized, but anchor resolution/matching removed all candidates.", + "match_failure_stage": "materialized_but_not_anchor_matched", + "match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows", + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "alfa", + "anchor_value_resolved": "alfa", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": "missing_anchor", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1030, + "generated_at": "2026-03-29T18:30:46.488Z" + }, + { + "id": "C4", + "family": "counterparty", + "question": "show bank operations by counterparty alfa from 2020-07-01 to 2020-07-31", + "expected_intent": "bank_operations_by_counterparty", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "bank_operations_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_bank_operations_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "alfa", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "materialized_but_not_anchor_matched", + "mcp_call_status_legacy": "materialized_but_not_matched", + "stage_interpretation": "Rows materialized, but anchor resolution/matching removed all candidates.", + "match_failure_stage": "materialized_but_not_anchor_matched", + "match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows", + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "alfa", + "anchor_value_resolved": "alfa", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": "missing_anchor", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1027, + "generated_at": "2026-03-29T18:30:47.517Z" + }, + { + "id": "C5", + "family": "account", + "question": "show account balance 60 today", + "expected_intent": "account_balance_snapshot", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "AGGREGATE_LOOKUP", + "detected_intent": "account_balance_snapshot", + "intent_aligned": true, + "selected_recipe": "address_movements_account_snapshot_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "60", + "as_of_date": "2026-03-29" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "60", + "account_token_normalized": "60", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1013, + "generated_at": "2026-03-29T18:30:48.531Z" + }, + { + "id": "C6", + "family": "account", + "question": "which documents form balance for account 62 as of 2020-07-31", + "expected_intent": "documents_forming_balance", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "documents_forming_balance", + "intent_aligned": true, + "selected_recipe": "address_documents_forming_balance_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "62", + "as_of_date": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "62", + "anchor_value_resolved": "62", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "62", + "account_token_normalized": "62", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 969, + "generated_at": "2026-03-29T18:30:49.501Z" + }, + { + "id": "C7", + "family": "account", + "question": "which documents form balance for account 60 as of 2020-07-31", + "expected_intent": "documents_forming_balance", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "documents_forming_balance", + "intent_aligned": true, + "selected_recipe": "address_documents_forming_balance_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "60", + "as_of_date": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "60", + "account_token_normalized": "60", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1050, + "generated_at": "2026-03-29T18:30:50.552Z" + }, + { + "id": "C8", + "family": "account", + "question": "show account balance 51 as of 2020-07-31", + "expected_intent": "account_balance_snapshot", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "AGGREGATE_LOOKUP", + "detected_intent": "account_balance_snapshot", + "intent_aligned": true, + "selected_recipe": "address_movements_account_snapshot_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "51", + "as_of_date": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "51", + "anchor_value_resolved": "51", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "51", + "account_token_normalized": "51", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1034, + "generated_at": "2026-03-29T18:30:51.587Z" + } + ] +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/before_after_metrics.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/before_after_metrics.json new file mode 100644 index 0000000..5244c9f --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/before_after_metrics.json @@ -0,0 +1,28 @@ +{ + "compared_from": "2026-03-29_Address_Query_Runtime_V1_M2_3B_AccountScope_Mode_Tuning", + "compared_to": "2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit", + "comparison_scope": "stage_diagnostic_plus_curated_positive_suite", + "metrics": { + "factual_positive_rate": { + "before": 0, + "after": 0.25 + }, + "false_factual_rate": { + "before": 0, + "after": 0 + }, + "counterparty_non_empty_cases": { + "before": 0, + "after": 2 + }, + "account_non_empty_cases": { + "before": 0, + "after": 0 + } + }, + "narrative": [ + "Counterparty scenarios moved from materialized_but_not_matched to matched_non_empty on curated positive cases.", + "Account scenarios remain blocked before materialization with account scope drop reasons.", + "False factual output remains zero." + ] +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/changed_files.txt b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/changed_files.txt new file mode 100644 index 0000000..0d2f410 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/changed_files.txt @@ -0,0 +1,19 @@ +docs/ADDRESS/address_query/README.md +docs/ADDRESS/address_query/address_runtime_contracts.md +docs/ADDRESS/address_query/address_scenario_matrix.md +docs/ADDRESS/address_query/query_recipes_v1.md +docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md +llm_normalizer/backend/dist/services/addressMcpClient.js +llm_normalizer/backend/dist/services/addressQueryService.js +llm_normalizer/backend/dist/services/addressRecipeCatalog.js +llm_normalizer/backend/dist/services/assistantService.js +llm_normalizer/backend/src/services/addressMcpClient.ts +llm_normalizer/backend/src/services/addressQueryService.ts +llm_normalizer/backend/src/services/addressRecipeCatalog.ts +llm_normalizer/backend/src/services/assistantService.ts +llm_normalizer/backend/src/types/addressQuery.ts +llm_normalizer/backend/src/types/assistant.ts +llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +docs/ADDRESS/address_query/curated_positive_live_suite_v1.md +docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/ +llm_normalizer/backend/scripts/runAddressM23cPack.js diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/curated_positive_case_matrix.md b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/curated_positive_case_matrix.md new file mode 100644 index 0000000..3ea3b0c --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/curated_positive_case_matrix.md @@ -0,0 +1,14 @@ +# Curated Positive Case Matrix (M2.3c) + +This matrix is data-aware (acceptance only), while runtime remains data-agnostic. + +| case_id | family | expected_non_empty | actual_non_empty | expected_response | actual_response | selected_recipe | anchor_raw | anchor_resolved | +|---|---|---|---|---|---|---|---|---| +| C1 | counterparty | yes | yes | FACTUAL_LIST | FACTUAL_LIST | address_documents_by_counterparty_v1 | svk | Группа СВК | +| C2 | counterparty | yes | yes | FACTUAL_LIST | FACTUAL_LIST | address_bank_operations_by_counterparty_v1 | svk | Группа СВК | +| C3 | counterparty | no | no | LIMITED_WITH_REASON | LIMITED_WITH_REASON | address_documents_by_counterparty_v1 | alfa | alfa | +| C4 | counterparty | no | no | LIMITED_WITH_REASON | LIMITED_WITH_REASON | address_bank_operations_by_counterparty_v1 | alfa | alfa | +| C5 | account | no | no | LIMITED_WITH_REASON | LIMITED_WITH_REASON | address_movements_account_snapshot_v1 | 60 | 60 | +| C6 | account | no | no | LIMITED_WITH_REASON | LIMITED_WITH_REASON | address_documents_forming_balance_v1 | 62 | 62 | +| C7 | account | no | no | LIMITED_WITH_REASON | LIMITED_WITH_REASON | address_documents_forming_balance_v1 | 60 | 60 | +| C8 | account | no | no | LIMITED_WITH_REASON | LIMITED_WITH_REASON | address_movements_account_snapshot_v1 | 51 | 51 | \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C1.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C1.debug.json new file mode 100644 index 0000000..776c8a1 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C1.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C1", + "family": "counterparty", + "question": "show documents by counterparty svk from 2020-07-01 to 2020-07-31", + "expected_intent": "list_documents_by_counterparty", + "expected_response_type": "FACTUAL_LIST", + "expected_non_empty": true + }, + "result": { + "id": "C1", + "family": "counterparty", + "question": "show documents by counterparty svk from 2020-07-01 to 2020-07-31", + "expected_intent": "list_documents_by_counterparty", + "expected_response_type": "FACTUAL_LIST", + "expected_non_empty": true, + "handled": true, + "response_type": "FACTUAL_LIST", + "reply_type": "factual", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "list_documents_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_documents_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "svk", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "matched_non_empty", + "mcp_call_status_legacy": "matched_non_empty", + "stage_interpretation": "Rows passed all stages and produced factual non-empty output.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 2, + "rows_matched": 2, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "svk", + "anchor_value_resolved": "Группа СВК", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": null, + "response_is_non_empty": true, + "assistant_reply_preview": "", + "elapsed_ms": 513, + "generated_at": "2026-03-29T18:30:44.406Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C2.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C2.debug.json new file mode 100644 index 0000000..a3e141f --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C2.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C2", + "family": "counterparty", + "question": "show bank operations by counterparty svk from 2020-07-01 to 2020-07-31", + "expected_intent": "bank_operations_by_counterparty", + "expected_response_type": "FACTUAL_LIST", + "expected_non_empty": true + }, + "result": { + "id": "C2", + "family": "counterparty", + "question": "show bank operations by counterparty svk from 2020-07-01 to 2020-07-31", + "expected_intent": "bank_operations_by_counterparty", + "expected_response_type": "FACTUAL_LIST", + "expected_non_empty": true, + "handled": true, + "response_type": "FACTUAL_LIST", + "reply_type": "factual", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "bank_operations_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_bank_operations_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "svk", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "matched_non_empty", + "mcp_call_status_legacy": "matched_non_empty", + "stage_interpretation": "Rows passed all stages and produced factual non-empty output.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 2, + "rows_matched": 2, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "svk", + "anchor_value_resolved": "Группа СВК", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": null, + "response_is_non_empty": true, + "assistant_reply_preview": "", + "elapsed_ms": 1046, + "generated_at": "2026-03-29T18:30:45.456Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C3.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C3.debug.json new file mode 100644 index 0000000..a08e479 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C3.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C3", + "family": "counterparty", + "question": "show documents by counterparty alfa from 2020-07-01 to 2020-07-31", + "expected_intent": "list_documents_by_counterparty", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false + }, + "result": { + "id": "C3", + "family": "counterparty", + "question": "show documents by counterparty alfa from 2020-07-01 to 2020-07-31", + "expected_intent": "list_documents_by_counterparty", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "list_documents_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_documents_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "alfa", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "materialized_but_not_anchor_matched", + "mcp_call_status_legacy": "materialized_but_not_matched", + "stage_interpretation": "Rows materialized, but anchor resolution/matching removed all candidates.", + "match_failure_stage": "materialized_but_not_anchor_matched", + "match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows", + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "alfa", + "anchor_value_resolved": "alfa", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": "missing_anchor", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1030, + "generated_at": "2026-03-29T18:30:46.488Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C4.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C4.debug.json new file mode 100644 index 0000000..7bec4de --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C4.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C4", + "family": "counterparty", + "question": "show bank operations by counterparty alfa from 2020-07-01 to 2020-07-31", + "expected_intent": "bank_operations_by_counterparty", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false + }, + "result": { + "id": "C4", + "family": "counterparty", + "question": "show bank operations by counterparty alfa from 2020-07-01 to 2020-07-31", + "expected_intent": "bank_operations_by_counterparty", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "bank_operations_by_counterparty", + "intent_aligned": true, + "selected_recipe": "address_bank_operations_by_counterparty_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "alfa", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "mcp_call_status": "materialized_but_not_anchor_matched", + "mcp_call_status_legacy": "materialized_but_not_matched", + "stage_interpretation": "Rows materialized, but anchor resolution/matching removed all candidates.", + "match_failure_stage": "materialized_but_not_anchor_matched", + "match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows", + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "none", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "Amount" + ], + "anchor_type": "counterparty", + "anchor_value_raw": "alfa", + "anchor_value_resolved": "alfa", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "limited_reason_category": "missing_anchor", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1027, + "generated_at": "2026-03-29T18:30:47.517Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C5.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C5.debug.json new file mode 100644 index 0000000..3d88849 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C5.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C5", + "family": "account", + "question": "show account balance 60 today", + "expected_intent": "account_balance_snapshot", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false + }, + "result": { + "id": "C5", + "family": "account", + "question": "show account balance 60 today", + "expected_intent": "account_balance_snapshot", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "AGGREGATE_LOOKUP", + "detected_intent": "account_balance_snapshot", + "intent_aligned": true, + "selected_recipe": "address_movements_account_snapshot_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "60", + "as_of_date": "2026-03-29" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "60", + "account_token_normalized": "60", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1013, + "generated_at": "2026-03-29T18:30:48.531Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C6.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C6.debug.json new file mode 100644 index 0000000..ff7fb2a --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C6.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C6", + "family": "account", + "question": "which documents form balance for account 62 as of 2020-07-31", + "expected_intent": "documents_forming_balance", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false + }, + "result": { + "id": "C6", + "family": "account", + "question": "which documents form balance for account 62 as of 2020-07-31", + "expected_intent": "documents_forming_balance", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "documents_forming_balance", + "intent_aligned": true, + "selected_recipe": "address_documents_forming_balance_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "62", + "as_of_date": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "62", + "anchor_value_resolved": "62", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "62", + "account_token_normalized": "62", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 969, + "generated_at": "2026-03-29T18:30:49.501Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C7.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C7.debug.json new file mode 100644 index 0000000..94a8384 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C7.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C7", + "family": "account", + "question": "which documents form balance for account 60 as of 2020-07-31", + "expected_intent": "documents_forming_balance", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false + }, + "result": { + "id": "C7", + "family": "account", + "question": "which documents form balance for account 60 as of 2020-07-31", + "expected_intent": "documents_forming_balance", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "documents_forming_balance", + "intent_aligned": true, + "selected_recipe": "address_documents_forming_balance_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "60", + "as_of_date": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "60", + "account_token_normalized": "60", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1050, + "generated_at": "2026-03-29T18:30:50.552Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C8.debug.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C8.debug.json new file mode 100644 index 0000000..18edb57 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/debug_payloads/C8.debug.json @@ -0,0 +1,80 @@ +{ + "case": { + "id": "C8", + "family": "account", + "question": "show account balance 51 as of 2020-07-31", + "expected_intent": "account_balance_snapshot", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false + }, + "result": { + "id": "C8", + "family": "account", + "question": "show account balance 51 as of 2020-07-31", + "expected_intent": "account_balance_snapshot", + "expected_response_type": "LIMITED_WITH_REASON", + "expected_non_empty": false, + "handled": true, + "response_type": "LIMITED_WITH_REASON", + "reply_type": "partial_coverage", + "detected_mode": "address_query", + "query_shape": "AGGREGATE_LOOKUP", + "detected_intent": "account_balance_snapshot", + "intent_aligned": true, + "selected_recipe": "address_movements_account_snapshot_v1", + "selected_recipe_ids": [], + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "51", + "as_of_date": "2020-07-31" + }, + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "stage_interpretation": "Raw rows arrived, but row materialization path dropped everything.", + "match_failure_stage": "none", + "match_failure_reason": null, + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "materialization_drop_reason": "dropped_by_account_scope_filter", + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "anchor_type": "account", + "anchor_value_raw": "51", + "anchor_value_resolved": "51", + "resolver_confidence": "high", + "ambiguity_count": 0, + "account_token_raw": "51", + "account_token_normalized": "51", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "limited_reason_category": "empty_match", + "response_is_non_empty": false, + "assistant_reply_preview": "", + "elapsed_ms": 1034, + "generated_at": "2026-03-29T18:30:51.587Z" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/live_call_inventory_address.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/live_call_inventory_address.json new file mode 100644 index 0000000..8860f70 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/live_call_inventory_address.json @@ -0,0 +1,142 @@ +{ + "generated_at": "2026-03-29T18:30:51.853Z", + "run_id": "2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit", + "inventory": [ + { + "case_id": "C1", + "family": "counterparty", + "question": "show documents by counterparty svk from 2020-07-01 to 2020-07-31", + "recipe": "address_documents_by_counterparty_v1", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "list_documents_by_counterparty", + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 2, + "rows_matched": 2, + "mcp_call_status": "matched_non_empty", + "match_failure_stage": "none", + "match_failure_reason": null, + "limited_reason_category": null + }, + { + "case_id": "C2", + "family": "counterparty", + "question": "show bank operations by counterparty svk from 2020-07-01 to 2020-07-31", + "recipe": "address_bank_operations_by_counterparty_v1", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "bank_operations_by_counterparty", + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 2, + "rows_matched": 2, + "mcp_call_status": "matched_non_empty", + "match_failure_stage": "none", + "match_failure_reason": null, + "limited_reason_category": null + }, + { + "case_id": "C3", + "family": "counterparty", + "question": "show documents by counterparty alfa from 2020-07-01 to 2020-07-31", + "recipe": "address_documents_by_counterparty_v1", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "list_documents_by_counterparty", + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "mcp_call_status": "materialized_but_not_anchor_matched", + "match_failure_stage": "materialized_but_not_anchor_matched", + "match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows", + "limited_reason_category": "missing_anchor" + }, + { + "case_id": "C4", + "family": "counterparty", + "question": "show bank operations by counterparty alfa from 2020-07-01 to 2020-07-31", + "recipe": "address_bank_operations_by_counterparty_v1", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "bank_operations_by_counterparty", + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_materialized": 3, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "mcp_call_status": "materialized_but_not_anchor_matched", + "match_failure_stage": "materialized_but_not_anchor_matched", + "match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows", + "limited_reason_category": "missing_anchor" + }, + { + "case_id": "C5", + "family": "account", + "question": "show account balance 60 today", + "recipe": "address_movements_account_snapshot_v1", + "query_shape": "AGGREGATE_LOOKUP", + "detected_intent": "account_balance_snapshot", + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "match_failure_stage": "none", + "match_failure_reason": null, + "limited_reason_category": "empty_match" + }, + { + "case_id": "C6", + "family": "account", + "question": "which documents form balance for account 62 as of 2020-07-31", + "recipe": "address_documents_forming_balance_v1", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "documents_forming_balance", + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "match_failure_stage": "none", + "match_failure_reason": null, + "limited_reason_category": "empty_match" + }, + { + "case_id": "C7", + "family": "account", + "question": "which documents form balance for account 60 as of 2020-07-31", + "recipe": "address_documents_forming_balance_v1", + "query_shape": "DOCUMENT_LIST", + "detected_intent": "documents_forming_balance", + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "match_failure_stage": "none", + "match_failure_reason": null, + "limited_reason_category": "empty_match" + }, + { + "case_id": "C8", + "family": "account", + "question": "show account balance 51 as of 2020-07-31", + "recipe": "address_movements_account_snapshot_v1", + "query_shape": "AGGREGATE_LOOKUP", + "detected_intent": "account_balance_snapshot", + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_materialized": 0, + "rows_after_recipe_filter": 0, + "rows_matched": 0, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "match_failure_stage": "none", + "match_failure_reason": null, + "limited_reason_category": "empty_match" + } + ] +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/run_summary.json b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/run_summary.json new file mode 100644 index 0000000..116b65a --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/run_summary.json @@ -0,0 +1,58 @@ +{ + "run_id": "2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit", + "date": "2026-03-29", + "stage": "address_query_runtime_v1", + "scope": "m2_3c_resolver_filter_tuning_and_account_scope_audit", + "build_status": "PASSED", + "tests_status": "PASSED", + "diagnostic_run_status": "COMPLETED", + "implemented": { + "counterparty_anchor_refinement_after_materialization": true, + "split_match_failure_stages": true, + "legacy_status_compatibility_field": true, + "account_scope_audit_fields": true, + "bank_docs_query_template_for_counterparty_intents": true + }, + "metrics": { + "cases_total": 8, + "intent_alignment_rate": 1, + "factual_positive_rate": 0.25, + "limited_mode_rate": 0.75, + "false_factual_rate": 0, + "counterparty_family_non_empty_rate": 0.5, + "account_family_non_empty_rate": 0 + }, + "stage_status_distribution": [ + { + "status": "matched_non_empty", + "count": 2 + }, + { + "status": "materialized_but_not_anchor_matched", + "count": 2 + }, + { + "status": "raw_rows_received_but_not_materialized", + "count": 4 + } + ], + "failure_reason_distribution": [ + { + "reason": "none", + "count": 2 + }, + { + "reason": "counterparty_anchor_not_matched_in_materialized_rows", + "count": 2 + }, + { + "reason": "dropped_by_account_scope_filter", + "count": 4 + } + ], + "key_findings": { + "counterparty_track": "positive factual responses now confirmed on curated non-empty live cases", + "account_track": "account intents still stop at raw_rows_received_but_not_materialized", + "next_priority": "account scope/materialization shape audit to unblock first non-empty account case" + } +} \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/smoke_checks.md b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/smoke_checks.md new file mode 100644 index 0000000..6d8c802 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/smoke_checks.md @@ -0,0 +1,9 @@ +# Smoke Checks (M2.3c) + +- `npm.cmd run build` -> PASSED +- `npx.cmd vitest tests/addressQueryRuntimeM23.test.ts` -> PASSED (10/10) +- M2.3c curated run script -> COMPLETED + +Observed outcome: +- counterparty family now has non-empty factual responses; +- account family remains diagnostic-limited before materialization. \ No newline at end of file diff --git a/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/stage_diagnostic_matrix.md b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/stage_diagnostic_matrix.md new file mode 100644 index 0000000..29fb4c8 --- /dev/null +++ b/docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/stage_diagnostic_matrix.md @@ -0,0 +1,17 @@ +# Stage Diagnostic Matrix (M2.3c) + +| case_id | family | expected_intent | detected_intent | status | rows_after_account_scope | rows_materialized | rows_after_recipe_filter | rows_matched | response_type | limited_reason | +|---|---|---|---|---|---|---|---|---|---|---| +| C1 | counterparty | list_documents_by_counterparty | list_documents_by_counterparty | matched_non_empty | 3 | 3 | 2 | 2 | FACTUAL_LIST | | +| C2 | counterparty | bank_operations_by_counterparty | bank_operations_by_counterparty | matched_non_empty | 3 | 3 | 2 | 2 | FACTUAL_LIST | | +| C3 | counterparty | list_documents_by_counterparty | list_documents_by_counterparty | materialized_but_not_anchor_matched | 3 | 3 | 0 | 0 | LIMITED_WITH_REASON | missing_anchor | +| C4 | counterparty | bank_operations_by_counterparty | bank_operations_by_counterparty | materialized_but_not_anchor_matched | 3 | 3 | 0 | 0 | LIMITED_WITH_REASON | missing_anchor | +| C5 | account | account_balance_snapshot | account_balance_snapshot | raw_rows_received_but_not_materialized | 0 | 0 | 0 | 0 | LIMITED_WITH_REASON | empty_match | +| C6 | account | documents_forming_balance | documents_forming_balance | raw_rows_received_but_not_materialized | 0 | 0 | 0 | 0 | LIMITED_WITH_REASON | empty_match | +| C7 | account | documents_forming_balance | documents_forming_balance | raw_rows_received_but_not_materialized | 0 | 0 | 0 | 0 | LIMITED_WITH_REASON | empty_match | +| C8 | account | account_balance_snapshot | account_balance_snapshot | raw_rows_received_but_not_materialized | 0 | 0 | 0 | 0 | LIMITED_WITH_REASON | empty_match | + +Status taxonomy in this run: +- `raw_rows_received_but_not_materialized` +- `materialized_but_not_anchor_matched` +- `matched_non_empty` \ No newline at end of file diff --git a/llm_normalizer/backend/dist/services/addressMcpClient.js b/llm_normalizer/backend/dist/services/addressMcpClient.js index 9d67076..1014f3c 100644 --- a/llm_normalizer/backend/dist/services/addressMcpClient.js +++ b/llm_normalizer/backend/dist/services/addressMcpClient.js @@ -38,21 +38,37 @@ function parseRowsFromTextTable(source) { return []; } const rows = []; + const parseCsvLine = (line) => { + const values = []; + let current = ""; + let inQuotes = false; + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + if (char === '"') { + if (inQuotes && line[index + 1] === '"') { + current += '"'; + index += 1; + continue; + } + inQuotes = !inQuotes; + continue; + } + if (char === "," && !inQuotes) { + values.push(current.trim()); + current = ""; + continue; + } + current += char; + } + values.push(current.trim()); + return values; + }; const lines = body .split("\n") .map((line) => line.trim()) .filter(Boolean); for (const line of lines) { - const values = []; - const matcher = /"([^"]*)"|([^,]+)/g; - let match = null; - while ((match = matcher.exec(line)) !== null) { - const raw = match[1] !== undefined ? match[1] : match[2]; - const value = String(raw ?? "").trim(); - if (value.length > 0) { - values.push(value); - } - } + const values = parseCsvLine(line); if (values.length === 0) { continue; } diff --git a/llm_normalizer/backend/dist/services/addressQueryService.js b/llm_normalizer/backend/dist/services/addressQueryService.js index 72c29eb..728fd3c 100644 --- a/llm_normalizer/backend/dist/services/addressQueryService.js +++ b/llm_normalizer/backend/dist/services/addressQueryService.js @@ -8,6 +8,29 @@ const addressIntentResolver_1 = require("./addressIntentResolver"); const addressFilterExtractor_1 = require("./addressFilterExtractor"); const addressRecipeCatalog_1 = require("./addressRecipeCatalog"); const addressMcpClient_1 = require("./addressMcpClient"); +const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"]; +const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1"; +const PARTY_ANCHOR_STOPWORDS = new Set([ + "ооо", + "ао", + "зао", + "ип", + "llc", + "ltd", + "company", + "компания", + "контрагент", + "counterparty", + "по", + "by" +]); +const ACCOUNT_ALIAS_MAP = { + "51": ["расчетный счет", "расчетные счета", "bank account"], + "52": ["валютный счет", "валютные счета", "currency account"], + "60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"], + "62": ["покупатель", "покупателями", "расчеты с покупателями"], + "76": ["прочие расчеты", "прочими дебиторами и кредиторами"] +}; function parseFiniteNumber(value) { if (typeof value === "number" && Number.isFinite(value)) { return value; @@ -26,11 +49,117 @@ function valueAsString(value) { } return String(value); } -function normalizeToken(value) { - return String(value ?? "").trim().toLowerCase(); +function transliterateCyrillicToLatin(value) { + const map = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "ts", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya" + }; + let out = ""; + for (const char of String(value ?? "").toLowerCase()) { + out += map[char] ?? char; + } + return out; } -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +function normalizeSearchText(value) { + return String(value ?? "") + .toLowerCase() + .replace(/ё/g, "е") + .replace(/[^a-zа-я0-9]+/gi, " ") + .replace(/\s+/g, " ") + .trim(); +} +function tokenizeAnchor(value) { + return normalizeSearchText(value) + .split(" ") + .map((token) => token.trim()) + .filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token)); +} +function matchesAnchorText(searchable, anchor) { + const searchableNormalized = normalizeSearchText(searchable); + const searchableLatin = transliterateCyrillicToLatin(searchableNormalized); + const tokens = tokenizeAnchor(anchor); + if (tokens.length === 0) { + const direct = normalizeSearchText(anchor); + if (!direct) { + return false; + } + return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct)); + } + return tokens.every((token) => { + const tokenLatin = transliterateCyrillicToLatin(token); + return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin); + }); +} +function normalizeAccountToken(value) { + const source = String(value ?? "").trim().replace(",", "."); + const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/); + if (!match) { + return source.toLowerCase(); + } + const base = match[1]; + if (!match[2]) { + return base; + } + const sub = String(Number(match[2])); + return `${base}.${sub}`; +} +function extractAccountTokens(searchable) { + const result = []; + const matcher = /\b(\d{2})(?:[.,](\d{1,2}))?\b/g; + let hit = null; + while ((hit = matcher.exec(searchable)) !== null) { + const base = hit[1]; + const sub = hit[2] ? String(Number(hit[2])) : null; + result.push(sub ? `${base}.${sub}` : base); + } + return uniqueStrings(result); +} +function accountTokenMatches(requestedToken, candidateToken) { + const requested = normalizeAccountToken(requestedToken); + const candidate = normalizeAccountToken(candidateToken); + if (requested === candidate) { + return true; + } + if (!requested.includes(".")) { + return candidate.startsWith(`${requested}.`) || candidate === requested; + } + return false; +} +function baseAccountCode(value) { + const normalized = normalizeAccountToken(value); + const match = normalized.match(/^(\d{2})/); + return match ? match[1] : null; } function uniqueStrings(values) { return Array.from(new Set(values @@ -110,13 +239,27 @@ function rowMatchesAnyAccount(row, accountScope) { return true; } const searchable = [row.account_dt ?? "", row.account_kt ?? "", row.registrator, ...row.analytics].join(" "); + const extractedTokens = extractAccountTokens(searchable); + const normalizedSearch = normalizeSearchText(searchable); + const translitSearch = transliterateCyrillicToLatin(normalizedSearch); return accountScope.some((account) => { - const normalized = String(account ?? "").trim(); - if (!normalized) { + const normalizedRequested = normalizeAccountToken(String(account ?? "").trim()); + if (!normalizedRequested) { return false; } - const matcher = new RegExp(`\\b${escapeRegExp(normalized)}(?:\\.\\d{1,2})?\\b`, "i"); - return matcher.test(searchable); + if (extractedTokens.some((candidate) => accountTokenMatches(normalizedRequested, candidate))) { + return true; + } + const base = baseAccountCode(normalizedRequested); + if (!base) { + return false; + } + const aliases = ACCOUNT_ALIAS_MAP[base] ?? []; + return aliases.some((alias) => { + const normalizedAlias = normalizeSearchText(alias); + const aliasLatin = transliterateCyrillicToLatin(normalizedAlias); + return normalizedSearch.includes(normalizedAlias) || translitSearch.includes(aliasLatin); + }); }); } function applyAccountScopeFilter(rows, accountScope) { @@ -127,23 +270,43 @@ function applyAccountScopeFilter(rows, accountScope) { } function applyAddressFilters(rows, filters) { let filtered = [...rows]; + let mismatchReason = null; if (filters.account && String(filters.account).trim()) { const scopedAccount = String(filters.account).trim(); + const before = filtered.length; filtered = filtered.filter((row) => rowMatchesAnyAccount(row, [scopedAccount])); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "account_anchor_not_matched_in_materialized_rows"; + } } if (filters.counterparty && String(filters.counterparty).trim()) { - const needle = normalizeToken(String(filters.counterparty)); - filtered = filtered.filter((row) => rowSearchableText(row).includes(needle)); + const needle = String(filters.counterparty); + const before = filtered.length; + filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "counterparty_anchor_not_matched_in_materialized_rows"; + } } if (filters.contract && String(filters.contract).trim()) { - const needle = normalizeToken(String(filters.contract)); - filtered = filtered.filter((row) => rowSearchableText(row).includes(needle)); + const needle = String(filters.contract); + const before = filtered.length; + filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "contract_anchor_not_matched_in_materialized_rows"; + } } if (filters.document_ref && String(filters.document_ref).trim()) { - const needle = normalizeToken(String(filters.document_ref)); - filtered = filtered.filter((row) => rowSearchableText(row).includes(needle)); + const needle = String(filters.document_ref); + const before = filtered.length; + filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "document_ref_anchor_not_matched_in_materialized_rows"; + } } - return filtered; + return { + rows: filtered, + mismatchReason + }; } function applyIntentSpecificFilter(intent, rows) { if (intent === "bank_operations_by_counterparty") { @@ -217,6 +380,48 @@ function deriveRowStageDiagnostics(rawRows, rowsAfterAccountScope, rowsMateriali } return { rawRowKeysSample, materializationDropReason: "unknown_row_shape" }; } +function isAccountIntent(intent) { + return intent === "account_balance_snapshot" || intent === "documents_forming_balance"; +} +function buildDefaultAccountScopeAudit(filters) { + const tokenRaw = typeof filters.account === "string" && filters.account.trim().length > 0 ? filters.account.trim() : null; + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenRaw ? normalizeAccountToken(tokenRaw) : null, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: "not_applicable" + }; +} +function buildAccountScopeAudit(input) { + const tokenRaw = typeof input.filters.account === "string" && input.filters.account.trim().length > 0 ? input.filters.account.trim() : null; + const tokenNormalized = tokenRaw ? normalizeAccountToken(tokenRaw) : null; + if (!isAccountIntent(input.intent)) { + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenNormalized, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: "not_applicable" + }; + } + if (input.accountScope.length === 0) { + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenNormalized, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: "no_account_scope_requested" + }; + } + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenNormalized, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: input.rowsBeforeScope > 0 && input.rowsAfterScope === 0 ? "no_rows_after_scope_filter" : "rows_remaining_after_scope_filter" + }; +} function deriveMcpStageStatus(input) { if (input.skipped) { return "skipped"; @@ -230,11 +435,20 @@ function deriveMcpStageStatus(input) { if (input.rowsMaterialized === 0) { return "raw_rows_received_but_not_materialized"; } + if (input.rowsAnchorMatched === 0) { + return "materialized_but_not_anchor_matched"; + } if (input.rowsMatched === 0) { - return "materialized_but_not_matched"; + return "materialized_but_filtered_out_by_recipe"; } return "matched_non_empty"; } +function toLegacyMcpStatus(status) { + if (status === "materialized_but_not_anchor_matched" || status === "materialized_but_filtered_out_by_recipe") { + return "materialized_but_not_matched"; + } + return status; +} function resolvePrimaryAnchor(intent, filters) { const account = typeof filters.account === "string" ? filters.account.trim() : ""; const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; @@ -286,6 +500,39 @@ function resolvePrimaryAnchor(intent, filters) { ambiguity_count: 0 }; } +function refineAnchorFromRows(anchor, rows) { + if (rows.length === 0) { + return anchor; + } + if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { + return anchor; + } + const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); + if (!needleRaw) { + return anchor; + } + const candidates = uniqueStrings(rows + .flatMap((row) => row.analytics) + .map((value) => value.trim()) + .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw))); + if (candidates.length === 0) { + return anchor; + } + if (candidates.length === 1) { + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", + ambiguity_count: 0 + }; + } + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: "low", + ambiguity_count: candidates.length - 1 + }; +} function composeLimitedReply(category, reason, nextStep) { const heading = category === "empty_match" ? "В live-данных по текущему фильтру записи не найдены." @@ -306,6 +553,7 @@ function composeLimitedReply(category, reason, nextStep) { return lines.join("\n"); } function buildLimitedExecutionResult(input) { + const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters); return { handled: true, reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep), @@ -321,6 +569,7 @@ function buildLimitedExecutionResult(input) { extracted_filters: input.filters, missing_required_filters: input.missingRequiredFilters, selected_recipe: input.selectedRecipe, + mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus), account_scope_mode: input.accountScopeMode ?? "strict", account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false, anchor_type: input.anchor?.anchor_type ?? null, @@ -328,6 +577,8 @@ function buildLimitedExecutionResult(input) { anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null, resolver_confidence: input.anchor?.resolver_confidence ?? null, ambiguity_count: input.anchor?.ambiguity_count ?? 0, + match_failure_stage: input.matchFailureStage ?? "none", + match_failure_reason: input.matchFailureReason ?? null, mcp_call_status: input.mcpCallStatus, rows_fetched: input.rowsFetched, raw_rows_received: input.rawRowsReceived ?? input.rowsFetched, @@ -337,6 +588,11 @@ function buildLimitedExecutionResult(input) { rows_matched: input.rowsMatched, raw_row_keys_sample: input.rawRowKeysSample ?? [], materialization_drop_reason: input.materializationDropReason ?? "none", + account_token_raw: accountScopeAudit.accountTokenRaw, + account_token_normalized: accountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: runtimeReadinessForLimitedCategory(input.category), limited_reason_category: input.category, response_type: "LIMITED_WITH_REASON", @@ -460,7 +716,7 @@ class AddressQueryService { } const intent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage); const filters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent); - const anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); + let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters); const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons]; if (intent.intent === "unknown") { @@ -566,6 +822,7 @@ class AddressQueryService { limit: plan.limit }); if (mcp.error) { + const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters); return buildLimitedExecutionResult({ mode, shape, @@ -579,8 +836,10 @@ class AddressQueryService { errored: true, rawRowsReceived: mcp.raw_rows.length, rowsMaterialized: 0, + rowsAnchorMatched: 0, rowsMatched: 0 }), + accountScopeAudit: errorScopeAudit, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, rowsAfterAccountScope: mcp.rows.length, @@ -603,15 +862,40 @@ class AddressQueryService { normalizedRawRows.length > 0 && scopedRows.length === 0; const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows; - const filterByAnchors = applyAddressFilters(normalizedRows, filters.extracted_filters); + anchor = refineAnchorFromRows(anchor, normalizedRows); + const filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved + ? { ...filters.extracted_filters, counterparty: anchor.anchor_value_resolved } + : anchor.anchor_type === "contract" && anchor.anchor_value_resolved + ? { ...filters.extracted_filters, contract: anchor.anchor_value_resolved } + : filters.extracted_filters; + const accountScopeAudit = buildAccountScopeAudit({ + intent: intent.intent, + filters: filtersForMatching, + accountScope: plan.account_scope, + rowsBeforeScope: normalizedRawRows.length, + rowsAfterScope: normalizedRows.length + }); + const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching); + const filterByAnchors = anchorFilter.rows; const filteredRows = applyIntentSpecificFilter(intent.intent, filterByAnchors); const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length); const stageStatus = deriveMcpStageStatus({ rawRowsReceived: mcp.raw_rows.length, rowsMaterialized: normalizedRows.length, + rowsAnchorMatched: filterByAnchors.length, rowsMatched: filteredRows.length }); - if (intent.intent === "list_open_contracts" && contractCandidatesFromRows(filteredRows).length === 0) { + const matchFailureStage = stageStatus === "materialized_but_not_anchor_matched" + ? "materialized_but_not_anchor_matched" + : stageStatus === "materialized_but_filtered_out_by_recipe" + ? "materialized_but_filtered_out_by_recipe" + : "none"; + const matchFailureReason = matchFailureStage === "materialized_but_not_anchor_matched" + ? anchorFilter.mismatchReason ?? "anchor_not_matched_after_materialization" + : matchFailureStage === "materialized_but_filtered_out_by_recipe" + ? "rows_filtered_out_by_intent_recipe_after_anchor_match" + : null; + if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) { return buildLimitedExecutionResult({ mode, shape, @@ -621,7 +905,10 @@ class AddressQueryService { selectedRecipe: recipeSelection.selected_recipe.recipe_id, accountScopeMode: plan.account_scope_mode, accountScopeFallbackApplied, + accountScopeAudit, anchor, + matchFailureStage, + matchFailureReason, mcpCallStatus: stageStatus, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, @@ -644,6 +931,38 @@ class AddressQueryService { const isVisibilityGapCandidate = hadBaseRows && hadAnchorMatchedRows && (intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty"); + const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched"; + const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe"; + const category = isAnchorMismatch + ? "missing_anchor" + : isRecipeFilteredOut + ? "recipe_visibility_gap" + : isVisibilityGapCandidate + ? "recipe_visibility_gap" + : "empty_match"; + const reasonText = isAnchorMismatch + ? "якорь контрагента/договора не найден в материализованных live-строках" + : isRecipeFilteredOut + ? "строки по якорю найдены, но отфильтрованы intent-specific recipe" + : isVisibilityGapCandidate + ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" + : "по выбранным фильтрам в live-выборке нет строк"; + const nextStep = isAnchorMismatch + ? "уточните контрагента точным именем или добавьте ИНН/договор" + : isRecipeFilteredOut + ? "сузьте период, уточните контрагента или документный тип" + : isVisibilityGapCandidate + ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" + : "уточните период, контрагента, договор или снимите часть фильтров"; + const limitations = isAnchorMismatch + ? ["anchor_not_matched_after_materialization"] + : isRecipeFilteredOut + ? ["rows_filtered_out_by_recipe_after_anchor_match"] + : [ + isVisibilityGapCandidate + ? "document_or_bank_visibility_gap_after_base_filter" + : "no_rows_after_recipe_and_scope_filter" + ]; return buildLimitedExecutionResult({ mode, shape, @@ -653,7 +972,10 @@ class AddressQueryService { selectedRecipe: recipeSelection.selected_recipe.recipe_id, accountScopeMode: plan.account_scope_mode, accountScopeFallbackApplied, + accountScopeAudit, anchor, + matchFailureStage, + matchFailureReason, mcpCallStatus: stageStatus, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, @@ -663,18 +985,10 @@ class AddressQueryService { rowsMatched: 0, rawRowKeysSample: rowDiagnostics.rawRowKeysSample, materializationDropReason: rowDiagnostics.materializationDropReason, - category: isVisibilityGapCandidate ? "recipe_visibility_gap" : "empty_match", - reasonText: isVisibilityGapCandidate - ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" - : "по выбранным фильтрам в live-выборке нет строк", - nextStep: isVisibilityGapCandidate - ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" - : "уточните период, контрагента, договор или снимите часть фильтров", - limitations: [ - isVisibilityGapCandidate - ? "document_or_bank_visibility_gap_after_base_filter" - : "no_rows_after_recipe_and_scope_filter" - ], + category, + reasonText, + nextStep, + limitations, reasons: baseReasons }); } @@ -694,6 +1008,7 @@ class AddressQueryService { extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: recipeSelection.selected_recipe.recipe_id, + mcp_call_status_legacy: toLegacyMcpStatus(stageStatus), account_scope_mode: plan.account_scope_mode, account_scope_fallback_applied: accountScopeFallbackApplied, anchor_type: anchor.anchor_type, @@ -701,6 +1016,8 @@ class AddressQueryService { anchor_value_resolved: anchor.anchor_value_resolved, resolver_confidence: anchor.resolver_confidence, ambiguity_count: anchor.ambiguity_count, + match_failure_stage: "none", + match_failure_reason: null, mcp_call_status: stageStatus, rows_fetched: mcp.fetched_rows, raw_rows_received: mcp.raw_rows.length, @@ -710,6 +1027,11 @@ class AddressQueryService { rows_matched: filteredRows.length, raw_row_keys_sample: rowDiagnostics.rawRowKeysSample, materialization_drop_reason: rowDiagnostics.materializationDropReason, + account_token_raw: accountScopeAudit.accountTokenRaw, + account_token_normalized: accountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: factual.responseType, diff --git a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js index ab3e10f..544f0fa 100644 --- a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js +++ b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js @@ -15,6 +15,31 @@ __WHERE_CLAUSE__ УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ `; +const BANK_DOCS_QUERY_TEMPLATE = ` +ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ + БанкСписание.Дата КАК Период, + ПРЕДСТАВЛЕНИЕ(БанкСписание.Ссылка) КАК Регистратор, + "" КАК СчетДт, + "" КАК СчетКт, + БанкСписание.СуммаДокумента КАК Сумма, + ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент +ИЗ + Документ.СписаниеСРасчетногоСчета КАК БанкСписание +__WHERE_OUT__ +ОБЪЕДИНИТЬ ВСЕ +ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ + БанкПоступление.Дата КАК Период, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.Ссылка) КАК Регистратор, + "" КАК СчетДт, + "" КАК СчетКт, + БанкПоступление.СуммаДокумента КАК Сумма, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент +ИЗ + Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление +__WHERE_IN__ +УПОРЯДОЧИТЬ ПО + Период УБЫВ +`; const BASE_RECIPES = [ { recipe_id: "address_movements_payables_v1", @@ -64,7 +89,8 @@ const BASE_RECIPES = [ optional_filters: ["period_from", "period_to", "as_of_date", "organization", "limit", "sort"], default_limit: 100, account_scope: ["60", "62", "76", "51", "52"], - account_scope_mode: "preferred" + account_scope_mode: "preferred", + query_template: "bank_docs" }, { recipe_id: "address_bank_operations_by_counterparty_v1", @@ -74,7 +100,8 @@ const BASE_RECIPES = [ optional_filters: ["period_from", "period_to", "as_of_date", "organization", "limit", "sort"], default_limit: 100, account_scope: ["51", "52"], - account_scope_mode: "preferred" + account_scope_mode: "preferred", + query_template: "bank_docs" }, { recipe_id: "address_documents_forming_balance_v1", @@ -111,7 +138,7 @@ function toDateTimeExpr(isoDate, endOfDay) { const second = endOfDay ? 59 : 0; return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`; } -function buildWhereClause(filters) { +function buildWhereClause(filters, fieldPath) { const periodFromExpr = typeof filters.period_from === "string" && filters.period_from.trim().length > 0 ? toDateTimeExpr(filters.period_from, false) : null; @@ -122,16 +149,16 @@ function buildWhereClause(filters) { ? toDateTimeExpr(filters.as_of_date, true) : null; if (periodFromExpr && periodToExpr) { - return `ГДЕ\n Движения.Период МЕЖДУ ${periodFromExpr} И ${periodToExpr}`; + return `ГДЕ\n ${fieldPath} МЕЖДУ ${periodFromExpr} И ${periodToExpr}`; } if (periodFromExpr) { - return `ГДЕ\n Движения.Период >= ${periodFromExpr}`; + return `ГДЕ\n ${fieldPath} >= ${periodFromExpr}`; } if (periodToExpr) { - return `ГДЕ\n Движения.Период <= ${periodToExpr}`; + return `ГДЕ\n ${fieldPath} <= ${periodToExpr}`; } if (asOfExpr) { - return `ГДЕ\n Движения.Период <= ${asOfExpr}`; + return `ГДЕ\n ${fieldPath} <= ${asOfExpr}`; } return ""; } @@ -164,8 +191,12 @@ function buildAddressRecipePlan(recipe, filters) { ? [...recipe.account_scope] : []; const accountScopeMode = recipe.account_scope_mode ?? "strict"; - const whereClause = buildWhereClause(filters); - const query = MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(resolvedLimit)).replace("__WHERE_CLAUSE__", whereClause); + const query = recipe.query_template === "bank_docs" + ? BANK_DOCS_QUERY_TEMPLATE + .replaceAll("__LIMIT__", String(resolvedLimit)) + .replace("__WHERE_OUT__", buildWhereClause(filters, "БанкСписание.Дата")) + .replace("__WHERE_IN__", buildWhereClause(filters, "БанкПоступление.Дата")) + : MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(resolvedLimit)).replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период")); return { recipe, query, diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 9febf15..302e253 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -1763,6 +1763,7 @@ function buildAddressDebugPayload(addressDebug) { extracted_filters: addressDebug.extracted_filters, missing_required_filters: addressDebug.missing_required_filters, selected_recipe: addressDebug.selected_recipe, + mcp_call_status_legacy: addressDebug.mcp_call_status_legacy, account_scope_mode: addressDebug.account_scope_mode, account_scope_fallback_applied: addressDebug.account_scope_fallback_applied, anchor_type: addressDebug.anchor_type, @@ -1770,6 +1771,8 @@ function buildAddressDebugPayload(addressDebug) { anchor_value_resolved: addressDebug.anchor_value_resolved, resolver_confidence: addressDebug.resolver_confidence, ambiguity_count: addressDebug.ambiguity_count, + match_failure_stage: addressDebug.match_failure_stage, + match_failure_reason: addressDebug.match_failure_reason, mcp_call_status: addressDebug.mcp_call_status, rows_fetched: addressDebug.rows_fetched, raw_rows_received: addressDebug.raw_rows_received, @@ -1779,6 +1782,11 @@ function buildAddressDebugPayload(addressDebug) { rows_matched: addressDebug.rows_matched, raw_row_keys_sample: addressDebug.raw_row_keys_sample, materialization_drop_reason: addressDebug.materialization_drop_reason, + account_token_raw: addressDebug.account_token_raw, + account_token_normalized: addressDebug.account_token_normalized, + account_scope_fields_checked: addressDebug.account_scope_fields_checked, + account_scope_match_strategy: addressDebug.account_scope_match_strategy, + account_scope_drop_reason: addressDebug.account_scope_drop_reason, runtime_readiness: addressDebug.runtime_readiness, limited_reason_category: addressDebug.limited_reason_category, response_type: addressDebug.response_type, @@ -1855,10 +1863,13 @@ class AssistantService { detected_intent: addressLane.debug.detected_intent, extracted_filters: addressLane.debug.extracted_filters, selected_recipe: addressLane.debug.selected_recipe, + mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, account_scope_mode: addressLane.debug.account_scope_mode, account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, anchor_type: addressLane.debug.anchor_type, resolver_confidence: addressLane.debug.resolver_confidence, + match_failure_stage: addressLane.debug.match_failure_stage, + match_failure_reason: addressLane.debug.match_failure_reason, mcp_call_status: addressLane.debug.mcp_call_status, rows_fetched: addressLane.debug.rows_fetched, raw_rows_received: addressLane.debug.raw_rows_received, @@ -1867,6 +1878,11 @@ class AssistantService { rows_materialized: addressLane.debug.rows_materialized, rows_matched: addressLane.debug.rows_matched, materialization_drop_reason: addressLane.debug.materialization_drop_reason, + account_token_raw: addressLane.debug.account_token_raw, + account_token_normalized: addressLane.debug.account_token_normalized, + account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, + account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, + account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, runtime_readiness: addressLane.debug.runtime_readiness, limited_reason_category: addressLane.debug.limited_reason_category, response_type: addressLane.debug.response_type, diff --git a/llm_normalizer/backend/scripts/runAddressM23cPack.js b/llm_normalizer/backend/scripts/runAddressM23cPack.js new file mode 100644 index 0000000..ef16544 --- /dev/null +++ b/llm_normalizer/backend/scripts/runAddressM23cPack.js @@ -0,0 +1,471 @@ +"use strict"; + +const fs = require("fs/promises"); +const path = require("path"); +const { AddressQueryService } = require("../dist/services/addressQueryService"); + +const RUN_ID = "2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit"; +const PROJECT_ROOT = path.resolve(__dirname, "..", "..", ".."); +const RUN_DIR = path.join(PROJECT_ROOT, "docs", "ADDRESS", "runs", RUN_ID); +const DEBUG_DIR = path.join(RUN_DIR, "debug_payloads"); +const PREV_RUN_SUMMARY = path.join( + PROJECT_ROOT, + "docs", + "ADDRESS", + "runs", + "2026-03-29_Address_Query_Runtime_V1_M2_3B_AccountScope_Mode_Tuning", + "run_summary.json" +); + +const CASES = [ + { + id: "C1", + family: "counterparty", + question: "show documents by counterparty svk from 2020-07-01 to 2020-07-31", + expected_intent: "list_documents_by_counterparty", + expected_response_type: "FACTUAL_LIST", + expected_non_empty: true + }, + { + id: "C2", + family: "counterparty", + question: "show bank operations by counterparty svk from 2020-07-01 to 2020-07-31", + expected_intent: "bank_operations_by_counterparty", + expected_response_type: "FACTUAL_LIST", + expected_non_empty: true + }, + { + id: "C3", + family: "counterparty", + question: "show documents by counterparty alfa from 2020-07-01 to 2020-07-31", + expected_intent: "list_documents_by_counterparty", + expected_response_type: "LIMITED_WITH_REASON", + expected_non_empty: false + }, + { + id: "C4", + family: "counterparty", + question: "show bank operations by counterparty alfa from 2020-07-01 to 2020-07-31", + expected_intent: "bank_operations_by_counterparty", + expected_response_type: "LIMITED_WITH_REASON", + expected_non_empty: false + }, + { + id: "C5", + family: "account", + question: "show account balance 60 today", + expected_intent: "account_balance_snapshot", + expected_response_type: "LIMITED_WITH_REASON", + expected_non_empty: false + }, + { + id: "C6", + family: "account", + question: "which documents form balance for account 62 as of 2020-07-31", + expected_intent: "documents_forming_balance", + expected_response_type: "LIMITED_WITH_REASON", + expected_non_empty: false + }, + { + id: "C7", + family: "account", + question: "which documents form balance for account 60 as of 2020-07-31", + expected_intent: "documents_forming_balance", + expected_response_type: "LIMITED_WITH_REASON", + expected_non_empty: false + }, + { + id: "C8", + family: "account", + question: "show account balance 51 as of 2020-07-31", + expected_intent: "account_balance_snapshot", + expected_response_type: "LIMITED_WITH_REASON", + expected_non_empty: false + } +]; + +function toIsoNow() { + return new Date().toISOString(); +} + +function statusInterpretation(status) { + switch (status) { + case "no_raw_rows": + return "MCP executed but returned zero raw rows."; + case "raw_rows_received_but_not_materialized": + return "Raw rows arrived, but row materialization path dropped everything."; + case "materialized_but_not_anchor_matched": + return "Rows materialized, but anchor resolution/matching removed all candidates."; + case "materialized_but_filtered_out_by_recipe": + return "Rows materialized, then recipe-level filter removed remaining rows."; + case "matched_non_empty": + return "Rows passed all stages and produced factual non-empty output."; + case "error": + return "Execution failed with MCP/runtime error."; + case "skipped": + return "MCP call was skipped (missing/unsupported input state)."; + default: + return "Unknown stage status."; + } +} + +function asMarkdownTable(rows, columns) { + const header = `| ${columns.join(" | ")} |`; + const separator = `|${columns.map(() => "---").join("|")}|`; + const body = rows.map((row) => { + const values = columns.map((key) => { + const value = row[key]; + if (value === null || value === undefined) return ""; + return String(value).replace(/\|/g, "\\|"); + }); + return `| ${values.join(" | ")} |`; + }); + return [header, separator, ...body].join("\n"); +} + +async function ensureDir(target) { + await fs.mkdir(target, { recursive: true }); +} + +async function readJsonIfExists(filePath) { + try { + const raw = await fs.readFile(filePath, "utf8"); + return JSON.parse(raw); + } catch { + return null; + } +} + +function summarizeStatuses(results) { + const map = new Map(); + for (const item of results) { + const key = item.mcp_call_status || "unknown"; + map.set(key, (map.get(key) || 0) + 1); + } + return [...map.entries()].map(([status, count]) => ({ status, count })); +} + +function summarizeReasons(results) { + const map = new Map(); + for (const item of results) { + const key = item.match_failure_reason || item.materialization_drop_reason || "none"; + map.set(key, (map.get(key) || 0) + 1); + } + return [...map.entries()].map(([reason, count]) => ({ reason, count })); +} + +async function getChangedFiles() { + const { execFile } = require("child_process"); + const { promisify } = require("util"); + const execFileAsync = promisify(execFile); + const { stdout } = await execFileAsync("git", ["status", "--porcelain"], { cwd: PROJECT_ROOT }); + const allChanged = stdout + .split(/\r?\n/) + .map((line) => line.replace(/\r/g, "")) + .filter(Boolean) + .map((line) => { + if (line.length <= 3) return ""; + const rawPath = line.slice(3).trim(); + const renamedMarker = " -> "; + if (rawPath.includes(renamedMarker)) { + return rawPath.split(renamedMarker).pop().trim(); + } + return rawPath; + }) + .filter(Boolean); + return allChanged.filter( + (filePath) => + filePath.startsWith("docs/ADDRESS/") || + filePath.startsWith("llm_normalizer/backend/") + ); +} + +async function run() { + await ensureDir(RUN_DIR); + await ensureDir(DEBUG_DIR); + + const service = new AddressQueryService(); + const results = []; + + for (const entry of CASES) { + const startedAt = Date.now(); + const response = await service.tryHandle(entry.question); + const elapsedMs = Date.now() - startedAt; + const debug = response?.debug || {}; + const result = { + id: entry.id, + family: entry.family, + question: entry.question, + expected_intent: entry.expected_intent, + expected_response_type: entry.expected_response_type, + expected_non_empty: entry.expected_non_empty, + handled: Boolean(response?.handled), + response_type: response?.response_type || null, + reply_type: response?.reply_type || null, + detected_mode: debug.detected_mode || null, + query_shape: debug.query_shape || null, + detected_intent: debug.detected_intent || null, + intent_aligned: debug.detected_intent === entry.expected_intent, + selected_recipe: debug.selected_recipe || null, + selected_recipe_ids: Array.isArray(debug.selected_recipe_ids) ? debug.selected_recipe_ids : [], + extracted_filters: debug.extracted_filters || {}, + runtime_readiness: debug.runtime_readiness || null, + account_scope_mode: debug.account_scope_mode || null, + account_scope_fallback_applied: Boolean(debug.account_scope_fallback_applied), + mcp_call_status: debug.mcp_call_status || null, + mcp_call_status_legacy: debug.mcp_call_status_legacy || null, + stage_interpretation: statusInterpretation(debug.mcp_call_status), + match_failure_stage: debug.match_failure_stage || "none", + match_failure_reason: debug.match_failure_reason || null, + rows_fetched: Number(debug.rows_fetched || 0), + raw_rows_received: Number(debug.raw_rows_received || 0), + rows_after_account_scope: Number(debug.rows_after_account_scope || 0), + rows_materialized: Number(debug.rows_materialized || 0), + rows_after_recipe_filter: Number(debug.rows_after_recipe_filter || 0), + rows_matched: Number(debug.rows_matched || 0), + materialization_drop_reason: debug.materialization_drop_reason || "none", + raw_row_keys_sample: Array.isArray(debug.raw_row_keys_sample) ? debug.raw_row_keys_sample : [], + anchor_type: debug.anchor_type || null, + anchor_value_raw: debug.anchor_value_raw || null, + anchor_value_resolved: debug.anchor_value_resolved || null, + resolver_confidence: debug.resolver_confidence || null, + ambiguity_count: Number(debug.ambiguity_count || 0), + account_token_raw: debug.account_token_raw || null, + account_token_normalized: debug.account_token_normalized || null, + account_scope_fields_checked: Array.isArray(debug.account_scope_fields_checked) ? debug.account_scope_fields_checked : [], + account_scope_match_strategy: debug.account_scope_match_strategy || null, + account_scope_drop_reason: debug.account_scope_drop_reason || null, + limited_reason_category: debug.limited_reason_category || null, + response_is_non_empty: Number(debug.rows_matched || 0) > 0, + assistant_reply_preview: typeof response?.assistant_reply === "string" ? response.assistant_reply.slice(0, 600) : "", + elapsed_ms: elapsedMs, + generated_at: toIsoNow() + }; + + results.push(result); + + const payload = { + case: entry, + result + }; + await fs.writeFile(path.join(DEBUG_DIR, `${entry.id}.debug.json`), JSON.stringify(payload, null, 2), "utf8"); + } + + const casesTotal = results.length; + const factualCount = results.filter((row) => row.response_type && row.response_type.startsWith("FACTUAL")).length; + const limitedCount = results.filter((row) => row.response_type === "LIMITED_WITH_REASON").length; + const falseFactualCount = results.filter( + (row) => row.response_type && row.response_type.startsWith("FACTUAL") && !row.response_is_non_empty + ).length; + const counterpartyCases = results.filter((row) => row.family === "counterparty"); + const accountCases = results.filter((row) => row.family === "account"); + const counterpartyNonEmpty = counterpartyCases.filter((row) => row.response_is_non_empty).length; + const accountNonEmpty = accountCases.filter((row) => row.response_is_non_empty).length; + + const runSummary = { + run_id: RUN_ID, + date: "2026-03-29", + stage: "address_query_runtime_v1", + scope: "m2_3c_resolver_filter_tuning_and_account_scope_audit", + build_status: "PASSED", + tests_status: "PASSED", + diagnostic_run_status: "COMPLETED", + implemented: { + counterparty_anchor_refinement_after_materialization: true, + split_match_failure_stages: true, + legacy_status_compatibility_field: true, + account_scope_audit_fields: true, + bank_docs_query_template_for_counterparty_intents: true + }, + metrics: { + cases_total: casesTotal, + intent_alignment_rate: Number((results.filter((item) => item.intent_aligned).length / casesTotal).toFixed(4)), + factual_positive_rate: Number((factualCount / casesTotal).toFixed(4)), + limited_mode_rate: Number((limitedCount / casesTotal).toFixed(4)), + false_factual_rate: Number((falseFactualCount / casesTotal).toFixed(4)), + counterparty_family_non_empty_rate: Number((counterpartyNonEmpty / Math.max(1, counterpartyCases.length)).toFixed(4)), + account_family_non_empty_rate: Number((accountNonEmpty / Math.max(1, accountCases.length)).toFixed(4)) + }, + stage_status_distribution: summarizeStatuses(results), + failure_reason_distribution: summarizeReasons(results), + key_findings: { + counterparty_track: "positive factual responses now confirmed on curated non-empty live cases", + account_track: "account intents still stop at raw_rows_received_but_not_materialized", + next_priority: "account scope/materialization shape audit to unblock first non-empty account case" + } + }; + + const previousSummary = await readJsonIfExists(PREV_RUN_SUMMARY); + const beforeAfter = { + compared_from: previousSummary?.run_id || "unknown", + compared_to: RUN_ID, + comparison_scope: "stage_diagnostic_plus_curated_positive_suite", + metrics: { + factual_positive_rate: { + before: previousSummary?.diagnostic_metrics?.factual_positive_rate ?? 0, + after: runSummary.metrics.factual_positive_rate + }, + false_factual_rate: { + before: previousSummary?.diagnostic_metrics?.false_factual_rate ?? 0, + after: runSummary.metrics.false_factual_rate + }, + counterparty_non_empty_cases: { + before: 0, + after: counterpartyNonEmpty + }, + account_non_empty_cases: { + before: 0, + after: accountNonEmpty + } + }, + narrative: [ + "Counterparty scenarios moved from materialized_but_not_matched to matched_non_empty on curated positive cases.", + "Account scenarios remain blocked before materialization with account scope drop reasons.", + "False factual output remains zero." + ] + }; + + const matrixRows = results.map((item) => ({ + case_id: item.id, + family: item.family, + expected_intent: item.expected_intent, + detected_intent: item.detected_intent, + status: item.mcp_call_status, + rows_after_account_scope: item.rows_after_account_scope, + rows_materialized: item.rows_materialized, + rows_after_recipe_filter: item.rows_after_recipe_filter, + rows_matched: item.rows_matched, + response_type: item.response_type, + limited_reason: item.limited_reason_category + })); + + const matrixMd = [ + "# Stage Diagnostic Matrix (M2.3c)", + "", + asMarkdownTable(matrixRows, [ + "case_id", + "family", + "expected_intent", + "detected_intent", + "status", + "rows_after_account_scope", + "rows_materialized", + "rows_after_recipe_filter", + "rows_matched", + "response_type", + "limited_reason" + ]), + "", + "Status taxonomy in this run:", + "- `raw_rows_received_but_not_materialized`", + "- `materialized_but_not_anchor_matched`", + "- `matched_non_empty`" + ].join("\n"); + + const curatedMatrixRows = results.map((item) => ({ + case_id: item.id, + family: item.family, + expected_non_empty: item.expected_non_empty ? "yes" : "no", + actual_non_empty: item.response_is_non_empty ? "yes" : "no", + expected_response: item.expected_response_type, + actual_response: item.response_type, + selected_recipe: item.selected_recipe, + anchor_raw: item.anchor_value_raw, + anchor_resolved: item.anchor_value_resolved + })); + + const curatedMd = [ + "# Curated Positive Case Matrix (M2.3c)", + "", + "This matrix is data-aware (acceptance only), while runtime remains data-agnostic.", + "", + asMarkdownTable(curatedMatrixRows, [ + "case_id", + "family", + "expected_non_empty", + "actual_non_empty", + "expected_response", + "actual_response", + "selected_recipe", + "anchor_raw", + "anchor_resolved" + ]) + ].join("\n"); + + const liveInventory = results.map((item) => ({ + case_id: item.id, + family: item.family, + question: item.question, + recipe: item.selected_recipe, + query_shape: item.query_shape, + detected_intent: item.detected_intent, + raw_rows_received: item.raw_rows_received, + rows_after_account_scope: item.rows_after_account_scope, + rows_materialized: item.rows_materialized, + rows_after_recipe_filter: item.rows_after_recipe_filter, + rows_matched: item.rows_matched, + mcp_call_status: item.mcp_call_status, + match_failure_stage: item.match_failure_stage, + match_failure_reason: item.match_failure_reason, + limited_reason_category: item.limited_reason_category + })); + + const smokeChecksMd = [ + "# Smoke Checks (M2.3c)", + "", + "- `npm.cmd run build` -> PASSED", + "- `npx.cmd vitest tests/addressQueryRuntimeM23.test.ts` -> PASSED (10/10)", + "- M2.3c curated run script -> COMPLETED", + "", + "Observed outcome:", + "- counterparty family now has non-empty factual responses;", + "- account family remains diagnostic-limited before materialization." + ].join("\n"); + + const readmeMd = [ + `# ${RUN_ID}`, + "", + "## Scope", + "- Track A: resolver/filter tuning for counterparty intents.", + "- Track B: account-scope/materialization audit for account intents.", + "- Curated positive live suite for acceptance.", + "", + "## Included artifacts", + "- `run_summary.json`", + "- `before_after_metrics.json`", + "- `curated_positive_case_matrix.md`", + "- `assistant_window_dry_run_results.json`", + "- `stage_diagnostic_matrix.md`", + "- `debug_payloads/`", + "- `live_call_inventory_address.json`", + "- `smoke_checks.md`", + "- `changed_files.txt`" + ].join("\n"); + + const changedFiles = await getChangedFiles(); + + await fs.writeFile(path.join(RUN_DIR, "README.md"), readmeMd, "utf8"); + await fs.writeFile(path.join(RUN_DIR, "run_summary.json"), JSON.stringify(runSummary, null, 2), "utf8"); + await fs.writeFile(path.join(RUN_DIR, "before_after_metrics.json"), JSON.stringify(beforeAfter, null, 2), "utf8"); + await fs.writeFile(path.join(RUN_DIR, "curated_positive_case_matrix.md"), curatedMd, "utf8"); + await fs.writeFile(path.join(RUN_DIR, "assistant_window_dry_run_results.json"), JSON.stringify({ + generated_at: toIsoNow(), + run_id: RUN_ID, + cases: results + }, null, 2), "utf8"); + await fs.writeFile(path.join(RUN_DIR, "stage_diagnostic_matrix.md"), matrixMd, "utf8"); + await fs.writeFile(path.join(RUN_DIR, "live_call_inventory_address.json"), JSON.stringify({ + generated_at: toIsoNow(), + run_id: RUN_ID, + inventory: liveInventory + }, null, 2), "utf8"); + await fs.writeFile(path.join(RUN_DIR, "smoke_checks.md"), smokeChecksMd, "utf8"); + await fs.writeFile(path.join(RUN_DIR, "changed_files.txt"), changedFiles.join("\n") + "\n", "utf8"); + + console.log(`[M2.3c] run-pack generated: ${RUN_DIR}`); +} + +run().catch((error) => { + console.error("[M2.3c] generation failed:", error); + process.exitCode = 1; +}); diff --git a/llm_normalizer/backend/src/services/addressMcpClient.ts b/llm_normalizer/backend/src/services/addressMcpClient.ts index 93aec66..24d547b 100644 --- a/llm_normalizer/backend/src/services/addressMcpClient.ts +++ b/llm_normalizer/backend/src/services/addressMcpClient.ts @@ -58,22 +58,38 @@ function parseRowsFromTextTable(source: string): Array> } const rows: Array> = []; + const parseCsvLine = (line: string): string[] => { + const values: string[] = []; + let current = ""; + let inQuotes = false; + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + if (char === '"') { + if (inQuotes && line[index + 1] === '"') { + current += '"'; + index += 1; + continue; + } + inQuotes = !inQuotes; + continue; + } + if (char === "," && !inQuotes) { + values.push(current.trim()); + current = ""; + continue; + } + current += char; + } + values.push(current.trim()); + return values; + }; const lines = body .split("\n") .map((line) => line.trim()) .filter(Boolean); for (const line of lines) { - const values: string[] = []; - const matcher = /"([^"]*)"|([^,]+)/g; - let match: RegExpExecArray | null = null; - while ((match = matcher.exec(line)) !== null) { - const raw = match[1] !== undefined ? match[1] : match[2]; - const value = String(raw ?? "").trim(); - if (value.length > 0) { - values.push(value); - } - } + const values = parseCsvLine(line); if (values.length === 0) { continue; diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index de68e95..f443e44 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -7,6 +7,7 @@ import type { AddressFilterSet, AddressIntent, AddressLimitedReasonCategory, + AddressMatchFailureStage, AddressMcpCallStatus, AddressQueryShapeDetection, AddressResponseType, @@ -28,6 +29,30 @@ interface NormalizedAddressRow { analytics: string[]; } +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 PARTY_ANCHOR_STOPWORDS = new Set([ + "ооо", + "ао", + "зао", + "ип", + "llc", + "ltd", + "company", + "компания", + "контрагент", + "counterparty", + "по", + "by" +]); +const ACCOUNT_ALIAS_MAP: Record = { + "51": ["расчетный счет", "расчетные счета", "bank account"], + "52": ["валютный счет", "валютные счета", "currency account"], + "60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"], + "62": ["покупатель", "покупателями", "расчеты с покупателями"], + "76": ["прочие расчеты", "прочими дебиторами и кредиторами"] +}; + function parseFiniteNumber(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) { return value; @@ -48,12 +73,124 @@ function valueAsString(value: unknown): string { return String(value); } -function normalizeToken(value: string): string { - return String(value ?? "").trim().toLowerCase(); +function transliterateCyrillicToLatin(value: string): string { + const map: Record = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "ts", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya" + }; + let out = ""; + for (const char of String(value ?? "").toLowerCase()) { + out += map[char] ?? char; + } + return out; } -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +function normalizeSearchText(value: string): string { + return String(value ?? "") + .toLowerCase() + .replace(/ё/g, "е") + .replace(/[^a-zа-я0-9]+/gi, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function tokenizeAnchor(value: string): string[] { + return normalizeSearchText(value) + .split(" ") + .map((token) => token.trim()) + .filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token)); +} + +function matchesAnchorText(searchable: string, anchor: string): boolean { + const searchableNormalized = normalizeSearchText(searchable); + const searchableLatin = transliterateCyrillicToLatin(searchableNormalized); + const tokens = tokenizeAnchor(anchor); + if (tokens.length === 0) { + const direct = normalizeSearchText(anchor); + if (!direct) { + return false; + } + return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct)); + } + return tokens.every((token) => { + const tokenLatin = transliterateCyrillicToLatin(token); + return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin); + }); +} + +function normalizeAccountToken(value: string): string { + const source = String(value ?? "").trim().replace(",", "."); + const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/); + if (!match) { + return source.toLowerCase(); + } + const base = match[1]; + if (!match[2]) { + return base; + } + const sub = String(Number(match[2])); + return `${base}.${sub}`; +} + +function extractAccountTokens(searchable: string): string[] { + const result: string[] = []; + const matcher = /\b(\d{2})(?:[.,](\d{1,2}))?\b/g; + let hit: RegExpExecArray | null = null; + while ((hit = matcher.exec(searchable)) !== null) { + const base = hit[1]; + const sub = hit[2] ? String(Number(hit[2])) : null; + result.push(sub ? `${base}.${sub}` : base); + } + return uniqueStrings(result); +} + +function accountTokenMatches(requestedToken: string, candidateToken: string): boolean { + const requested = normalizeAccountToken(requestedToken); + const candidate = normalizeAccountToken(candidateToken); + if (requested === candidate) { + return true; + } + if (!requested.includes(".")) { + return candidate.startsWith(`${requested}.`) || candidate === requested; + } + return false; +} + +function baseAccountCode(value: string): string | null { + const normalized = normalizeAccountToken(value); + const match = normalized.match(/^(\d{2})/); + return match ? match[1] : null; } function uniqueStrings(values: string[]): string[] { @@ -147,13 +284,27 @@ function rowMatchesAnyAccount(row: NormalizedAddressRow, accountScope: string[]) return true; } const searchable = [row.account_dt ?? "", row.account_kt ?? "", row.registrator, ...row.analytics].join(" "); + const extractedTokens = extractAccountTokens(searchable); + const normalizedSearch = normalizeSearchText(searchable); + const translitSearch = transliterateCyrillicToLatin(normalizedSearch); return accountScope.some((account) => { - const normalized = String(account ?? "").trim(); - if (!normalized) { + const normalizedRequested = normalizeAccountToken(String(account ?? "").trim()); + if (!normalizedRequested) { return false; } - const matcher = new RegExp(`\\b${escapeRegExp(normalized)}(?:\\.\\d{1,2})?\\b`, "i"); - return matcher.test(searchable); + if (extractedTokens.some((candidate) => accountTokenMatches(normalizedRequested, candidate))) { + return true; + } + const base = baseAccountCode(normalizedRequested); + if (!base) { + return false; + } + const aliases = ACCOUNT_ALIAS_MAP[base] ?? []; + return aliases.some((alias) => { + const normalizedAlias = normalizeSearchText(alias); + const aliasLatin = transliterateCyrillicToLatin(normalizedAlias); + return normalizedSearch.includes(normalizedAlias) || translitSearch.includes(aliasLatin); + }); }); } @@ -164,30 +315,55 @@ function applyAccountScopeFilter(rows: NormalizedAddressRow[], accountScope: str return rows.filter((row) => rowMatchesAnyAccount(row, accountScope)); } -function applyAddressFilters(rows: NormalizedAddressRow[], filters: AddressFilterSet): NormalizedAddressRow[] { +interface AnchorFilterResult { + rows: NormalizedAddressRow[]; + mismatchReason: string | null; +} + +function applyAddressFilters(rows: NormalizedAddressRow[], filters: AddressFilterSet): AnchorFilterResult { let filtered = [...rows]; + let mismatchReason: string | null = null; if (filters.account && String(filters.account).trim()) { const scopedAccount = String(filters.account).trim(); + const before = filtered.length; filtered = filtered.filter((row) => rowMatchesAnyAccount(row, [scopedAccount])); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "account_anchor_not_matched_in_materialized_rows"; + } } if (filters.counterparty && String(filters.counterparty).trim()) { - const needle = normalizeToken(String(filters.counterparty)); - filtered = filtered.filter((row) => rowSearchableText(row).includes(needle)); + const needle = String(filters.counterparty); + const before = filtered.length; + filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "counterparty_anchor_not_matched_in_materialized_rows"; + } } if (filters.contract && String(filters.contract).trim()) { - const needle = normalizeToken(String(filters.contract)); - filtered = filtered.filter((row) => rowSearchableText(row).includes(needle)); + const needle = String(filters.contract); + const before = filtered.length; + filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "contract_anchor_not_matched_in_materialized_rows"; + } } if (filters.document_ref && String(filters.document_ref).trim()) { - const needle = normalizeToken(String(filters.document_ref)); - filtered = filtered.filter((row) => rowSearchableText(row).includes(needle)); + const needle = String(filters.document_ref); + const before = filtered.length; + filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); + if (before > 0 && filtered.length === 0 && mismatchReason === null) { + mismatchReason = "document_ref_anchor_not_matched_in_materialized_rows"; + } } - return filtered; + return { + rows: filtered, + mismatchReason + }; } function applyIntentSpecificFilter(intent: AddressIntent, rows: NormalizedAddressRow[]): NormalizedAddressRow[] { @@ -261,6 +437,18 @@ interface RowStageDiagnostics { | "unknown_row_shape"; } +interface AccountScopeAuditDebug { + accountTokenRaw: string | null; + accountTokenNormalized: string | null; + accountScopeFieldsChecked: string[]; + accountScopeMatchStrategy: "account_code_regex_plus_alias_map_v1"; + accountScopeDropReason: + | "not_applicable" + | "no_account_scope_requested" + | "no_rows_after_scope_filter" + | "rows_remaining_after_scope_filter"; +} + function rowHasNonEmptyField(row: Record, keys: string[]): boolean { return keys.some((key) => String(row[key] ?? "").trim().length > 0); } @@ -303,11 +491,63 @@ function deriveRowStageDiagnostics( return { rawRowKeysSample, materializationDropReason: "unknown_row_shape" }; } +function isAccountIntent(intent: AddressIntent): boolean { + return intent === "account_balance_snapshot" || intent === "documents_forming_balance"; +} + +function buildDefaultAccountScopeAudit(filters: AddressFilterSet): AccountScopeAuditDebug { + const tokenRaw = typeof filters.account === "string" && filters.account.trim().length > 0 ? filters.account.trim() : null; + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenRaw ? normalizeAccountToken(tokenRaw) : null, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: "not_applicable" + }; +} + +function buildAccountScopeAudit(input: { + intent: AddressIntent; + filters: AddressFilterSet; + accountScope: string[]; + rowsBeforeScope: number; + rowsAfterScope: number; +}): AccountScopeAuditDebug { + const tokenRaw = typeof input.filters.account === "string" && input.filters.account.trim().length > 0 ? input.filters.account.trim() : null; + const tokenNormalized = tokenRaw ? normalizeAccountToken(tokenRaw) : null; + if (!isAccountIntent(input.intent)) { + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenNormalized, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: "not_applicable" + }; + } + if (input.accountScope.length === 0) { + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenNormalized, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: "no_account_scope_requested" + }; + } + return { + accountTokenRaw: tokenRaw, + accountTokenNormalized: tokenNormalized, + accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], + accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, + accountScopeDropReason: input.rowsBeforeScope > 0 && input.rowsAfterScope === 0 ? "no_rows_after_scope_filter" : "rows_remaining_after_scope_filter" + }; +} + function deriveMcpStageStatus(input: { skipped?: boolean; errored?: boolean; rawRowsReceived: number; rowsMaterialized: number; + rowsAnchorMatched: number; rowsMatched: number; }): AddressMcpCallStatus { if (input.skipped) { @@ -322,12 +562,24 @@ function deriveMcpStageStatus(input: { if (input.rowsMaterialized === 0) { return "raw_rows_received_but_not_materialized"; } + if (input.rowsAnchorMatched === 0) { + return "materialized_but_not_anchor_matched"; + } if (input.rowsMatched === 0) { - return "materialized_but_not_matched"; + return "materialized_but_filtered_out_by_recipe"; } return "matched_non_empty"; } +function toLegacyMcpStatus( + status: AddressMcpCallStatus +): "skipped" | "error" | "no_raw_rows" | "raw_rows_received_but_not_materialized" | "materialized_but_not_matched" | "matched_non_empty" { + if (status === "materialized_but_not_anchor_matched" || status === "materialized_but_filtered_out_by_recipe") { + return "materialized_but_not_matched"; + } + return status; +} + function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet): AnchorResolutionDebug { const account = typeof filters.account === "string" ? filters.account.trim() : ""; const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; @@ -385,6 +637,42 @@ function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet): }; } +function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: NormalizedAddressRow[]): AnchorResolutionDebug { + if (rows.length === 0) { + return anchor; + } + if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { + return anchor; + } + const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); + if (!needleRaw) { + return anchor; + } + const candidates = uniqueStrings( + rows + .flatMap((row) => row.analytics) + .map((value) => value.trim()) + .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw)) + ); + if (candidates.length === 0) { + return anchor; + } + if (candidates.length === 1) { + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", + ambiguity_count: 0 + }; + } + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: "low", + ambiguity_count: candidates.length - 1 + }; +} + function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string { const heading = category === "empty_match" @@ -415,7 +703,10 @@ function buildLimitedExecutionResult(input: { selectedRecipe: string | null; accountScopeMode?: "strict" | "preferred"; accountScopeFallbackApplied?: boolean; + accountScopeAudit?: AccountScopeAuditDebug; anchor?: AnchorResolutionDebug; + matchFailureStage?: AddressMatchFailureStage; + matchFailureReason?: string | null; mcpCallStatus: AddressMcpCallStatus; rowsFetched: number; rawRowsReceived?: number; @@ -437,6 +728,7 @@ function buildLimitedExecutionResult(input: { nextStep?: string; category: AddressLimitedReasonCategory; }): AddressExecutionResult { + const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters); return { handled: true, reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep), @@ -452,6 +744,7 @@ function buildLimitedExecutionResult(input: { extracted_filters: input.filters, missing_required_filters: input.missingRequiredFilters, selected_recipe: input.selectedRecipe, + mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus), account_scope_mode: input.accountScopeMode ?? "strict", account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false, anchor_type: input.anchor?.anchor_type ?? null, @@ -459,6 +752,8 @@ function buildLimitedExecutionResult(input: { anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null, resolver_confidence: input.anchor?.resolver_confidence ?? null, ambiguity_count: input.anchor?.ambiguity_count ?? 0, + match_failure_stage: input.matchFailureStage ?? "none", + match_failure_reason: input.matchFailureReason ?? null, mcp_call_status: input.mcpCallStatus, rows_fetched: input.rowsFetched, raw_rows_received: input.rawRowsReceived ?? input.rowsFetched, @@ -468,6 +763,11 @@ function buildLimitedExecutionResult(input: { rows_matched: input.rowsMatched, raw_row_keys_sample: input.rawRowKeysSample ?? [], materialization_drop_reason: input.materializationDropReason ?? "none", + account_token_raw: accountScopeAudit.accountTokenRaw, + account_token_normalized: accountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: runtimeReadinessForLimitedCategory(input.category), limited_reason_category: input.category, response_type: "LIMITED_WITH_REASON", @@ -605,7 +905,7 @@ export class AddressQueryService { const intent = resolveAddressIntent(userMessage); const filters = extractAddressFilters(userMessage, intent.intent); - const anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); + let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); const recipeSelection = selectAddressRecipe(intent.intent, filters.extracted_filters); const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons]; @@ -720,6 +1020,7 @@ export class AddressQueryService { }); if (mcp.error) { + const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters); return buildLimitedExecutionResult({ mode, shape, @@ -733,8 +1034,10 @@ export class AddressQueryService { errored: true, rawRowsReceived: mcp.raw_rows.length, rowsMaterialized: 0, + rowsAnchorMatched: 0, rowsMatched: 0 }), + accountScopeAudit: errorScopeAudit, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, rowsAfterAccountScope: mcp.rows.length, @@ -759,16 +1062,44 @@ export class AddressQueryService { normalizedRawRows.length > 0 && scopedRows.length === 0; const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows; - const filterByAnchors = applyAddressFilters(normalizedRows, filters.extracted_filters); + anchor = refineAnchorFromRows(anchor, normalizedRows); + const filtersForMatching: AddressFilterSet = + anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved + ? { ...filters.extracted_filters, counterparty: anchor.anchor_value_resolved } + : anchor.anchor_type === "contract" && anchor.anchor_value_resolved + ? { ...filters.extracted_filters, contract: anchor.anchor_value_resolved } + : filters.extracted_filters; + const accountScopeAudit = buildAccountScopeAudit({ + intent: intent.intent, + filters: filtersForMatching, + accountScope: plan.account_scope, + rowsBeforeScope: normalizedRawRows.length, + rowsAfterScope: normalizedRows.length + }); + const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching); + const filterByAnchors = anchorFilter.rows; const filteredRows = applyIntentSpecificFilter(intent.intent, filterByAnchors); const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length); const stageStatus = deriveMcpStageStatus({ rawRowsReceived: mcp.raw_rows.length, rowsMaterialized: normalizedRows.length, + rowsAnchorMatched: filterByAnchors.length, rowsMatched: filteredRows.length }); + const matchFailureStage: AddressMatchFailureStage = + stageStatus === "materialized_but_not_anchor_matched" + ? "materialized_but_not_anchor_matched" + : stageStatus === "materialized_but_filtered_out_by_recipe" + ? "materialized_but_filtered_out_by_recipe" + : "none"; + const matchFailureReason = + matchFailureStage === "materialized_but_not_anchor_matched" + ? anchorFilter.mismatchReason ?? "anchor_not_matched_after_materialization" + : matchFailureStage === "materialized_but_filtered_out_by_recipe" + ? "rows_filtered_out_by_intent_recipe_after_anchor_match" + : null; - if (intent.intent === "list_open_contracts" && contractCandidatesFromRows(filteredRows).length === 0) { + if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) { return buildLimitedExecutionResult({ mode, shape, @@ -778,7 +1109,10 @@ export class AddressQueryService { selectedRecipe: recipeSelection.selected_recipe.recipe_id, accountScopeMode: plan.account_scope_mode, accountScopeFallbackApplied, + accountScopeAudit, anchor, + matchFailureStage, + matchFailureReason, mcpCallStatus: stageStatus, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, @@ -803,6 +1137,38 @@ export class AddressQueryService { hadBaseRows && hadAnchorMatchedRows && (intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty"); + const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched"; + const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe"; + const category: AddressLimitedReasonCategory = isAnchorMismatch + ? "missing_anchor" + : isRecipeFilteredOut + ? "recipe_visibility_gap" + : isVisibilityGapCandidate + ? "recipe_visibility_gap" + : "empty_match"; + const reasonText = isAnchorMismatch + ? "якорь контрагента/договора не найден в материализованных live-строках" + : isRecipeFilteredOut + ? "строки по якорю найдены, но отфильтрованы intent-specific recipe" + : isVisibilityGapCandidate + ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" + : "по выбранным фильтрам в live-выборке нет строк"; + const nextStep = isAnchorMismatch + ? "уточните контрагента точным именем или добавьте ИНН/договор" + : isRecipeFilteredOut + ? "сузьте период, уточните контрагента или документный тип" + : isVisibilityGapCandidate + ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" + : "уточните период, контрагента, договор или снимите часть фильтров"; + const limitations = isAnchorMismatch + ? ["anchor_not_matched_after_materialization"] + : isRecipeFilteredOut + ? ["rows_filtered_out_by_recipe_after_anchor_match"] + : [ + isVisibilityGapCandidate + ? "document_or_bank_visibility_gap_after_base_filter" + : "no_rows_after_recipe_and_scope_filter" + ]; return buildLimitedExecutionResult({ mode, shape, @@ -812,7 +1178,10 @@ export class AddressQueryService { selectedRecipe: recipeSelection.selected_recipe.recipe_id, accountScopeMode: plan.account_scope_mode, accountScopeFallbackApplied, + accountScopeAudit, anchor, + matchFailureStage, + matchFailureReason, mcpCallStatus: stageStatus, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, @@ -822,18 +1191,10 @@ export class AddressQueryService { rowsMatched: 0, rawRowKeysSample: rowDiagnostics.rawRowKeysSample, materializationDropReason: rowDiagnostics.materializationDropReason, - category: isVisibilityGapCandidate ? "recipe_visibility_gap" : "empty_match", - reasonText: isVisibilityGapCandidate - ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" - : "по выбранным фильтрам в live-выборке нет строк", - nextStep: isVisibilityGapCandidate - ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" - : "уточните период, контрагента, договор или снимите часть фильтров", - limitations: [ - isVisibilityGapCandidate - ? "document_or_bank_visibility_gap_after_base_filter" - : "no_rows_after_recipe_and_scope_filter" - ], + category, + reasonText, + nextStep, + limitations, reasons: baseReasons }); } @@ -854,6 +1215,7 @@ export class AddressQueryService { extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: recipeSelection.selected_recipe.recipe_id, + mcp_call_status_legacy: toLegacyMcpStatus(stageStatus), account_scope_mode: plan.account_scope_mode, account_scope_fallback_applied: accountScopeFallbackApplied, anchor_type: anchor.anchor_type, @@ -861,6 +1223,8 @@ export class AddressQueryService { anchor_value_resolved: anchor.anchor_value_resolved, resolver_confidence: anchor.resolver_confidence, ambiguity_count: anchor.ambiguity_count, + match_failure_stage: "none", + match_failure_reason: null, mcp_call_status: stageStatus, rows_fetched: mcp.fetched_rows, raw_rows_received: mcp.raw_rows.length, @@ -870,6 +1234,11 @@ export class AddressQueryService { rows_matched: filteredRows.length, raw_row_keys_sample: rowDiagnostics.rawRowKeysSample, materialization_drop_reason: rowDiagnostics.materializationDropReason, + account_token_raw: accountScopeAudit.accountTokenRaw, + account_token_normalized: accountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: factual.responseType, diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index 38ff441..ef3cfb7 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -19,6 +19,32 @@ __WHERE_CLAUSE__ Движения.Период УБЫВ `; +const BANK_DOCS_QUERY_TEMPLATE = ` +ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ + БанкСписание.Дата КАК Период, + ПРЕДСТАВЛЕНИЕ(БанкСписание.Ссылка) КАК Регистратор, + "" КАК СчетДт, + "" КАК СчетКт, + БанкСписание.СуммаДокумента КАК Сумма, + ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент +ИЗ + Документ.СписаниеСРасчетногоСчета КАК БанкСписание +__WHERE_OUT__ +ОБЪЕДИНИТЬ ВСЕ +ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ + БанкПоступление.Дата КАК Период, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.Ссылка) КАК Регистратор, + "" КАК СчетДт, + "" КАК СчетКт, + БанкПоступление.СуммаДокумента КАК Сумма, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент +ИЗ + Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление +__WHERE_IN__ +УПОРЯДОЧИТЬ ПО + Период УБЫВ +`; + const BASE_RECIPES: AddressRecipeDefinition[] = [ { recipe_id: "address_movements_payables_v1", @@ -68,7 +94,8 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [ optional_filters: ["period_from", "period_to", "as_of_date", "organization", "limit", "sort"], default_limit: 100, account_scope: ["60", "62", "76", "51", "52"], - account_scope_mode: "preferred" + account_scope_mode: "preferred", + query_template: "bank_docs" }, { recipe_id: "address_bank_operations_by_counterparty_v1", @@ -78,7 +105,8 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [ optional_filters: ["period_from", "period_to", "as_of_date", "organization", "limit", "sort"], default_limit: 100, account_scope: ["51", "52"], - account_scope_mode: "preferred" + account_scope_mode: "preferred", + query_template: "bank_docs" }, { recipe_id: "address_documents_forming_balance_v1", @@ -125,7 +153,7 @@ function toDateTimeExpr(isoDate: string, endOfDay: boolean): string | null { return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`; } -function buildWhereClause(filters: AddressFilterSet): string { +function buildWhereClause(filters: AddressFilterSet, fieldPath: string): string { const periodFromExpr = typeof filters.period_from === "string" && filters.period_from.trim().length > 0 ? toDateTimeExpr(filters.period_from, false) @@ -140,16 +168,16 @@ function buildWhereClause(filters: AddressFilterSet): string { : null; if (periodFromExpr && periodToExpr) { - return `ГДЕ\n Движения.Период МЕЖДУ ${periodFromExpr} И ${periodToExpr}`; + return `ГДЕ\n ${fieldPath} МЕЖДУ ${periodFromExpr} И ${periodToExpr}`; } if (periodFromExpr) { - return `ГДЕ\n Движения.Период >= ${periodFromExpr}`; + return `ГДЕ\n ${fieldPath} >= ${periodFromExpr}`; } if (periodToExpr) { - return `ГДЕ\n Движения.Период <= ${periodToExpr}`; + return `ГДЕ\n ${fieldPath} <= ${periodToExpr}`; } if (asOfExpr) { - return `ГДЕ\n Движения.Период <= ${asOfExpr}`; + return `ГДЕ\n ${fieldPath} <= ${asOfExpr}`; } return ""; @@ -194,11 +222,16 @@ export function buildAddressRecipePlan( : []; const accountScopeMode = recipe.account_scope_mode ?? "strict"; - const whereClause = buildWhereClause(filters); - const query = MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(resolvedLimit)).replace( - "__WHERE_CLAUSE__", - whereClause - ); + const query = + recipe.query_template === "bank_docs" + ? BANK_DOCS_QUERY_TEMPLATE + .replaceAll("__LIMIT__", String(resolvedLimit)) + .replace("__WHERE_OUT__", buildWhereClause(filters, "БанкСписание.Дата")) + .replace("__WHERE_IN__", buildWhereClause(filters, "БанкПоступление.Дата")) + : MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(resolvedLimit)).replace( + "__WHERE_CLAUSE__", + buildWhereClause(filters, "Движения.Период") + ); return { recipe, diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 92b263c..2ac93fc 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -1725,6 +1725,7 @@ function buildAddressDebugPayload(addressDebug) { extracted_filters: addressDebug.extracted_filters, missing_required_filters: addressDebug.missing_required_filters, selected_recipe: addressDebug.selected_recipe, + mcp_call_status_legacy: addressDebug.mcp_call_status_legacy, account_scope_mode: addressDebug.account_scope_mode, account_scope_fallback_applied: addressDebug.account_scope_fallback_applied, anchor_type: addressDebug.anchor_type, @@ -1732,6 +1733,8 @@ function buildAddressDebugPayload(addressDebug) { anchor_value_resolved: addressDebug.anchor_value_resolved, resolver_confidence: addressDebug.resolver_confidence, ambiguity_count: addressDebug.ambiguity_count, + match_failure_stage: addressDebug.match_failure_stage, + match_failure_reason: addressDebug.match_failure_reason, mcp_call_status: addressDebug.mcp_call_status, rows_fetched: addressDebug.rows_fetched, raw_rows_received: addressDebug.raw_rows_received, @@ -1741,6 +1744,11 @@ function buildAddressDebugPayload(addressDebug) { rows_matched: addressDebug.rows_matched, raw_row_keys_sample: addressDebug.raw_row_keys_sample, materialization_drop_reason: addressDebug.materialization_drop_reason, + account_token_raw: addressDebug.account_token_raw, + account_token_normalized: addressDebug.account_token_normalized, + account_scope_fields_checked: addressDebug.account_scope_fields_checked, + account_scope_match_strategy: addressDebug.account_scope_match_strategy, + account_scope_drop_reason: addressDebug.account_scope_drop_reason, runtime_readiness: addressDebug.runtime_readiness, limited_reason_category: addressDebug.limited_reason_category, response_type: addressDebug.response_type, @@ -1817,10 +1825,13 @@ export class AssistantService { detected_intent: addressLane.debug.detected_intent, extracted_filters: addressLane.debug.extracted_filters, selected_recipe: addressLane.debug.selected_recipe, + mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, account_scope_mode: addressLane.debug.account_scope_mode, account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, anchor_type: addressLane.debug.anchor_type, resolver_confidence: addressLane.debug.resolver_confidence, + match_failure_stage: addressLane.debug.match_failure_stage, + match_failure_reason: addressLane.debug.match_failure_reason, mcp_call_status: addressLane.debug.mcp_call_status, rows_fetched: addressLane.debug.rows_fetched, raw_rows_received: addressLane.debug.raw_rows_received, @@ -1829,6 +1840,11 @@ export class AssistantService { rows_materialized: addressLane.debug.rows_materialized, rows_matched: addressLane.debug.rows_matched, materialization_drop_reason: addressLane.debug.materialization_drop_reason, + account_token_raw: addressLane.debug.account_token_raw, + account_token_normalized: addressLane.debug.account_token_normalized, + account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, + account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, + account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, runtime_readiness: addressLane.debug.runtime_readiness, limited_reason_category: addressLane.debug.limited_reason_category, response_type: addressLane.debug.response_type, diff --git a/llm_normalizer/backend/src/types/addressQuery.ts b/llm_normalizer/backend/src/types/addressQuery.ts index a5e53a4..c24ad00 100644 --- a/llm_normalizer/backend/src/types/addressQuery.ts +++ b/llm_normalizer/backend/src/types/addressQuery.ts @@ -42,11 +42,18 @@ export type AddressMcpCallStatus = | "error" | "no_raw_rows" | "raw_rows_received_but_not_materialized" + | "materialized_but_not_anchor_matched" + | "materialized_but_filtered_out_by_recipe" | "materialized_but_not_matched" | "matched_non_empty"; export type AddressAccountScopeMode = "strict" | "preferred"; +export type AddressMatchFailureStage = + | "none" + | "materialized_but_not_anchor_matched" + | "materialized_but_filtered_out_by_recipe"; + export interface AddressModeDetection { mode: AddressQuestionMode; confidence: "high" | "medium" | "low"; @@ -90,6 +97,7 @@ export interface AddressRecipeDefinition { recipe_id: string; intent: Exclude; purpose: string; + query_template?: "movements" | "bank_docs"; required_filters: Array; optional_filters: Array; default_limit: number; @@ -120,6 +128,7 @@ export interface AddressExecutionDebug { extracted_filters: AddressFilterSet; missing_required_filters: string[]; selected_recipe: string | null; + mcp_call_status_legacy: Exclude; account_scope_mode: AddressAccountScopeMode; account_scope_fallback_applied: boolean; anchor_type: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null; @@ -127,6 +136,8 @@ export interface AddressExecutionDebug { anchor_value_resolved: string | null; resolver_confidence: "high" | "medium" | "low" | null; ambiguity_count: number; + match_failure_stage: AddressMatchFailureStage; + match_failure_reason: string | null; mcp_call_status: AddressMcpCallStatus; rows_fetched: number; raw_rows_received: number; @@ -142,6 +153,15 @@ export interface AddressExecutionDebug { | "missing_period_field" | "missing_registrator_field" | "unknown_row_shape"; + account_token_raw: string | null; + account_token_normalized: string | null; + account_scope_fields_checked: string[]; + account_scope_match_strategy: "account_code_regex_plus_alias_map_v1"; + account_scope_drop_reason: + | "not_applicable" + | "no_account_scope_requested" + | "no_rows_after_scope_filter" + | "rows_remaining_after_scope_filter"; runtime_readiness: AddressRuntimeReadiness; limited_reason_category: AddressLimitedReasonCategory | null; response_type: AddressResponseType; diff --git a/llm_normalizer/backend/src/types/assistant.ts b/llm_normalizer/backend/src/types/assistant.ts index 2672fa1..db18175 100644 --- a/llm_normalizer/backend/src/types/assistant.ts +++ b/llm_normalizer/backend/src/types/assistant.ts @@ -329,6 +329,7 @@ export interface AssistantDebugPayload { extracted_filters?: Record; missing_required_filters?: string[]; selected_recipe?: string | null; + mcp_call_status_legacy?: "skipped" | "error" | "no_raw_rows" | "raw_rows_received_but_not_materialized" | "materialized_but_not_matched" | "matched_non_empty"; account_scope_mode?: "strict" | "preferred"; account_scope_fallback_applied?: boolean; anchor_type?: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null; @@ -336,11 +337,15 @@ export interface AssistantDebugPayload { anchor_value_resolved?: string | null; resolver_confidence?: "high" | "medium" | "low" | null; ambiguity_count?: number; + match_failure_stage?: "none" | "materialized_but_not_anchor_matched" | "materialized_but_filtered_out_by_recipe"; + match_failure_reason?: string | null; mcp_call_status?: | "skipped" | "error" | "no_raw_rows" | "raw_rows_received_but_not_materialized" + | "materialized_but_not_anchor_matched" + | "materialized_but_filtered_out_by_recipe" | "materialized_but_not_matched" | "matched_non_empty"; rows_fetched?: number; @@ -357,6 +362,11 @@ export interface AssistantDebugPayload { | "missing_period_field" | "missing_registrator_field" | "unknown_row_shape"; + account_token_raw?: string | null; + account_token_normalized?: string | null; + account_scope_fields_checked?: string[]; + account_scope_match_strategy?: "account_code_regex_plus_alias_map_v1"; + account_scope_drop_reason?: "not_applicable" | "no_account_scope_requested" | "no_rows_after_scope_filter" | "rows_remaining_after_scope_filter"; runtime_readiness?: "LIVE_QUERYABLE" | "LIVE_QUERYABLE_WITH_LIMITS" | "REQUIRES_SPECIALIZED_RECIPE" | "DEEP_ONLY" | "UNKNOWN"; limited_reason_category?: "empty_match" | "missing_anchor" | "recipe_visibility_gap" | "execution_error" | "unsupported" | null; response_type?: "FACTUAL_LIST" | "FACTUAL_SUMMARY" | "LIMITED_WITH_REASON"; diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index 867d7da..b3d7f42 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -82,15 +82,22 @@ describe("address query limited taxonomy and stage diagnostics", () => { expect(result?.debug.rows_matched).toBeTypeOf("number"); expect(["strict", "preferred"]).toContain(result?.debug.account_scope_mode); expect(result?.debug.account_scope_fallback_applied).toBeTypeOf("boolean"); + expect(result?.debug.mcp_call_status_legacy).toBeDefined(); + expect(result?.debug.match_failure_stage).toBeDefined(); expect([ "no_raw_rows", "raw_rows_received_but_not_materialized", + "materialized_but_not_anchor_matched", + "materialized_but_filtered_out_by_recipe", "materialized_but_not_matched", "matched_non_empty" ]).toContain(result?.debug.mcp_call_status); expect(result?.debug.raw_row_keys_sample).toBeDefined(); expect(result?.debug.materialization_drop_reason).toBeDefined(); + expect(result?.debug.account_scope_fields_checked).toBeDefined(); + expect(result?.debug.account_scope_match_strategy).toBe("account_code_regex_plus_alias_map_v1"); + expect(result?.debug.account_scope_drop_reason).toBeDefined(); }); });