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

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) {
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:
// "<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;
@ -356,6 +370,28 @@ function extractLooseByAnchorValue(text) {
}
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) {
const token = String(rawToken ?? "").trim();
const lowered = token.toLowerCase();
@ -525,8 +561,14 @@ function requiredFiltersByIntent(intent) {
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
return ["counterparty"];
}
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
return ["contract"];
}
return [];
}
function usesAsOfPrimaryWindow(intent) {
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
}
function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText);
@ -575,6 +617,13 @@ function extractAddressFilters(userMessage, intent) {
if (contractMatch) {
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);
if (periodRange.period_from) {
filters.period_from = periodRange.period_from;
@ -606,23 +655,27 @@ function extractAddressFilters(userMessage, intent) {
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 (!filters.period_from && !filters.period_to) {
if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
const asOfDate = extractAsOfDate(text);
if (asOfDate) {
filters.as_of_date = asOfDate;
}
}
// For document/bank lists we default to a short recent window if no explicit period was provided.
if ((intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") &&
!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 counterparty document/bank lists we keep period open by default (all-time over available data)
// and rely on runtime limits/recovery instead of forcing a recent window.
// For balance-style intents we force as_of_date deterministically:
// - explicit as_of has priority;
// - else use period_to boundary when provided;

View File

@ -27,13 +27,19 @@ const ACCOUNT_BALANCE_HINTS = [
"account balance",
"balance by account",
"saldo",
"баланс",
"остаток по счет",
"сальдо по счет",
"по счету"
"по счету",
"что на счете",
"что на счёте",
"на конец"
];
const DOCUMENTS_FORMING_BALANCE_HINTS = [
"documents forming balance",
"docs forming balance",
"documents form balance",
"docs form balance",
"balance documents",
"documents for balance",
"which documents form balance",
@ -57,6 +63,8 @@ const OPEN_ITEMS_HINTS = [
"висят",
"незакрыт",
"открыт",
"долг",
"задолж",
"позици"
];
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
@ -87,31 +95,132 @@ const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
"bank operations by customer",
"show bank operations by counterparty",
"bank ops",
"bank oper",
"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) {
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) {
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
return true;
}
const hasDocLexeme = text.includes("документ") || text.includes("доки");
const hasLooseAccountCodeToken = hasCompactAccountCodeToken(text);
const hasDocLexeme = /(?:документ|док(?:и|ам|ах|ов|а)?)/u.test(text);
const hasFormingLexeme = 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) {
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) {
const token = String(rawToken ?? "").trim().toLowerCase();
@ -185,6 +294,67 @@ function hasPartyAnchorMention(text) {
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) {
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
@ -236,8 +406,18 @@ function hasImplicitCounterpartyAnchorAroundDocs(text) {
function hasDocsOrBankSignal(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) {
if (!hasDocsOrBankSignal(text)) {
if (!hasDocsOrBankSignal(text) && !hasBankOperationSignal(text)) {
return false;
}
const tokens = String(text ?? "")
@ -289,13 +469,44 @@ function resolveAddressIntent(userMessage) {
reasons: ["payables_signal_detected"]
};
}
if (hasDocumentsFormingBalanceSignal(text) && (hasAccountNumberAnchor(text) || text.includes("счет"))) {
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
confidence: "high",
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) &&
(hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) {
return {
@ -315,7 +526,7 @@ function resolveAddressIntent(userMessage) {
reasons: ["documents_by_counterparty_signal_detected"]
};
}
if (hasAny(text, ACCOUNT_BALANCE_HINTS) || hasAccountNumberAnchor(text)) {
if (hasAccountBalanceSignal(text)) {
return {
intent: "account_balance_snapshot",
confidence: "high",
@ -329,13 +540,6 @@ function resolveAddressIntent(userMessage) {
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"))) {
return {
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;
}
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) {
const stopWords = new Set([
@ -237,21 +245,22 @@ function detectAddressQuestionMode(userMessage) {
const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS);
const hasLooseByAnchor = hasLooseByAnchorMention(text);
const hasFollowupSignal = hasAddressFollowupSignal(text);
if (hasAddressAction && hasAddressEntity && !hasDeepReasoning) {
const hasAccountCode = hasAccountCodeAnchor(text);
if (hasAddressAction && (hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
return {
mode: "address_query",
confidence: "high",
reasons: ["address_action_detected", "address_entity_detected"]
};
}
if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal) && !hasDeepReasoning) {
if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal || hasAccountCode) && !hasDeepReasoning) {
return {
mode: "address_query",
confidence: "medium",
reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])]
};
}
if (hasAddressEntity && !hasDeepReasoning) {
if ((hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
return {
mode: "address_query",
confidence: "medium",

View File

@ -9,6 +9,7 @@ const resolveStage_1 = require("./address_runtime/resolveStage");
const composeStage_1 = require("./address_runtime/composeStage");
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
const PARTY_ANCHOR_STOPWORDS = new Set([
"ооо",
"ао",
@ -23,6 +24,35 @@ const PARTY_ANCHOR_STOPWORDS = new Set([
"по",
"by"
]);
const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([
"что",
"чо",
"были",
"был",
"была",
"было",
"ли",
"какие",
"какой",
"покажи",
"показать",
"выведи",
"списания",
"списание",
"поступления",
"поступление",
"доки",
"документ",
"документы",
"документов",
"банковские",
"операции",
"платежи",
"платеж",
"платежи",
"плс",
"please"
]);
const ACCOUNT_ALIAS_MAP = {
"51": ["расчетный счет", "расчетные счета", "bank account"],
"52": ["валютный счет", "валютные счета", "currency account"],
@ -120,6 +150,29 @@ function matchesAnchorText(searchable, anchor) {
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) {
const source = String(value ?? "").trim().replace(",", ".");
const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/);
@ -308,11 +361,11 @@ function applyAddressFilters(rows, filters) {
};
}
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;
return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase()));
}
if (intent === "list_documents_by_counterparty") {
if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") {
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i;
return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
}
@ -330,7 +383,18 @@ function canAutoBroadenPeriodWindow(intent, filters) {
if (!hasExplicitPeriodWindow(filters)) {
return false;
}
return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty";
return (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract");
}
function 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) {
if (!value) {
@ -741,35 +805,240 @@ class AddressQueryService {
: matchFailureStage === "materialized_but_filtered_out_by_recipe"
? "rows_filtered_out_by_intent_recipe_after_anchor_match"
: null;
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && (0, composeStage_1.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 (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) {
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
if (recoveredRows.length > 0) {
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows);
const recoveryReason = recoveredBankRows.length > 0
? "contract_docs_recovered_via_bank_fallback"
: "contract_docs_recovered_via_anchor_rows";
const replyPrefix = recoveredBankRows.length > 0
? "Документный фильтр в live дал пустой набор; показываю связанные банковские операции по договору."
: "Документный фильтр в live дал пустой набор; показываю найденные строки по договорному якорю.";
return {
handled: true,
reply_text: `${replyPrefix}\n${factual.text}`,
reply_type: (0, composeStage_1.inferReplyType)(factual.responseType),
response_type: factual.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: toLegacyMcpStatus("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: "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)) {
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) {
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
const hadAnchorMatchedRows = filterByAnchors.length > 0;
const isVisibilityGapCandidate = hadBaseRows &&
hadAnchorMatchedRows &&
(intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty");
(intent.intent === "list_documents_by_counterparty" ||
intent.intent === "bank_operations_by_counterparty" ||
intent.intent === "list_documents_by_contract" ||
intent.intent === "bank_operations_by_contract");
const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched";
const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe";
const isFollowupAnchorCarryover = Array.isArray(filters.warnings) &&
(filters.warnings.includes("counterparty_from_followup_context") ||
filters.warnings.includes("contract_from_followup_context"));
const isLowQualityPartyAnchor = (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") &&
isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw);
const anchorMismatchCategory = isFollowupAnchorCarryover || !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor";
const category = isAnchorMismatch
? "missing_anchor"
? anchorMismatchCategory
: isRecipeFilteredOut
? "recipe_visibility_gap"
: isVisibilityGapCandidate
? "recipe_visibility_gap"
: "empty_match";
const reasonText = isAnchorMismatch
? "якорь контрагента/договора не найден в материализованных live-строках"
? anchorMismatchCategory === "missing_anchor"
? "якорь контрагента/договора не найден в материализованных live-строках"
: "по указанному якорю и фильтрам в live-выборке нет строк"
: isRecipeFilteredOut
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
: isVisibilityGapCandidate
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
: "по выбранным фильтрам в live-выборке нет строк";
const nextStep = isAnchorMismatch
? "уточните контрагента точным именем или добавьте ИНН/договор"
? anchorMismatchCategory === "missing_anchor"
? "уточните контрагента точным именем или добавьте ИНН/договор"
: "уточните период или снимите часть фильтров"
: isRecipeFilteredOut
? "сузьте период, уточните контрагента или документный тип"
: isVisibilityGapCandidate
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
: "уточните период, контрагента, договор или снимите часть фильтров";
const limitations = isAnchorMismatch
? ["anchor_not_matched_after_materialization"]
? [
anchorMismatchCategory === "missing_anchor"
? "anchor_not_matched_after_materialization"
: "no_rows_for_anchor_after_materialization"
]
: isRecipeFilteredOut
? ["rows_filtered_out_by_recipe_after_anchor_match"]
: [

View File

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

View File

@ -67,7 +67,13 @@ function composeFactualReply(intent, rows) {
`Строк движения: ${rows.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 {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
@ -95,6 +101,17 @@ function composeFactualReply(intent, rows) {
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") {
const lines = [
"Собран список банковских операций по контрагенту (live address lane).",
@ -106,6 +123,17 @@ function composeFactualReply(intent, rows) {
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"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: 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 (!toNonEmptyString(merged.account)) {
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"],
documents_forming_balance: ["account", "as_of_date"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"]
bank_operations_by_counterparty: ["counterparty"],
list_documents_by_contract: ["contract"],
bank_operations_by_contract: ["contract"]
};
const required = requiredByIntent[intent] ?? [];
return required.filter((key) => {

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) {
return {
anchor_type: "counterparty",

View File

@ -54,6 +54,8 @@ const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"))
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence"));
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"));
function retrievalSummaryForRoute(route) {
if (route === "store_canonical")
@ -1802,6 +1804,10 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
llm_decomposition_trace_id: llmMeta?.traceId ?? null,
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? 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,
investigation_state_snapshot: null,
normalized: null,
@ -1865,9 +1871,24 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"show",
"list",
"выведи",
"что",
"чо",
"которые",
"какие",
"какой",
"были",
"был",
"была",
"было",
"ли",
"списания",
"списание",
"поступления",
"поступление",
"расчетного",
"расчётного",
"счета",
"счёта",
"есть",
"est",
"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) {
const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
@ -2203,21 +2619,56 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
}
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
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 = {
attempted: false,
applied: false,
provider,
traceId: null,
effectiveMessage: userMessage,
reason: "not_attempted"
reason: "not_attempted",
fallbackRuleHit: null,
sanitizedUserMessage,
toolGateDecision: null,
toolGateReason: null
};
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 {
...baseMeta,
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 {
...baseMeta,
reason: "not_address_like"
@ -2242,6 +2693,22 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output);
const candidate = candidateFromNormalized ?? candidateFromRaw;
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 {
...baseMeta,
attempted: true,
@ -2249,6 +2716,32 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
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 candidateCompact = compactWhitespace(candidate.toLowerCase());
const applied = sourceCompact !== candidateCompact;
@ -2270,10 +2763,27 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
provider,
traceId: normalized?.trace_id ?? null,
effectiveMessage: applied ? candidate : userMessage,
reason
reason,
fallbackRuleHit: null,
sanitizedUserMessage
};
}
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 {
...baseMeta,
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 {
normalizerService;
sessions;
@ -2313,12 +2852,13 @@ class AssistantService {
};
this.sessions.appendItem(sessionId, userItem);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? "");
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
const assistantItem = {
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
session_id: sessionId,
role: "assistant",
text: addressLane.reply_text,
text: safeAddressReply,
reply_type: addressLane.reply_type,
created_at: new Date().toISOString(),
trace_id: debug.trace_id,
@ -2350,6 +2890,10 @@ class AssistantService {
address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null,
address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? 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,
query_shape: addressLane.debug.query_shape,
detected_intent: addressLane.debug.detected_intent,
@ -2403,29 +2947,62 @@ class AssistantService {
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
traceId: null,
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 carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items);
const shouldPreferContextualLane = Boolean(carryover?.followupContext);
if (shouldPreferContextualLane) {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
followupContext: carryover.followupContext
const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null);
const addressRuntimeMeta = {
...addressPreDecompose,
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);
if (primaryAddressLane?.handled) {
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressPreDecompose);
}
if (!shouldPreferContextualLane && carryover?.followupContext) {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
followupContext: carryover.followupContext
});
if (contextualAddressLane?.handled) {
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose);
const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage);
if (primaryAddressLane?.handled) {
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressRuntimeMeta);
}
if (!shouldPreferContextualLane && carryover?.followupContext) {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
followupContext: carryover.followupContext
});
if (contextualAddressLane?.handled) {
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
}
}
}
}

View File

@ -104,6 +104,9 @@ function shouldFallbackToChatCompletions(error) {
if (!(error instanceof http_1.ApiError)) {
return false;
}
if (error.code === "OPENAI_OUTPUT_PARSE_FAILED" || error.code === "OPENAI_NON_JSON_RESPONSE") {
return true;
}
if (error.code !== "OPENAI_REQUEST_FAILED") {
return false;
}
@ -115,6 +118,31 @@ function shouldFallbackToChatCompletions(error) {
const message = String(error.message ?? "").toLowerCase();
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) {
const schemaFile = schemaVersion === "v1"
? "normalized_query_v1.json"
@ -331,9 +359,13 @@ class OpenAIResponsesClient {
});
}
}
const modelErrorMessage = extractModelErrorMessage(data);
if (modelErrorMessage && canFallbackToAlternativeBase && !isLastCandidate && isRouteMismatchErrorMessage(modelErrorMessage)) {
continue;
}
if (!response.ok) {
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,
url,
status: response.status,
@ -341,6 +373,13 @@ class OpenAIResponsesClient {
code: errorObj.code ?? null
});
}
if (modelErrorMessage) {
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", modelErrorMessage, 502, {
route: routePath,
url,
status: response.status
});
}
return data;
}
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)) {
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:
// "<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 [];
}
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 {
const rawText = String(userMessage ?? "").trim();
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 (!filters.period_from && !filters.period_to) {
if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
const asOfDate = extractAsOfDate(text);
if (asOfDate) {
filters.as_of_date = asOfDate;

View File

@ -224,6 +224,23 @@ function hasAccountBalanceSignal(text: string): boolean {
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 {
const token = String(rawToken ?? "").trim().toLowerCase();
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 (
hasAny(text, OPEN_ITEMS_HINTS) &&
(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 { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
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 {
period: string | null;
@ -464,12 +464,14 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
);
}
function isDocumentOrBankIntent(intent: AddressIntent): boolean {
function isAnchorRecoveryIntent(intent: AddressIntent): boolean {
return (
intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
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 (
filteredRows.length === 0 &&
isDocumentOrBankIntent(intent.intent) &&
isAnchorRecoveryIntent(intent.intent) &&
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")
) {
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) {
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
const hadAnchorMatchedRows = filterByAnchors.length > 0;

View File

@ -280,7 +280,9 @@ function maxLimitForIntent(intent: AddressIntent): number {
intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
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;
}

View File

@ -85,7 +85,12 @@ export function composeFactualReply(
`Строк движения: ${rows.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 {
responseType: "FACTUAL_LIST",
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");
});
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", () => {
const result = resolveAddressIntent(
"Показать банковские операции (счета 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");
});
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'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
@ -703,6 +720,34 @@ describe("address recipe catalog counterparty filtering", () => {
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", () => {
const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31",

View File

@ -23,6 +23,7 @@ class QuestionCase:
text: str
expected_intent: str | None
expected_mode: str | None
expected_reply_type: 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("--timeout-sec", type=int, default=120)
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(
"--output-root",
default=str(PROJECT_ROOT / "docs" / "ADDRESS" / "runs"),
@ -61,7 +68,7 @@ def now_stamp() -> str:
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):
raise ValueError("questions-file must contain JSON array")
@ -77,6 +84,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
text=text,
expected_intent=None,
expected_mode="address_query",
expected_reply_type=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}"
expected_intent = item.get("expected_intent")
expected_mode = item.get("expected_mode", "address_query")
expected_reply_type = item.get("expected_reply_type")
session = item.get("session")
cases.append(
QuestionCase(
@ -98,6 +107,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
text=text,
expected_intent=str(expected_intent).strip() if expected_intent 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,
)
)
@ -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}}
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:
args = parse_args()
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
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)
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 = {
"index": index,
@ -193,18 +255,32 @@ def main() -> None:
"reply_type": reply_type,
"trace_id": trace_id,
"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,
"actual_intent": actual_intent,
"intent_match": intent_match,
"expected_mode": case.expected_mode,
"actual_mode": actual_mode,
"mode_match": mode_match,
"expected_reply_type": case.expected_reply_type,
"reply_match": reply_match,
"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"),
"limited_reason_category": debug.get("limited_reason_category"),
"llm_decomposition_applied": debug.get("llm_decomposition_applied"),
"llm_decomposition_reason": debug.get("llm_decomposition_reason"),
"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_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(
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)
@ -220,8 +296,11 @@ def main() -> None:
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)
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"))
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")
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)
@ -235,11 +314,16 @@ def main() -> None:
"llm_provider": args.llm_provider,
"llm_model": args.llm_model,
"llm_base_url": args.llm_base_url,
"strict_policy": args.strict_policy,
"totals": {
"questions_total": len(rows),
"ok_200_count": ok_200_count,
"semantic_pass_count": semantic_pass_count,
"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,
"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"),
@ -253,13 +337,14 @@ def main() -> None:
"actual_mode": dict(mode_counter),
"mcp_call_status": dict(mcp_counter),
"limited_reason_category": dict(limited_counter),
"route_health": dict(route_health_counter),
},
}
failures = [
r
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("reply_type") in {"clarification_required", "backend_error"}
]
@ -279,12 +364,17 @@ def main() -> None:
f"Questions file: {questions_path}",
f"Backend URL: {args.backend_url}",
f"LLM: {args.llm_provider} / {args.llm_model} @ {args.llm_base_url}",
f"Strict policy: {args.strict_policy}",
"",
"## Totals",
f"- questions_total: {summary['totals']['questions_total']}",
f"- ok_200_count: {summary['totals']['ok_200_count']}",
f"- semantic_pass_count: {summary['totals']['semantic_pass_count']}",
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"- partial_coverage_count: {summary['totals']['partial_coverage_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")
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"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__":
main()