АДРЕСНЫЙ РЕЖИМ -

This commit is contained in:
dctouch 2026-04-02 12:02:30 +03:00
parent da8a4eb872
commit 4dff069ae3
43 changed files with 7289 additions and 138 deletions

View File

@ -0,0 +1,5 @@
[
{ "id": "OC001", "text": "Покажи незакрытые договоры на 2020-12-31", "expected_intent": "list_open_contracts", "expected_mode": "address_query", "expected_reply_type": "factual" },
{ "id": "OC002", "text": "Есть ли долг по договору 15/24 на 2020-07-31", "expected_intent": "open_items_by_counterparty_or_contract", "expected_mode": "address_query" },
{ "id": "OC003", "text": "Show open contracts as of 2020-12-31", "expected_intent": "list_open_contracts", "expected_mode": "address_query", "expected_reply_type": "factual" }
]

View File

@ -0,0 +1,44 @@
[
{
"id": "OI001",
"text": "Покажи хвосты по контрагенту СВК на 2020-12-31",
"expected_intent": "open_items_by_counterparty_or_contract",
"expected_mode": "address_query"
},
{
"id": "OI002",
"text": "Покажи хвосты по контрагенту СВК на конец периода 2020-12-31",
"expected_intent": "open_items_by_counterparty_or_contract",
"expected_mode": "address_query"
},
{
"id": "OI003",
"text": "Есть ли незакрытые позиции по договору 15/24 на 2020-12-31",
"expected_intent": "open_items_by_counterparty_or_contract",
"expected_mode": "address_query"
},
{
"id": "OI004",
"text": "Покажи открытые позиции по договору 15/24",
"expected_intent": "open_items_by_counterparty_or_contract",
"expected_mode": "address_query"
},
{
"id": "OI005",
"text": "Покажи незакрытые договоры на 2020-12-31",
"expected_intent": "list_open_contracts",
"expected_mode": "address_query"
},
{
"id": "OI006",
"text": "Покажи документы по договору 15/24 за 2020",
"expected_intent": "list_documents_by_contract",
"expected_mode": "address_query"
},
{
"id": "OI007",
"text": "Покажи банковские операции по договору 15/24",
"expected_intent": "bank_operations_by_contract",
"expected_mode": "address_query"
}
]

View File

@ -0,0 +1,23 @@
# 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36
Generated at: 2026-04-02T11:17:54
Questions file: X:\1C\NDC_1C\docs\ADDRESS\question_sets\address_open_contracts_focus_2026-04-02.json
Backend URL: http://127.0.0.1:8787/api/assistant/message
LLM: local / qwen2.5-14b-instruct-1m @ http://127.0.0.1:1234
## Totals
- questions_total: 3
- ok_200_count: 3
- semantic_pass_count: 3
- semantic_pass_rate: 1.0
- factual_count: 2
- partial_coverage_count: 1
- clarification_required_count: 0
- http_error_count: 0
- llm_decomposition_applied_count: 2
- avg_elapsed_ms: 6046
## Files
- run_summary.json
- full_live_results.json
- failures_only.json

View File

@ -0,0 +1,139 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36",
"generated_at": "2026-04-02T11:17:54",
"summary": {
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36",
"generated_at": "2026-04-02T11:17:54",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_contracts_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"totals": {
"questions_total": 3,
"ok_200_count": 3,
"semantic_pass_count": 3,
"semantic_pass_rate": 1.0,
"strict_pass_count": 3,
"strict_pass_rate": 1.0,
"factual_count": 2,
"partial_coverage_count": 1,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 2,
"avg_elapsed_ms": 6046
},
"distributions": {
"reply_type": {
"factual": 2,
"partial_coverage": 1
},
"actual_intent": {
"list_open_contracts": 2,
"open_items_by_counterparty_or_contract": 1
},
"actual_mode": {
"address_query": 3
},
"mcp_call_status": {
"matched_non_empty": 2,
"materialized_but_not_anchor_matched": 1
},
"limited_reason_category": {
"empty_match": 1
}
}
},
"rows": [
{
"index": 1,
"id": "OC001",
"question": "Покажи незакрытые договоры на 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36-oc001",
"status_code": 200,
"ok": true,
"elapsed_ms": 6945,
"reply_type": "factual",
"trace_id": "address-LcdN7m1NSW",
"assistant_reply": "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).\nСтрок движения: 20.\nДоговорных кандидатов: 0.\nДоговорные якоря в live-строках не выделены; показаны связанные движения как fallback.\n1. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 127216.46\n2. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 235682.42\n3. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 337318.8\n4. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.09 / 91.02 | 10615989.53\n5. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.01 / 91.09 | 1614344.98\n6. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 90.09 / 90.02.1 | 6430415.34",
"expected_intent": "list_open_contracts",
"actual_intent": "list_open_contracts",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": "factual",
"reply_match": true,
"semantic_pass": true,
"strict_pass": true,
"mcp_call_status": "matched_non_empty",
"limited_reason_category": null,
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"error_code": null,
"error_message": null
},
{
"index": 2,
"id": "OC002",
"question": "Есть ли долг по договору 15/24 на 2020-07-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36-oc002",
"status_code": 200,
"ok": true,
"elapsed_ms": 6118,
"reply_type": "partial_coverage",
"trace_id": "address-p8eGdSMphA",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"strict_pass": true,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"error_code": null,
"error_message": null
},
{
"index": 3,
"id": "OC003",
"question": "Show open contracts as of 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36-oc003",
"status_code": 200,
"ok": true,
"elapsed_ms": 5075,
"reply_type": "factual",
"trace_id": "address-POqrwAVYkI",
"assistant_reply": "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).\nСтрок движения: 20.\nДоговорных кандидатов: 0.\nДоговорные якоря в live-строках не выделены; показаны связанные движения как fallback.\n1. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 127216.46\n2. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 235682.42\n3. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 337318.8\n4. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.09 / 91.02 | 10615989.53\n5. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.01 / 91.09 | 1614344.98\n6. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 90.09 / 90.02.1 | 6430415.34",
"expected_intent": "list_open_contracts",
"actual_intent": "list_open_contracts",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": "factual",
"reply_match": true,
"semantic_pass": true,
"strict_pass": true,
"mcp_call_status": "matched_non_empty",
"limited_reason_category": null,
"llm_decomposition_applied": false,
"llm_decomposition_reason": "normalized_fragment_rejected_intent_drop",
"fallback_rule_hit": null,
"error_code": null,
"error_message": null
}
]
}

View File

@ -0,0 +1,43 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-17-36",
"generated_at": "2026-04-02T11:17:54",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_contracts_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"totals": {
"questions_total": 3,
"ok_200_count": 3,
"semantic_pass_count": 3,
"semantic_pass_rate": 1.0,
"strict_pass_count": 3,
"strict_pass_rate": 1.0,
"factual_count": 2,
"partial_coverage_count": 1,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 2,
"avg_elapsed_ms": 6046
},
"distributions": {
"reply_type": {
"factual": 2,
"partial_coverage": 1
},
"actual_intent": {
"list_open_contracts": 2,
"open_items_by_counterparty_or_contract": 1
},
"actual_mode": {
"address_query": 3
},
"mcp_call_status": {
"matched_non_empty": 2,
"materialized_but_not_anchor_matched": 1
},
"limited_reason_category": {
"empty_match": 1
}
}
}

View File

@ -0,0 +1,28 @@
# 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30
Generated at: 2026-04-02T11:25:48
Questions file: X:\1C\NDC_1C\docs\ADDRESS\question_sets\address_open_contracts_focus_2026-04-02.json
Backend URL: http://127.0.0.1:8787/api/assistant/message
LLM: local / qwen2.5-14b-instruct-1m @ http://127.0.0.1:1234
Strict policy: route
## Totals
- questions_total: 3
- ok_200_count: 3
- semantic_pass_count: 3
- semantic_pass_rate: 1.0
- route_pass_count: 2
- route_pass_rate: 0.6667
- strict_pass_count: 2
- strict_pass_rate: 0.6667
- factual_count: 2
- partial_coverage_count: 1
- clarification_required_count: 0
- http_error_count: 0
- llm_decomposition_applied_count: 2
- avg_elapsed_ms: 6135.3
## Files
- run_summary.json
- full_live_results.json
- failures_only.json

View File

@ -0,0 +1,154 @@
[
{
"index": 2,
"id": "OC002",
"question": "Есть ли долг по договору 15/24 на 2020-07-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30-oc002",
"status_code": 200,
"ok": true,
"elapsed_ms": 7108,
"reply_type": "partial_coverage",
"trace_id": "address-jY3DOwe_a4",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-jY3DOwe_a4",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"verify_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "VERIFY_FACTUAL",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"contract": "15/24",
"period_from": "2020-07-01",
"period_to": "2020-07-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "contract",
"anchor_value_raw": "15/24",
"anchor_value_resolved": "15/24",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "3w1iqg2uMfQRU2",
"llm_decomposition_effective_message": "Проверить наличие долга по договору 15/24 на дату 2020-07-31.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "есть ли долг по договору 15/24 на 2020-07-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "3w1iqg2uMfQRU2",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Проверить наличие долга по договору 15/24 на дату 2020-07-31."
}
},
"error_code": null,
"error_message": null
}
]

View File

@ -0,0 +1,510 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30",
"generated_at": "2026-04-02T11:25:48",
"summary": {
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30",
"generated_at": "2026-04-02T11:25:48",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_contracts_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"strict_policy": "route",
"totals": {
"questions_total": 3,
"ok_200_count": 3,
"semantic_pass_count": 3,
"semantic_pass_rate": 1.0,
"route_pass_count": 2,
"route_pass_rate": 0.6667,
"strict_pass_count": 2,
"strict_pass_rate": 0.6667,
"factual_count": 2,
"partial_coverage_count": 1,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 2,
"avg_elapsed_ms": 6135.3
},
"distributions": {
"reply_type": {
"factual": 2,
"partial_coverage": 1
},
"actual_intent": {
"list_open_contracts": 2,
"open_items_by_counterparty_or_contract": 1
},
"actual_mode": {
"address_query": 3
},
"mcp_call_status": {
"matched_non_empty": 2,
"materialized_but_not_anchor_matched": 1
},
"limited_reason_category": {
"empty_match": 1
},
"route_health": {
"ok_or_factual": 2,
"likely_blocked_route": 1
}
}
},
"rows": [
{
"index": 1,
"id": "OC001",
"question": "Покажи незакрытые договоры на 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30-oc001",
"status_code": 200,
"ok": true,
"elapsed_ms": 6211,
"reply_type": "factual",
"trace_id": "address-Fgx_nQgSif",
"assistant_reply": "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).\nСтрок движения: 20.\nДоговорных кандидатов: 0.\nДоговорные якоря в live-строках не выделены; показаны связанные движения как fallback.\n1. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 127216.46\n2. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 235682.42\n3. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 337318.8\n4. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.09 / 91.02 | 10615989.53\n5. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.01 / 91.09 | 1614344.98\n6. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 90.09 / 90.02.1 | 6430415.34",
"assistant_reply_first_line": "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
"expected_intent": "list_open_contracts",
"actual_intent": "list_open_contracts",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": "factual",
"reply_match": true,
"semantic_pass": true,
"route_pass": true,
"route_health": "ok_or_factual",
"strict_policy": "route",
"strict_pass": true,
"selected_recipe": "address_open_contracts_candidates_v1",
"missing_required_filters": [],
"match_failure_stage": "none",
"match_failure_reason": null,
"rows_fetched": 20,
"rows_matched": 20,
"mcp_call_status": "matched_non_empty",
"limited_reason_category": null,
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-Fgx_nQgSif",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "none",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "grounded",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_contract_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "list_open_contracts",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"period_from": "2020-12-01",
"period_to": "2020-12-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_contracts_candidates_v1",
"mcp_call_status_legacy": "matched_non_empty",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "unknown",
"anchor_value_raw": null,
"anchor_value_resolved": null,
"resolver_confidence": "low",
"ambiguity_count": 0,
"match_failure_stage": "none",
"match_failure_reason": null,
"mcp_call_status": "matched_non_empty",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 20,
"rows_materialized": 20,
"rows_matched": 20,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": null,
"response_type": "FACTUAL_LIST",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "UEfU9FKCNBm898",
"llm_decomposition_effective_message": "Показать незакрытые договоры по состоянию на конец декабря 2020 года.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "покажи незакрытые договоры на 2020-12-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "UEfU9FKCNBm898",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Показать незакрытые договоры по состоянию на конец декабря 2020 года."
}
},
"error_code": null,
"error_message": null
},
{
"index": 2,
"id": "OC002",
"question": "Есть ли долг по договору 15/24 на 2020-07-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30-oc002",
"status_code": 200,
"ok": true,
"elapsed_ms": 7108,
"reply_type": "partial_coverage",
"trace_id": "address-jY3DOwe_a4",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-jY3DOwe_a4",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"verify_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "VERIFY_FACTUAL",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"contract": "15/24",
"period_from": "2020-07-01",
"period_to": "2020-07-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "contract",
"anchor_value_raw": "15/24",
"anchor_value_resolved": "15/24",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "3w1iqg2uMfQRU2",
"llm_decomposition_effective_message": "Проверить наличие долга по договору 15/24 на дату 2020-07-31.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "есть ли долг по договору 15/24 на 2020-07-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "3w1iqg2uMfQRU2",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Проверить наличие долга по договору 15/24 на дату 2020-07-31."
}
},
"error_code": null,
"error_message": null
},
{
"index": 3,
"id": "OC003",
"question": "Show open contracts as of 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30-oc003",
"status_code": 200,
"ok": true,
"elapsed_ms": 5087,
"reply_type": "factual",
"trace_id": "address-RNbEKp80R9",
"assistant_reply": "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).\nСтрок движения: 20.\nДоговорных кандидатов: 0.\nДоговорные якоря в live-строках не выделены; показаны связанные движения как fallback.\n1. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 127216.46\n2. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 235682.42\n3. 2020-12-31T23:59:59Z | Формирование записей книги покупок 00000000004 от 31.12.2020 23:59:59 | 68.02 / 19.09 | 337318.8\n4. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.09 / 91.02 | 10615989.53\n5. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 91.01 / 91.09 | 1614344.98\n6. 2020-12-31T23:59:59Z | Регламентная операция 00000000097 от 31.12.2020 23:59:59 | 90.09 / 90.02.1 | 6430415.34",
"assistant_reply_first_line": "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
"expected_intent": "list_open_contracts",
"actual_intent": "list_open_contracts",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": "factual",
"reply_match": true,
"semantic_pass": true,
"route_pass": true,
"route_health": "ok_or_factual",
"strict_policy": "route",
"strict_pass": true,
"selected_recipe": "address_open_contracts_candidates_v1",
"missing_required_filters": [],
"match_failure_stage": "none",
"match_failure_reason": null,
"rows_fetched": 20,
"rows_matched": 20,
"mcp_call_status": "matched_non_empty",
"limited_reason_category": null,
"llm_decomposition_applied": false,
"llm_decomposition_reason": "normalized_fragment_rejected_intent_drop",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-RNbEKp80R9",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "none",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "grounded",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_contract_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "list_open_contracts",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"period_from": "2020-12-01",
"period_to": "2020-12-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_contracts_candidates_v1",
"mcp_call_status_legacy": "matched_non_empty",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "unknown",
"anchor_value_raw": null,
"anchor_value_resolved": null,
"resolver_confidence": "low",
"ambiguity_count": 0,
"match_failure_stage": "none",
"match_failure_reason": null,
"mcp_call_status": "matched_non_empty",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 20,
"rows_materialized": 20,
"rows_matched": 20,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": null,
"response_type": "FACTUAL_LIST",
"execution_lane": "address_query",
"llm_decomposition_applied": false,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "Cc8Nu8vmBtCmxF",
"llm_decomposition_effective_message": "Show open contracts as of 2020-12-31",
"llm_decomposition_reason": "normalized_fragment_rejected_intent_drop",
"fallback_rule_hit": null,
"sanitized_user_message": "show open contracts as of 2020-12-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "Cc8Nu8vmBtCmxF",
"prompt_version": "normalizer_v2_0_2",
"applied": false,
"effective_message": "Show open contracts as of 2020-12-31"
}
},
"error_code": null,
"error_message": null
}
]
}

View File

@ -0,0 +1,7 @@
# Response Audit: 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30
| id | strict | route_health | reply_type | intent | limited_reason | question | assistant_first_line |
|---|---|---|---|---|---|---|---|
| OC001 | True | ok_or_factual | factual | list_open_contracts | None | Покажи незакрытые договоры на 2020-12-31 | Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76). |
| OC002 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Есть ли долг по договору 15/24 на 2020-07-31 | В live-данных по текущему фильтру записи не найдены. |
| OC003 | True | ok_or_factual | factual | list_open_contracts | None | Show open contracts as of 2020-12-31 | Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76). |

View File

@ -0,0 +1,50 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-25-30",
"generated_at": "2026-04-02T11:25:48",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_contracts_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"strict_policy": "route",
"totals": {
"questions_total": 3,
"ok_200_count": 3,
"semantic_pass_count": 3,
"semantic_pass_rate": 1.0,
"route_pass_count": 2,
"route_pass_rate": 0.6667,
"strict_pass_count": 2,
"strict_pass_rate": 0.6667,
"factual_count": 2,
"partial_coverage_count": 1,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 2,
"avg_elapsed_ms": 6135.3
},
"distributions": {
"reply_type": {
"factual": 2,
"partial_coverage": 1
},
"actual_intent": {
"list_open_contracts": 2,
"open_items_by_counterparty_or_contract": 1
},
"actual_mode": {
"address_query": 3
},
"mcp_call_status": {
"matched_non_empty": 2,
"materialized_but_not_anchor_matched": 1
},
"limited_reason_category": {
"empty_match": 1
},
"route_health": {
"ok_or_factual": 2,
"likely_blocked_route": 1
}
}
}

View File

@ -0,0 +1,28 @@
# 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-43-21
Generated at: 2026-04-02T11:44:13
Questions file: X:\1C\NDC_1C\docs\ADDRESS\question_sets\address_open_items_focus_2026-04-02.json
Backend URL: http://127.0.0.1:8787/api/assistant/message
LLM: local / qwen2.5-14b-instruct-1m @ http://127.0.0.1:1234
Strict policy: route
## Totals
- questions_total: 7
- ok_200_count: 7
- semantic_pass_count: 7
- semantic_pass_rate: 1.0
- route_pass_count: 4
- route_pass_rate: 0.5714
- strict_pass_count: 4
- strict_pass_rate: 0.5714
- factual_count: 4
- partial_coverage_count: 3
- clarification_required_count: 0
- http_error_count: 0
- llm_decomposition_applied_count: 7
- avg_elapsed_ms: 7423
## Files
- run_summary.json
- full_live_results.json
- failures_only.json

View File

@ -0,0 +1,454 @@
[
{
"index": 1,
"id": "OI001",
"question": "Покажи хвосты по контрагенту СВК на 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-43-21-oi001",
"status_code": 200,
"ok": true,
"elapsed_ms": 9351,
"reply_type": "partial_coverage",
"trace_id": "address-Mjn6ZDlzkm",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-Mjn6ZDlzkm",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"counterparty": "СВК",
"as_of_date": "2020-12-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "counterparty",
"anchor_value_raw": "СВК",
"anchor_value_resolved": "СВК",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "PDTZVysmUMSJ3U",
"llm_decomposition_effective_message": "Показать незакрытые или несбалансированные записи (хвосты) для контрагента СВК на конец периода 2020-12-31.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "покажи хвосты по контрагенту свк на 2020-12-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "PDTZVysmUMSJ3U",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Показать незакрытые или несбалансированные записи (хвосты) для контрагента СВК на конец периода 2020-12-31."
}
},
"error_code": null,
"error_message": null
},
{
"index": 2,
"id": "OI002",
"question": "Покажи хвосты по контрагенту СВК на конец периода 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-43-21-oi002",
"status_code": 200,
"ok": true,
"elapsed_ms": 8137,
"reply_type": "partial_coverage",
"trace_id": "address-XMrKo9tL1X",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-XMrKo9tL1X",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"counterparty": "СВК",
"as_of_date": "2020-12-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "counterparty",
"anchor_value_raw": "СВК",
"anchor_value_resolved": "СВК",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "Nvww2yZD2a6NwG",
"llm_decomposition_effective_message": "Показать хвосты (не сходящиеся или повисшие записи) для контрагента СВК на дату 2020-12-31.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "покажи хвосты по контрагенту свк на конец периода 2020-12-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "Nvww2yZD2a6NwG",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Показать хвосты (не сходящиеся или повисшие записи) для контрагента СВК на дату 2020-12-31."
}
},
"error_code": null,
"error_message": null
},
{
"index": 4,
"id": "OI004",
"question": "Покажи открытые позиции по договору 15/24",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-43-21-oi004",
"status_code": 200,
"ok": true,
"elapsed_ms": 7126,
"reply_type": "partial_coverage",
"trace_id": "address-sy7tBdK8xj",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-sy7tBdK8xj",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"contract": "15/24"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "contract",
"anchor_value_raw": "15/24",
"anchor_value_resolved": "15/24",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "fpG27I3Bq2rPIb",
"llm_decomposition_effective_message": "Показать открытые позиции по договору 15/24.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "покажи открытые позиции по договору 15/24",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "fpG27I3Bq2rPIb",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Показать открытые позиции по договору 15/24."
}
},
"error_code": null,
"error_message": null
}
]

View File

@ -0,0 +1,11 @@
# Response Audit: 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-43-21
| id | strict | route_health | reply_type | intent | limited_reason | question | assistant_first_line |
|---|---|---|---|---|---|---|---|
| OI001 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Покажи хвосты по контрагенту СВК на 2020-12-31 | В live-данных по текущему фильтру записи не найдены. |
| OI002 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Покажи хвосты по контрагенту СВК на конец периода 2020-12-31 | В live-данных по текущему фильтру записи не найдены. |
| OI003 | True | ok_or_factual | factual | open_items_by_counterparty_or_contract | None | Есть ли незакрытые позиции по договору 15/24 на 2020-12-31 | Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. |
| OI004 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Покажи открытые позиции по договору 15/24 | В live-данных по текущему фильтру записи не найдены. |
| OI005 | True | ok_or_factual | factual | list_open_contracts | None | Покажи незакрытые договоры на 2020-12-31 | Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76). |
| OI006 | True | ok_or_factual | factual | list_documents_by_contract | None | Покажи документы по договору 15/24 за 2020 | Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. |
| OI007 | True | ok_or_factual | factual | bank_operations_by_contract | None | Покажи банковские операции по договору 15/24 | Собран список банковских операций по договору (live address lane). |

View File

@ -0,0 +1,52 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-43-21",
"generated_at": "2026-04-02T11:44:13",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_items_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"strict_policy": "route",
"totals": {
"questions_total": 7,
"ok_200_count": 7,
"semantic_pass_count": 7,
"semantic_pass_rate": 1.0,
"route_pass_count": 4,
"route_pass_rate": 0.5714,
"strict_pass_count": 4,
"strict_pass_rate": 0.5714,
"factual_count": 4,
"partial_coverage_count": 3,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 7,
"avg_elapsed_ms": 7423
},
"distributions": {
"reply_type": {
"partial_coverage": 3,
"factual": 4
},
"actual_intent": {
"open_items_by_counterparty_or_contract": 4,
"list_open_contracts": 1,
"list_documents_by_contract": 1,
"bank_operations_by_contract": 1
},
"actual_mode": {
"address_query": 7
},
"mcp_call_status": {
"materialized_but_not_anchor_matched": 3,
"matched_non_empty": 4
},
"limited_reason_category": {
"empty_match": 3
},
"route_health": {
"likely_blocked_route": 3,
"ok_or_factual": 4
}
}
}

View File

@ -0,0 +1,28 @@
# 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-48-09
Generated at: 2026-04-02T11:49:03
Questions file: X:\1C\NDC_1C\docs\ADDRESS\question_sets\address_open_items_focus_2026-04-02.json
Backend URL: http://127.0.0.1:8787/api/assistant/message
LLM: local / qwen2.5-14b-instruct-1m @ http://127.0.0.1:1234
Strict policy: route
## Totals
- questions_total: 7
- ok_200_count: 7
- semantic_pass_count: 7
- semantic_pass_rate: 1.0
- route_pass_count: 4
- route_pass_rate: 0.5714
- strict_pass_count: 4
- strict_pass_rate: 0.5714
- factual_count: 4
- partial_coverage_count: 3
- clarification_required_count: 0
- http_error_count: 0
- llm_decomposition_applied_count: 7
- avg_elapsed_ms: 7639.6
## Files
- run_summary.json
- full_live_results.json
- failures_only.json

View File

@ -0,0 +1,454 @@
[
{
"index": 1,
"id": "OI001",
"question": "Покажи хвосты по контрагенту СВК на 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-48-09-oi001",
"status_code": 200,
"ok": true,
"elapsed_ms": 8786,
"reply_type": "partial_coverage",
"trace_id": "address-6hcMXNyYve",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-6hcMXNyYve",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"counterparty": "СВК",
"as_of_date": "2020-12-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "counterparty",
"anchor_value_raw": "СВК",
"anchor_value_resolved": "СВК",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "counterparty_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "l8lpC5yEkyWxDI",
"llm_decomposition_effective_message": "Показать незакрытые или несбалансированные записи (хвосты) для контрагента СВК на конец периода 2020-12-31.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "покажи хвосты по контрагенту свк на 2020-12-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "l8lpC5yEkyWxDI",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Показать незакрытые или несбалансированные записи (хвосты) для контрагента СВК на конец периода 2020-12-31."
}
},
"error_code": null,
"error_message": null
},
{
"index": 3,
"id": "OI003",
"question": "Есть ли незакрытые позиции по договору 15/24 на 2020-12-31",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-48-09-oi003",
"status_code": 200,
"ok": true,
"elapsed_ms": 8041,
"reply_type": "partial_coverage",
"trace_id": "address-ehEjGkLvsN",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-ehEjGkLvsN",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"verify_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "VERIFY_FACTUAL",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"contract": "15/24",
"as_of_date": "2020-12-31"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "contract",
"anchor_value_raw": "15/24",
"anchor_value_resolved": "15/24",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "FxjH16DJXAPNHq",
"llm_decomposition_effective_message": "Проверить наличие незакрытых позиций по договору 15/24 на конец периода 2020-12-31.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "есть ли незакрытые позиции по договору 15/24 на 2020-12-31",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "FxjH16DJXAPNHq",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Проверить наличие незакрытых позиций по договору 15/24 на конец периода 2020-12-31."
}
},
"error_code": null,
"error_message": null
},
{
"index": 4,
"id": "OI004",
"question": "Покажи открытые позиции по договору 15/24",
"session": null,
"session_id": "asst-2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-48-09-oi004",
"status_code": 200,
"ok": true,
"elapsed_ms": 7120,
"reply_type": "partial_coverage",
"trace_id": "address-jOeG7s-nHc",
"assistant_reply": "В live-данных по текущему фильтру записи не найдены.\nПричина: по указанному якорю и фильтрам в live-выборке нет строк.\nЧто нужно уточнить: уточните период или снимите часть фильтров.",
"assistant_reply_first_line": "В live-данных по текущему фильтру записи не найдены.",
"expected_intent": "open_items_by_counterparty_or_contract",
"actual_intent": "open_items_by_counterparty_or_contract",
"intent_match": true,
"expected_mode": "address_query",
"actual_mode": "address_query",
"mode_match": true,
"expected_reply_type": null,
"reply_match": true,
"semantic_pass": true,
"route_pass": false,
"route_health": "likely_blocked_route",
"strict_policy": "route",
"strict_pass": false,
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"missing_required_filters": [],
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"rows_fetched": 20,
"rows_matched": 0,
"mcp_call_status": "materialized_but_not_anchor_matched",
"limited_reason_category": "empty_match",
"llm_decomposition_applied": true,
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"debug_payload": {
"trace_id": "address-jOeG7s-nHc",
"prompt_version": "address_query_runtime_v1",
"schema_version": "address_query_runtime_v1",
"fallback_type": "partial",
"route_summary": null,
"fragments": [],
"requirements_extracted": [],
"coverage_report": {
"requirements_total": 0,
"requirements_covered": 0,
"requirements_uncovered": [],
"requirements_partially_covered": [],
"clarification_needed_for": [],
"out_of_scope_requirements": []
},
"routes": [],
"retrieval_status": [],
"retrieval_results": [],
"answer_grounding_check": {
"status": "partial",
"route_subject_match": true,
"missing_requirements": [],
"reasons": [
"address_action_detected",
"address_entity_detected",
"object_signal_detected",
"open_items_signal_detected"
],
"why_included_summary": [],
"selection_reason_summary": []
},
"dropped_intent_segments": [],
"detected_mode": "address_query",
"detected_mode_confidence": "high",
"query_shape": "OBJECT_LOOKUP",
"query_shape_confidence": "medium",
"detected_intent": "open_items_by_counterparty_or_contract",
"detected_intent_confidence": "medium",
"extracted_filters": {
"sort": "period_desc",
"limit": 20,
"contract": "15/24"
},
"missing_required_filters": [],
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"mcp_call_status_legacy": "materialized_but_not_matched",
"account_scope_mode": "preferred",
"account_scope_fallback_applied": true,
"anchor_type": "contract",
"anchor_value_raw": "15/24",
"anchor_value_resolved": "15/24",
"resolver_confidence": "medium",
"ambiguity_count": 0,
"match_failure_stage": "materialized_but_not_anchor_matched",
"match_failure_reason": "contract_anchor_not_matched_in_materialized_rows",
"mcp_call_status": "materialized_but_not_anchor_matched",
"rows_fetched": 20,
"raw_rows_received": 20,
"rows_after_account_scope": 20,
"rows_after_recipe_filter": 0,
"rows_materialized": 20,
"rows_matched": 0,
"raw_row_keys_sample": [
"Период",
"Регистратор",
"СчетДт",
"СчетКт",
"Сумма",
"Period",
"Registrator",
"AccountDt",
"AccountKt",
"Amount"
],
"materialization_drop_reason": "none",
"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",
"runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS",
"limited_reason_category": "empty_match",
"response_type": "LIMITED_WITH_REASON",
"execution_lane": "address_query",
"llm_decomposition_applied": true,
"llm_decomposition_attempted": true,
"llm_provider_used": "local",
"llm_decomposition_trace_id": "Q08pHjSGHcJ8sq",
"llm_decomposition_effective_message": "Показать открытые позиции по договору 15/24.",
"llm_decomposition_reason": "normalized_fragment_applied",
"fallback_rule_hit": null,
"sanitized_user_message": "покажи открытые позиции по договору 15/24",
"tool_gate_decision": "run_address_lane",
"tool_gate_reason": "address_mode_classifier_detected",
"answer_structure_v11": null,
"investigation_state_snapshot": null,
"normalized": null,
"normalizer_output": {
"trace_id": "Q08pHjSGHcJ8sq",
"prompt_version": "normalizer_v2_0_2",
"applied": true,
"effective_message": "Показать открытые позиции по договору 15/24."
}
},
"error_code": null,
"error_message": null
}
]

View File

@ -0,0 +1,11 @@
# Response Audit: 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-48-09
| id | strict | route_health | reply_type | intent | limited_reason | question | assistant_first_line |
|---|---|---|---|---|---|---|---|
| OI001 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Покажи хвосты по контрагенту СВК на 2020-12-31 | В live-данных по текущему фильтру записи не найдены. |
| OI002 | True | ok_or_factual | factual | open_items_by_counterparty_or_contract | None | Покажи хвосты по контрагенту СВК на конец периода 2020-12-31 | Точный якорь не подтвердился в live-строках даже после расширения до 1000; показаны ближайшие доступные позиции по счетам 60/62/76. |
| OI003 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Есть ли незакрытые позиции по договору 15/24 на 2020-12-31 | В live-данных по текущему фильтру записи не найдены. |
| OI004 | False | likely_blocked_route | partial_coverage | open_items_by_counterparty_or_contract | empty_match | Покажи открытые позиции по договору 15/24 | В live-данных по текущему фильтру записи не найдены. |
| OI005 | True | ok_or_factual | factual | list_open_contracts | None | Покажи незакрытые договоры на 2020-12-31 | Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76). |
| OI006 | True | ok_or_factual | factual | list_documents_by_contract | None | Покажи документы по договору 15/24 за 2020 | Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. |
| OI007 | True | ok_or_factual | factual | bank_operations_by_contract | None | Покажи банковские операции по договору 15/24 | Собран список банковских операций по договору (live address lane). |

View File

@ -0,0 +1,52 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-48-09",
"generated_at": "2026-04-02T11:49:03",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_items_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"strict_policy": "route",
"totals": {
"questions_total": 7,
"ok_200_count": 7,
"semantic_pass_count": 7,
"semantic_pass_rate": 1.0,
"route_pass_count": 4,
"route_pass_rate": 0.5714,
"strict_pass_count": 4,
"strict_pass_rate": 0.5714,
"factual_count": 4,
"partial_coverage_count": 3,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 7,
"avg_elapsed_ms": 7639.6
},
"distributions": {
"reply_type": {
"partial_coverage": 3,
"factual": 4
},
"actual_intent": {
"open_items_by_counterparty_or_contract": 4,
"list_open_contracts": 1,
"list_documents_by_contract": 1,
"bank_operations_by_contract": 1
},
"actual_mode": {
"address_query": 7
},
"mcp_call_status": {
"materialized_but_not_anchor_matched": 3,
"matched_non_empty": 4
},
"limited_reason_category": {
"empty_match": 3
},
"route_health": {
"likely_blocked_route": 3,
"ok_or_factual": 4
}
}
}

View File

@ -0,0 +1,28 @@
# 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-54-08
Generated at: 2026-04-02T11:55:01
Questions file: X:\1C\NDC_1C\docs\ADDRESS\question_sets\address_open_items_focus_2026-04-02.json
Backend URL: http://127.0.0.1:8787/api/assistant/message
LLM: local / qwen2.5-14b-instruct-1m @ http://127.0.0.1:1234
Strict policy: route
## Totals
- questions_total: 7
- ok_200_count: 7
- semantic_pass_count: 7
- semantic_pass_rate: 1.0
- route_pass_count: 7
- route_pass_rate: 1.0
- strict_pass_count: 7
- strict_pass_rate: 1.0
- factual_count: 7
- partial_coverage_count: 0
- clarification_required_count: 0
- http_error_count: 0
- llm_decomposition_applied_count: 7
- avg_elapsed_ms: 7674.1
## Files
- run_summary.json
- full_live_results.json
- failures_only.json

View File

@ -0,0 +1,11 @@
# Response Audit: 2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-54-08
| id | strict | route_health | reply_type | intent | limited_reason | question | assistant_first_line |
|---|---|---|---|---|---|---|---|
| OI001 | True | ok_or_factual | factual | open_items_by_counterparty_or_contract | None | Покажи хвосты по контрагенту СВК на 2020-12-31 | Точный якорь не подтвердился в live-строках даже после расширения до 1000; показаны ближайшие доступные позиции по счетам 60/62/76. |
| OI002 | True | ok_or_factual | factual | open_items_by_counterparty_or_contract | None | Покажи хвосты по контрагенту СВК на конец периода 2020-12-31 | Точный якорь не подтвердился в live-строках даже после расширения до 1000; показаны ближайшие доступные позиции по счетам 60/62/76. |
| OI003 | True | ok_or_factual | factual | open_items_by_counterparty_or_contract | None | Есть ли незакрытые позиции по договору 15/24 на 2020-12-31 | Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76. |
| OI004 | True | ok_or_factual | factual | open_items_by_counterparty_or_contract | None | Покажи открытые позиции по договору 15/24 | Точный якорь не подтвердился в live-строках даже после расширения до 1000; показаны ближайшие доступные позиции по счетам 60/62/76. |
| OI005 | True | ok_or_factual | factual | list_open_contracts | None | Покажи незакрытые договоры на 2020-12-31 | Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76). |
| OI006 | True | ok_or_factual | factual | list_documents_by_contract | None | Покажи документы по договору 15/24 за 2020 | Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. |
| OI007 | True | ok_or_factual | factual | bank_operations_by_contract | None | Покажи банковские операции по договору 15/24 | Собран список банковских операций по договору (live address lane). |

View File

@ -0,0 +1,47 @@
{
"run_id": "2026-04-02_Address_Slang_Live_Stress_2026-04-02_11-54-08",
"generated_at": "2026-04-02T11:55:01",
"source_questions_file": "X:\\1C\\NDC_1C\\docs\\ADDRESS\\question_sets\\address_open_items_focus_2026-04-02.json",
"backend_url": "http://127.0.0.1:8787/api/assistant/message",
"llm_provider": "local",
"llm_model": "qwen2.5-14b-instruct-1m",
"llm_base_url": "http://127.0.0.1:1234",
"strict_policy": "route",
"totals": {
"questions_total": 7,
"ok_200_count": 7,
"semantic_pass_count": 7,
"semantic_pass_rate": 1.0,
"route_pass_count": 7,
"route_pass_rate": 1.0,
"strict_pass_count": 7,
"strict_pass_rate": 1.0,
"factual_count": 7,
"partial_coverage_count": 0,
"clarification_required_count": 0,
"http_error_count": 0,
"llm_decomposition_applied_count": 7,
"avg_elapsed_ms": 7674.1
},
"distributions": {
"reply_type": {
"factual": 7
},
"actual_intent": {
"open_items_by_counterparty_or_contract": 4,
"list_open_contracts": 1,
"list_documents_by_contract": 1,
"bank_operations_by_contract": 1
},
"actual_mode": {
"address_query": 7
},
"mcp_call_status": {
"matched_non_empty": 7
},
"limited_reason_category": {},
"route_health": {
"ok_or_factual": 7
}
}
}

View File

@ -290,6 +290,20 @@ function cleanupAnchorValue(value) {
if (!normalized) { if (!normalized) {
return ""; return "";
} }
// Remove trailing as-of qualifiers often captured by broad contract/counterparty regexes:
// "<anchor> на 2020-07-31", "<anchor> на дату 31.07.2020", "<anchor> as of 2020-07-31".
const asOfTailPattern = /\s+(?:на\s+(?:дат[ауеы]\s+)?\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|as\s+of\s+\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?)(?:\s+|$)[\s\S]*$/iu;
if (asOfTailPattern.test(normalized)) {
return normalized.replace(asOfTailPattern, "").trim();
}
const asOfTruncatedTailPattern = /\s+на\s+дат[ауеы]\s+\d{1,2}(?:\s+|$)[\s\S]*$/iu;
if (asOfTruncatedTailPattern.test(normalized)) {
return normalized.replace(asOfTruncatedTailPattern, "").trim();
}
const periodEndTailPattern = /\s+на\s+конец(?:\s+период[а-яё]*)?\s+(?:\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|\d{4}|[a-zа-яё]+\s+\d{4})(?:\s+|$)[\s\S]*$/iu;
if (periodEndTailPattern.test(normalized)) {
return normalized.replace(periodEndTailPattern, "").trim();
}
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes: // Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31" // "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
const periodTailPattern = /\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu; const periodTailPattern = /\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu;
@ -356,6 +370,28 @@ function extractLooseByAnchorValue(text) {
} }
return token; return token;
} }
function extractContractTokenHeuristic(text) {
const source = String(text ?? "");
const explicit = source.match(/(?:№|#|n)\s*([a-zа-яё0-9][a-zа-яё0-9./_-]{1,})/iu);
if (explicit) {
const token = String(explicit[1] ?? "").trim();
if (token.length >= 2) {
return token;
}
}
const slashLike = source.match(/\b(\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?)\b/iu);
if (!slashLike) {
return undefined;
}
const token = String(slashLike[1] ?? "").trim();
if (!token) {
return undefined;
}
if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])$/.test(token)) {
return undefined;
}
return token;
}
function isLikelyCounterpartyToken(rawToken) { function isLikelyCounterpartyToken(rawToken) {
const token = String(rawToken ?? "").trim(); const token = String(rawToken ?? "").trim();
const lowered = token.toLowerCase(); const lowered = token.toLowerCase();
@ -525,8 +561,14 @@ function requiredFiltersByIntent(intent) {
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
return ["counterparty"]; return ["counterparty"];
} }
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
return ["contract"];
}
return []; return [];
} }
function usesAsOfPrimaryWindow(intent) {
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
}
function extractAddressFilters(userMessage, intent) { function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText); const text = normalizeMojibakeString(rawText);
@ -575,6 +617,13 @@ function extractAddressFilters(userMessage, intent) {
if (contractMatch) { if (contractMatch) {
filters.contract = cleanupAnchorValue(String(contractMatch[1])); filters.contract = cleanupAnchorValue(String(contractMatch[1]));
} }
if (!filters.contract && (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract")) {
const heuristicContract = extractContractTokenHeuristic(text);
if (heuristicContract) {
filters.contract = cleanupAnchorValue(heuristicContract);
warnings.push("contract_anchor_derived_from_heuristic_token");
}
}
const periodRange = extractPeriodRange(text); const periodRange = extractPeriodRange(text);
if (periodRange.period_from) { if (periodRange.period_from) {
filters.period_from = periodRange.period_from; filters.period_from = periodRange.period_from;
@ -606,23 +655,27 @@ function extractAddressFilters(userMessage, intent) {
warnings.push("period_derived_from_year_phrase"); warnings.push("period_derived_from_year_phrase");
} }
} }
const explicitAsOfDate = extractAsOfDate(text);
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
filters.as_of_date = explicitAsOfDate;
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
warnings.includes("period_derived_from_year_range_phrase") ||
warnings.includes("period_derived_from_year_phrase");
if (periodWasDerivedHeuristically && !periodRange.period_from && !periodRange.period_to) {
delete filters.period_from;
delete filters.period_to;
warnings.push("period_window_cleared_for_as_of_intent");
}
}
// If explicit period window exists, do not infer as_of_date from one of its boundary dates. // If explicit period window exists, do not infer as_of_date from one of its boundary dates.
if (!filters.period_from && !filters.period_to) { if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
const asOfDate = extractAsOfDate(text); const asOfDate = extractAsOfDate(text);
if (asOfDate) { if (asOfDate) {
filters.as_of_date = asOfDate; filters.as_of_date = asOfDate;
} }
} }
// For document/bank lists we default to a short recent window if no explicit period was provided. // For counterparty document/bank lists we keep period open by default (all-time over available data)
if ((intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") && // and rely on runtime limits/recovery instead of forcing a recent window.
!filters.period_from &&
!filters.period_to &&
!hasAllTimeHint(text)) {
const today = new Date().toISOString().slice(0, 10);
filters.period_to = today;
filters.period_from = shiftDaysIso(today, -90);
warnings.push("period_defaulted_last_90_days");
}
// For balance-style intents we force as_of_date deterministically: // For balance-style intents we force as_of_date deterministically:
// - explicit as_of has priority; // - explicit as_of has priority;
// - else use period_to boundary when provided; // - else use period_to boundary when provided;

View File

@ -27,13 +27,19 @@ const ACCOUNT_BALANCE_HINTS = [
"account balance", "account balance",
"balance by account", "balance by account",
"saldo", "saldo",
"баланс",
"остаток по счет", "остаток по счет",
"сальдо по счет", "сальдо по счет",
"по счету" "по счету",
"что на счете",
"что на счёте",
"на конец"
]; ];
const DOCUMENTS_FORMING_BALANCE_HINTS = [ const DOCUMENTS_FORMING_BALANCE_HINTS = [
"documents forming balance", "documents forming balance",
"docs forming balance",
"documents form balance", "documents form balance",
"docs form balance",
"balance documents", "balance documents",
"documents for balance", "documents for balance",
"which documents form balance", "which documents form balance",
@ -57,6 +63,8 @@ const OPEN_ITEMS_HINTS = [
"висят", "висят",
"незакрыт", "незакрыт",
"открыт", "открыт",
"долг",
"задолж",
"позици" "позици"
]; ];
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [ const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
@ -87,31 +95,132 @@ const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
"bank operations by customer", "bank operations by customer",
"show bank operations by counterparty", "show bank operations by counterparty",
"bank ops", "bank ops",
"bank oper",
"transactions by counterparty", "transactions by counterparty",
"транзак",
"банк",
"банков", "банков",
"по банку",
"опер",
"выписк",
"платеж",
"платёж",
"оплат",
"списан",
"списани",
"поступлен",
"поступлени",
"движени"
];
const DOCUMENTS_BY_CONTRACT_HINTS = [
"documents by contract",
"docs by contract",
"show documents by contract",
"list documents by contract",
"документы по договору",
"доки по договору",
"док по договору",
"документы договор",
"договор"
];
const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
"bank operations by contract",
"bank payments by contract",
"payment orders by contract",
"transactions by contract",
"bank ops by contract",
"банковские операции по договору",
"платежи по договору",
"выписка по договору"
];
const BANK_OPERATION_CORE_HINTS = [
"банк",
"банков",
"операц",
"опер",
"выписк", "выписк",
"платеж", "платеж",
"платёж", "платёж",
"оплат", "оплат",
"списан", "списан",
"поступлен", "поступлен",
"движени" "движени",
"транзак",
"bank",
"payment",
"payments",
"transaction",
"transactions",
"statement",
"wire"
]; ];
function hasAny(text, patterns) { function hasAny(text, patterns) {
return patterns.some((item) => text.includes(item)); return patterns.some((item) => text.includes(item));
} }
function hasCompactAccountCodeToken(text) {
// Match compact account tokens like 60.01 / 62, while avoiding date fragments.
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})(?![\d-])/u.test(text);
}
function hasDocumentsFormingBalanceSignal(text) { function hasDocumentsFormingBalanceSignal(text) {
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) { if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
return true; return true;
} }
const hasDocLexeme = text.includes("документ") || text.includes("доки"); const hasLooseAccountCodeToken = hasCompactAccountCodeToken(text);
const hasDocLexeme = /(?:документ|док(?:и|ам|ах|ов|а)?)/u.test(text);
const hasFormingLexeme = text.includes("формир"); const hasFormingLexeme = text.includes("формир");
const hasBalanceLexeme = text.includes("остат"); const hasBalanceLexeme = text.includes("остат");
const hasAccountLexeme = text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text); const hasAccountLexeme = text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text) || hasLooseAccountCodeToken;
if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) { if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) {
return true; return true;
} }
return hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит"); if (hasDocLexeme &&
hasBalanceLexeme &&
hasAccountLexeme &&
(text.includes("раскрой") || text.includes("раскид") || text.includes("под остатк"))) {
return true;
}
if (hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит")) {
return true;
}
return hasBalanceLexeme && hasAccountLexeme && /из\s+чего\s+остат/u.test(text);
}
function hasDocumentsFormingBalanceAccountAnchor(text) {
if (hasAccountNumberAnchor(text) || text.includes("счет") || text.includes("счёт")) {
return true;
}
// Allow compact account mentions like "60.01" in slang prompts without explicit "счет".
return hasCompactAccountCodeToken(text);
}
function hasAccountBalanceSignal(text) {
if (hasAny(text, ACCOUNT_BALANCE_HINTS)) {
return true;
}
const hasAccountLexeme = hasAccountNumberAnchor(text) || hasCompactAccountCodeToken(text) || /(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/u.test(text);
const hasBalanceLexeme = text.includes("баланс") ||
text.includes("остат") ||
text.includes("сальд") ||
text.includes("saldo") ||
text.includes("balance") ||
text.includes("скока") ||
text.includes("сколько") ||
/на\s+конец/u.test(text);
return hasAccountLexeme && hasBalanceLexeme;
}
function hasOpenContractsListSignal(text) {
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
if (!hasContractLexeme || !hasOpenLexeme) {
return false;
}
// Query about a specific contract should stay in open-items lane.
if (hasContractNumberLikeToken(text)) {
return false;
}
// Debt/tail wording indicates open-items intent, not contract list.
if (/(?:долг|задолж|хвост|позиц|open\s+items|unclosed\s+items|взаиморасчет|взаиморасчёт)/iu.test(text)) {
return false;
}
return true;
} }
function isLikelyCounterpartyToken(rawToken) { function isLikelyCounterpartyToken(rawToken) {
const token = String(rawToken ?? "").trim().toLowerCase(); const token = String(rawToken ?? "").trim().toLowerCase();
@ -185,6 +294,67 @@ function hasPartyAnchorMention(text) {
text.includes("покупател") || text.includes("покупател") ||
text.includes("партнер")); text.includes("партнер"));
} }
function hasContractAnchorMention(text) {
return (text.includes("договор") ||
text.includes("контракт") ||
/\bдог\.?\b/iu.test(text) ||
text.includes("дог.") ||
text.includes("contract") ||
text.includes("dogovor"));
}
function hasContractNumberLikeToken(text) {
if (/(?:^|[\s([{])(?:№|#|n)\s*[a-zа-яё0-9][a-zа-яё0-9./_-]{1,}(?=$|[\s,.;:!?)\]}])/iu.test(text)) {
return true;
}
const rawTokens = text
.split(/[\s,;:!?()[\]{}"«»]+/u)
.map((token) => token.replace(/^[^\p{L}\p{N}#№]+|[^\p{L}\p{N}./_-]+$/gu, "").trim())
.filter((token) => token.length > 0);
for (const rawToken of rawTokens) {
const token = String(rawToken ?? "").trim();
if (!/^\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?$/u.test(token)) {
continue;
}
if (!token) {
continue;
}
if (/^\d{1,2}\.\d{1,2}$/u.test(token)) {
// Likely an account code like 60.01/51.00, not a contract number.
continue;
}
const parts = token.split(/[./_-]+/u).map((part) => Number(part));
if (!parts.every((part) => Number.isFinite(part))) {
return true;
}
if (parts.length === 2) {
const [a, b] = parts;
const yearFirst = a >= 1900 && a <= 2099 && b >= 1 && b <= 12;
const yearSecond = b >= 1900 && b <= 2099 && a >= 1 && a <= 12;
if (yearFirst || yearSecond) {
continue;
}
return true;
}
if (parts.length === 3) {
const [a, b, c] = parts;
const ymd = a >= 1900 && a <= 2099 && b >= 1 && b <= 12 && c >= 1 && c <= 31;
const dmy = c >= 1900 && c <= 2099 && a >= 1 && a <= 31 && b >= 1 && b <= 12;
if (ymd || dmy) {
continue;
}
return true;
}
return true;
}
return false;
}
function hasContractAnchorSignal(text) {
if (hasContractAnchorMention(text)) {
return true;
}
// Allow short forms like "15/24" for follow-up prompts if document/bank signal exists.
return hasContractNumberLikeToken(text) && hasDocsOrBankSignal(text);
}
function hasLooseByAnchorMention(text) { function hasLooseByAnchorMention(text) {
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) { if (!match) {
@ -236,8 +406,18 @@ function hasImplicitCounterpartyAnchorAroundDocs(text) {
function hasDocsOrBankSignal(text) { function hasDocsOrBankSignal(text) {
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text); return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
} }
function hasBankOperationSignal(text) {
return hasAny(text, BANK_OPERATION_CORE_HINTS) || hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) || hasAny(text, BANK_OPERATIONS_BY_CONTRACT_HINTS);
}
function hasDocumentSignal(text) {
return (text.includes("док") ||
text.includes("доки") ||
text.includes("документ") ||
text.includes("docs") ||
text.includes("documents"));
}
function hasHeuristicCounterpartyAnchor(text) { function hasHeuristicCounterpartyAnchor(text) {
if (!hasDocsOrBankSignal(text)) { if (!hasDocsOrBankSignal(text) && !hasBankOperationSignal(text)) {
return false; return false;
} }
const tokens = String(text ?? "") const tokens = String(text ?? "")
@ -289,13 +469,44 @@ function resolveAddressIntent(userMessage) {
reasons: ["payables_signal_detected"] reasons: ["payables_signal_detected"]
}; };
} }
if (hasDocumentsFormingBalanceSignal(text) && (hasAccountNumberAnchor(text) || text.includes("счет"))) { if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return { return {
intent: "documents_forming_balance", intent: "documents_forming_balance",
confidence: "high", confidence: "high",
reasons: ["documents_forming_balance_signal_detected"] reasons: ["documents_forming_balance_signal_detected"]
}; };
} }
if (hasOpenContractsListSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["open_contract_signal_detected"]
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
reasons: ["open_items_signal_detected"]
};
}
if (hasContractAnchorSignal(text) &&
hasBankOperationSignal(text)) {
return {
intent: "bank_operations_by_contract",
confidence: "medium",
reasons: ["bank_ops_by_contract_signal_detected"]
};
}
if (hasContractAnchorSignal(text) &&
(hasAny(text, DOCUMENTS_BY_CONTRACT_HINTS) || hasDocumentSignal(text))) {
return {
intent: "list_documents_by_contract",
confidence: "medium",
reasons: ["documents_by_contract_signal_detected"]
};
}
if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) && if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) &&
(hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) { (hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) {
return { return {
@ -315,7 +526,7 @@ function resolveAddressIntent(userMessage) {
reasons: ["documents_by_counterparty_signal_detected"] reasons: ["documents_by_counterparty_signal_detected"]
}; };
} }
if (hasAny(text, ACCOUNT_BALANCE_HINTS) || hasAccountNumberAnchor(text)) { if (hasAccountBalanceSignal(text)) {
return { return {
intent: "account_balance_snapshot", intent: "account_balance_snapshot",
confidence: "high", confidence: "high",
@ -329,13 +540,6 @@ function resolveAddressIntent(userMessage) {
reasons: ["generic_lookup_with_loose_anchor_fallback"] reasons: ["generic_lookup_with_loose_anchor_fallback"]
}; };
} }
if (hasAny(text, OPEN_ITEMS_HINTS) && (text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
reasons: ["open_items_signal_detected"]
};
}
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) { if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) {
return { return {
intent: "list_open_contracts", intent: "list_open_contracts",

View File

@ -23,7 +23,11 @@ const ADDRESS_ACTION_TOKENS = [
"кому", "кому",
"какие", "какие",
"что по", "что по",
"че по",
"чё по",
"остаток", "остаток",
"скока",
"сколько",
"долг", "долг",
"задолж", "задолж",
"хвост", "хвост",
@ -64,6 +68,7 @@ const ADDRESS_ENTITY_TOKENS = [
"банк", "банк",
"выписк", "выписк",
"операц", "операц",
"транзак",
"договор", "договор",
"счет", "счет",
"счёт", "счёт",
@ -148,7 +153,10 @@ function hasAddressFollowupSignal(text) {
return false; return false;
} }
function hasDocsOrBankSignal(text) { function hasDocsOrBankSignal(text) {
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text); return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|транзак|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
}
function hasAccountCodeAnchor(text) {
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})(?![\d-])/u.test(text);
} }
function hasLikelyCounterpartyToken(text) { function hasLikelyCounterpartyToken(text) {
const stopWords = new Set([ const stopWords = new Set([
@ -237,21 +245,22 @@ function detectAddressQuestionMode(userMessage) {
const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS); const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS);
const hasLooseByAnchor = hasLooseByAnchorMention(text); const hasLooseByAnchor = hasLooseByAnchorMention(text);
const hasFollowupSignal = hasAddressFollowupSignal(text); const hasFollowupSignal = hasAddressFollowupSignal(text);
if (hasAddressAction && hasAddressEntity && !hasDeepReasoning) { const hasAccountCode = hasAccountCodeAnchor(text);
if (hasAddressAction && (hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
return { return {
mode: "address_query", mode: "address_query",
confidence: "high", confidence: "high",
reasons: ["address_action_detected", "address_entity_detected"] reasons: ["address_action_detected", "address_entity_detected"]
}; };
} }
if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal) && !hasDeepReasoning) { if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal || hasAccountCode) && !hasDeepReasoning) {
return { return {
mode: "address_query", mode: "address_query",
confidence: "medium", confidence: "medium",
reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])] reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])]
}; };
} }
if (hasAddressEntity && !hasDeepReasoning) { if ((hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
return { return {
mode: "address_query", mode: "address_query",
confidence: "medium", confidence: "medium",

View File

@ -9,6 +9,7 @@ const resolveStage_1 = require("./address_runtime/resolveStage");
const composeStage_1 = require("./address_runtime/composeStage"); const composeStage_1 = require("./address_runtime/composeStage");
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"]; const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1"; const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
const PARTY_ANCHOR_STOPWORDS = new Set([ const PARTY_ANCHOR_STOPWORDS = new Set([
"ооо", "ооо",
"ао", "ао",
@ -23,6 +24,35 @@ const PARTY_ANCHOR_STOPWORDS = new Set([
"по", "по",
"by" "by"
]); ]);
const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([
"что",
"чо",
"были",
"был",
"была",
"было",
"ли",
"какие",
"какой",
"покажи",
"показать",
"выведи",
"списания",
"списание",
"поступления",
"поступление",
"доки",
"документ",
"документы",
"документов",
"банковские",
"операции",
"платежи",
"платеж",
"платежи",
"плс",
"please"
]);
const ACCOUNT_ALIAS_MAP = { const ACCOUNT_ALIAS_MAP = {
"51": ["расчетный счет", "расчетные счета", "bank account"], "51": ["расчетный счет", "расчетные счета", "bank account"],
"52": ["валютный счет", "валютные счета", "currency account"], "52": ["валютный счет", "валютные счета", "currency account"],
@ -120,6 +150,29 @@ function matchesAnchorText(searchable, anchor) {
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin); return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
}); });
} }
function isLikelyLowQualityPartyAnchor(value) {
const normalized = normalizeSearchText(String(value ?? ""));
if (!normalized) {
return true;
}
const tokens = normalized.split(" ").filter(Boolean);
if (tokens.length === 0) {
return true;
}
const meaningfulTokens = tokens.filter((token) => {
if (token.length < 2) {
return false;
}
if (PARTY_ANCHOR_STOPWORDS.has(token) || LOW_QUALITY_PARTY_ANCHOR_TOKENS.has(token)) {
return false;
}
if (/^(?:19|20)\d{2}$/.test(token)) {
return false;
}
return true;
});
return meaningfulTokens.length === 0;
}
function normalizeAccountToken(value) { function normalizeAccountToken(value) {
const source = String(value ?? "").trim().replace(",", "."); const source = String(value ?? "").trim().replace(",", ".");
const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/); const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/);
@ -308,11 +361,11 @@ function applyAddressFilters(rows, filters) {
}; };
} }
function applyIntentSpecificFilter(intent, rows) { function applyIntentSpecificFilter(intent, rows) {
if (intent === "bank_operations_by_counterparty") { if (intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_contract") {
const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i; const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i;
return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase())); return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase()));
} }
if (intent === "list_documents_by_counterparty") { if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") {
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i; const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i;
return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0); return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
} }
@ -330,7 +383,18 @@ function canAutoBroadenPeriodWindow(intent, filters) {
if (!hasExplicitPeriodWindow(filters)) { if (!hasExplicitPeriodWindow(filters)) {
return false; return false;
} }
return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty"; return (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract");
}
function isAnchorRecoveryIntent(intent) {
return (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract" ||
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts");
} }
function toIsoDatePrefix(value) { function toIsoDatePrefix(value) {
if (!value) { if (!value) {
@ -741,35 +805,240 @@ class AddressQueryService {
: matchFailureStage === "materialized_but_filtered_out_by_recipe" : matchFailureStage === "materialized_but_filtered_out_by_recipe"
? "rows_filtered_out_by_intent_recipe_after_anchor_match" ? "rows_filtered_out_by_intent_recipe_after_anchor_match"
: null; : null;
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && (0, composeStage_1.contractCandidatesFromRows)(filteredRows).length === 0) { if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) {
return buildLimitedExecutionResult({ const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
mode, const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
shape, if (recoveredRows.length > 0) {
intent, const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows);
filters: filters.extracted_filters, const recoveryReason = recoveredBankRows.length > 0
missingRequiredFilters: [], ? "contract_docs_recovered_via_bank_fallback"
selectedRecipe: recipeSelection.selected_recipe.recipe_id, : "contract_docs_recovered_via_anchor_rows";
accountScopeMode: plan.account_scope_mode, const replyPrefix = recoveredBankRows.length > 0
accountScopeFallbackApplied, ? "Документный фильтр в live дал пустой набор; показываю связанные банковские операции по договору."
accountScopeAudit, : "Документный фильтр в live дал пустой набор; показываю найденные строки по договорному якорю.";
anchor, return {
matchFailureStage, handled: true,
matchFailureReason, reply_text: `${replyPrefix}\n${factual.text}`,
mcpCallStatus: stageStatus, reply_type: (0, composeStage_1.inferReplyType)(factual.responseType),
rowsFetched: mcp.fetched_rows, response_type: factual.responseType,
rawRowsReceived: mcp.raw_rows.length, debug: {
rowsAfterAccountScope: normalizedRows.length, detected_mode: mode.mode,
rowsAfterRecipeFilter: filterByAnchors.length, detected_mode_confidence: mode.confidence,
rowsMaterialized: normalizedRows.length, query_shape: shape.shape,
rowsMatched: filteredRows.length, query_shape_confidence: shape.confidence,
rawRowKeysSample: rowDiagnostics.rawRowKeysSample, detected_intent: intent.intent,
materializationDropReason: rowDiagnostics.materializationDropReason, detected_intent_confidence: intent.confidence,
category: "recipe_visibility_gap", extracted_filters: filters.extracted_filters,
reasonText: "в live строках нет договорных якорей для уверенного списка незакрытых договоров", missing_required_filters: [],
nextStep: "сузьте запрос по контрагенту или добавьте номер договора", selected_recipe: recipeSelection.selected_recipe.recipe_id,
limitations: ["no_contract_anchors_in_live_rows"], mcp_call_status_legacy: toLegacyMcpStatus("matched_non_empty"),
reasons: baseReasons account_scope_mode: plan.account_scope_mode,
}); account_scope_fallback_applied: accountScopeFallbackApplied,
anchor_type: anchor.anchor_type,
anchor_value_raw: anchor.anchor_value_raw,
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: "matched_non_empty",
rows_fetched: mcp.fetched_rows,
raw_rows_received: mcp.raw_rows.length,
rows_after_account_scope: normalizedRows.length,
rows_after_recipe_filter: filterByAnchors.length,
rows_materialized: normalizedRows.length,
rows_matched: recoveredRows.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,
limitations: [...filters.warnings, recoveryReason],
reasons: [...baseReasons, recoveryReason]
}
};
}
}
if (filteredRows.length === 0 &&
isAnchorRecoveryIntent(intent.intent) &&
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) {
const currentLimit = typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit)
? Math.max(1, Math.trunc(filters.extracted_filters.limit))
: plan.limit;
if (currentLimit < ADDRESS_ANCHOR_RECOVERY_LIMIT) {
const expandedLimitFilters = {
...filters.extracted_filters,
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
};
const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, expandedLimitFilters);
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
const expandedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters);
if (expandedPlan.limit > currentLimit) {
const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: expandedPlan.query,
limit: expandedPlan.limit
});
if (!expandedMcp.error) {
const expandedRawRows = toNormalizedRows(expandedMcp.raw_rows);
const expandedScopedRows = applyAccountScopeFilter(expandedRawRows, expandedPlan.account_scope);
const expandedAccountScopeFallbackApplied = expandedPlan.account_scope_mode === "preferred" &&
expandedPlan.account_scope.length > 0 &&
expandedRawRows.length > 0 &&
expandedScopedRows.length === 0;
const expandedNormalizedRows = expandedAccountScopeFallbackApplied ? expandedRawRows : expandedScopedRows;
let expandedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, expandedLimitFilters);
expandedAnchor = (0, resolveStage_1.refineAnchorFromRows)(expandedAnchor, expandedNormalizedRows);
const expandedFiltersForMatching = expandedAnchor.anchor_type === "counterparty" && expandedAnchor.anchor_value_resolved
? { ...expandedLimitFilters, counterparty: expandedAnchor.anchor_value_resolved }
: expandedAnchor.anchor_type === "contract" && expandedAnchor.anchor_value_resolved
? { ...expandedLimitFilters, contract: expandedAnchor.anchor_value_resolved }
: expandedLimitFilters;
const expandedAccountScopeAudit = buildAccountScopeAudit({
intent: intent.intent,
filters: expandedFiltersForMatching,
accountScope: expandedPlan.account_scope,
rowsBeforeScope: expandedRawRows.length,
rowsAfterScope: expandedNormalizedRows.length
});
const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching);
const expandedRowsByAnchor = expandedAnchorFilter.rows;
const expandedFilteredRows = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
if (expandedFilteredRows.length > 0) {
const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length);
const expandedStageStatus = deriveMcpStageStatus({
rawRowsReceived: expandedMcp.raw_rows.length,
rowsMaterialized: expandedNormalizedRows.length,
rowsAnchorMatched: expandedRowsByAnchor.length,
rowsMatched: expandedFilteredRows.length
});
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows);
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
return {
handled: true,
reply_text: `${expandedPrefix}\n${expandedFactual.text}`,
reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType),
response_type: expandedFactual.responseType,
debug: {
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
query_shape_confidence: shape.confidence,
detected_intent: intent.intent,
detected_intent_confidence: intent.confidence,
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: expandedSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: toLegacyMcpStatus(expandedStageStatus),
account_scope_mode: expandedPlan.account_scope_mode,
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
anchor_type: expandedAnchor.anchor_type,
anchor_value_raw: expandedAnchor.anchor_value_raw,
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
resolver_confidence: expandedAnchor.resolver_confidence,
ambiguity_count: expandedAnchor.ambiguity_count,
match_failure_stage: "none",
match_failure_reason: null,
mcp_call_status: expandedStageStatus,
rows_fetched: expandedMcp.fetched_rows,
raw_rows_received: expandedMcp.raw_rows.length,
rows_after_account_scope: expandedNormalizedRows.length,
rows_after_recipe_filter: expandedRowsByAnchor.length,
rows_materialized: expandedNormalizedRows.length,
rows_matched: expandedFilteredRows.length,
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: expandedFactual.responseType,
limitations: expandedLimitations,
reasons: expandedReasons
}
};
}
if (intent.intent === "open_items_by_counterparty_or_contract" &&
expandedFilteredRows.length === 0 &&
expandedRowsByAnchor.length === 0 &&
expandedNormalizedRows.length > 0) {
const expandedFallbackRows = applyIntentSpecificFilter(intent.intent, expandedNormalizedRows);
if (expandedFallbackRows.length > 0) {
const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length);
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFallbackRows);
const expandedLimitations = [
...filters.warnings,
"query_limit_auto_expanded_for_anchor_recovery",
"open_items_anchor_not_matched_fallback_rows"
];
const expandedReasons = [
...baseReasons,
"query_limit_auto_expanded_for_anchor_recovery",
"open_items_anchor_not_matched_fallback_rows"
];
return {
handled: true,
reply_text: `Точный якорь не подтвердился в live-строках даже после расширения до ${expandedPlan.limit}; ` +
"показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
expandedFactual.text,
reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType),
response_type: expandedFactual.responseType,
debug: {
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
query_shape_confidence: shape.confidence,
detected_intent: intent.intent,
detected_intent_confidence: intent.confidence,
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: expandedSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: "matched_non_empty",
account_scope_mode: expandedPlan.account_scope_mode,
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
anchor_type: expandedAnchor.anchor_type,
anchor_value_raw: expandedAnchor.anchor_value_raw,
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
resolver_confidence: expandedAnchor.resolver_confidence,
ambiguity_count: expandedAnchor.ambiguity_count,
match_failure_stage: "materialized_but_not_anchor_matched",
match_failure_reason: expandedAnchorFilter.mismatchReason ?? "anchor_not_matched_after_query_limit_expansion",
mcp_call_status: "matched_non_empty",
rows_fetched: expandedMcp.fetched_rows,
raw_rows_received: expandedMcp.raw_rows.length,
rows_after_account_scope: expandedNormalizedRows.length,
rows_after_recipe_filter: expandedRowsByAnchor.length,
rows_materialized: expandedNormalizedRows.length,
rows_matched: expandedFallbackRows.length,
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: expandedFactual.responseType,
limitations: expandedLimitations,
reasons: expandedReasons
}
};
}
}
}
}
}
}
} }
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
const autoBroadenedFilters = { ...filters.extracted_filters }; const autoBroadenedFilters = { ...filters.extracted_filters };
@ -870,37 +1139,111 @@ class AddressQueryService {
} }
} }
} }
if (filteredRows.length === 0 &&
intent.intent === "open_items_by_counterparty_or_contract" &&
normalizedRows.length > 0) {
const openItemsFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
if (openItemsFallbackRows.length > 0) {
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, openItemsFallbackRows);
const fallbackLimitations = [...filters.warnings, "open_items_anchor_not_matched_fallback_rows"];
const fallbackReasons = [...baseReasons, "open_items_anchor_not_matched_fallback_rows"];
return {
handled: true,
reply_text: "Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
fallbackFactual.text,
reply_type: (0, composeStage_1.inferReplyType)(fallbackFactual.responseType),
response_type: fallbackFactual.responseType,
debug: {
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
query_shape_confidence: shape.confidence,
detected_intent: intent.intent,
detected_intent_confidence: intent.confidence,
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: recipeSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: "matched_non_empty",
account_scope_mode: plan.account_scope_mode,
account_scope_fallback_applied: accountScopeFallbackApplied,
anchor_type: anchor.anchor_type,
anchor_value_raw: anchor.anchor_value_raw,
anchor_value_resolved: anchor.anchor_value_resolved,
resolver_confidence: anchor.resolver_confidence,
ambiguity_count: anchor.ambiguity_count,
match_failure_stage: matchFailureStage,
match_failure_reason: matchFailureReason,
mcp_call_status: "matched_non_empty",
rows_fetched: mcp.fetched_rows,
raw_rows_received: mcp.raw_rows.length,
rows_after_account_scope: normalizedRows.length,
rows_after_recipe_filter: filterByAnchors.length,
rows_materialized: normalizedRows.length,
rows_matched: openItemsFallbackRows.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: fallbackFactual.responseType,
limitations: fallbackLimitations,
reasons: fallbackReasons
}
};
}
}
if (filteredRows.length === 0) { if (filteredRows.length === 0) {
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0; const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
const hadAnchorMatchedRows = filterByAnchors.length > 0; const hadAnchorMatchedRows = filterByAnchors.length > 0;
const isVisibilityGapCandidate = hadBaseRows && const isVisibilityGapCandidate = hadBaseRows &&
hadAnchorMatchedRows && hadAnchorMatchedRows &&
(intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty"); (intent.intent === "list_documents_by_counterparty" ||
intent.intent === "bank_operations_by_counterparty" ||
intent.intent === "list_documents_by_contract" ||
intent.intent === "bank_operations_by_contract");
const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched"; const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched";
const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe"; const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe";
const isFollowupAnchorCarryover = Array.isArray(filters.warnings) &&
(filters.warnings.includes("counterparty_from_followup_context") ||
filters.warnings.includes("contract_from_followup_context"));
const isLowQualityPartyAnchor = (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") &&
isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw);
const anchorMismatchCategory = isFollowupAnchorCarryover || !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor";
const category = isAnchorMismatch const category = isAnchorMismatch
? "missing_anchor" ? anchorMismatchCategory
: isRecipeFilteredOut : isRecipeFilteredOut
? "recipe_visibility_gap" ? "recipe_visibility_gap"
: isVisibilityGapCandidate : isVisibilityGapCandidate
? "recipe_visibility_gap" ? "recipe_visibility_gap"
: "empty_match"; : "empty_match";
const reasonText = isAnchorMismatch const reasonText = isAnchorMismatch
? "якорь контрагента/договора не найден в материализованных live-строках" ? anchorMismatchCategory === "missing_anchor"
? "якорь контрагента/договора не найден в материализованных live-строках"
: "по указанному якорю и фильтрам в live-выборке нет строк"
: isRecipeFilteredOut : isRecipeFilteredOut
? "строки по якорю найдены, но отфильтрованы intent-specific recipe" ? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
: isVisibilityGapCandidate : isVisibilityGapCandidate
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
: "по выбранным фильтрам в live-выборке нет строк"; : "по выбранным фильтрам в live-выборке нет строк";
const nextStep = isAnchorMismatch const nextStep = isAnchorMismatch
? "уточните контрагента точным именем или добавьте ИНН/договор" ? anchorMismatchCategory === "missing_anchor"
? "уточните контрагента точным именем или добавьте ИНН/договор"
: "уточните период или снимите часть фильтров"
: isRecipeFilteredOut : isRecipeFilteredOut
? "сузьте период, уточните контрагента или документный тип" ? "сузьте период, уточните контрагента или документный тип"
: isVisibilityGapCandidate : isVisibilityGapCandidate
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
: "уточните период, контрагента, договор или снимите часть фильтров"; : "уточните период, контрагента, договор или снимите часть фильтров";
const limitations = isAnchorMismatch const limitations = isAnchorMismatch
? ["anchor_not_matched_after_materialization"] ? [
anchorMismatchCategory === "missing_anchor"
? "anchor_not_matched_after_materialization"
: "no_rows_for_anchor_after_materialization"
]
: isRecipeFilteredOut : isRecipeFilteredOut
? ["rows_filtered_out_by_recipe_after_anchor_match"] ? ["rows_filtered_out_by_recipe_after_anchor_match"]
: [ : [

View File

@ -103,6 +103,26 @@ const BASE_RECIPES = [
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "bank_docs" query_template: "bank_docs"
}, },
{
recipe_id: "address_documents_by_contract_v1",
intent: "list_documents_by_contract",
purpose: "Collect contract-related document lines from movements",
required_filters: ["contract"],
optional_filters: ["period_from", "period_to", "as_of_date", "organization", "counterparty", "limit", "sort"],
default_limit: 100,
account_scope: ["60", "62", "76", "51", "52"],
account_scope_mode: "preferred"
},
{
recipe_id: "address_bank_operations_by_contract_v1",
intent: "bank_operations_by_contract",
purpose: "Collect bank operation candidates by contract from movement lines",
required_filters: ["contract"],
optional_filters: ["period_from", "period_to", "as_of_date", "organization", "counterparty", "limit", "sort"],
default_limit: 100,
account_scope: ["51", "52"],
account_scope_mode: "preferred"
},
{ {
recipe_id: "address_documents_forming_balance_v1", recipe_id: "address_documents_forming_balance_v1",
intent: "documents_forming_balance", intent: "documents_forming_balance",
@ -216,8 +236,9 @@ function buildMovementAccountCondition(filters) {
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`; return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
} }
function shouldBoostLimitForAllTimeCounterparty(filters) { function shouldBoostLimitForAllTimeCounterparty(filters) {
const hasCounterparty = typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0; const hasAnchor = (typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0) ||
if (!hasCounterparty) { (typeof filters.contract === "string" && filters.contract.trim().length > 0);
if (!hasAnchor) {
return false; return false;
} }
const hasPeriod = Boolean((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) || const hasPeriod = Boolean((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
@ -226,7 +247,12 @@ function shouldBoostLimitForAllTimeCounterparty(filters) {
return !hasPeriod; return !hasPeriod;
} }
function maxLimitForIntent(intent) { function maxLimitForIntent(intent) {
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { if (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract" ||
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts") {
return ADDRESS_MAX_LIMIT_EXTENDED; return ADDRESS_MAX_LIMIT_EXTENDED;
} }
return ADDRESS_MAX_LIMIT_DEFAULT; return ADDRESS_MAX_LIMIT_DEFAULT;
@ -255,7 +281,10 @@ function buildAddressRecipePlan(recipe, filters) {
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit) const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit))) ? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
: recipe.default_limit; : recipe.default_limit;
const boostedLimit = (recipe.intent === "list_documents_by_counterparty" || recipe.intent === "bank_operations_by_counterparty") && const boostedLimit = (recipe.intent === "list_documents_by_counterparty" ||
recipe.intent === "bank_operations_by_counterparty" ||
recipe.intent === "list_documents_by_contract" ||
recipe.intent === "bank_operations_by_contract") &&
shouldBoostLimitForAllTimeCounterparty(filters) shouldBoostLimitForAllTimeCounterparty(filters)
? Math.max(baseLimit, maxLimit) ? Math.max(baseLimit, maxLimit)
: (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && : (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") &&

View File

@ -67,7 +67,13 @@ function composeFactualReply(intent, rows) {
`Строк движения: ${rows.length}.`, `Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.` `Договорных кандидатов: ${contracts.length}.`
]; ];
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); if (contracts.length > 0) {
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
}
else {
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
lines.push(...formatTopRows(rows, 6));
}
return { return {
responseType: "FACTUAL_LIST", responseType: "FACTUAL_LIST",
text: lines.join("\n") text: lines.join("\n")
@ -95,6 +101,17 @@ function composeFactualReply(intent, rows) {
text: lines.join("\n") text: lines.join("\n")
}; };
} }
if (intent === "list_documents_by_contract") {
const lines = [
"Собран список документов по договору (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "bank_operations_by_counterparty") { if (intent === "bank_operations_by_counterparty") {
const lines = [ const lines = [
"Собран список банковских операций по контрагенту (live address lane).", "Собран список банковских операций по контрагенту (live address lane).",
@ -106,6 +123,17 @@ function composeFactualReply(intent, rows) {
text: lines.join("\n") text: lines.join("\n")
}; };
} }
if (intent === "bank_operations_by_contract") {
const lines = [
"Собран список банковских операций по договору (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
const title = intent === "list_payables_counterparties" const title = intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76." ? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties" : intent === "list_receivables_counterparties"

View File

@ -71,6 +71,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
} }
} }
} }
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
if (!toNonEmptyString(merged.contract)) {
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
if (inheritedContract) {
merged.contract = inheritedContract;
reasons.push("contract_from_followup_context");
}
}
}
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
if (!toNonEmptyString(merged.account)) { if (!toNonEmptyString(merged.account)) {
const inheritedAccount = previousAccount ?? (followupContext.previous_anchor_type === "account" ? previousAnchorValue : null); const inheritedAccount = previousAccount ?? (followupContext.previous_anchor_type === "account" ? previousAnchorValue : null);
@ -130,7 +139,9 @@ function resolveMissingRequiredFilters(intent, filters) {
account_balance_snapshot: ["account", "as_of_date"], account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"], documents_forming_balance: ["account", "as_of_date"],
list_documents_by_counterparty: ["counterparty"], list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"] bank_operations_by_counterparty: ["counterparty"],
list_documents_by_contract: ["contract"],
bank_operations_by_contract: ["contract"]
}; };
const required = requiredByIntent[intent] ?? []; const required = requiredByIntent[intent] ?? [];
return required.filter((key) => { return required.filter((key) => {

View File

@ -109,6 +109,17 @@ function resolvePrimaryAnchor(intent, filters) {
}; };
} }
} }
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
if (contract) {
return {
anchor_type: "contract",
anchor_value_raw: contract,
anchor_value_resolved: contract,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
}
if (counterparty) { if (counterparty) {
return { return {
anchor_type: "counterparty", anchor_type: "counterparty",

View File

@ -54,6 +54,8 @@ const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"))
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards")); const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence")); const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence"));
const addressQueryService_1 = __importStar(require("./addressQueryService")); const addressQueryService_1 = __importStar(require("./addressQueryService"));
const addressQueryClassifier_1 = __importStar(require("./addressQueryClassifier"));
const addressIntentResolver_1 = __importStar(require("./addressIntentResolver"));
const iconv_lite_1 = __importDefault(require("iconv-lite")); const iconv_lite_1 = __importDefault(require("iconv-lite"));
function retrievalSummaryForRoute(route) { function retrievalSummaryForRoute(route) {
if (route === "store_canonical") if (route === "store_canonical")
@ -1802,6 +1804,10 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
llm_decomposition_trace_id: llmMeta?.traceId ?? null, llm_decomposition_trace_id: llmMeta?.traceId ?? null,
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null, llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null,
llm_decomposition_reason: llmMeta?.reason ?? null, llm_decomposition_reason: llmMeta?.reason ?? null,
fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null,
sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null,
tool_gate_decision: llmMeta?.toolGateDecision ?? null,
tool_gate_reason: llmMeta?.toolGateReason ?? null,
answer_structure_v11: null, answer_structure_v11: null,
investigation_state_snapshot: null, investigation_state_snapshot: null,
normalized: null, normalized: null,
@ -1865,9 +1871,24 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"show", "show",
"list", "list",
"выведи", "выведи",
"что",
"чо",
"которые", "которые",
"какие", "какие",
"какой", "какой",
"были",
"был",
"была",
"было",
"ли",
"списания",
"списание",
"поступления",
"поступление",
"расчетного",
"расчётного",
"счета",
"счёта",
"есть", "есть",
"est", "est",
"kakie", "kakie",
@ -1880,6 +1901,401 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"ёпт", "ёпт",
"бля" "бля"
]); ]);
const ADDRESS_FALLBACK_STRIP_TOKENS = new Set([
"бля",
"блять",
"blya",
"blyat",
"епт",
"ёпт",
"епта",
"нах",
"нахуй",
"плс",
"pls",
"пж",
"пжлст",
"пожалуйста",
"please"
]);
const ADDRESS_MONTH_ALIAS_MAP = {
янв: "01",
январ: "01",
january: "01",
jan: "01",
фев: "02",
феврал: "02",
february: "02",
feb: "02",
мар: "03",
март: "03",
march: "03",
apr: "04",
апр: "04",
апрел: "04",
april: "04",
май: "05",
ма: "05",
may: "05",
июн: "06",
июнь: "06",
june: "06",
jun: "06",
июл: "07",
июль: "07",
july: "07",
jul: "07",
авг: "08",
август: "08",
august: "08",
aug: "08",
сен: "09",
сент: "09",
сентябр: "09",
september: "09",
sep: "09",
окт: "10",
октябр: "10",
october: "10",
oct: "10",
ноя: "11",
ноябр: "11",
november: "11",
nov: "11",
дек: "12",
декабр: "12",
december: "12",
dec: "12"
};
const ADDRESS_DOCS_SIGNAL_PATTERN = /(?:док|доки|документ|документы|документов|docs?|documents?|bank|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|transaction)/i;
const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|расчетн|транзак)/i;
const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|(?:^|[^\p{L}\p{N}_])(?:дог\.?|[dд][oо][gг]\.?|dog\.?)(?=$|[^\p{L}\p{N}_])|contract|dogovor)/iu;
const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i;
const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu;
function normalizeAddressMonthAliasToken(token) {
const source = String(token ?? "").trim().toLowerCase();
if (!source) {
return null;
}
const direct = ADDRESS_MONTH_ALIAS_MAP[source];
if (direct) {
return direct;
}
for (const [key, value] of Object.entries(ADDRESS_MONTH_ALIAS_MAP)) {
if (source.startsWith(key)) {
return value;
}
}
return null;
}
function normalizeAddressShortYearMentions(text) {
return String(text ?? "").replace(/(^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/giu, (_full, prefix, shortYear) => {
const normalized = Number(shortYear);
if (!Number.isFinite(normalized) || normalized < 0 || normalized > 99) {
return _full;
}
return `${prefix}${String(2000 + normalized)} год`;
});
}
function sanitizeAddressMessageForFallback(userMessage) {
const repaired = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
if (!repaired) {
return "";
}
let sanitized = repaired.toLowerCase();
sanitized = sanitized
.replace(/\bpokezh\b/giu, "покажи")
.replace(/\bpokazh(?:i)?\b/giu, "покажи")
.replace(/\bpokaji\b/giu, "покажи")
.replace(/\bop(?:er|ers?)\b/giu, "операции")
.replace(/(^|[^\p{L}\p{N}_])опер(?:аци[яиюе]|ы|)?(?=$|[^\p{L}\p{N}_])/giu, "$1операции")
.replace(/(^|[^\p{L}\p{N}_])дог\.?(?=$|[^\p{L}\p{N}_])/giu, "$1договор")
.replace(/(^|[^\p{L}\p{N}_])dog\.?(?=$|[^\p{L}\p{N}_])/giu, "$1contract")
.replace(/\bdok(?:i|y)?\b/giu, "доки")
.replace(/\bdocuments?\b/giu, "документы")
.replace(/\bdocs?\b/giu, "документы")
.replace(/\bschet(?:u)?\b/giu, "счет")
.replace(/\bsaldo\b/giu, "сальдо")
.replace(/\bgod\b/giu, "год");
sanitized = normalizeAddressShortYearMentions(sanitized);
const tokens = sanitized
.split(/\s+/)
.map((item) => item.trim())
.filter(Boolean);
const filteredTokens = tokens.filter((token) => {
const normalizedToken = token.replace(/^[^a-zа-яё0-9]+|[^a-zа-яё0-9]+$/giu, "");
if (!normalizedToken) {
return true;
}
return !ADDRESS_FALLBACK_STRIP_TOKENS.has(normalizedToken);
});
const compact = compactWhitespace(filteredTokens.join(" "));
return compact || compactWhitespace(repaired.toLowerCase());
}
function extractAddressFallbackYear(text) {
const source = String(text ?? "");
const fullYearMatch = source.match(/\b(20\d{2})\b/);
if (fullYearMatch) {
return fullYearMatch[1];
}
const shortYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/iu);
if (shortYearMatch) {
const shortYear = Number(shortYearMatch[1]);
if (Number.isFinite(shortYear) && shortYear >= 0 && shortYear <= 99) {
return String(2000 + shortYear);
}
}
const shortOrdinalYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:[-\s]?(?:й|ый|ой|th))(?=$|[^a-zа-яё0-9])/iu);
if (!shortOrdinalYearMatch) {
return null;
}
const shortOrdinalYear = Number(shortOrdinalYearMatch[1]);
if (!Number.isFinite(shortOrdinalYear) || shortOrdinalYear < 0 || shortOrdinalYear > 99) {
return null;
}
return String(2000 + shortOrdinalYear);
}
function extractAddressFallbackMonthYear(text) {
const source = String(text ?? "");
const numericYearMonth = source.match(/\b(20\d{2})[./-](0?[1-9]|1[0-2])\b/);
if (numericYearMonth) {
const year = numericYearMonth[1];
const month = String(Number(numericYearMonth[2])).padStart(2, "0");
return `${year}-${month}`;
}
const numericMonthYear = source.match(/\b(0?[1-9]|1[0-2])[./-](20\d{2})\b/);
if (numericMonthYear) {
const month = String(Number(numericMonthYear[1])).padStart(2, "0");
const year = numericMonthYear[2];
return `${year}-${month}`;
}
const namedMonthYear = source.match(/(?:^|[^a-zа-яё0-9])([a-zа-яё]+)\s+(20\d{2})(?=$|[^a-zа-яё0-9])/iu);
if (namedMonthYear) {
const month = normalizeAddressMonthAliasToken(namedMonthYear[1]);
if (month) {
return `${namedMonthYear[2]}-${month}`;
}
}
const yearNamedMonth = source.match(/(?:^|[^a-zа-яё0-9])(20\d{2})\s+([a-zа-яё]+)(?=$|[^a-zа-яё0-9])/iu);
if (yearNamedMonth) {
const month = normalizeAddressMonthAliasToken(yearNamedMonth[2]);
if (month) {
return `${yearNamedMonth[1]}-${month}`;
}
}
return null;
}
function extractAddressFallbackAccountToken(text) {
const source = String(text ?? "");
const explicitMatch = source.match(/(?:сч[её]т(?:а|у|ом|е)?|account)\D{0,12}(\d{2}(?:[.,]\d{1,2})?)/iu);
if (explicitMatch && explicitMatch[1]) {
return String(explicitMatch[1]).replace(",", ".");
}
const tokenPattern = /\b(\d{2}(?:[.,]\d{1,2})?)\b/giu;
let match = tokenPattern.exec(source);
while (match) {
const raw = String(match[1] ?? "");
const start = match.index;
const end = start + raw.length;
const prev = start > 0 ? source[start - 1] : " ";
const next = end < source.length ? source[end] : " ";
if (!/[./-]/.test(prev) && !/[./-]/.test(next) && !/\d/.test(prev) && !/\d/.test(next)) {
return raw.replace(",", ".");
}
match = tokenPattern.exec(source);
}
return null;
}
function pickAddressFallbackCounterpartyToken(text) {
const source = String(text ?? "");
const byAnchor = source.match(/(?:^|[\s,.;:!?()\-])(?:по|от)\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=$|[\s,.;:!?()\-])/iu);
if (byAnchor && byAnchor[1]) {
const byToken = String(byAnchor[1]).trim();
const normalizedByToken = byToken.toLowerCase();
if (byToken &&
!ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(normalizedByToken) &&
!/^\d{2}(?:\.\d{1,2})?$/.test(normalizedByToken) &&
!/^(?:19|20)\d{2}$/.test(normalizedByToken) &&
!/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalizedByToken) &&
!/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalizedByToken)) {
return byToken;
}
}
const candidates = extractAddressAnchorTokens(text);
for (const token of candidates) {
const normalized = String(token ?? "").toLowerCase();
if (!normalized || /^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
continue;
}
if (/^(?:19|20)\d{2}$/.test(normalized)) {
continue;
}
if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalized)) {
continue;
}
if (/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalized)) {
continue;
}
return token;
}
return null;
}
function extractAddressFallbackContractToken(text) {
const source = String(text ?? "");
const patterns = [
/(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)\s*(?:№|#|n|no\.?)?\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})/iu,
/(?:№|#|n|no\.?)\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})\s*(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)/iu
];
for (const pattern of patterns) {
const match = pattern.exec(source);
if (!match || !match[1]) {
continue;
}
const candidate = String(match[1]).replace(/^[^a-zа-я0-9]+|[^a-zа-я0-9/_-]+$/giu, "");
if (!candidate || candidate.length < 2) {
continue;
}
if (/^(?:19|20)\d{2}$/.test(candidate)) {
continue;
}
if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])(?:[./-](?:0?[1-9]|[12]\d|3[01]))?$/.test(candidate)) {
continue;
}
return candidate;
}
if (ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source) || /(?:^|[^\p{L}\p{N}_])(?:[dд][oо][gг]|dogovor)(?=$|[^\p{L}\p{N}_])/iu.test(source)) {
const generic = source.match(/\b([a-zа-я0-9]{1,10}[/-][a-zа-я0-9]{1,10}(?:[/-][a-zа-я0-9]{1,10})?)\b/iu);
if (generic && generic[1]) {
return generic[1];
}
}
return null;
}
function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) {
const sourceRaw = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
const source = compactWhitespace(String(sanitizedUserMessage ?? sourceRaw).toLowerCase());
if (!source) {
return null;
}
const monthYear = extractAddressFallbackMonthYear(source);
const year = extractAddressFallbackYear(source);
const allTime = ADDRESS_ALL_TIME_PATTERN.test(source);
const account = extractAddressFallbackAccountToken(source);
const docsSignal = ADDRESS_DOCS_SIGNAL_PATTERN.test(source);
const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source);
const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source);
if (balanceSignal && account) {
let periodClause = "";
let rule = "balance_account_rewrite";
if (monthYear) {
periodClause = ` на ${monthYear}`;
rule = "balance_month_period_rewrite";
}
else if (year) {
periodClause = ` на ${year}-12-31`;
rule = "balance_year_period_rewrite";
}
const candidate = compactWhitespace(`остаток по счету ${account}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
}
if (!docsSignal && !contractSignal && !balanceSignal) {
const counterparty = pickAddressFallbackCounterpartyToken(source);
const genericLookupSignal = /(?:\bесть\b|\bпокажи\b|\bвыведи\b|\bч[её]\b|\bчто\b)/iu.test(source);
if (counterparty && (allTime || monthYear || year) && genericLookupSignal) {
let periodClause = "";
let rule = "documents_counterparty_rewrite_from_generic_lookup";
if (allTime) {
periodClause = " за все время";
rule = "documents_counterparty_all_time_rewrite_from_generic_lookup";
}
else if (monthYear) {
periodClause = ` за ${monthYear}`;
rule = "documents_counterparty_month_rewrite_from_generic_lookup";
}
else if (year) {
periodClause = ` за ${year} год`;
rule = "documents_counterparty_year_rewrite_from_generic_lookup";
}
const candidate = compactWhitespace(`документы по контрагенту ${counterparty}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
}
}
if (docsSignal) {
const contract = extractAddressFallbackContractToken(sourceRaw || source);
if (contractSignal || contract) {
if (contract) {
let periodClause = "";
let rule = bankSignal ? "bank_operations_contract_rewrite" : "documents_contract_rewrite";
if (allTime) {
periodClause = " за все время";
rule = bankSignal ? "bank_operations_contract_all_time_rewrite" : "documents_contract_all_time_rewrite";
}
else if (monthYear) {
periodClause = ` за ${monthYear}`;
rule = bankSignal ? "bank_operations_contract_month_rewrite" : "documents_contract_month_rewrite";
}
else if (year) {
periodClause = ` за ${year} год`;
rule = bankSignal ? "bank_operations_contract_year_rewrite" : "documents_contract_year_rewrite";
}
const subject = bankSignal ? "банковские операции" : "документы";
const candidate = compactWhitespace(`${subject} по договору ${contract}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
}
}
else {
const counterparty = pickAddressFallbackCounterpartyToken(source);
if (counterparty) {
let periodClause = "";
const subject = bankSignal ? "банковские операции" : "документы";
const rulePrefix = bankSignal ? "bank_operations_counterparty" : "documents_counterparty";
let rule = `${rulePrefix}_rewrite`;
if (allTime) {
periodClause = " за все время";
rule = `${rulePrefix}_all_time_rewrite`;
}
else if (monthYear) {
periodClause = ` за ${monthYear}`;
rule = `${rulePrefix}_month_rewrite`;
}
else if (year) {
periodClause = ` за ${year} год`;
rule = `${rulePrefix}_year_rewrite`;
}
const candidate = compactWhitespace(`${subject} по контрагенту ${counterparty}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
}
}
}
if (source !== sourceRaw.toLowerCase() && isAddressLlmPreDecomposeCandidate(source)) {
return {
candidate: source,
rule: "noise_cleanup"
};
}
return null;
}
function textMojibakeScoreForAddress(value) { function textMojibakeScoreForAddress(value) {
const source = String(value ?? ""); const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length; const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
@ -2203,21 +2619,56 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
} }
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) { async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null; const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null;
const sanitizedUserMessage = sanitizeAddressMessageForFallback(userMessage);
const fallbackCandidate = resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage);
const hasAddressSignal = isAddressLlmPreDecomposeCandidate(userMessage) || isAddressLlmPreDecomposeCandidate(sanitizedUserMessage);
const baseMeta = { const baseMeta = {
attempted: false, attempted: false,
applied: false, applied: false,
provider, provider,
traceId: null, traceId: null,
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "not_attempted" reason: "not_attempted",
fallbackRuleHit: null,
sanitizedUserMessage,
toolGateDecision: null,
toolGateReason: null
}; };
if (Boolean(payload?.useMock)) { if (Boolean(payload?.useMock)) {
if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
...baseMeta,
applied: true,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_without_llm",
fallbackRuleHit: fallbackCandidate.rule
};
}
}
return { return {
...baseMeta, ...baseMeta,
reason: "skipped_in_mock" reason: "skipped_in_mock"
}; };
} }
if (!isAddressLlmPreDecomposeCandidate(userMessage)) { if (!hasAddressSignal) {
if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
...baseMeta,
applied: true,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_without_llm",
fallbackRuleHit: fallbackCandidate.rule
};
}
}
return { return {
...baseMeta, ...baseMeta,
reason: "not_address_like" reason: "not_address_like"
@ -2242,6 +2693,22 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output); const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output);
const candidate = candidateFromNormalized ?? candidateFromRaw; const candidate = candidateFromNormalized ?? candidateFromRaw;
if (!candidate) { if (!candidate) {
if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
...baseMeta,
attempted: true,
applied: true,
traceId: normalized?.trace_id ?? null,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_after_llm",
fallbackRuleHit: fallbackCandidate.rule
};
}
}
return { return {
...baseMeta, ...baseMeta,
attempted: true, attempted: true,
@ -2249,6 +2716,32 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed" reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed"
}; };
} }
const repairedSourceMessage = repairAddressMojibake(userMessage);
const sourceIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedSourceMessage || userMessage);
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
const intentConflict = sourceIntentKnown &&
candidateIntentKnown &&
sourceIntentResolution.intent !== candidateIntentResolution.intent;
const intentDroppedByCandidate = sourceIntentKnown && !candidateIntentKnown;
const rejectCandidateForIntentSafety = intentDroppedByCandidate ||
(intentConflict &&
(sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high"));
if (rejectCandidateForIntentSafety) {
return {
...baseMeta,
attempted: true,
applied: false,
traceId: normalized?.trace_id ?? null,
effectiveMessage: userMessage,
reason: intentDroppedByCandidate
? "normalized_fragment_rejected_intent_drop"
: "normalized_fragment_rejected_intent_conflict",
fallbackRuleHit: null,
sanitizedUserMessage
};
}
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const candidateCompact = compactWhitespace(candidate.toLowerCase()); const candidateCompact = compactWhitespace(candidate.toLowerCase());
const applied = sourceCompact !== candidateCompact; const applied = sourceCompact !== candidateCompact;
@ -2270,10 +2763,27 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
provider, provider,
traceId: normalized?.trace_id ?? null, traceId: normalized?.trace_id ?? null,
effectiveMessage: applied ? candidate : userMessage, effectiveMessage: applied ? candidate : userMessage,
reason reason,
fallbackRuleHit: null,
sanitizedUserMessage
}; };
} }
catch (error) { catch (error) {
if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
...baseMeta,
attempted: true,
applied: true,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_after_llm_error",
fallbackRuleHit: fallbackCandidate.rule
};
}
}
return { return {
...baseMeta, ...baseMeta,
attempted: true, attempted: true,
@ -2281,6 +2791,35 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
}; };
} }
} }
function resolveAddressToolGateDecision(addressInputMessage, followupContext) {
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
const hasClassifierSignal = modeDetection.mode === "address_query";
const hasMessageSignal = hasClassifierSignal ||
isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
hasAccountingSignal(addressInputMessage) ||
hasAccountingSignal(repairedInputMessage);
if (hasMessageSignal) {
return {
runAddressLane: true,
decision: "run_address_lane",
reason: hasClassifierSignal ? "address_mode_classifier_detected" : "address_signal_detected"
};
}
if (followupContext) {
return {
runAddressLane: true,
decision: "run_address_lane",
reason: "followup_context_detected"
};
}
return {
runAddressLane: false,
decision: "skip_address_lane",
reason: "no_address_signal_after_l0"
};
}
class AssistantService { class AssistantService {
normalizerService; normalizerService;
sessions; sessions;
@ -2313,12 +2852,13 @@ class AssistantService {
}; };
this.sessions.appendItem(sessionId, userItem); this.sessions.appendItem(sessionId, userItem);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? "");
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta); const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
const assistantItem = { const assistantItem = {
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
session_id: sessionId, session_id: sessionId,
role: "assistant", role: "assistant",
text: addressLane.reply_text, text: safeAddressReply,
reply_type: addressLane.reply_type, reply_type: addressLane.reply_type,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
trace_id: debug.trace_id, trace_id: debug.trace_id,
@ -2350,6 +2890,10 @@ class AssistantService {
address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null, address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null,
address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null, address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null,
address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null, address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null,
address_fallback_rule_hit: llmPreDecomposeMeta?.fallbackRuleHit ?? null,
address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null,
address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null,
address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null,
detected_mode: addressLane.debug.detected_mode, detected_mode: addressLane.debug.detected_mode,
query_shape: addressLane.debug.query_shape, query_shape: addressLane.debug.query_shape,
detected_intent: addressLane.debug.detected_intent, detected_intent: addressLane.debug.detected_intent,
@ -2403,29 +2947,62 @@ class AssistantService {
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null, provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
traceId: null, traceId: null,
effectiveMessage: userMessage, effectiveMessage: userMessage,
reason: "disabled_by_feature_flag" reason: "disabled_by_feature_flag",
fallbackRuleHit: null,
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
toolGateDecision: null,
toolGateReason: null
}; };
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage; const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items); const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items);
const shouldPreferContextualLane = Boolean(carryover?.followupContext); const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null);
if (shouldPreferContextualLane) { const addressRuntimeMeta = {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, { ...addressPreDecompose,
followupContext: carryover.followupContext toolGateDecision: toolGate.decision,
toolGateReason: toolGate.reason
};
if (!toolGate.runAddressLane) {
(0, log_1.logJson)({
timestamp: new Date().toISOString(),
level: "info",
service: "assistant_loop",
message: "assistant_address_tool_gate_skip",
sessionId,
details: {
session_id: sessionId,
user_message: userMessage,
effective_address_user_message: addressInputMessage,
address_llm_predecompose_attempted: Boolean(addressRuntimeMeta?.attempted),
address_llm_predecompose_applied: Boolean(addressRuntimeMeta?.applied),
address_llm_predecompose_reason: addressRuntimeMeta?.reason ?? null,
address_fallback_rule_hit: addressRuntimeMeta?.fallbackRuleHit ?? null,
address_sanitized_user_message: addressRuntimeMeta?.sanitizedUserMessage ?? null,
address_tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null
}
}); });
if (contextualAddressLane?.handled) { }
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose); if (toolGate.runAddressLane) {
const shouldPreferContextualLane = Boolean(carryover?.followupContext);
if (shouldPreferContextualLane) {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
followupContext: carryover.followupContext
});
if (contextualAddressLane?.handled) {
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
}
} }
} const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage);
const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage); if (primaryAddressLane?.handled) {
if (primaryAddressLane?.handled) { return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressRuntimeMeta);
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressPreDecompose); }
} if (!shouldPreferContextualLane && carryover?.followupContext) {
if (!shouldPreferContextualLane && carryover?.followupContext) { const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, { followupContext: carryover.followupContext
followupContext: carryover.followupContext });
}); if (contextualAddressLane?.handled) {
if (contextualAddressLane?.handled) { return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose); }
} }
} }
} }

View File

@ -104,6 +104,9 @@ function shouldFallbackToChatCompletions(error) {
if (!(error instanceof http_1.ApiError)) { if (!(error instanceof http_1.ApiError)) {
return false; return false;
} }
if (error.code === "OPENAI_OUTPUT_PARSE_FAILED" || error.code === "OPENAI_NON_JSON_RESPONSE") {
return true;
}
if (error.code !== "OPENAI_REQUEST_FAILED") { if (error.code !== "OPENAI_REQUEST_FAILED") {
return false; return false;
} }
@ -115,6 +118,31 @@ function shouldFallbackToChatCompletions(error) {
const message = String(error.message ?? "").toLowerCase(); const message = String(error.message ?? "").toLowerCase();
return message.includes("/responses") || message.includes("responses"); return message.includes("/responses") || message.includes("responses");
} }
function extractModelErrorMessage(data) {
const rawError = data.error;
if (typeof rawError === "string" && rawError.trim().length > 0) {
return rawError.trim();
}
if (rawError && typeof rawError === "object") {
const errorObj = rawError;
const message = errorObj.message;
if (typeof message === "string" && message.trim().length > 0) {
return message.trim();
}
}
return null;
}
function isRouteMismatchErrorMessage(message) {
const source = String(message ?? "").toLowerCase();
if (!source) {
return false;
}
return (/unexpected endpoint|unexpected route|unknown endpoint|unknown route|unsupported endpoint|unsupported route/.test(source) ||
(/endpoint/.test(source) && /method/.test(source)) ||
(/endpoint/.test(source) && /not found/.test(source)) ||
(/route/.test(source) && /not found/.test(source)) ||
(/path/.test(source) && /not found/.test(source)));
}
function loadSchemaForTransport(schemaVersion) { function loadSchemaForTransport(schemaVersion) {
const schemaFile = schemaVersion === "v1" const schemaFile = schemaVersion === "v1"
? "normalized_query_v1.json" ? "normalized_query_v1.json"
@ -331,9 +359,13 @@ class OpenAIResponsesClient {
}); });
} }
} }
const modelErrorMessage = extractModelErrorMessage(data);
if (modelErrorMessage && canFallbackToAlternativeBase && !isLastCandidate && isRouteMismatchErrorMessage(modelErrorMessage)) {
continue;
}
if (!response.ok) { if (!response.ok) {
const errorObj = (data.error ?? {}); const errorObj = (data.error ?? {});
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", String(errorObj.message ?? `Model endpoint failed: ${response.status}`), response.status, { throw new http_1.ApiError("OPENAI_REQUEST_FAILED", modelErrorMessage ?? String(errorObj.message ?? `Model endpoint failed: ${response.status}`), response.status, {
route: routePath, route: routePath,
url, url,
status: response.status, status: response.status,
@ -341,6 +373,13 @@ class OpenAIResponsesClient {
code: errorObj.code ?? null code: errorObj.code ?? null
}); });
} }
if (modelErrorMessage) {
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", modelErrorMessage, 502, {
route: routePath,
url,
status: response.status
});
}
return data; return data;
} }
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, { throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, {

View File

@ -323,6 +323,11 @@ function cleanupAnchorValue(value: string): string {
if (asOfTruncatedTailPattern.test(normalized)) { if (asOfTruncatedTailPattern.test(normalized)) {
return normalized.replace(asOfTruncatedTailPattern, "").trim(); return normalized.replace(asOfTruncatedTailPattern, "").trim();
} }
const periodEndTailPattern =
/\s+на\s+конец(?:\s+период[а-яё]*)?\s+(?:\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|\d{4}|[a-zа-яё]+\s+\d{4})(?:\s+|$)[\s\S]*$/iu;
if (periodEndTailPattern.test(normalized)) {
return normalized.replace(periodEndTailPattern, "").trim();
}
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes: // Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31" // "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
@ -618,6 +623,10 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
return []; return [];
} }
function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
}
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction { export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText); const text = normalizeMojibakeString(rawText);
@ -713,8 +722,22 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
} }
} }
const explicitAsOfDate = extractAsOfDate(text);
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
filters.as_of_date = explicitAsOfDate;
const periodWasDerivedHeuristically =
warnings.includes("period_derived_from_month_phrase") ||
warnings.includes("period_derived_from_year_range_phrase") ||
warnings.includes("period_derived_from_year_phrase");
if (periodWasDerivedHeuristically && !periodRange.period_from && !periodRange.period_to) {
delete filters.period_from;
delete filters.period_to;
warnings.push("period_window_cleared_for_as_of_intent");
}
}
// If explicit period window exists, do not infer as_of_date from one of its boundary dates. // If explicit period window exists, do not infer as_of_date from one of its boundary dates.
if (!filters.period_from && !filters.period_to) { if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
const asOfDate = extractAsOfDate(text); const asOfDate = extractAsOfDate(text);
if (asOfDate) { if (asOfDate) {
filters.as_of_date = asOfDate; filters.as_of_date = asOfDate;

View File

@ -224,6 +224,23 @@ function hasAccountBalanceSignal(text: string): boolean {
return hasAccountLexeme && hasBalanceLexeme; return hasAccountLexeme && hasBalanceLexeme;
} }
function hasOpenContractsListSignal(text: string): boolean {
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
if (!hasContractLexeme || !hasOpenLexeme) {
return false;
}
// Query about a specific contract should stay in open-items lane.
if (hasContractNumberLikeToken(text)) {
return false;
}
// Debt/tail wording indicates open-items intent, not contract list.
if (/(?:долг|задолж|хвост|позиц|open\s+items|unclosed\s+items|взаиморасчет|взаиморасчёт)/iu.test(text)) {
return false;
}
return true;
}
function isLikelyCounterpartyToken(rawToken: string): boolean { function isLikelyCounterpartyToken(rawToken: string): boolean {
const token = String(rawToken ?? "").trim().toLowerCase(); const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) { if (!token || token.length < 2) {
@ -517,6 +534,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
}; };
} }
if (hasOpenContractsListSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["open_contract_signal_detected"]
};
}
if ( if (
hasAny(text, OPEN_ITEMS_HINTS) && hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract")) (text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))

View File

@ -17,7 +17,7 @@ import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCata
import { executeAddressMcpQuery } from "./addressMcpClient"; import { executeAddressMcpQuery } from "./addressMcpClient";
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage"; import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage"; import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
import { composeFactualReply, contractCandidatesFromRows, inferReplyType } from "./address_runtime/composeStage"; import { composeFactualReply, inferReplyType } from "./address_runtime/composeStage";
interface NormalizedAddressRow { interface NormalizedAddressRow {
period: string | null; period: string | null;
@ -464,12 +464,14 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
); );
} }
function isDocumentOrBankIntent(intent: AddressIntent): boolean { function isAnchorRecoveryIntent(intent: AddressIntent): boolean {
return ( return (
intent === "list_documents_by_counterparty" || intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" || intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract" intent === "bank_operations_by_contract" ||
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts"
); );
} }
@ -1058,40 +1060,9 @@ export class AddressQueryService {
} }
} }
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: [],
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,
rowsAfterAccountScope: normalizedRows.length,
rowsAfterRecipeFilter: filterByAnchors.length,
rowsMaterialized: normalizedRows.length,
rowsMatched: filteredRows.length,
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
materializationDropReason: rowDiagnostics.materializationDropReason,
category: "recipe_visibility_gap",
reasonText: "в live строках нет договорных якорей для уверенного списка незакрытых договоров",
nextStep: "сузьте запрос по контрагенту или добавьте номер договора",
limitations: ["no_contract_anchors_in_live_rows"],
reasons: baseReasons
});
}
if ( if (
filteredRows.length === 0 && filteredRows.length === 0 &&
isDocumentOrBankIntent(intent.intent) && isAnchorRecoveryIntent(intent.intent) &&
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe") (stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")
) { ) {
const currentLimit = const currentLimit =
@ -1201,6 +1172,82 @@ export class AddressQueryService {
} }
}; };
} }
if (
intent.intent === "open_items_by_counterparty_or_contract" &&
expandedFilteredRows.length === 0 &&
expandedRowsByAnchor.length === 0 &&
expandedNormalizedRows.length > 0
) {
const expandedFallbackRows = applyIntentSpecificFilter(intent.intent, expandedNormalizedRows);
if (expandedFallbackRows.length > 0) {
const expandedRowDiagnostics = deriveRowStageDiagnostics(
expandedMcp.raw_rows,
expandedNormalizedRows.length,
expandedNormalizedRows.length
);
const expandedFactual = composeFactualReply(intent.intent, expandedFallbackRows);
const expandedLimitations = [
...filters.warnings,
"query_limit_auto_expanded_for_anchor_recovery",
"open_items_anchor_not_matched_fallback_rows"
];
const expandedReasons = [
...baseReasons,
"query_limit_auto_expanded_for_anchor_recovery",
"open_items_anchor_not_matched_fallback_rows"
];
return {
handled: true,
reply_text:
`Точный якорь не подтвердился в live-строках даже после расширения до ${expandedPlan.limit}; ` +
"показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
expandedFactual.text,
reply_type: inferReplyType(expandedFactual.responseType),
response_type: expandedFactual.responseType,
debug: {
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
query_shape_confidence: shape.confidence,
detected_intent: intent.intent,
detected_intent_confidence: intent.confidence,
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: expandedSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: "matched_non_empty",
account_scope_mode: expandedPlan.account_scope_mode,
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
anchor_type: expandedAnchor.anchor_type,
anchor_value_raw: expandedAnchor.anchor_value_raw,
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
resolver_confidence: expandedAnchor.resolver_confidence,
ambiguity_count: expandedAnchor.ambiguity_count,
match_failure_stage: "materialized_but_not_anchor_matched",
match_failure_reason:
expandedAnchorFilter.mismatchReason ?? "anchor_not_matched_after_query_limit_expansion",
mcp_call_status: "matched_non_empty",
rows_fetched: expandedMcp.fetched_rows,
raw_rows_received: expandedMcp.raw_rows.length,
rows_after_account_scope: expandedNormalizedRows.length,
rows_after_recipe_filter: expandedRowsByAnchor.length,
rows_materialized: expandedNormalizedRows.length,
rows_matched: expandedFallbackRows.length,
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: expandedFactual.responseType,
limitations: expandedLimitations,
reasons: expandedReasons
}
};
}
}
} }
} }
} }
@ -1313,6 +1360,67 @@ export class AddressQueryService {
} }
} }
if (
filteredRows.length === 0 &&
intent.intent === "open_items_by_counterparty_or_contract" &&
normalizedRows.length > 0
) {
const openItemsFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
if (openItemsFallbackRows.length > 0) {
const fallbackFactual = composeFactualReply(intent.intent, openItemsFallbackRows);
const fallbackLimitations = [...filters.warnings, "open_items_anchor_not_matched_fallback_rows"];
const fallbackReasons = [...baseReasons, "open_items_anchor_not_matched_fallback_rows"];
return {
handled: true,
reply_text:
"Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
fallbackFactual.text,
reply_type: inferReplyType(fallbackFactual.responseType),
response_type: fallbackFactual.responseType,
debug: {
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
query_shape_confidence: shape.confidence,
detected_intent: intent.intent,
detected_intent_confidence: intent.confidence,
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: recipeSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: "matched_non_empty",
account_scope_mode: plan.account_scope_mode,
account_scope_fallback_applied: accountScopeFallbackApplied,
anchor_type: anchor.anchor_type,
anchor_value_raw: anchor.anchor_value_raw,
anchor_value_resolved: anchor.anchor_value_resolved,
resolver_confidence: anchor.resolver_confidence,
ambiguity_count: anchor.ambiguity_count,
match_failure_stage: matchFailureStage,
match_failure_reason: matchFailureReason,
mcp_call_status: "matched_non_empty",
rows_fetched: mcp.fetched_rows,
raw_rows_received: mcp.raw_rows.length,
rows_after_account_scope: normalizedRows.length,
rows_after_recipe_filter: filterByAnchors.length,
rows_materialized: normalizedRows.length,
rows_matched: openItemsFallbackRows.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: fallbackFactual.responseType,
limitations: fallbackLimitations,
reasons: fallbackReasons
}
};
}
}
if (filteredRows.length === 0) { if (filteredRows.length === 0) {
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0; const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
const hadAnchorMatchedRows = filterByAnchors.length > 0; const hadAnchorMatchedRows = filterByAnchors.length > 0;

View File

@ -280,7 +280,9 @@ function maxLimitForIntent(intent: AddressIntent): number {
intent === "list_documents_by_counterparty" || intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" || intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract" intent === "bank_operations_by_contract" ||
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts"
) { ) {
return ADDRESS_MAX_LIMIT_EXTENDED; return ADDRESS_MAX_LIMIT_EXTENDED;
} }

View File

@ -85,7 +85,12 @@ export function composeFactualReply(
`Строк движения: ${rows.length}.`, `Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.` `Договорных кандидатов: ${contracts.length}.`
]; ];
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); if (contracts.length > 0) {
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
} else {
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
lines.push(...formatTopRows(rows, 6));
}
return { return {
responseType: "FACTUAL_LIST", responseType: "FACTUAL_LIST",
text: lines.join("\n") text: lines.join("\n")

View File

@ -138,6 +138,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("open_items_by_counterparty_or_contract"); expect(result.intent).toBe("open_items_by_counterparty_or_contract");
}); });
it("resolves unclosed contracts list query without specific anchor", () => {
const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31");
expect(result.intent).toBe("list_open_contracts");
});
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => { it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
const result = resolveAddressIntent( const result = resolveAddressIntent(
"Показать банковские операции (счета 51, 60, 62) связанные с договором 15/24." "Показать банковские операции (счета 51, 60, 62) связанные с договором 15/24."
@ -268,6 +273,18 @@ describe("address filter extraction for balance drilldown", () => {
expect(result.warnings).toContain("period_derived_from_month_phrase"); expect(result.warnings).toContain("period_derived_from_month_phrase");
}); });
it("cuts period-end tail from counterparty anchor and keeps as_of for open-items query", () => {
const result = extractAddressFilters(
"Покажи хвосты по контрагенту СВК на конец периода 2020-12-31",
"open_items_by_counterparty_or_contract"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).toContain("period_window_cleared_for_as_of_intent");
});
it("derives month period for balance snapshot from 'на май 2020'", () => { it("derives month period for balance snapshot from 'на май 2020'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot"); const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60"); expect(result.extracted_filters.account).toBe("60");
@ -703,6 +720,34 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.limit).toBe(200); expect(plan.limit).toBe(200);
}); });
it("allows extended limit for open-items by contract intent", () => {
const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", {
contract: "15/24",
limit: 1000
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
contract: "15/24",
limit: 1000
});
expect(plan.limit).toBe(1000);
});
it("allows extended limit for open-contracts intent", () => {
const selected = selectAddressRecipe("list_open_contracts", {
as_of_date: "2020-12-31",
limit: 1000
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
as_of_date: "2020-12-31",
limit: 1000
});
expect(plan.limit).toBe(1000);
});
it("injects account condition into movements query for account snapshot", () => { it("injects account condition into movements query for account snapshot", () => {
const filters = extractAddressFilters( const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31", "Какой остаток по счету 60 на дату 2020-07-31",

View File

@ -23,6 +23,7 @@ class QuestionCase:
text: str text: str
expected_intent: str | None expected_intent: str | None
expected_mode: str | None expected_mode: str | None
expected_reply_type: str | None
session: str | None session: str | None
@ -48,6 +49,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--max-output-tokens", type=int, default=900) parser.add_argument("--max-output-tokens", type=int, default=900)
parser.add_argument("--timeout-sec", type=int, default=120) parser.add_argument("--timeout-sec", type=int, default=120)
parser.add_argument("--run-id", default="") parser.add_argument("--run-id", default="")
parser.add_argument(
"--strict-policy",
default="route",
choices=["semantic", "route", "factual"],
help="Pass policy: semantic=intent/mode only, route=semantic+non-blocked route, factual=semantic+factual reply",
)
parser.add_argument( parser.add_argument(
"--output-root", "--output-root",
default=str(PROJECT_ROOT / "docs" / "ADDRESS" / "runs"), default=str(PROJECT_ROOT / "docs" / "ADDRESS" / "runs"),
@ -61,7 +68,7 @@ def now_stamp() -> str:
def load_cases(path: Path) -> list[QuestionCase]: def load_cases(path: Path) -> list[QuestionCase]:
raw = json.loads(path.read_text(encoding="utf-8")) raw = json.loads(path.read_text(encoding="utf-8-sig"))
if not isinstance(raw, list): if not isinstance(raw, list):
raise ValueError("questions-file must contain JSON array") raise ValueError("questions-file must contain JSON array")
@ -77,6 +84,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
text=text, text=text,
expected_intent=None, expected_intent=None,
expected_mode="address_query", expected_mode="address_query",
expected_reply_type=None,
session=None, session=None,
) )
) )
@ -91,6 +99,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
case_id = str(item.get("id", f"Q{idx:03d}")).strip() or f"Q{idx:03d}" case_id = str(item.get("id", f"Q{idx:03d}")).strip() or f"Q{idx:03d}"
expected_intent = item.get("expected_intent") expected_intent = item.get("expected_intent")
expected_mode = item.get("expected_mode", "address_query") expected_mode = item.get("expected_mode", "address_query")
expected_reply_type = item.get("expected_reply_type")
session = item.get("session") session = item.get("session")
cases.append( cases.append(
QuestionCase( QuestionCase(
@ -98,6 +107,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
text=text, text=text,
expected_intent=str(expected_intent).strip() if expected_intent else None, expected_intent=str(expected_intent).strip() if expected_intent else None,
expected_mode=str(expected_mode).strip() if expected_mode else None, expected_mode=str(expected_mode).strip() if expected_mode else None,
expected_reply_type=str(expected_reply_type).strip() if expected_reply_type else None,
session=str(session).strip() if session else None, session=str(session).strip() if session else None,
) )
) )
@ -124,6 +134,42 @@ def post_json(url: str, payload: dict[str, Any], timeout_sec: int) -> tuple[int,
return status, {"ok": False, "error": {"code": "HTTP_ERROR", "message": raw}} return status, {"ok": False, "error": {"code": "HTTP_ERROR", "message": raw}}
def first_line(text: str | None) -> str:
value = str(text or "").strip()
if not value:
return ""
return value.splitlines()[0].strip()
def classify_route_health(
*,
status_code: int,
ok_flag: bool,
reply_type: str | None,
limited_reason_category: str | None,
mcp_call_status: str | None,
) -> str:
if status_code != 200 or not ok_flag:
return "http_or_backend_error"
if reply_type == "clarification_required":
return "blocked_clarification"
if reply_type == "backend_error":
return "blocked_backend_error"
if reply_type != "partial_coverage":
return "ok_or_factual"
if limited_reason_category == "missing_anchor":
return "blocked_missing_anchor"
if limited_reason_category == "unsupported":
return "blocked_unsupported"
if limited_reason_category == "recipe_visibility_gap":
return "blocked_recipe_visibility_gap"
if limited_reason_category == "execution_error":
return "blocked_execution_error"
if mcp_call_status in {"skipped", "materialized_but_not_anchor_matched", "materialized_but_filtered_out_by_recipe"}:
return "likely_blocked_route"
return "partial_non_blocking"
def main() -> None: def main() -> None:
args = parse_args() args = parse_args()
questions_path = Path(args.questions_file).resolve() questions_path = Path(args.questions_file).resolve()
@ -179,7 +225,23 @@ def main() -> None:
intent_match = case.expected_intent is None or actual_intent == case.expected_intent intent_match = case.expected_intent is None or actual_intent == case.expected_intent
mode_match = case.expected_mode is None or actual_mode == case.expected_mode mode_match = case.expected_mode is None or actual_mode == case.expected_mode
reply_match = case.expected_reply_type is None or reply_type == case.expected_reply_type
semantic_pass = bool(intent_match and mode_match and status_code == 200 and ok_flag) semantic_pass = bool(intent_match and mode_match and status_code == 200 and ok_flag)
route_health = classify_route_health(
status_code=status_code,
ok_flag=ok_flag,
reply_type=str(reply_type) if reply_type is not None else None,
limited_reason_category=debug.get("limited_reason_category"),
mcp_call_status=debug.get("mcp_call_status"),
)
route_pass = bool(semantic_pass and not route_health.startswith("blocked") and route_health != "likely_blocked_route")
if args.strict_policy == "semantic":
policy_pass = semantic_pass
elif args.strict_policy == "factual":
policy_pass = semantic_pass if case.expected_reply_type is not None else bool(semantic_pass and reply_type == "factual")
else:
policy_pass = route_pass
strict_pass = bool(policy_pass and reply_match)
row = { row = {
"index": index, "index": index,
@ -193,18 +255,32 @@ def main() -> None:
"reply_type": reply_type, "reply_type": reply_type,
"trace_id": trace_id, "trace_id": trace_id,
"assistant_reply": body.get("assistant_reply") if isinstance(body, dict) else None, "assistant_reply": body.get("assistant_reply") if isinstance(body, dict) else None,
"assistant_reply_first_line": first_line(body.get("assistant_reply") if isinstance(body, dict) else None),
"expected_intent": case.expected_intent, "expected_intent": case.expected_intent,
"actual_intent": actual_intent, "actual_intent": actual_intent,
"intent_match": intent_match, "intent_match": intent_match,
"expected_mode": case.expected_mode, "expected_mode": case.expected_mode,
"actual_mode": actual_mode, "actual_mode": actual_mode,
"mode_match": mode_match, "mode_match": mode_match,
"expected_reply_type": case.expected_reply_type,
"reply_match": reply_match,
"semantic_pass": semantic_pass, "semantic_pass": semantic_pass,
"route_pass": route_pass,
"route_health": route_health,
"strict_policy": args.strict_policy,
"strict_pass": strict_pass,
"selected_recipe": debug.get("selected_recipe"),
"missing_required_filters": debug.get("missing_required_filters"),
"match_failure_stage": debug.get("match_failure_stage"),
"match_failure_reason": debug.get("match_failure_reason"),
"rows_fetched": debug.get("rows_fetched"),
"rows_matched": debug.get("rows_matched"),
"mcp_call_status": debug.get("mcp_call_status"), "mcp_call_status": debug.get("mcp_call_status"),
"limited_reason_category": debug.get("limited_reason_category"), "limited_reason_category": debug.get("limited_reason_category"),
"llm_decomposition_applied": debug.get("llm_decomposition_applied"), "llm_decomposition_applied": debug.get("llm_decomposition_applied"),
"llm_decomposition_reason": debug.get("llm_decomposition_reason"), "llm_decomposition_reason": debug.get("llm_decomposition_reason"),
"fallback_rule_hit": debug.get("fallback_rule_hit"), "fallback_rule_hit": debug.get("fallback_rule_hit"),
"debug_payload": debug,
"error_code": body.get("error", {}).get("code") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None, "error_code": body.get("error", {}).get("code") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None,
"error_message": body.get("error", {}).get("message") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None, "error_message": body.get("error", {}).get("message") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None,
} }
@ -212,7 +288,7 @@ def main() -> None:
print( print(
f"[{index:03d}/{len(cases):03d}] {case.id} | status={status_code} reply={reply_type} " f"[{index:03d}/{len(cases):03d}] {case.id} | status={status_code} reply={reply_type} "
f"intent={actual_intent} mode={actual_mode} pass={semantic_pass}" f"intent={actual_intent} mode={actual_mode} semantic={semantic_pass} route={route_pass} strict={strict_pass} health={route_health}"
) )
reply_counter = Counter(str(r.get("reply_type")) for r in rows) reply_counter = Counter(str(r.get("reply_type")) for r in rows)
@ -220,8 +296,11 @@ def main() -> None:
mode_counter = Counter(str(r.get("actual_mode")) for r in rows) mode_counter = Counter(str(r.get("actual_mode")) for r in rows)
mcp_counter = Counter(str(r.get("mcp_call_status")) for r in rows) mcp_counter = Counter(str(r.get("mcp_call_status")) for r in rows)
limited_counter = Counter(str(r.get("limited_reason_category")) for r in rows if r.get("limited_reason_category") is not None) limited_counter = Counter(str(r.get("limited_reason_category")) for r in rows if r.get("limited_reason_category") is not None)
route_health_counter = Counter(str(r.get("route_health")) for r in rows)
semantic_pass_count = sum(1 for r in rows if r.get("semantic_pass")) semantic_pass_count = sum(1 for r in rows if r.get("semantic_pass"))
route_pass_count = sum(1 for r in rows if r.get("route_pass"))
strict_pass_count = sum(1 for r in rows if r.get("strict_pass"))
factual_count = sum(1 for r in rows if r.get("reply_type") == "factual") factual_count = sum(1 for r in rows if r.get("reply_type") == "factual")
ok_200_count = sum(1 for r in rows if r.get("status_code") == 200 and r.get("ok")) ok_200_count = sum(1 for r in rows if r.get("status_code") == 200 and r.get("ok"))
llm_decomposition_applied_count = sum(1 for r in rows if r.get("llm_decomposition_applied") is True) llm_decomposition_applied_count = sum(1 for r in rows if r.get("llm_decomposition_applied") is True)
@ -235,11 +314,16 @@ def main() -> None:
"llm_provider": args.llm_provider, "llm_provider": args.llm_provider,
"llm_model": args.llm_model, "llm_model": args.llm_model,
"llm_base_url": args.llm_base_url, "llm_base_url": args.llm_base_url,
"strict_policy": args.strict_policy,
"totals": { "totals": {
"questions_total": len(rows), "questions_total": len(rows),
"ok_200_count": ok_200_count, "ok_200_count": ok_200_count,
"semantic_pass_count": semantic_pass_count, "semantic_pass_count": semantic_pass_count,
"semantic_pass_rate": round(semantic_pass_count / len(rows), 4) if rows else 0.0, "semantic_pass_rate": round(semantic_pass_count / len(rows), 4) if rows else 0.0,
"route_pass_count": route_pass_count,
"route_pass_rate": round(route_pass_count / len(rows), 4) if rows else 0.0,
"strict_pass_count": strict_pass_count,
"strict_pass_rate": round(strict_pass_count / len(rows), 4) if rows else 0.0,
"factual_count": factual_count, "factual_count": factual_count,
"partial_coverage_count": sum(1 for r in rows if r.get("reply_type") == "partial_coverage"), "partial_coverage_count": sum(1 for r in rows if r.get("reply_type") == "partial_coverage"),
"clarification_required_count": sum(1 for r in rows if r.get("reply_type") == "clarification_required"), "clarification_required_count": sum(1 for r in rows if r.get("reply_type") == "clarification_required"),
@ -253,13 +337,14 @@ def main() -> None:
"actual_mode": dict(mode_counter), "actual_mode": dict(mode_counter),
"mcp_call_status": dict(mcp_counter), "mcp_call_status": dict(mcp_counter),
"limited_reason_category": dict(limited_counter), "limited_reason_category": dict(limited_counter),
"route_health": dict(route_health_counter),
}, },
} }
failures = [ failures = [
r r
for r in rows for r in rows
if not r.get("semantic_pass") if not r.get("strict_pass")
or r.get("status_code") != 200 or r.get("status_code") != 200
or r.get("reply_type") in {"clarification_required", "backend_error"} or r.get("reply_type") in {"clarification_required", "backend_error"}
] ]
@ -279,12 +364,17 @@ def main() -> None:
f"Questions file: {questions_path}", f"Questions file: {questions_path}",
f"Backend URL: {args.backend_url}", f"Backend URL: {args.backend_url}",
f"LLM: {args.llm_provider} / {args.llm_model} @ {args.llm_base_url}", f"LLM: {args.llm_provider} / {args.llm_model} @ {args.llm_base_url}",
f"Strict policy: {args.strict_policy}",
"", "",
"## Totals", "## Totals",
f"- questions_total: {summary['totals']['questions_total']}", f"- questions_total: {summary['totals']['questions_total']}",
f"- ok_200_count: {summary['totals']['ok_200_count']}", f"- ok_200_count: {summary['totals']['ok_200_count']}",
f"- semantic_pass_count: {summary['totals']['semantic_pass_count']}", f"- semantic_pass_count: {summary['totals']['semantic_pass_count']}",
f"- semantic_pass_rate: {summary['totals']['semantic_pass_rate']}", f"- semantic_pass_rate: {summary['totals']['semantic_pass_rate']}",
f"- route_pass_count: {summary['totals']['route_pass_count']}",
f"- route_pass_rate: {summary['totals']['route_pass_rate']}",
f"- strict_pass_count: {summary['totals']['strict_pass_count']}",
f"- strict_pass_rate: {summary['totals']['strict_pass_rate']}",
f"- factual_count: {summary['totals']['factual_count']}", f"- factual_count: {summary['totals']['factual_count']}",
f"- partial_coverage_count: {summary['totals']['partial_coverage_count']}", f"- partial_coverage_count: {summary['totals']['partial_coverage_count']}",
f"- clarification_required_count: {summary['totals']['clarification_required_count']}", f"- clarification_required_count: {summary['totals']['clarification_required_count']}",
@ -299,10 +389,25 @@ def main() -> None:
] ]
(run_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8") (run_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
audit_lines = [
f"# Response Audit: {run_id}",
"",
"| id | strict | route_health | reply_type | intent | limited_reason | question | assistant_first_line |",
"|---|---|---|---|---|---|---|---|",
]
for row in rows:
audit_lines.append(
f"| {row.get('id')} | {row.get('strict_pass')} | {row.get('route_health')} | {row.get('reply_type')} | "
f"{row.get('actual_intent')} | {row.get('limited_reason_category')} | "
f"{str(row.get('question', '')).replace('|', '/')} | {str(row.get('assistant_reply_first_line', '')).replace('|', '/')} |"
)
(run_dir / "response_audit.md").write_text("\n".join(audit_lines) + "\n", encoding="utf-8")
print(f"\nRun directory: {run_dir}") print(f"\nRun directory: {run_dir}")
print(f"Semantic pass: {semantic_pass_count}/{len(rows)}") print(f"Semantic pass: {semantic_pass_count}/{len(rows)}")
print(f"Route pass: {route_pass_count}/{len(rows)}")
print(f"Strict pass ({args.strict_policy}): {strict_pass_count}/{len(rows)}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()