АДРЕСНЫЙ РЕЖИМ -
This commit is contained in:
parent
da8a4eb872
commit
4dff069ae3
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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). |
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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). |
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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). |
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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). |
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -290,6 +290,20 @@ function cleanupAnchorValue(value) {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
// Remove trailing as-of qualifiers often captured by broad contract/counterparty regexes:
|
||||||
|
// "<anchor> на 2020-07-31", "<anchor> на дату 31.07.2020", "<anchor> as of 2020-07-31".
|
||||||
|
const asOfTailPattern = /\s+(?:на\s+(?:дат[ауеы]\s+)?\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|as\s+of\s+\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?)(?:\s+|$)[\s\S]*$/iu;
|
||||||
|
if (asOfTailPattern.test(normalized)) {
|
||||||
|
return normalized.replace(asOfTailPattern, "").trim();
|
||||||
|
}
|
||||||
|
const asOfTruncatedTailPattern = /\s+на\s+дат[ауеы]\s+\d{1,2}(?:\s+|$)[\s\S]*$/iu;
|
||||||
|
if (asOfTruncatedTailPattern.test(normalized)) {
|
||||||
|
return normalized.replace(asOfTruncatedTailPattern, "").trim();
|
||||||
|
}
|
||||||
|
const periodEndTailPattern = /\s+на\s+конец(?:\s+период[а-яё]*)?\s+(?:\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|\d{4}|[a-zа-яё]+\s+\d{4})(?:\s+|$)[\s\S]*$/iu;
|
||||||
|
if (periodEndTailPattern.test(normalized)) {
|
||||||
|
return normalized.replace(periodEndTailPattern, "").trim();
|
||||||
|
}
|
||||||
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
||||||
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
||||||
const periodTailPattern = /\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu;
|
const periodTailPattern = /\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu;
|
||||||
|
|
@ -356,6 +370,28 @@ function extractLooseByAnchorValue(text) {
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
function extractContractTokenHeuristic(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const explicit = source.match(/(?:№|#|n)\s*([a-zа-яё0-9][a-zа-яё0-9./_-]{1,})/iu);
|
||||||
|
if (explicit) {
|
||||||
|
const token = String(explicit[1] ?? "").trim();
|
||||||
|
if (token.length >= 2) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const slashLike = source.match(/\b(\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?)\b/iu);
|
||||||
|
if (!slashLike) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const token = String(slashLike[1] ?? "").trim();
|
||||||
|
if (!token) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])$/.test(token)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
function isLikelyCounterpartyToken(rawToken) {
|
function isLikelyCounterpartyToken(rawToken) {
|
||||||
const token = String(rawToken ?? "").trim();
|
const token = String(rawToken ?? "").trim();
|
||||||
const lowered = token.toLowerCase();
|
const lowered = token.toLowerCase();
|
||||||
|
|
@ -525,8 +561,14 @@ function requiredFiltersByIntent(intent) {
|
||||||
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
|
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
|
||||||
return ["counterparty"];
|
return ["counterparty"];
|
||||||
}
|
}
|
||||||
|
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
|
||||||
|
return ["contract"];
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
function usesAsOfPrimaryWindow(intent) {
|
||||||
|
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
|
||||||
|
}
|
||||||
function extractAddressFilters(userMessage, intent) {
|
function extractAddressFilters(userMessage, intent) {
|
||||||
const rawText = String(userMessage ?? "").trim();
|
const rawText = String(userMessage ?? "").trim();
|
||||||
const text = normalizeMojibakeString(rawText);
|
const text = normalizeMojibakeString(rawText);
|
||||||
|
|
@ -575,6 +617,13 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
if (contractMatch) {
|
if (contractMatch) {
|
||||||
filters.contract = cleanupAnchorValue(String(contractMatch[1]));
|
filters.contract = cleanupAnchorValue(String(contractMatch[1]));
|
||||||
}
|
}
|
||||||
|
if (!filters.contract && (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract")) {
|
||||||
|
const heuristicContract = extractContractTokenHeuristic(text);
|
||||||
|
if (heuristicContract) {
|
||||||
|
filters.contract = cleanupAnchorValue(heuristicContract);
|
||||||
|
warnings.push("contract_anchor_derived_from_heuristic_token");
|
||||||
|
}
|
||||||
|
}
|
||||||
const periodRange = extractPeriodRange(text);
|
const periodRange = extractPeriodRange(text);
|
||||||
if (periodRange.period_from) {
|
if (periodRange.period_from) {
|
||||||
filters.period_from = periodRange.period_from;
|
filters.period_from = periodRange.period_from;
|
||||||
|
|
@ -606,23 +655,27 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
warnings.push("period_derived_from_year_phrase");
|
warnings.push("period_derived_from_year_phrase");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const explicitAsOfDate = extractAsOfDate(text);
|
||||||
|
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
||||||
|
filters.as_of_date = explicitAsOfDate;
|
||||||
|
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||||
|
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||||
|
warnings.includes("period_derived_from_year_phrase");
|
||||||
|
if (periodWasDerivedHeuristically && !periodRange.period_from && !periodRange.period_to) {
|
||||||
|
delete filters.period_from;
|
||||||
|
delete filters.period_to;
|
||||||
|
warnings.push("period_window_cleared_for_as_of_intent");
|
||||||
|
}
|
||||||
|
}
|
||||||
// If explicit period window exists, do not infer as_of_date from one of its boundary dates.
|
// If explicit period window exists, do not infer as_of_date from one of its boundary dates.
|
||||||
if (!filters.period_from && !filters.period_to) {
|
if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
|
||||||
const asOfDate = extractAsOfDate(text);
|
const asOfDate = extractAsOfDate(text);
|
||||||
if (asOfDate) {
|
if (asOfDate) {
|
||||||
filters.as_of_date = asOfDate;
|
filters.as_of_date = asOfDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For document/bank lists we default to a short recent window if no explicit period was provided.
|
// For counterparty document/bank lists we keep period open by default (all-time over available data)
|
||||||
if ((intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") &&
|
// and rely on runtime limits/recovery instead of forcing a recent window.
|
||||||
!filters.period_from &&
|
|
||||||
!filters.period_to &&
|
|
||||||
!hasAllTimeHint(text)) {
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
filters.period_to = today;
|
|
||||||
filters.period_from = shiftDaysIso(today, -90);
|
|
||||||
warnings.push("period_defaulted_last_90_days");
|
|
||||||
}
|
|
||||||
// For balance-style intents we force as_of_date deterministically:
|
// For balance-style intents we force as_of_date deterministically:
|
||||||
// - explicit as_of has priority;
|
// - explicit as_of has priority;
|
||||||
// - else use period_to boundary when provided;
|
// - else use period_to boundary when provided;
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,19 @@ const ACCOUNT_BALANCE_HINTS = [
|
||||||
"account balance",
|
"account balance",
|
||||||
"balance by account",
|
"balance by account",
|
||||||
"saldo",
|
"saldo",
|
||||||
|
"баланс",
|
||||||
"остаток по счет",
|
"остаток по счет",
|
||||||
"сальдо по счет",
|
"сальдо по счет",
|
||||||
"по счету"
|
"по счету",
|
||||||
|
"что на счете",
|
||||||
|
"что на счёте",
|
||||||
|
"на конец"
|
||||||
];
|
];
|
||||||
const DOCUMENTS_FORMING_BALANCE_HINTS = [
|
const DOCUMENTS_FORMING_BALANCE_HINTS = [
|
||||||
"documents forming balance",
|
"documents forming balance",
|
||||||
|
"docs forming balance",
|
||||||
"documents form balance",
|
"documents form balance",
|
||||||
|
"docs form balance",
|
||||||
"balance documents",
|
"balance documents",
|
||||||
"documents for balance",
|
"documents for balance",
|
||||||
"which documents form balance",
|
"which documents form balance",
|
||||||
|
|
@ -57,6 +63,8 @@ const OPEN_ITEMS_HINTS = [
|
||||||
"висят",
|
"висят",
|
||||||
"незакрыт",
|
"незакрыт",
|
||||||
"открыт",
|
"открыт",
|
||||||
|
"долг",
|
||||||
|
"задолж",
|
||||||
"позици"
|
"позици"
|
||||||
];
|
];
|
||||||
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
|
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
|
||||||
|
|
@ -87,31 +95,132 @@ const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
|
||||||
"bank operations by customer",
|
"bank operations by customer",
|
||||||
"show bank operations by counterparty",
|
"show bank operations by counterparty",
|
||||||
"bank ops",
|
"bank ops",
|
||||||
|
"bank oper",
|
||||||
"transactions by counterparty",
|
"transactions by counterparty",
|
||||||
|
"транзак",
|
||||||
|
"банк",
|
||||||
"банков",
|
"банков",
|
||||||
|
"по банку",
|
||||||
|
"опер",
|
||||||
|
"выписк",
|
||||||
|
"платеж",
|
||||||
|
"платёж",
|
||||||
|
"оплат",
|
||||||
|
"списан",
|
||||||
|
"списани",
|
||||||
|
"поступлен",
|
||||||
|
"поступлени",
|
||||||
|
"движени"
|
||||||
|
];
|
||||||
|
const DOCUMENTS_BY_CONTRACT_HINTS = [
|
||||||
|
"documents by contract",
|
||||||
|
"docs by contract",
|
||||||
|
"show documents by contract",
|
||||||
|
"list documents by contract",
|
||||||
|
"документы по договору",
|
||||||
|
"доки по договору",
|
||||||
|
"док по договору",
|
||||||
|
"документы договор",
|
||||||
|
"договор"
|
||||||
|
];
|
||||||
|
const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
|
||||||
|
"bank operations by contract",
|
||||||
|
"bank payments by contract",
|
||||||
|
"payment orders by contract",
|
||||||
|
"transactions by contract",
|
||||||
|
"bank ops by contract",
|
||||||
|
"банковские операции по договору",
|
||||||
|
"платежи по договору",
|
||||||
|
"выписка по договору"
|
||||||
|
];
|
||||||
|
const BANK_OPERATION_CORE_HINTS = [
|
||||||
|
"банк",
|
||||||
|
"банков",
|
||||||
|
"операц",
|
||||||
|
"опер",
|
||||||
"выписк",
|
"выписк",
|
||||||
"платеж",
|
"платеж",
|
||||||
"платёж",
|
"платёж",
|
||||||
"оплат",
|
"оплат",
|
||||||
"списан",
|
"списан",
|
||||||
"поступлен",
|
"поступлен",
|
||||||
"движени"
|
"движени",
|
||||||
|
"транзак",
|
||||||
|
"bank",
|
||||||
|
"payment",
|
||||||
|
"payments",
|
||||||
|
"transaction",
|
||||||
|
"transactions",
|
||||||
|
"statement",
|
||||||
|
"wire"
|
||||||
];
|
];
|
||||||
function hasAny(text, patterns) {
|
function hasAny(text, patterns) {
|
||||||
return patterns.some((item) => text.includes(item));
|
return patterns.some((item) => text.includes(item));
|
||||||
}
|
}
|
||||||
|
function hasCompactAccountCodeToken(text) {
|
||||||
|
// Match compact account tokens like 60.01 / 62, while avoiding date fragments.
|
||||||
|
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})(?![\d-])/u.test(text);
|
||||||
|
}
|
||||||
function hasDocumentsFormingBalanceSignal(text) {
|
function hasDocumentsFormingBalanceSignal(text) {
|
||||||
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
|
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const hasDocLexeme = text.includes("документ") || text.includes("доки");
|
const hasLooseAccountCodeToken = hasCompactAccountCodeToken(text);
|
||||||
|
const hasDocLexeme = /(?:документ|док(?:и|ам|ах|ов|а)?)/u.test(text);
|
||||||
const hasFormingLexeme = text.includes("формир");
|
const hasFormingLexeme = text.includes("формир");
|
||||||
const hasBalanceLexeme = text.includes("остат");
|
const hasBalanceLexeme = text.includes("остат");
|
||||||
const hasAccountLexeme = text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text);
|
const hasAccountLexeme = text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text) || hasLooseAccountCodeToken;
|
||||||
if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) {
|
if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит");
|
if (hasDocLexeme &&
|
||||||
|
hasBalanceLexeme &&
|
||||||
|
hasAccountLexeme &&
|
||||||
|
(text.includes("раскрой") || text.includes("раскид") || text.includes("под остатк"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasBalanceLexeme && hasAccountLexeme && /из\s+чего\s+остат/u.test(text);
|
||||||
|
}
|
||||||
|
function hasDocumentsFormingBalanceAccountAnchor(text) {
|
||||||
|
if (hasAccountNumberAnchor(text) || text.includes("счет") || text.includes("счёт")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Allow compact account mentions like "60.01" in slang prompts without explicit "счет".
|
||||||
|
return hasCompactAccountCodeToken(text);
|
||||||
|
}
|
||||||
|
function hasAccountBalanceSignal(text) {
|
||||||
|
if (hasAny(text, ACCOUNT_BALANCE_HINTS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasAccountLexeme = hasAccountNumberAnchor(text) || hasCompactAccountCodeToken(text) || /(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/u.test(text);
|
||||||
|
const hasBalanceLexeme = text.includes("баланс") ||
|
||||||
|
text.includes("остат") ||
|
||||||
|
text.includes("сальд") ||
|
||||||
|
text.includes("saldo") ||
|
||||||
|
text.includes("balance") ||
|
||||||
|
text.includes("скока") ||
|
||||||
|
text.includes("сколько") ||
|
||||||
|
/на\s+конец/u.test(text);
|
||||||
|
return hasAccountLexeme && hasBalanceLexeme;
|
||||||
|
}
|
||||||
|
function hasOpenContractsListSignal(text) {
|
||||||
|
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
|
||||||
|
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
|
||||||
|
if (!hasContractLexeme || !hasOpenLexeme) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Query about a specific contract should stay in open-items lane.
|
||||||
|
if (hasContractNumberLikeToken(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Debt/tail wording indicates open-items intent, not contract list.
|
||||||
|
if (/(?:долг|задолж|хвост|позиц|open\s+items|unclosed\s+items|взаиморасчет|взаиморасчёт)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
function isLikelyCounterpartyToken(rawToken) {
|
function isLikelyCounterpartyToken(rawToken) {
|
||||||
const token = String(rawToken ?? "").trim().toLowerCase();
|
const token = String(rawToken ?? "").trim().toLowerCase();
|
||||||
|
|
@ -185,6 +294,67 @@ function hasPartyAnchorMention(text) {
|
||||||
text.includes("покупател") ||
|
text.includes("покупател") ||
|
||||||
text.includes("партнер"));
|
text.includes("партнер"));
|
||||||
}
|
}
|
||||||
|
function hasContractAnchorMention(text) {
|
||||||
|
return (text.includes("договор") ||
|
||||||
|
text.includes("контракт") ||
|
||||||
|
/\bдог\.?\b/iu.test(text) ||
|
||||||
|
text.includes("дог.") ||
|
||||||
|
text.includes("contract") ||
|
||||||
|
text.includes("dogovor"));
|
||||||
|
}
|
||||||
|
function hasContractNumberLikeToken(text) {
|
||||||
|
if (/(?:^|[\s([{])(?:№|#|n)\s*[a-zа-яё0-9][a-zа-яё0-9./_-]{1,}(?=$|[\s,.;:!?)\]}])/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rawTokens = text
|
||||||
|
.split(/[\s,;:!?()[\]{}"«»]+/u)
|
||||||
|
.map((token) => token.replace(/^[^\p{L}\p{N}#№]+|[^\p{L}\p{N}./_-]+$/gu, "").trim())
|
||||||
|
.filter((token) => token.length > 0);
|
||||||
|
for (const rawToken of rawTokens) {
|
||||||
|
const token = String(rawToken ?? "").trim();
|
||||||
|
if (!/^\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?$/u.test(token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^\d{1,2}\.\d{1,2}$/u.test(token)) {
|
||||||
|
// Likely an account code like 60.01/51.00, not a contract number.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parts = token.split(/[./_-]+/u).map((part) => Number(part));
|
||||||
|
if (!parts.every((part) => Number.isFinite(part))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const [a, b] = parts;
|
||||||
|
const yearFirst = a >= 1900 && a <= 2099 && b >= 1 && b <= 12;
|
||||||
|
const yearSecond = b >= 1900 && b <= 2099 && a >= 1 && a <= 12;
|
||||||
|
if (yearFirst || yearSecond) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const [a, b, c] = parts;
|
||||||
|
const ymd = a >= 1900 && a <= 2099 && b >= 1 && b <= 12 && c >= 1 && c <= 31;
|
||||||
|
const dmy = c >= 1900 && c <= 2099 && a >= 1 && a <= 31 && b >= 1 && b <= 12;
|
||||||
|
if (ymd || dmy) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function hasContractAnchorSignal(text) {
|
||||||
|
if (hasContractAnchorMention(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Allow short forms like "15/24" for follow-up prompts if document/bank signal exists.
|
||||||
|
return hasContractNumberLikeToken(text) && hasDocsOrBankSignal(text);
|
||||||
|
}
|
||||||
function hasLooseByAnchorMention(text) {
|
function hasLooseByAnchorMention(text) {
|
||||||
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -236,8 +406,18 @@ function hasImplicitCounterpartyAnchorAroundDocs(text) {
|
||||||
function hasDocsOrBankSignal(text) {
|
function hasDocsOrBankSignal(text) {
|
||||||
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
||||||
}
|
}
|
||||||
|
function hasBankOperationSignal(text) {
|
||||||
|
return hasAny(text, BANK_OPERATION_CORE_HINTS) || hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) || hasAny(text, BANK_OPERATIONS_BY_CONTRACT_HINTS);
|
||||||
|
}
|
||||||
|
function hasDocumentSignal(text) {
|
||||||
|
return (text.includes("док") ||
|
||||||
|
text.includes("доки") ||
|
||||||
|
text.includes("документ") ||
|
||||||
|
text.includes("docs") ||
|
||||||
|
text.includes("documents"));
|
||||||
|
}
|
||||||
function hasHeuristicCounterpartyAnchor(text) {
|
function hasHeuristicCounterpartyAnchor(text) {
|
||||||
if (!hasDocsOrBankSignal(text)) {
|
if (!hasDocsOrBankSignal(text) && !hasBankOperationSignal(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const tokens = String(text ?? "")
|
const tokens = String(text ?? "")
|
||||||
|
|
@ -289,13 +469,44 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons: ["payables_signal_detected"]
|
reasons: ["payables_signal_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasDocumentsFormingBalanceSignal(text) && (hasAccountNumberAnchor(text) || text.includes("счет"))) {
|
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "documents_forming_balance",
|
intent: "documents_forming_balance",
|
||||||
confidence: "high",
|
confidence: "high",
|
||||||
reasons: ["documents_forming_balance_signal_detected"]
|
reasons: ["documents_forming_balance_signal_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (hasOpenContractsListSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_open_contracts",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["open_contract_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasAny(text, OPEN_ITEMS_HINTS) &&
|
||||||
|
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
|
||||||
|
return {
|
||||||
|
intent: "open_items_by_counterparty_or_contract",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["open_items_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasContractAnchorSignal(text) &&
|
||||||
|
hasBankOperationSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "bank_operations_by_contract",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["bank_ops_by_contract_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasContractAnchorSignal(text) &&
|
||||||
|
(hasAny(text, DOCUMENTS_BY_CONTRACT_HINTS) || hasDocumentSignal(text))) {
|
||||||
|
return {
|
||||||
|
intent: "list_documents_by_contract",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["documents_by_contract_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) &&
|
if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) &&
|
||||||
(hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) {
|
(hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -315,7 +526,7 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons: ["documents_by_counterparty_signal_detected"]
|
reasons: ["documents_by_counterparty_signal_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasAny(text, ACCOUNT_BALANCE_HINTS) || hasAccountNumberAnchor(text)) {
|
if (hasAccountBalanceSignal(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "account_balance_snapshot",
|
intent: "account_balance_snapshot",
|
||||||
confidence: "high",
|
confidence: "high",
|
||||||
|
|
@ -329,13 +540,6 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons: ["generic_lookup_with_loose_anchor_fallback"]
|
reasons: ["generic_lookup_with_loose_anchor_fallback"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasAny(text, OPEN_ITEMS_HINTS) && (text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
|
|
||||||
return {
|
|
||||||
intent: "open_items_by_counterparty_or_contract",
|
|
||||||
confidence: "medium",
|
|
||||||
reasons: ["open_items_signal_detected"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) {
|
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) {
|
||||||
return {
|
return {
|
||||||
intent: "list_open_contracts",
|
intent: "list_open_contracts",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ const ADDRESS_ACTION_TOKENS = [
|
||||||
"кому",
|
"кому",
|
||||||
"какие",
|
"какие",
|
||||||
"что по",
|
"что по",
|
||||||
|
"че по",
|
||||||
|
"чё по",
|
||||||
"остаток",
|
"остаток",
|
||||||
|
"скока",
|
||||||
|
"сколько",
|
||||||
"долг",
|
"долг",
|
||||||
"задолж",
|
"задолж",
|
||||||
"хвост",
|
"хвост",
|
||||||
|
|
@ -64,6 +68,7 @@ const ADDRESS_ENTITY_TOKENS = [
|
||||||
"банк",
|
"банк",
|
||||||
"выписк",
|
"выписк",
|
||||||
"операц",
|
"операц",
|
||||||
|
"транзак",
|
||||||
"договор",
|
"договор",
|
||||||
"счет",
|
"счет",
|
||||||
"счёт",
|
"счёт",
|
||||||
|
|
@ -148,7 +153,10 @@ function hasAddressFollowupSignal(text) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function hasDocsOrBankSignal(text) {
|
function hasDocsOrBankSignal(text) {
|
||||||
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|транзак|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
||||||
|
}
|
||||||
|
function hasAccountCodeAnchor(text) {
|
||||||
|
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})(?![\d-])/u.test(text);
|
||||||
}
|
}
|
||||||
function hasLikelyCounterpartyToken(text) {
|
function hasLikelyCounterpartyToken(text) {
|
||||||
const stopWords = new Set([
|
const stopWords = new Set([
|
||||||
|
|
@ -237,21 +245,22 @@ function detectAddressQuestionMode(userMessage) {
|
||||||
const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS);
|
const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS);
|
||||||
const hasLooseByAnchor = hasLooseByAnchorMention(text);
|
const hasLooseByAnchor = hasLooseByAnchorMention(text);
|
||||||
const hasFollowupSignal = hasAddressFollowupSignal(text);
|
const hasFollowupSignal = hasAddressFollowupSignal(text);
|
||||||
if (hasAddressAction && hasAddressEntity && !hasDeepReasoning) {
|
const hasAccountCode = hasAccountCodeAnchor(text);
|
||||||
|
if (hasAddressAction && (hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
|
||||||
return {
|
return {
|
||||||
mode: "address_query",
|
mode: "address_query",
|
||||||
confidence: "high",
|
confidence: "high",
|
||||||
reasons: ["address_action_detected", "address_entity_detected"]
|
reasons: ["address_action_detected", "address_entity_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal) && !hasDeepReasoning) {
|
if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal || hasAccountCode) && !hasDeepReasoning) {
|
||||||
return {
|
return {
|
||||||
mode: "address_query",
|
mode: "address_query",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])]
|
reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasAddressEntity && !hasDeepReasoning) {
|
if ((hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
|
||||||
return {
|
return {
|
||||||
mode: "address_query",
|
mode: "address_query",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const resolveStage_1 = require("./address_runtime/resolveStage");
|
||||||
const composeStage_1 = require("./address_runtime/composeStage");
|
const composeStage_1 = require("./address_runtime/composeStage");
|
||||||
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
|
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
|
||||||
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
|
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
|
||||||
|
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
||||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||||
"ооо",
|
"ооо",
|
||||||
"ао",
|
"ао",
|
||||||
|
|
@ -23,6 +24,35 @@ const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||||
"по",
|
"по",
|
||||||
"by"
|
"by"
|
||||||
]);
|
]);
|
||||||
|
const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([
|
||||||
|
"что",
|
||||||
|
"чо",
|
||||||
|
"были",
|
||||||
|
"был",
|
||||||
|
"была",
|
||||||
|
"было",
|
||||||
|
"ли",
|
||||||
|
"какие",
|
||||||
|
"какой",
|
||||||
|
"покажи",
|
||||||
|
"показать",
|
||||||
|
"выведи",
|
||||||
|
"списания",
|
||||||
|
"списание",
|
||||||
|
"поступления",
|
||||||
|
"поступление",
|
||||||
|
"доки",
|
||||||
|
"документ",
|
||||||
|
"документы",
|
||||||
|
"документов",
|
||||||
|
"банковские",
|
||||||
|
"операции",
|
||||||
|
"платежи",
|
||||||
|
"платеж",
|
||||||
|
"платежи",
|
||||||
|
"плс",
|
||||||
|
"please"
|
||||||
|
]);
|
||||||
const ACCOUNT_ALIAS_MAP = {
|
const ACCOUNT_ALIAS_MAP = {
|
||||||
"51": ["расчетный счет", "расчетные счета", "bank account"],
|
"51": ["расчетный счет", "расчетные счета", "bank account"],
|
||||||
"52": ["валютный счет", "валютные счета", "currency account"],
|
"52": ["валютный счет", "валютные счета", "currency account"],
|
||||||
|
|
@ -120,6 +150,29 @@ function matchesAnchorText(searchable, anchor) {
|
||||||
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
|
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function isLikelyLowQualityPartyAnchor(value) {
|
||||||
|
const normalized = normalizeSearchText(String(value ?? ""));
|
||||||
|
if (!normalized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const tokens = normalized.split(" ").filter(Boolean);
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const meaningfulTokens = tokens.filter((token) => {
|
||||||
|
if (token.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (PARTY_ANCHOR_STOPWORDS.has(token) || LOW_QUALITY_PARTY_ANCHOR_TOKENS.has(token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}$/.test(token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return meaningfulTokens.length === 0;
|
||||||
|
}
|
||||||
function normalizeAccountToken(value) {
|
function normalizeAccountToken(value) {
|
||||||
const source = String(value ?? "").trim().replace(",", ".");
|
const source = String(value ?? "").trim().replace(",", ".");
|
||||||
const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/);
|
const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/);
|
||||||
|
|
@ -308,11 +361,11 @@ function applyAddressFilters(rows, filters) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function applyIntentSpecificFilter(intent, rows) {
|
function applyIntentSpecificFilter(intent, rows) {
|
||||||
if (intent === "bank_operations_by_counterparty") {
|
if (intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_contract") {
|
||||||
const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i;
|
const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i;
|
||||||
return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase()));
|
return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase()));
|
||||||
}
|
}
|
||||||
if (intent === "list_documents_by_counterparty") {
|
if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") {
|
||||||
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i;
|
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i;
|
||||||
return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||||||
}
|
}
|
||||||
|
|
@ -330,7 +383,18 @@ function canAutoBroadenPeriodWindow(intent, filters) {
|
||||||
if (!hasExplicitPeriodWindow(filters)) {
|
if (!hasExplicitPeriodWindow(filters)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty";
|
return (intent === "list_documents_by_counterparty" ||
|
||||||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
intent === "list_documents_by_contract" ||
|
||||||
|
intent === "bank_operations_by_contract");
|
||||||
|
}
|
||||||
|
function isAnchorRecoveryIntent(intent) {
|
||||||
|
return (intent === "list_documents_by_counterparty" ||
|
||||||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
intent === "list_documents_by_contract" ||
|
||||||
|
intent === "bank_operations_by_contract" ||
|
||||||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
intent === "list_open_contracts");
|
||||||
}
|
}
|
||||||
function toIsoDatePrefix(value) {
|
function toIsoDatePrefix(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|
@ -741,35 +805,240 @@ class AddressQueryService {
|
||||||
: matchFailureStage === "materialized_but_filtered_out_by_recipe"
|
: matchFailureStage === "materialized_but_filtered_out_by_recipe"
|
||||||
? "rows_filtered_out_by_intent_recipe_after_anchor_match"
|
? "rows_filtered_out_by_intent_recipe_after_anchor_match"
|
||||||
: null;
|
: null;
|
||||||
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && (0, composeStage_1.contractCandidatesFromRows)(filteredRows).length === 0) {
|
if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) {
|
||||||
return buildLimitedExecutionResult({
|
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
|
||||||
mode,
|
const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
|
||||||
shape,
|
if (recoveredRows.length > 0) {
|
||||||
intent,
|
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows);
|
||||||
filters: filters.extracted_filters,
|
const recoveryReason = recoveredBankRows.length > 0
|
||||||
missingRequiredFilters: [],
|
? "contract_docs_recovered_via_bank_fallback"
|
||||||
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
|
: "contract_docs_recovered_via_anchor_rows";
|
||||||
accountScopeMode: plan.account_scope_mode,
|
const replyPrefix = recoveredBankRows.length > 0
|
||||||
accountScopeFallbackApplied,
|
? "Документный фильтр в live дал пустой набор; показываю связанные банковские операции по договору."
|
||||||
accountScopeAudit,
|
: "Документный фильтр в live дал пустой набор; показываю найденные строки по договорному якорю.";
|
||||||
anchor,
|
return {
|
||||||
matchFailureStage,
|
handled: true,
|
||||||
matchFailureReason,
|
reply_text: `${replyPrefix}\n${factual.text}`,
|
||||||
mcpCallStatus: stageStatus,
|
reply_type: (0, composeStage_1.inferReplyType)(factual.responseType),
|
||||||
rowsFetched: mcp.fetched_rows,
|
response_type: factual.responseType,
|
||||||
rawRowsReceived: mcp.raw_rows.length,
|
debug: {
|
||||||
rowsAfterAccountScope: normalizedRows.length,
|
detected_mode: mode.mode,
|
||||||
rowsAfterRecipeFilter: filterByAnchors.length,
|
detected_mode_confidence: mode.confidence,
|
||||||
rowsMaterialized: normalizedRows.length,
|
query_shape: shape.shape,
|
||||||
rowsMatched: filteredRows.length,
|
query_shape_confidence: shape.confidence,
|
||||||
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
detected_intent: intent.intent,
|
||||||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
detected_intent_confidence: intent.confidence,
|
||||||
category: "recipe_visibility_gap",
|
extracted_filters: filters.extracted_filters,
|
||||||
reasonText: "в live строках нет договорных якорей для уверенного списка незакрытых договоров",
|
missing_required_filters: [],
|
||||||
nextStep: "сузьте запрос по контрагенту или добавьте номер договора",
|
selected_recipe: recipeSelection.selected_recipe.recipe_id,
|
||||||
limitations: ["no_contract_anchors_in_live_rows"],
|
mcp_call_status_legacy: toLegacyMcpStatus("matched_non_empty"),
|
||||||
reasons: baseReasons
|
account_scope_mode: plan.account_scope_mode,
|
||||||
});
|
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||||||
|
anchor_type: anchor.anchor_type,
|
||||||
|
anchor_value_raw: anchor.anchor_value_raw,
|
||||||
|
anchor_value_resolved: anchor.anchor_value_resolved,
|
||||||
|
resolver_confidence: anchor.resolver_confidence,
|
||||||
|
ambiguity_count: anchor.ambiguity_count,
|
||||||
|
match_failure_stage: "none",
|
||||||
|
match_failure_reason: null,
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
rows_fetched: mcp.fetched_rows,
|
||||||
|
raw_rows_received: mcp.raw_rows.length,
|
||||||
|
rows_after_account_scope: normalizedRows.length,
|
||||||
|
rows_after_recipe_filter: filterByAnchors.length,
|
||||||
|
rows_materialized: normalizedRows.length,
|
||||||
|
rows_matched: recoveredRows.length,
|
||||||
|
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||||
|
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||||
|
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||||
|
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||||
|
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||||
|
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||||
|
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||||
|
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||||
|
limited_reason_category: null,
|
||||||
|
response_type: factual.responseType,
|
||||||
|
limitations: [...filters.warnings, recoveryReason],
|
||||||
|
reasons: [...baseReasons, recoveryReason]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filteredRows.length === 0 &&
|
||||||
|
isAnchorRecoveryIntent(intent.intent) &&
|
||||||
|
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) {
|
||||||
|
const currentLimit = typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit)
|
||||||
|
? Math.max(1, Math.trunc(filters.extracted_filters.limit))
|
||||||
|
: plan.limit;
|
||||||
|
if (currentLimit < ADDRESS_ANCHOR_RECOVERY_LIMIT) {
|
||||||
|
const expandedLimitFilters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
|
||||||
|
};
|
||||||
|
const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, expandedLimitFilters);
|
||||||
|
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
|
||||||
|
const expandedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters);
|
||||||
|
if (expandedPlan.limit > currentLimit) {
|
||||||
|
const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||||
|
query: expandedPlan.query,
|
||||||
|
limit: expandedPlan.limit
|
||||||
|
});
|
||||||
|
if (!expandedMcp.error) {
|
||||||
|
const expandedRawRows = toNormalizedRows(expandedMcp.raw_rows);
|
||||||
|
const expandedScopedRows = applyAccountScopeFilter(expandedRawRows, expandedPlan.account_scope);
|
||||||
|
const expandedAccountScopeFallbackApplied = expandedPlan.account_scope_mode === "preferred" &&
|
||||||
|
expandedPlan.account_scope.length > 0 &&
|
||||||
|
expandedRawRows.length > 0 &&
|
||||||
|
expandedScopedRows.length === 0;
|
||||||
|
const expandedNormalizedRows = expandedAccountScopeFallbackApplied ? expandedRawRows : expandedScopedRows;
|
||||||
|
let expandedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, expandedLimitFilters);
|
||||||
|
expandedAnchor = (0, resolveStage_1.refineAnchorFromRows)(expandedAnchor, expandedNormalizedRows);
|
||||||
|
const expandedFiltersForMatching = expandedAnchor.anchor_type === "counterparty" && expandedAnchor.anchor_value_resolved
|
||||||
|
? { ...expandedLimitFilters, counterparty: expandedAnchor.anchor_value_resolved }
|
||||||
|
: expandedAnchor.anchor_type === "contract" && expandedAnchor.anchor_value_resolved
|
||||||
|
? { ...expandedLimitFilters, contract: expandedAnchor.anchor_value_resolved }
|
||||||
|
: expandedLimitFilters;
|
||||||
|
const expandedAccountScopeAudit = buildAccountScopeAudit({
|
||||||
|
intent: intent.intent,
|
||||||
|
filters: expandedFiltersForMatching,
|
||||||
|
accountScope: expandedPlan.account_scope,
|
||||||
|
rowsBeforeScope: expandedRawRows.length,
|
||||||
|
rowsAfterScope: expandedNormalizedRows.length
|
||||||
|
});
|
||||||
|
const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching);
|
||||||
|
const expandedRowsByAnchor = expandedAnchorFilter.rows;
|
||||||
|
const expandedFilteredRows = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
|
||||||
|
if (expandedFilteredRows.length > 0) {
|
||||||
|
const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length);
|
||||||
|
const expandedStageStatus = deriveMcpStageStatus({
|
||||||
|
rawRowsReceived: expandedMcp.raw_rows.length,
|
||||||
|
rowsMaterialized: expandedNormalizedRows.length,
|
||||||
|
rowsAnchorMatched: expandedRowsByAnchor.length,
|
||||||
|
rowsMatched: expandedFilteredRows.length
|
||||||
|
});
|
||||||
|
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows);
|
||||||
|
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
|
||||||
|
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
|
||||||
|
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text: `${expandedPrefix}\n${expandedFactual.text}`,
|
||||||
|
reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType),
|
||||||
|
response_type: expandedFactual.responseType,
|
||||||
|
debug: {
|
||||||
|
detected_mode: mode.mode,
|
||||||
|
detected_mode_confidence: mode.confidence,
|
||||||
|
query_shape: shape.shape,
|
||||||
|
query_shape_confidence: shape.confidence,
|
||||||
|
detected_intent: intent.intent,
|
||||||
|
detected_intent_confidence: intent.confidence,
|
||||||
|
extracted_filters: filters.extracted_filters,
|
||||||
|
missing_required_filters: [],
|
||||||
|
selected_recipe: expandedSelection.selected_recipe.recipe_id,
|
||||||
|
mcp_call_status_legacy: toLegacyMcpStatus(expandedStageStatus),
|
||||||
|
account_scope_mode: expandedPlan.account_scope_mode,
|
||||||
|
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
|
||||||
|
anchor_type: expandedAnchor.anchor_type,
|
||||||
|
anchor_value_raw: expandedAnchor.anchor_value_raw,
|
||||||
|
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
|
||||||
|
resolver_confidence: expandedAnchor.resolver_confidence,
|
||||||
|
ambiguity_count: expandedAnchor.ambiguity_count,
|
||||||
|
match_failure_stage: "none",
|
||||||
|
match_failure_reason: null,
|
||||||
|
mcp_call_status: expandedStageStatus,
|
||||||
|
rows_fetched: expandedMcp.fetched_rows,
|
||||||
|
raw_rows_received: expandedMcp.raw_rows.length,
|
||||||
|
rows_after_account_scope: expandedNormalizedRows.length,
|
||||||
|
rows_after_recipe_filter: expandedRowsByAnchor.length,
|
||||||
|
rows_materialized: expandedNormalizedRows.length,
|
||||||
|
rows_matched: expandedFilteredRows.length,
|
||||||
|
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
|
||||||
|
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
|
||||||
|
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
|
||||||
|
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
|
||||||
|
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
|
||||||
|
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
|
||||||
|
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
|
||||||
|
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||||
|
limited_reason_category: null,
|
||||||
|
response_type: expandedFactual.responseType,
|
||||||
|
limitations: expandedLimitations,
|
||||||
|
reasons: expandedReasons
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (intent.intent === "open_items_by_counterparty_or_contract" &&
|
||||||
|
expandedFilteredRows.length === 0 &&
|
||||||
|
expandedRowsByAnchor.length === 0 &&
|
||||||
|
expandedNormalizedRows.length > 0) {
|
||||||
|
const expandedFallbackRows = applyIntentSpecificFilter(intent.intent, expandedNormalizedRows);
|
||||||
|
if (expandedFallbackRows.length > 0) {
|
||||||
|
const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length);
|
||||||
|
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFallbackRows);
|
||||||
|
const expandedLimitations = [
|
||||||
|
...filters.warnings,
|
||||||
|
"query_limit_auto_expanded_for_anchor_recovery",
|
||||||
|
"open_items_anchor_not_matched_fallback_rows"
|
||||||
|
];
|
||||||
|
const expandedReasons = [
|
||||||
|
...baseReasons,
|
||||||
|
"query_limit_auto_expanded_for_anchor_recovery",
|
||||||
|
"open_items_anchor_not_matched_fallback_rows"
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text: `Точный якорь не подтвердился в live-строках даже после расширения до ${expandedPlan.limit}; ` +
|
||||||
|
"показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||||
|
expandedFactual.text,
|
||||||
|
reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType),
|
||||||
|
response_type: expandedFactual.responseType,
|
||||||
|
debug: {
|
||||||
|
detected_mode: mode.mode,
|
||||||
|
detected_mode_confidence: mode.confidence,
|
||||||
|
query_shape: shape.shape,
|
||||||
|
query_shape_confidence: shape.confidence,
|
||||||
|
detected_intent: intent.intent,
|
||||||
|
detected_intent_confidence: intent.confidence,
|
||||||
|
extracted_filters: filters.extracted_filters,
|
||||||
|
missing_required_filters: [],
|
||||||
|
selected_recipe: expandedSelection.selected_recipe.recipe_id,
|
||||||
|
mcp_call_status_legacy: "matched_non_empty",
|
||||||
|
account_scope_mode: expandedPlan.account_scope_mode,
|
||||||
|
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
|
||||||
|
anchor_type: expandedAnchor.anchor_type,
|
||||||
|
anchor_value_raw: expandedAnchor.anchor_value_raw,
|
||||||
|
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
|
||||||
|
resolver_confidence: expandedAnchor.resolver_confidence,
|
||||||
|
ambiguity_count: expandedAnchor.ambiguity_count,
|
||||||
|
match_failure_stage: "materialized_but_not_anchor_matched",
|
||||||
|
match_failure_reason: expandedAnchorFilter.mismatchReason ?? "anchor_not_matched_after_query_limit_expansion",
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
rows_fetched: expandedMcp.fetched_rows,
|
||||||
|
raw_rows_received: expandedMcp.raw_rows.length,
|
||||||
|
rows_after_account_scope: expandedNormalizedRows.length,
|
||||||
|
rows_after_recipe_filter: expandedRowsByAnchor.length,
|
||||||
|
rows_materialized: expandedNormalizedRows.length,
|
||||||
|
rows_matched: expandedFallbackRows.length,
|
||||||
|
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
|
||||||
|
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
|
||||||
|
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
|
||||||
|
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
|
||||||
|
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
|
||||||
|
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
|
||||||
|
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
|
||||||
|
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||||
|
limited_reason_category: null,
|
||||||
|
response_type: expandedFactual.responseType,
|
||||||
|
limitations: expandedLimitations,
|
||||||
|
reasons: expandedReasons
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
|
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
|
||||||
const autoBroadenedFilters = { ...filters.extracted_filters };
|
const autoBroadenedFilters = { ...filters.extracted_filters };
|
||||||
|
|
@ -870,37 +1139,111 @@ class AddressQueryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (filteredRows.length === 0 &&
|
||||||
|
intent.intent === "open_items_by_counterparty_or_contract" &&
|
||||||
|
normalizedRows.length > 0) {
|
||||||
|
const openItemsFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
|
||||||
|
if (openItemsFallbackRows.length > 0) {
|
||||||
|
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, openItemsFallbackRows);
|
||||||
|
const fallbackLimitations = [...filters.warnings, "open_items_anchor_not_matched_fallback_rows"];
|
||||||
|
const fallbackReasons = [...baseReasons, "open_items_anchor_not_matched_fallback_rows"];
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text: "Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||||
|
fallbackFactual.text,
|
||||||
|
reply_type: (0, composeStage_1.inferReplyType)(fallbackFactual.responseType),
|
||||||
|
response_type: fallbackFactual.responseType,
|
||||||
|
debug: {
|
||||||
|
detected_mode: mode.mode,
|
||||||
|
detected_mode_confidence: mode.confidence,
|
||||||
|
query_shape: shape.shape,
|
||||||
|
query_shape_confidence: shape.confidence,
|
||||||
|
detected_intent: intent.intent,
|
||||||
|
detected_intent_confidence: intent.confidence,
|
||||||
|
extracted_filters: filters.extracted_filters,
|
||||||
|
missing_required_filters: [],
|
||||||
|
selected_recipe: recipeSelection.selected_recipe.recipe_id,
|
||||||
|
mcp_call_status_legacy: "matched_non_empty",
|
||||||
|
account_scope_mode: plan.account_scope_mode,
|
||||||
|
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||||||
|
anchor_type: anchor.anchor_type,
|
||||||
|
anchor_value_raw: anchor.anchor_value_raw,
|
||||||
|
anchor_value_resolved: anchor.anchor_value_resolved,
|
||||||
|
resolver_confidence: anchor.resolver_confidence,
|
||||||
|
ambiguity_count: anchor.ambiguity_count,
|
||||||
|
match_failure_stage: matchFailureStage,
|
||||||
|
match_failure_reason: matchFailureReason,
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
rows_fetched: mcp.fetched_rows,
|
||||||
|
raw_rows_received: mcp.raw_rows.length,
|
||||||
|
rows_after_account_scope: normalizedRows.length,
|
||||||
|
rows_after_recipe_filter: filterByAnchors.length,
|
||||||
|
rows_materialized: normalizedRows.length,
|
||||||
|
rows_matched: openItemsFallbackRows.length,
|
||||||
|
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||||
|
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||||
|
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||||
|
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||||
|
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||||
|
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||||
|
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||||
|
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||||
|
limited_reason_category: null,
|
||||||
|
response_type: fallbackFactual.responseType,
|
||||||
|
limitations: fallbackLimitations,
|
||||||
|
reasons: fallbackReasons
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if (filteredRows.length === 0) {
|
if (filteredRows.length === 0) {
|
||||||
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
|
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
|
||||||
const hadAnchorMatchedRows = filterByAnchors.length > 0;
|
const hadAnchorMatchedRows = filterByAnchors.length > 0;
|
||||||
const isVisibilityGapCandidate = hadBaseRows &&
|
const isVisibilityGapCandidate = hadBaseRows &&
|
||||||
hadAnchorMatchedRows &&
|
hadAnchorMatchedRows &&
|
||||||
(intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty");
|
(intent.intent === "list_documents_by_counterparty" ||
|
||||||
|
intent.intent === "bank_operations_by_counterparty" ||
|
||||||
|
intent.intent === "list_documents_by_contract" ||
|
||||||
|
intent.intent === "bank_operations_by_contract");
|
||||||
const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched";
|
const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched";
|
||||||
const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe";
|
const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe";
|
||||||
|
const isFollowupAnchorCarryover = Array.isArray(filters.warnings) &&
|
||||||
|
(filters.warnings.includes("counterparty_from_followup_context") ||
|
||||||
|
filters.warnings.includes("contract_from_followup_context"));
|
||||||
|
const isLowQualityPartyAnchor = (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") &&
|
||||||
|
isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw);
|
||||||
|
const anchorMismatchCategory = isFollowupAnchorCarryover || !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor";
|
||||||
const category = isAnchorMismatch
|
const category = isAnchorMismatch
|
||||||
? "missing_anchor"
|
? anchorMismatchCategory
|
||||||
: isRecipeFilteredOut
|
: isRecipeFilteredOut
|
||||||
? "recipe_visibility_gap"
|
? "recipe_visibility_gap"
|
||||||
: isVisibilityGapCandidate
|
: isVisibilityGapCandidate
|
||||||
? "recipe_visibility_gap"
|
? "recipe_visibility_gap"
|
||||||
: "empty_match";
|
: "empty_match";
|
||||||
const reasonText = isAnchorMismatch
|
const reasonText = isAnchorMismatch
|
||||||
? "якорь контрагента/договора не найден в материализованных live-строках"
|
? anchorMismatchCategory === "missing_anchor"
|
||||||
|
? "якорь контрагента/договора не найден в материализованных live-строках"
|
||||||
|
: "по указанному якорю и фильтрам в live-выборке нет строк"
|
||||||
: isRecipeFilteredOut
|
: isRecipeFilteredOut
|
||||||
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
|
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
|
||||||
: isVisibilityGapCandidate
|
: isVisibilityGapCandidate
|
||||||
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
|
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
|
||||||
: "по выбранным фильтрам в live-выборке нет строк";
|
: "по выбранным фильтрам в live-выборке нет строк";
|
||||||
const nextStep = isAnchorMismatch
|
const nextStep = isAnchorMismatch
|
||||||
? "уточните контрагента точным именем или добавьте ИНН/договор"
|
? anchorMismatchCategory === "missing_anchor"
|
||||||
|
? "уточните контрагента точным именем или добавьте ИНН/договор"
|
||||||
|
: "уточните период или снимите часть фильтров"
|
||||||
: isRecipeFilteredOut
|
: isRecipeFilteredOut
|
||||||
? "сузьте период, уточните контрагента или документный тип"
|
? "сузьте период, уточните контрагента или документный тип"
|
||||||
: isVisibilityGapCandidate
|
: isVisibilityGapCandidate
|
||||||
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
|
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
|
||||||
: "уточните период, контрагента, договор или снимите часть фильтров";
|
: "уточните период, контрагента, договор или снимите часть фильтров";
|
||||||
const limitations = isAnchorMismatch
|
const limitations = isAnchorMismatch
|
||||||
? ["anchor_not_matched_after_materialization"]
|
? [
|
||||||
|
anchorMismatchCategory === "missing_anchor"
|
||||||
|
? "anchor_not_matched_after_materialization"
|
||||||
|
: "no_rows_for_anchor_after_materialization"
|
||||||
|
]
|
||||||
: isRecipeFilteredOut
|
: isRecipeFilteredOut
|
||||||
? ["rows_filtered_out_by_recipe_after_anchor_match"]
|
? ["rows_filtered_out_by_recipe_after_anchor_match"]
|
||||||
: [
|
: [
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,26 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "bank_docs"
|
query_template: "bank_docs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
recipe_id: "address_documents_by_contract_v1",
|
||||||
|
intent: "list_documents_by_contract",
|
||||||
|
purpose: "Collect contract-related document lines from movements",
|
||||||
|
required_filters: ["contract"],
|
||||||
|
optional_filters: ["period_from", "period_to", "as_of_date", "organization", "counterparty", "limit", "sort"],
|
||||||
|
default_limit: 100,
|
||||||
|
account_scope: ["60", "62", "76", "51", "52"],
|
||||||
|
account_scope_mode: "preferred"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipe_id: "address_bank_operations_by_contract_v1",
|
||||||
|
intent: "bank_operations_by_contract",
|
||||||
|
purpose: "Collect bank operation candidates by contract from movement lines",
|
||||||
|
required_filters: ["contract"],
|
||||||
|
optional_filters: ["period_from", "period_to", "as_of_date", "organization", "counterparty", "limit", "sort"],
|
||||||
|
default_limit: 100,
|
||||||
|
account_scope: ["51", "52"],
|
||||||
|
account_scope_mode: "preferred"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
recipe_id: "address_documents_forming_balance_v1",
|
recipe_id: "address_documents_forming_balance_v1",
|
||||||
intent: "documents_forming_balance",
|
intent: "documents_forming_balance",
|
||||||
|
|
@ -216,8 +236,9 @@ function buildMovementAccountCondition(filters) {
|
||||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||||
}
|
}
|
||||||
function shouldBoostLimitForAllTimeCounterparty(filters) {
|
function shouldBoostLimitForAllTimeCounterparty(filters) {
|
||||||
const hasCounterparty = typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0;
|
const hasAnchor = (typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0) ||
|
||||||
if (!hasCounterparty) {
|
(typeof filters.contract === "string" && filters.contract.trim().length > 0);
|
||||||
|
if (!hasAnchor) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasPeriod = Boolean((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
const hasPeriod = Boolean((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||||
|
|
@ -226,7 +247,12 @@ function shouldBoostLimitForAllTimeCounterparty(filters) {
|
||||||
return !hasPeriod;
|
return !hasPeriod;
|
||||||
}
|
}
|
||||||
function maxLimitForIntent(intent) {
|
function maxLimitForIntent(intent) {
|
||||||
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
|
if (intent === "list_documents_by_counterparty" ||
|
||||||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
intent === "list_documents_by_contract" ||
|
||||||
|
intent === "bank_operations_by_contract" ||
|
||||||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
intent === "list_open_contracts") {
|
||||||
return ADDRESS_MAX_LIMIT_EXTENDED;
|
return ADDRESS_MAX_LIMIT_EXTENDED;
|
||||||
}
|
}
|
||||||
return ADDRESS_MAX_LIMIT_DEFAULT;
|
return ADDRESS_MAX_LIMIT_DEFAULT;
|
||||||
|
|
@ -255,7 +281,10 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||||
: recipe.default_limit;
|
: recipe.default_limit;
|
||||||
const boostedLimit = (recipe.intent === "list_documents_by_counterparty" || recipe.intent === "bank_operations_by_counterparty") &&
|
const boostedLimit = (recipe.intent === "list_documents_by_counterparty" ||
|
||||||
|
recipe.intent === "bank_operations_by_counterparty" ||
|
||||||
|
recipe.intent === "list_documents_by_contract" ||
|
||||||
|
recipe.intent === "bank_operations_by_contract") &&
|
||||||
shouldBoostLimitForAllTimeCounterparty(filters)
|
shouldBoostLimitForAllTimeCounterparty(filters)
|
||||||
? Math.max(baseLimit, maxLimit)
|
? Math.max(baseLimit, maxLimit)
|
||||||
: (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") &&
|
: (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") &&
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,13 @@ function composeFactualReply(intent, rows) {
|
||||||
`Строк движения: ${rows.length}.`,
|
`Строк движения: ${rows.length}.`,
|
||||||
`Договорных кандидатов: ${contracts.length}.`
|
`Договорных кандидатов: ${contracts.length}.`
|
||||||
];
|
];
|
||||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
if (contracts.length > 0) {
|
||||||
|
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
|
||||||
|
lines.push(...formatTopRows(rows, 6));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
|
|
@ -95,6 +101,17 @@ function composeFactualReply(intent, rows) {
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "list_documents_by_contract") {
|
||||||
|
const lines = [
|
||||||
|
"Собран список документов по договору (live address lane).",
|
||||||
|
`Строк отобрано: ${rows.length}.`,
|
||||||
|
...formatTopRows(rows, rows.length)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
text: lines.join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "bank_operations_by_counterparty") {
|
if (intent === "bank_operations_by_counterparty") {
|
||||||
const lines = [
|
const lines = [
|
||||||
"Собран список банковских операций по контрагенту (live address lane).",
|
"Собран список банковских операций по контрагенту (live address lane).",
|
||||||
|
|
@ -106,6 +123,17 @@ function composeFactualReply(intent, rows) {
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "bank_operations_by_contract") {
|
||||||
|
const lines = [
|
||||||
|
"Собран список банковских операций по договору (live address lane).",
|
||||||
|
`Строк отобрано: ${rows.length}.`,
|
||||||
|
...formatTopRows(rows, rows.length)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
text: lines.join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
const title = intent === "list_payables_counterparties"
|
const title = intent === "list_payables_counterparties"
|
||||||
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
|
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
|
||||||
: intent === "list_receivables_counterparties"
|
: intent === "list_receivables_counterparties"
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
|
||||||
|
if (!toNonEmptyString(merged.contract)) {
|
||||||
|
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||||
|
if (inheritedContract) {
|
||||||
|
merged.contract = inheritedContract;
|
||||||
|
reasons.push("contract_from_followup_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||||
if (!toNonEmptyString(merged.account)) {
|
if (!toNonEmptyString(merged.account)) {
|
||||||
const inheritedAccount = previousAccount ?? (followupContext.previous_anchor_type === "account" ? previousAnchorValue : null);
|
const inheritedAccount = previousAccount ?? (followupContext.previous_anchor_type === "account" ? previousAnchorValue : null);
|
||||||
|
|
@ -130,7 +139,9 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||||
account_balance_snapshot: ["account", "as_of_date"],
|
account_balance_snapshot: ["account", "as_of_date"],
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
list_documents_by_counterparty: ["counterparty"],
|
list_documents_by_counterparty: ["counterparty"],
|
||||||
bank_operations_by_counterparty: ["counterparty"]
|
bank_operations_by_counterparty: ["counterparty"],
|
||||||
|
list_documents_by_contract: ["contract"],
|
||||||
|
bank_operations_by_contract: ["contract"]
|
||||||
};
|
};
|
||||||
const required = requiredByIntent[intent] ?? [];
|
const required = requiredByIntent[intent] ?? [];
|
||||||
return required.filter((key) => {
|
return required.filter((key) => {
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,17 @@ function resolvePrimaryAnchor(intent, filters) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
|
||||||
|
if (contract) {
|
||||||
|
return {
|
||||||
|
anchor_type: "contract",
|
||||||
|
anchor_value_raw: contract,
|
||||||
|
anchor_value_resolved: contract,
|
||||||
|
resolver_confidence: "medium",
|
||||||
|
ambiguity_count: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if (counterparty) {
|
if (counterparty) {
|
||||||
return {
|
return {
|
||||||
anchor_type: "counterparty",
|
anchor_type: "counterparty",
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"))
|
||||||
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
|
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
|
||||||
const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence"));
|
const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence"));
|
||||||
const addressQueryService_1 = __importStar(require("./addressQueryService"));
|
const addressQueryService_1 = __importStar(require("./addressQueryService"));
|
||||||
|
const addressQueryClassifier_1 = __importStar(require("./addressQueryClassifier"));
|
||||||
|
const addressIntentResolver_1 = __importStar(require("./addressIntentResolver"));
|
||||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||||
function retrievalSummaryForRoute(route) {
|
function retrievalSummaryForRoute(route) {
|
||||||
if (route === "store_canonical")
|
if (route === "store_canonical")
|
||||||
|
|
@ -1802,6 +1804,10 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||||||
llm_decomposition_trace_id: llmMeta?.traceId ?? null,
|
llm_decomposition_trace_id: llmMeta?.traceId ?? null,
|
||||||
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null,
|
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null,
|
||||||
llm_decomposition_reason: llmMeta?.reason ?? null,
|
llm_decomposition_reason: llmMeta?.reason ?? null,
|
||||||
|
fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null,
|
||||||
|
sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null,
|
||||||
|
tool_gate_decision: llmMeta?.toolGateDecision ?? null,
|
||||||
|
tool_gate_reason: llmMeta?.toolGateReason ?? null,
|
||||||
answer_structure_v11: null,
|
answer_structure_v11: null,
|
||||||
investigation_state_snapshot: null,
|
investigation_state_snapshot: null,
|
||||||
normalized: null,
|
normalized: null,
|
||||||
|
|
@ -1865,9 +1871,24 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
|
||||||
"show",
|
"show",
|
||||||
"list",
|
"list",
|
||||||
"выведи",
|
"выведи",
|
||||||
|
"что",
|
||||||
|
"чо",
|
||||||
"которые",
|
"которые",
|
||||||
"какие",
|
"какие",
|
||||||
"какой",
|
"какой",
|
||||||
|
"были",
|
||||||
|
"был",
|
||||||
|
"была",
|
||||||
|
"было",
|
||||||
|
"ли",
|
||||||
|
"списания",
|
||||||
|
"списание",
|
||||||
|
"поступления",
|
||||||
|
"поступление",
|
||||||
|
"расчетного",
|
||||||
|
"расчётного",
|
||||||
|
"счета",
|
||||||
|
"счёта",
|
||||||
"есть",
|
"есть",
|
||||||
"est",
|
"est",
|
||||||
"kakie",
|
"kakie",
|
||||||
|
|
@ -1880,6 +1901,401 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
|
||||||
"ёпт",
|
"ёпт",
|
||||||
"бля"
|
"бля"
|
||||||
]);
|
]);
|
||||||
|
const ADDRESS_FALLBACK_STRIP_TOKENS = new Set([
|
||||||
|
"бля",
|
||||||
|
"блять",
|
||||||
|
"blya",
|
||||||
|
"blyat",
|
||||||
|
"епт",
|
||||||
|
"ёпт",
|
||||||
|
"епта",
|
||||||
|
"нах",
|
||||||
|
"нахуй",
|
||||||
|
"плс",
|
||||||
|
"pls",
|
||||||
|
"пж",
|
||||||
|
"пжлст",
|
||||||
|
"пожалуйста",
|
||||||
|
"please"
|
||||||
|
]);
|
||||||
|
const ADDRESS_MONTH_ALIAS_MAP = {
|
||||||
|
янв: "01",
|
||||||
|
январ: "01",
|
||||||
|
january: "01",
|
||||||
|
jan: "01",
|
||||||
|
фев: "02",
|
||||||
|
феврал: "02",
|
||||||
|
february: "02",
|
||||||
|
feb: "02",
|
||||||
|
мар: "03",
|
||||||
|
март: "03",
|
||||||
|
march: "03",
|
||||||
|
apr: "04",
|
||||||
|
апр: "04",
|
||||||
|
апрел: "04",
|
||||||
|
april: "04",
|
||||||
|
май: "05",
|
||||||
|
ма: "05",
|
||||||
|
may: "05",
|
||||||
|
июн: "06",
|
||||||
|
июнь: "06",
|
||||||
|
june: "06",
|
||||||
|
jun: "06",
|
||||||
|
июл: "07",
|
||||||
|
июль: "07",
|
||||||
|
july: "07",
|
||||||
|
jul: "07",
|
||||||
|
авг: "08",
|
||||||
|
август: "08",
|
||||||
|
august: "08",
|
||||||
|
aug: "08",
|
||||||
|
сен: "09",
|
||||||
|
сент: "09",
|
||||||
|
сентябр: "09",
|
||||||
|
september: "09",
|
||||||
|
sep: "09",
|
||||||
|
окт: "10",
|
||||||
|
октябр: "10",
|
||||||
|
october: "10",
|
||||||
|
oct: "10",
|
||||||
|
ноя: "11",
|
||||||
|
ноябр: "11",
|
||||||
|
november: "11",
|
||||||
|
nov: "11",
|
||||||
|
дек: "12",
|
||||||
|
декабр: "12",
|
||||||
|
december: "12",
|
||||||
|
dec: "12"
|
||||||
|
};
|
||||||
|
const ADDRESS_DOCS_SIGNAL_PATTERN = /(?:док|доки|документ|документы|документов|docs?|documents?|bank|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|transaction)/i;
|
||||||
|
const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|расчетн|транзак)/i;
|
||||||
|
const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|(?:^|[^\p{L}\p{N}_])(?:дог\.?|[dд][oо][gг]\.?|dog\.?)(?=$|[^\p{L}\p{N}_])|contract|dogovor)/iu;
|
||||||
|
const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i;
|
||||||
|
const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu;
|
||||||
|
function normalizeAddressMonthAliasToken(token) {
|
||||||
|
const source = String(token ?? "").trim().toLowerCase();
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const direct = ADDRESS_MONTH_ALIAS_MAP[source];
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(ADDRESS_MONTH_ALIAS_MAP)) {
|
||||||
|
if (source.startsWith(key)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function normalizeAddressShortYearMentions(text) {
|
||||||
|
return String(text ?? "").replace(/(^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/giu, (_full, prefix, shortYear) => {
|
||||||
|
const normalized = Number(shortYear);
|
||||||
|
if (!Number.isFinite(normalized) || normalized < 0 || normalized > 99) {
|
||||||
|
return _full;
|
||||||
|
}
|
||||||
|
return `${prefix}${String(2000 + normalized)} год`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function sanitizeAddressMessageForFallback(userMessage) {
|
||||||
|
const repaired = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
|
||||||
|
if (!repaired) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
let sanitized = repaired.toLowerCase();
|
||||||
|
sanitized = sanitized
|
||||||
|
.replace(/\bpokezh\b/giu, "покажи")
|
||||||
|
.replace(/\bpokazh(?:i)?\b/giu, "покажи")
|
||||||
|
.replace(/\bpokaji\b/giu, "покажи")
|
||||||
|
.replace(/\bop(?:er|ers?)\b/giu, "операции")
|
||||||
|
.replace(/(^|[^\p{L}\p{N}_])опер(?:аци[яиюе]|ы|)?(?=$|[^\p{L}\p{N}_])/giu, "$1операции")
|
||||||
|
.replace(/(^|[^\p{L}\p{N}_])дог\.?(?=$|[^\p{L}\p{N}_])/giu, "$1договор")
|
||||||
|
.replace(/(^|[^\p{L}\p{N}_])dog\.?(?=$|[^\p{L}\p{N}_])/giu, "$1contract")
|
||||||
|
.replace(/\bdok(?:i|y)?\b/giu, "доки")
|
||||||
|
.replace(/\bdocuments?\b/giu, "документы")
|
||||||
|
.replace(/\bdocs?\b/giu, "документы")
|
||||||
|
.replace(/\bschet(?:u)?\b/giu, "счет")
|
||||||
|
.replace(/\bsaldo\b/giu, "сальдо")
|
||||||
|
.replace(/\bgod\b/giu, "год");
|
||||||
|
sanitized = normalizeAddressShortYearMentions(sanitized);
|
||||||
|
const tokens = sanitized
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const filteredTokens = tokens.filter((token) => {
|
||||||
|
const normalizedToken = token.replace(/^[^a-zа-яё0-9]+|[^a-zа-яё0-9]+$/giu, "");
|
||||||
|
if (!normalizedToken) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !ADDRESS_FALLBACK_STRIP_TOKENS.has(normalizedToken);
|
||||||
|
});
|
||||||
|
const compact = compactWhitespace(filteredTokens.join(" "));
|
||||||
|
return compact || compactWhitespace(repaired.toLowerCase());
|
||||||
|
}
|
||||||
|
function extractAddressFallbackYear(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const fullYearMatch = source.match(/\b(20\d{2})\b/);
|
||||||
|
if (fullYearMatch) {
|
||||||
|
return fullYearMatch[1];
|
||||||
|
}
|
||||||
|
const shortYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/iu);
|
||||||
|
if (shortYearMatch) {
|
||||||
|
const shortYear = Number(shortYearMatch[1]);
|
||||||
|
if (Number.isFinite(shortYear) && shortYear >= 0 && shortYear <= 99) {
|
||||||
|
return String(2000 + shortYear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shortOrdinalYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:[-\s]?(?:й|ый|ой|th))(?=$|[^a-zа-яё0-9])/iu);
|
||||||
|
if (!shortOrdinalYearMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const shortOrdinalYear = Number(shortOrdinalYearMatch[1]);
|
||||||
|
if (!Number.isFinite(shortOrdinalYear) || shortOrdinalYear < 0 || shortOrdinalYear > 99) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return String(2000 + shortOrdinalYear);
|
||||||
|
}
|
||||||
|
function extractAddressFallbackMonthYear(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const numericYearMonth = source.match(/\b(20\d{2})[./-](0?[1-9]|1[0-2])\b/);
|
||||||
|
if (numericYearMonth) {
|
||||||
|
const year = numericYearMonth[1];
|
||||||
|
const month = String(Number(numericYearMonth[2])).padStart(2, "0");
|
||||||
|
return `${year}-${month}`;
|
||||||
|
}
|
||||||
|
const numericMonthYear = source.match(/\b(0?[1-9]|1[0-2])[./-](20\d{2})\b/);
|
||||||
|
if (numericMonthYear) {
|
||||||
|
const month = String(Number(numericMonthYear[1])).padStart(2, "0");
|
||||||
|
const year = numericMonthYear[2];
|
||||||
|
return `${year}-${month}`;
|
||||||
|
}
|
||||||
|
const namedMonthYear = source.match(/(?:^|[^a-zа-яё0-9])([a-zа-яё]+)\s+(20\d{2})(?=$|[^a-zа-яё0-9])/iu);
|
||||||
|
if (namedMonthYear) {
|
||||||
|
const month = normalizeAddressMonthAliasToken(namedMonthYear[1]);
|
||||||
|
if (month) {
|
||||||
|
return `${namedMonthYear[2]}-${month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const yearNamedMonth = source.match(/(?:^|[^a-zа-яё0-9])(20\d{2})\s+([a-zа-яё]+)(?=$|[^a-zа-яё0-9])/iu);
|
||||||
|
if (yearNamedMonth) {
|
||||||
|
const month = normalizeAddressMonthAliasToken(yearNamedMonth[2]);
|
||||||
|
if (month) {
|
||||||
|
return `${yearNamedMonth[1]}-${month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function extractAddressFallbackAccountToken(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const explicitMatch = source.match(/(?:сч[её]т(?:а|у|ом|е)?|account)\D{0,12}(\d{2}(?:[.,]\d{1,2})?)/iu);
|
||||||
|
if (explicitMatch && explicitMatch[1]) {
|
||||||
|
return String(explicitMatch[1]).replace(",", ".");
|
||||||
|
}
|
||||||
|
const tokenPattern = /\b(\d{2}(?:[.,]\d{1,2})?)\b/giu;
|
||||||
|
let match = tokenPattern.exec(source);
|
||||||
|
while (match) {
|
||||||
|
const raw = String(match[1] ?? "");
|
||||||
|
const start = match.index;
|
||||||
|
const end = start + raw.length;
|
||||||
|
const prev = start > 0 ? source[start - 1] : " ";
|
||||||
|
const next = end < source.length ? source[end] : " ";
|
||||||
|
if (!/[./-]/.test(prev) && !/[./-]/.test(next) && !/\d/.test(prev) && !/\d/.test(next)) {
|
||||||
|
return raw.replace(",", ".");
|
||||||
|
}
|
||||||
|
match = tokenPattern.exec(source);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function pickAddressFallbackCounterpartyToken(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const byAnchor = source.match(/(?:^|[\s,.;:!?()\-])(?:по|от)\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=$|[\s,.;:!?()\-])/iu);
|
||||||
|
if (byAnchor && byAnchor[1]) {
|
||||||
|
const byToken = String(byAnchor[1]).trim();
|
||||||
|
const normalizedByToken = byToken.toLowerCase();
|
||||||
|
if (byToken &&
|
||||||
|
!ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(normalizedByToken) &&
|
||||||
|
!/^\d{2}(?:\.\d{1,2})?$/.test(normalizedByToken) &&
|
||||||
|
!/^(?:19|20)\d{2}$/.test(normalizedByToken) &&
|
||||||
|
!/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalizedByToken) &&
|
||||||
|
!/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalizedByToken)) {
|
||||||
|
return byToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const candidates = extractAddressAnchorTokens(text);
|
||||||
|
for (const token of candidates) {
|
||||||
|
const normalized = String(token ?? "").toLowerCase();
|
||||||
|
if (!normalized || /^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}$/.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function extractAddressFallbackContractToken(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const patterns = [
|
||||||
|
/(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)\s*(?:№|#|n|no\.?)?\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})/iu,
|
||||||
|
/(?:№|#|n|no\.?)\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})\s*(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)/iu
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = pattern.exec(source);
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const candidate = String(match[1]).replace(/^[^a-zа-я0-9]+|[^a-zа-я0-9/_-]+$/giu, "");
|
||||||
|
if (!candidate || candidate.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}$/.test(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])(?:[./-](?:0?[1-9]|[12]\d|3[01]))?$/.test(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source) || /(?:^|[^\p{L}\p{N}_])(?:[dд][oо][gг]|dogovor)(?=$|[^\p{L}\p{N}_])/iu.test(source)) {
|
||||||
|
const generic = source.match(/\b([a-zа-я0-9]{1,10}[/-][a-zа-я0-9]{1,10}(?:[/-][a-zа-я0-9]{1,10})?)\b/iu);
|
||||||
|
if (generic && generic[1]) {
|
||||||
|
return generic[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) {
|
||||||
|
const sourceRaw = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
|
||||||
|
const source = compactWhitespace(String(sanitizedUserMessage ?? sourceRaw).toLowerCase());
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const monthYear = extractAddressFallbackMonthYear(source);
|
||||||
|
const year = extractAddressFallbackYear(source);
|
||||||
|
const allTime = ADDRESS_ALL_TIME_PATTERN.test(source);
|
||||||
|
const account = extractAddressFallbackAccountToken(source);
|
||||||
|
const docsSignal = ADDRESS_DOCS_SIGNAL_PATTERN.test(source);
|
||||||
|
const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source);
|
||||||
|
const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
||||||
|
const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source);
|
||||||
|
if (balanceSignal && account) {
|
||||||
|
let periodClause = "";
|
||||||
|
let rule = "balance_account_rewrite";
|
||||||
|
if (monthYear) {
|
||||||
|
periodClause = ` на ${monthYear}`;
|
||||||
|
rule = "balance_month_period_rewrite";
|
||||||
|
}
|
||||||
|
else if (year) {
|
||||||
|
periodClause = ` на ${year}-12-31`;
|
||||||
|
rule = "balance_year_period_rewrite";
|
||||||
|
}
|
||||||
|
const candidate = compactWhitespace(`остаток по счету ${account}${periodClause}`);
|
||||||
|
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||||||
|
return {
|
||||||
|
candidate,
|
||||||
|
rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!docsSignal && !contractSignal && !balanceSignal) {
|
||||||
|
const counterparty = pickAddressFallbackCounterpartyToken(source);
|
||||||
|
const genericLookupSignal = /(?:\bесть\b|\bпокажи\b|\bвыведи\b|\bч[её]\b|\bчто\b)/iu.test(source);
|
||||||
|
if (counterparty && (allTime || monthYear || year) && genericLookupSignal) {
|
||||||
|
let periodClause = "";
|
||||||
|
let rule = "documents_counterparty_rewrite_from_generic_lookup";
|
||||||
|
if (allTime) {
|
||||||
|
periodClause = " за все время";
|
||||||
|
rule = "documents_counterparty_all_time_rewrite_from_generic_lookup";
|
||||||
|
}
|
||||||
|
else if (monthYear) {
|
||||||
|
periodClause = ` за ${monthYear}`;
|
||||||
|
rule = "documents_counterparty_month_rewrite_from_generic_lookup";
|
||||||
|
}
|
||||||
|
else if (year) {
|
||||||
|
periodClause = ` за ${year} год`;
|
||||||
|
rule = "documents_counterparty_year_rewrite_from_generic_lookup";
|
||||||
|
}
|
||||||
|
const candidate = compactWhitespace(`документы по контрагенту ${counterparty}${periodClause}`);
|
||||||
|
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||||||
|
return {
|
||||||
|
candidate,
|
||||||
|
rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (docsSignal) {
|
||||||
|
const contract = extractAddressFallbackContractToken(sourceRaw || source);
|
||||||
|
if (contractSignal || contract) {
|
||||||
|
if (contract) {
|
||||||
|
let periodClause = "";
|
||||||
|
let rule = bankSignal ? "bank_operations_contract_rewrite" : "documents_contract_rewrite";
|
||||||
|
if (allTime) {
|
||||||
|
periodClause = " за все время";
|
||||||
|
rule = bankSignal ? "bank_operations_contract_all_time_rewrite" : "documents_contract_all_time_rewrite";
|
||||||
|
}
|
||||||
|
else if (monthYear) {
|
||||||
|
periodClause = ` за ${monthYear}`;
|
||||||
|
rule = bankSignal ? "bank_operations_contract_month_rewrite" : "documents_contract_month_rewrite";
|
||||||
|
}
|
||||||
|
else if (year) {
|
||||||
|
periodClause = ` за ${year} год`;
|
||||||
|
rule = bankSignal ? "bank_operations_contract_year_rewrite" : "documents_contract_year_rewrite";
|
||||||
|
}
|
||||||
|
const subject = bankSignal ? "банковские операции" : "документы";
|
||||||
|
const candidate = compactWhitespace(`${subject} по договору ${contract}${periodClause}`);
|
||||||
|
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||||||
|
return {
|
||||||
|
candidate,
|
||||||
|
rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const counterparty = pickAddressFallbackCounterpartyToken(source);
|
||||||
|
if (counterparty) {
|
||||||
|
let periodClause = "";
|
||||||
|
const subject = bankSignal ? "банковские операции" : "документы";
|
||||||
|
const rulePrefix = bankSignal ? "bank_operations_counterparty" : "documents_counterparty";
|
||||||
|
let rule = `${rulePrefix}_rewrite`;
|
||||||
|
if (allTime) {
|
||||||
|
periodClause = " за все время";
|
||||||
|
rule = `${rulePrefix}_all_time_rewrite`;
|
||||||
|
}
|
||||||
|
else if (monthYear) {
|
||||||
|
periodClause = ` за ${monthYear}`;
|
||||||
|
rule = `${rulePrefix}_month_rewrite`;
|
||||||
|
}
|
||||||
|
else if (year) {
|
||||||
|
periodClause = ` за ${year} год`;
|
||||||
|
rule = `${rulePrefix}_year_rewrite`;
|
||||||
|
}
|
||||||
|
const candidate = compactWhitespace(`${subject} по контрагенту ${counterparty}${periodClause}`);
|
||||||
|
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||||||
|
return {
|
||||||
|
candidate,
|
||||||
|
rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (source !== sourceRaw.toLowerCase() && isAddressLlmPreDecomposeCandidate(source)) {
|
||||||
|
return {
|
||||||
|
candidate: source,
|
||||||
|
rule: "noise_cleanup"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function textMojibakeScoreForAddress(value) {
|
function textMojibakeScoreForAddress(value) {
|
||||||
const source = String(value ?? "");
|
const source = String(value ?? "");
|
||||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||||
|
|
@ -2203,21 +2619,56 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
|
||||||
}
|
}
|
||||||
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
|
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
|
||||||
const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null;
|
const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null;
|
||||||
|
const sanitizedUserMessage = sanitizeAddressMessageForFallback(userMessage);
|
||||||
|
const fallbackCandidate = resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage);
|
||||||
|
const hasAddressSignal = isAddressLlmPreDecomposeCandidate(userMessage) || isAddressLlmPreDecomposeCandidate(sanitizedUserMessage);
|
||||||
const baseMeta = {
|
const baseMeta = {
|
||||||
attempted: false,
|
attempted: false,
|
||||||
applied: false,
|
applied: false,
|
||||||
provider,
|
provider,
|
||||||
traceId: null,
|
traceId: null,
|
||||||
effectiveMessage: userMessage,
|
effectiveMessage: userMessage,
|
||||||
reason: "not_attempted"
|
reason: "not_attempted",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage,
|
||||||
|
toolGateDecision: null,
|
||||||
|
toolGateReason: null
|
||||||
};
|
};
|
||||||
if (Boolean(payload?.useMock)) {
|
if (Boolean(payload?.useMock)) {
|
||||||
|
if (fallbackCandidate) {
|
||||||
|
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||||||
|
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||||||
|
if (fallbackApplied) {
|
||||||
|
return {
|
||||||
|
...baseMeta,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage: fallbackCandidate.candidate,
|
||||||
|
reason: "fallback_rule_applied_without_llm",
|
||||||
|
fallbackRuleHit: fallbackCandidate.rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...baseMeta,
|
...baseMeta,
|
||||||
reason: "skipped_in_mock"
|
reason: "skipped_in_mock"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!isAddressLlmPreDecomposeCandidate(userMessage)) {
|
if (!hasAddressSignal) {
|
||||||
|
if (fallbackCandidate) {
|
||||||
|
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||||||
|
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||||||
|
if (fallbackApplied) {
|
||||||
|
return {
|
||||||
|
...baseMeta,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage: fallbackCandidate.candidate,
|
||||||
|
reason: "fallback_rule_applied_without_llm",
|
||||||
|
fallbackRuleHit: fallbackCandidate.rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...baseMeta,
|
...baseMeta,
|
||||||
reason: "not_address_like"
|
reason: "not_address_like"
|
||||||
|
|
@ -2242,6 +2693,22 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output);
|
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output);
|
||||||
const candidate = candidateFromNormalized ?? candidateFromRaw;
|
const candidate = candidateFromNormalized ?? candidateFromRaw;
|
||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
|
if (fallbackCandidate) {
|
||||||
|
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||||||
|
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||||||
|
if (fallbackApplied) {
|
||||||
|
return {
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
traceId: normalized?.trace_id ?? null,
|
||||||
|
effectiveMessage: fallbackCandidate.candidate,
|
||||||
|
reason: "fallback_rule_applied_after_llm",
|
||||||
|
fallbackRuleHit: fallbackCandidate.rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...baseMeta,
|
...baseMeta,
|
||||||
attempted: true,
|
attempted: true,
|
||||||
|
|
@ -2249,6 +2716,32 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed"
|
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const repairedSourceMessage = repairAddressMojibake(userMessage);
|
||||||
|
const sourceIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedSourceMessage || userMessage);
|
||||||
|
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
|
||||||
|
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
|
||||||
|
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
|
||||||
|
const intentConflict = sourceIntentKnown &&
|
||||||
|
candidateIntentKnown &&
|
||||||
|
sourceIntentResolution.intent !== candidateIntentResolution.intent;
|
||||||
|
const intentDroppedByCandidate = sourceIntentKnown && !candidateIntentKnown;
|
||||||
|
const rejectCandidateForIntentSafety = intentDroppedByCandidate ||
|
||||||
|
(intentConflict &&
|
||||||
|
(sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high"));
|
||||||
|
if (rejectCandidateForIntentSafety) {
|
||||||
|
return {
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: false,
|
||||||
|
traceId: normalized?.trace_id ?? null,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: intentDroppedByCandidate
|
||||||
|
? "normalized_fragment_rejected_intent_drop"
|
||||||
|
: "normalized_fragment_rejected_intent_conflict",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
const candidateCompact = compactWhitespace(candidate.toLowerCase());
|
const candidateCompact = compactWhitespace(candidate.toLowerCase());
|
||||||
const applied = sourceCompact !== candidateCompact;
|
const applied = sourceCompact !== candidateCompact;
|
||||||
|
|
@ -2270,10 +2763,27 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
provider,
|
provider,
|
||||||
traceId: normalized?.trace_id ?? null,
|
traceId: normalized?.trace_id ?? null,
|
||||||
effectiveMessage: applied ? candidate : userMessage,
|
effectiveMessage: applied ? candidate : userMessage,
|
||||||
reason
|
reason,
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
if (fallbackCandidate) {
|
||||||
|
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||||||
|
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||||||
|
if (fallbackApplied) {
|
||||||
|
return {
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage: fallbackCandidate.candidate,
|
||||||
|
reason: "fallback_rule_applied_after_llm_error",
|
||||||
|
fallbackRuleHit: fallbackCandidate.rule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...baseMeta,
|
...baseMeta,
|
||||||
attempted: true,
|
attempted: true,
|
||||||
|
|
@ -2281,6 +2791,35 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function resolveAddressToolGateDecision(addressInputMessage, followupContext) {
|
||||||
|
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
|
||||||
|
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
|
||||||
|
const hasClassifierSignal = modeDetection.mode === "address_query";
|
||||||
|
const hasMessageSignal = hasClassifierSignal ||
|
||||||
|
isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||||||
|
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||||||
|
hasAccountingSignal(addressInputMessage) ||
|
||||||
|
hasAccountingSignal(repairedInputMessage);
|
||||||
|
if (hasMessageSignal) {
|
||||||
|
return {
|
||||||
|
runAddressLane: true,
|
||||||
|
decision: "run_address_lane",
|
||||||
|
reason: hasClassifierSignal ? "address_mode_classifier_detected" : "address_signal_detected"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (followupContext) {
|
||||||
|
return {
|
||||||
|
runAddressLane: true,
|
||||||
|
decision: "run_address_lane",
|
||||||
|
reason: "followup_context_detected"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
runAddressLane: false,
|
||||||
|
decision: "skip_address_lane",
|
||||||
|
reason: "no_address_signal_after_l0"
|
||||||
|
};
|
||||||
|
}
|
||||||
class AssistantService {
|
class AssistantService {
|
||||||
normalizerService;
|
normalizerService;
|
||||||
sessions;
|
sessions;
|
||||||
|
|
@ -2313,12 +2852,13 @@ class AssistantService {
|
||||||
};
|
};
|
||||||
this.sessions.appendItem(sessionId, userItem);
|
this.sessions.appendItem(sessionId, userItem);
|
||||||
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
|
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
|
||||||
|
const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? "");
|
||||||
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
|
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
|
||||||
const assistantItem = {
|
const assistantItem = {
|
||||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
text: addressLane.reply_text,
|
text: safeAddressReply,
|
||||||
reply_type: addressLane.reply_type,
|
reply_type: addressLane.reply_type,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
trace_id: debug.trace_id,
|
trace_id: debug.trace_id,
|
||||||
|
|
@ -2350,6 +2890,10 @@ class AssistantService {
|
||||||
address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null,
|
address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null,
|
||||||
address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null,
|
address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null,
|
||||||
address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null,
|
address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null,
|
||||||
|
address_fallback_rule_hit: llmPreDecomposeMeta?.fallbackRuleHit ?? null,
|
||||||
|
address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null,
|
||||||
|
address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null,
|
||||||
|
address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null,
|
||||||
detected_mode: addressLane.debug.detected_mode,
|
detected_mode: addressLane.debug.detected_mode,
|
||||||
query_shape: addressLane.debug.query_shape,
|
query_shape: addressLane.debug.query_shape,
|
||||||
detected_intent: addressLane.debug.detected_intent,
|
detected_intent: addressLane.debug.detected_intent,
|
||||||
|
|
@ -2403,29 +2947,62 @@ class AssistantService {
|
||||||
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
|
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
|
||||||
traceId: null,
|
traceId: null,
|
||||||
effectiveMessage: userMessage,
|
effectiveMessage: userMessage,
|
||||||
reason: "disabled_by_feature_flag"
|
reason: "disabled_by_feature_flag",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
||||||
|
toolGateDecision: null,
|
||||||
|
toolGateReason: null
|
||||||
};
|
};
|
||||||
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
|
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
|
||||||
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items);
|
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items);
|
||||||
const shouldPreferContextualLane = Boolean(carryover?.followupContext);
|
const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null);
|
||||||
if (shouldPreferContextualLane) {
|
const addressRuntimeMeta = {
|
||||||
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
|
...addressPreDecompose,
|
||||||
followupContext: carryover.followupContext
|
toolGateDecision: toolGate.decision,
|
||||||
|
toolGateReason: toolGate.reason
|
||||||
|
};
|
||||||
|
if (!toolGate.runAddressLane) {
|
||||||
|
(0, log_1.logJson)({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: "info",
|
||||||
|
service: "assistant_loop",
|
||||||
|
message: "assistant_address_tool_gate_skip",
|
||||||
|
sessionId,
|
||||||
|
details: {
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: userMessage,
|
||||||
|
effective_address_user_message: addressInputMessage,
|
||||||
|
address_llm_predecompose_attempted: Boolean(addressRuntimeMeta?.attempted),
|
||||||
|
address_llm_predecompose_applied: Boolean(addressRuntimeMeta?.applied),
|
||||||
|
address_llm_predecompose_reason: addressRuntimeMeta?.reason ?? null,
|
||||||
|
address_fallback_rule_hit: addressRuntimeMeta?.fallbackRuleHit ?? null,
|
||||||
|
address_sanitized_user_message: addressRuntimeMeta?.sanitizedUserMessage ?? null,
|
||||||
|
address_tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
|
||||||
|
address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (contextualAddressLane?.handled) {
|
}
|
||||||
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose);
|
if (toolGate.runAddressLane) {
|
||||||
|
const shouldPreferContextualLane = Boolean(carryover?.followupContext);
|
||||||
|
if (shouldPreferContextualLane) {
|
||||||
|
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
|
||||||
|
followupContext: carryover.followupContext
|
||||||
|
});
|
||||||
|
if (contextualAddressLane?.handled) {
|
||||||
|
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage);
|
||||||
const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage);
|
if (primaryAddressLane?.handled) {
|
||||||
if (primaryAddressLane?.handled) {
|
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressRuntimeMeta);
|
||||||
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressPreDecompose);
|
}
|
||||||
}
|
if (!shouldPreferContextualLane && carryover?.followupContext) {
|
||||||
if (!shouldPreferContextualLane && carryover?.followupContext) {
|
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
|
||||||
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
|
followupContext: carryover.followupContext
|
||||||
followupContext: carryover.followupContext
|
});
|
||||||
});
|
if (contextualAddressLane?.handled) {
|
||||||
if (contextualAddressLane?.handled) {
|
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
|
||||||
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ function shouldFallbackToChatCompletions(error) {
|
||||||
if (!(error instanceof http_1.ApiError)) {
|
if (!(error instanceof http_1.ApiError)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (error.code === "OPENAI_OUTPUT_PARSE_FAILED" || error.code === "OPENAI_NON_JSON_RESPONSE") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (error.code !== "OPENAI_REQUEST_FAILED") {
|
if (error.code !== "OPENAI_REQUEST_FAILED") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +118,31 @@ function shouldFallbackToChatCompletions(error) {
|
||||||
const message = String(error.message ?? "").toLowerCase();
|
const message = String(error.message ?? "").toLowerCase();
|
||||||
return message.includes("/responses") || message.includes("responses");
|
return message.includes("/responses") || message.includes("responses");
|
||||||
}
|
}
|
||||||
|
function extractModelErrorMessage(data) {
|
||||||
|
const rawError = data.error;
|
||||||
|
if (typeof rawError === "string" && rawError.trim().length > 0) {
|
||||||
|
return rawError.trim();
|
||||||
|
}
|
||||||
|
if (rawError && typeof rawError === "object") {
|
||||||
|
const errorObj = rawError;
|
||||||
|
const message = errorObj.message;
|
||||||
|
if (typeof message === "string" && message.trim().length > 0) {
|
||||||
|
return message.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function isRouteMismatchErrorMessage(message) {
|
||||||
|
const source = String(message ?? "").toLowerCase();
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (/unexpected endpoint|unexpected route|unknown endpoint|unknown route|unsupported endpoint|unsupported route/.test(source) ||
|
||||||
|
(/endpoint/.test(source) && /method/.test(source)) ||
|
||||||
|
(/endpoint/.test(source) && /not found/.test(source)) ||
|
||||||
|
(/route/.test(source) && /not found/.test(source)) ||
|
||||||
|
(/path/.test(source) && /not found/.test(source)));
|
||||||
|
}
|
||||||
function loadSchemaForTransport(schemaVersion) {
|
function loadSchemaForTransport(schemaVersion) {
|
||||||
const schemaFile = schemaVersion === "v1"
|
const schemaFile = schemaVersion === "v1"
|
||||||
? "normalized_query_v1.json"
|
? "normalized_query_v1.json"
|
||||||
|
|
@ -331,9 +359,13 @@ class OpenAIResponsesClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const modelErrorMessage = extractModelErrorMessage(data);
|
||||||
|
if (modelErrorMessage && canFallbackToAlternativeBase && !isLastCandidate && isRouteMismatchErrorMessage(modelErrorMessage)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorObj = (data.error ?? {});
|
const errorObj = (data.error ?? {});
|
||||||
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", String(errorObj.message ?? `Model endpoint failed: ${response.status}`), response.status, {
|
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", modelErrorMessage ?? String(errorObj.message ?? `Model endpoint failed: ${response.status}`), response.status, {
|
||||||
route: routePath,
|
route: routePath,
|
||||||
url,
|
url,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
|
|
@ -341,6 +373,13 @@ class OpenAIResponsesClient {
|
||||||
code: errorObj.code ?? null
|
code: errorObj.code ?? null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (modelErrorMessage) {
|
||||||
|
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", modelErrorMessage, 502, {
|
||||||
|
route: routePath,
|
||||||
|
url,
|
||||||
|
status: response.status
|
||||||
|
});
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, {
|
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, {
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,11 @@ function cleanupAnchorValue(value: string): string {
|
||||||
if (asOfTruncatedTailPattern.test(normalized)) {
|
if (asOfTruncatedTailPattern.test(normalized)) {
|
||||||
return normalized.replace(asOfTruncatedTailPattern, "").trim();
|
return normalized.replace(asOfTruncatedTailPattern, "").trim();
|
||||||
}
|
}
|
||||||
|
const periodEndTailPattern =
|
||||||
|
/\s+на\s+конец(?:\s+период[а-яё]*)?\s+(?:\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|\d{4}|[a-zа-яё]+\s+\d{4})(?:\s+|$)[\s\S]*$/iu;
|
||||||
|
if (periodEndTailPattern.test(normalized)) {
|
||||||
|
return normalized.replace(periodEndTailPattern, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
||||||
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
||||||
|
|
@ -618,6 +623,10 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
|
||||||
|
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
|
||||||
|
}
|
||||||
|
|
||||||
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
||||||
const rawText = String(userMessage ?? "").trim();
|
const rawText = String(userMessage ?? "").trim();
|
||||||
const text = normalizeMojibakeString(rawText);
|
const text = normalizeMojibakeString(rawText);
|
||||||
|
|
@ -713,8 +722,22 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const explicitAsOfDate = extractAsOfDate(text);
|
||||||
|
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
||||||
|
filters.as_of_date = explicitAsOfDate;
|
||||||
|
const periodWasDerivedHeuristically =
|
||||||
|
warnings.includes("period_derived_from_month_phrase") ||
|
||||||
|
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||||
|
warnings.includes("period_derived_from_year_phrase");
|
||||||
|
if (periodWasDerivedHeuristically && !periodRange.period_from && !periodRange.period_to) {
|
||||||
|
delete filters.period_from;
|
||||||
|
delete filters.period_to;
|
||||||
|
warnings.push("period_window_cleared_for_as_of_intent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If explicit period window exists, do not infer as_of_date from one of its boundary dates.
|
// If explicit period window exists, do not infer as_of_date from one of its boundary dates.
|
||||||
if (!filters.period_from && !filters.period_to) {
|
if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
|
||||||
const asOfDate = extractAsOfDate(text);
|
const asOfDate = extractAsOfDate(text);
|
||||||
if (asOfDate) {
|
if (asOfDate) {
|
||||||
filters.as_of_date = asOfDate;
|
filters.as_of_date = asOfDate;
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,23 @@ function hasAccountBalanceSignal(text: string): boolean {
|
||||||
return hasAccountLexeme && hasBalanceLexeme;
|
return hasAccountLexeme && hasBalanceLexeme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasOpenContractsListSignal(text: string): boolean {
|
||||||
|
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
|
||||||
|
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
|
||||||
|
if (!hasContractLexeme || !hasOpenLexeme) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Query about a specific contract should stay in open-items lane.
|
||||||
|
if (hasContractNumberLikeToken(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Debt/tail wording indicates open-items intent, not contract list.
|
||||||
|
if (/(?:долг|задолж|хвост|позиц|open\s+items|unclosed\s+items|взаиморасчет|взаиморасчёт)/iu.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||||
const token = String(rawToken ?? "").trim().toLowerCase();
|
const token = String(rawToken ?? "").trim().toLowerCase();
|
||||||
if (!token || token.length < 2) {
|
if (!token || token.length < 2) {
|
||||||
|
|
@ -517,6 +534,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasOpenContractsListSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_open_contracts",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["open_contract_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasAny(text, OPEN_ITEMS_HINTS) &&
|
hasAny(text, OPEN_ITEMS_HINTS) &&
|
||||||
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))
|
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCata
|
||||||
import { executeAddressMcpQuery } from "./addressMcpClient";
|
import { executeAddressMcpQuery } from "./addressMcpClient";
|
||||||
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
||||||
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
||||||
import { composeFactualReply, contractCandidatesFromRows, inferReplyType } from "./address_runtime/composeStage";
|
import { composeFactualReply, inferReplyType } from "./address_runtime/composeStage";
|
||||||
|
|
||||||
interface NormalizedAddressRow {
|
interface NormalizedAddressRow {
|
||||||
period: string | null;
|
period: string | null;
|
||||||
|
|
@ -464,12 +464,14 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDocumentOrBankIntent(intent: AddressIntent): boolean {
|
function isAnchorRecoveryIntent(intent: AddressIntent): boolean {
|
||||||
return (
|
return (
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
intent === "list_documents_by_contract" ||
|
intent === "list_documents_by_contract" ||
|
||||||
intent === "bank_operations_by_contract"
|
intent === "bank_operations_by_contract" ||
|
||||||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
intent === "list_open_contracts"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1058,40 +1060,9 @@ export class AddressQueryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) {
|
|
||||||
return buildLimitedExecutionResult({
|
|
||||||
mode,
|
|
||||||
shape,
|
|
||||||
intent,
|
|
||||||
filters: filters.extracted_filters,
|
|
||||||
missingRequiredFilters: [],
|
|
||||||
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
|
|
||||||
accountScopeMode: plan.account_scope_mode,
|
|
||||||
accountScopeFallbackApplied,
|
|
||||||
accountScopeAudit,
|
|
||||||
anchor,
|
|
||||||
matchFailureStage,
|
|
||||||
matchFailureReason,
|
|
||||||
mcpCallStatus: stageStatus,
|
|
||||||
rowsFetched: mcp.fetched_rows,
|
|
||||||
rawRowsReceived: mcp.raw_rows.length,
|
|
||||||
rowsAfterAccountScope: normalizedRows.length,
|
|
||||||
rowsAfterRecipeFilter: filterByAnchors.length,
|
|
||||||
rowsMaterialized: normalizedRows.length,
|
|
||||||
rowsMatched: filteredRows.length,
|
|
||||||
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
|
||||||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
|
||||||
category: "recipe_visibility_gap",
|
|
||||||
reasonText: "в live строках нет договорных якорей для уверенного списка незакрытых договоров",
|
|
||||||
nextStep: "сузьте запрос по контрагенту или добавьте номер договора",
|
|
||||||
limitations: ["no_contract_anchors_in_live_rows"],
|
|
||||||
reasons: baseReasons
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
filteredRows.length === 0 &&
|
filteredRows.length === 0 &&
|
||||||
isDocumentOrBankIntent(intent.intent) &&
|
isAnchorRecoveryIntent(intent.intent) &&
|
||||||
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")
|
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")
|
||||||
) {
|
) {
|
||||||
const currentLimit =
|
const currentLimit =
|
||||||
|
|
@ -1201,6 +1172,82 @@ export class AddressQueryService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
intent.intent === "open_items_by_counterparty_or_contract" &&
|
||||||
|
expandedFilteredRows.length === 0 &&
|
||||||
|
expandedRowsByAnchor.length === 0 &&
|
||||||
|
expandedNormalizedRows.length > 0
|
||||||
|
) {
|
||||||
|
const expandedFallbackRows = applyIntentSpecificFilter(intent.intent, expandedNormalizedRows);
|
||||||
|
if (expandedFallbackRows.length > 0) {
|
||||||
|
const expandedRowDiagnostics = deriveRowStageDiagnostics(
|
||||||
|
expandedMcp.raw_rows,
|
||||||
|
expandedNormalizedRows.length,
|
||||||
|
expandedNormalizedRows.length
|
||||||
|
);
|
||||||
|
const expandedFactual = composeFactualReply(intent.intent, expandedFallbackRows);
|
||||||
|
const expandedLimitations = [
|
||||||
|
...filters.warnings,
|
||||||
|
"query_limit_auto_expanded_for_anchor_recovery",
|
||||||
|
"open_items_anchor_not_matched_fallback_rows"
|
||||||
|
];
|
||||||
|
const expandedReasons = [
|
||||||
|
...baseReasons,
|
||||||
|
"query_limit_auto_expanded_for_anchor_recovery",
|
||||||
|
"open_items_anchor_not_matched_fallback_rows"
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text:
|
||||||
|
`Точный якорь не подтвердился в live-строках даже после расширения до ${expandedPlan.limit}; ` +
|
||||||
|
"показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||||
|
expandedFactual.text,
|
||||||
|
reply_type: inferReplyType(expandedFactual.responseType),
|
||||||
|
response_type: expandedFactual.responseType,
|
||||||
|
debug: {
|
||||||
|
detected_mode: mode.mode,
|
||||||
|
detected_mode_confidence: mode.confidence,
|
||||||
|
query_shape: shape.shape,
|
||||||
|
query_shape_confidence: shape.confidence,
|
||||||
|
detected_intent: intent.intent,
|
||||||
|
detected_intent_confidence: intent.confidence,
|
||||||
|
extracted_filters: filters.extracted_filters,
|
||||||
|
missing_required_filters: [],
|
||||||
|
selected_recipe: expandedSelection.selected_recipe.recipe_id,
|
||||||
|
mcp_call_status_legacy: "matched_non_empty",
|
||||||
|
account_scope_mode: expandedPlan.account_scope_mode,
|
||||||
|
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
|
||||||
|
anchor_type: expandedAnchor.anchor_type,
|
||||||
|
anchor_value_raw: expandedAnchor.anchor_value_raw,
|
||||||
|
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
|
||||||
|
resolver_confidence: expandedAnchor.resolver_confidence,
|
||||||
|
ambiguity_count: expandedAnchor.ambiguity_count,
|
||||||
|
match_failure_stage: "materialized_but_not_anchor_matched",
|
||||||
|
match_failure_reason:
|
||||||
|
expandedAnchorFilter.mismatchReason ?? "anchor_not_matched_after_query_limit_expansion",
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
rows_fetched: expandedMcp.fetched_rows,
|
||||||
|
raw_rows_received: expandedMcp.raw_rows.length,
|
||||||
|
rows_after_account_scope: expandedNormalizedRows.length,
|
||||||
|
rows_after_recipe_filter: expandedRowsByAnchor.length,
|
||||||
|
rows_materialized: expandedNormalizedRows.length,
|
||||||
|
rows_matched: expandedFallbackRows.length,
|
||||||
|
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
|
||||||
|
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
|
||||||
|
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
|
||||||
|
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
|
||||||
|
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
|
||||||
|
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
|
||||||
|
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
|
||||||
|
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||||
|
limited_reason_category: null,
|
||||||
|
response_type: expandedFactual.responseType,
|
||||||
|
limitations: expandedLimitations,
|
||||||
|
reasons: expandedReasons
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1313,6 +1360,67 @@ export class AddressQueryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filteredRows.length === 0 &&
|
||||||
|
intent.intent === "open_items_by_counterparty_or_contract" &&
|
||||||
|
normalizedRows.length > 0
|
||||||
|
) {
|
||||||
|
const openItemsFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
|
||||||
|
if (openItemsFallbackRows.length > 0) {
|
||||||
|
const fallbackFactual = composeFactualReply(intent.intent, openItemsFallbackRows);
|
||||||
|
const fallbackLimitations = [...filters.warnings, "open_items_anchor_not_matched_fallback_rows"];
|
||||||
|
const fallbackReasons = [...baseReasons, "open_items_anchor_not_matched_fallback_rows"];
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text:
|
||||||
|
"Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||||
|
fallbackFactual.text,
|
||||||
|
reply_type: inferReplyType(fallbackFactual.responseType),
|
||||||
|
response_type: fallbackFactual.responseType,
|
||||||
|
debug: {
|
||||||
|
detected_mode: mode.mode,
|
||||||
|
detected_mode_confidence: mode.confidence,
|
||||||
|
query_shape: shape.shape,
|
||||||
|
query_shape_confidence: shape.confidence,
|
||||||
|
detected_intent: intent.intent,
|
||||||
|
detected_intent_confidence: intent.confidence,
|
||||||
|
extracted_filters: filters.extracted_filters,
|
||||||
|
missing_required_filters: [],
|
||||||
|
selected_recipe: recipeSelection.selected_recipe.recipe_id,
|
||||||
|
mcp_call_status_legacy: "matched_non_empty",
|
||||||
|
account_scope_mode: plan.account_scope_mode,
|
||||||
|
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||||||
|
anchor_type: anchor.anchor_type,
|
||||||
|
anchor_value_raw: anchor.anchor_value_raw,
|
||||||
|
anchor_value_resolved: anchor.anchor_value_resolved,
|
||||||
|
resolver_confidence: anchor.resolver_confidence,
|
||||||
|
ambiguity_count: anchor.ambiguity_count,
|
||||||
|
match_failure_stage: matchFailureStage,
|
||||||
|
match_failure_reason: matchFailureReason,
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
rows_fetched: mcp.fetched_rows,
|
||||||
|
raw_rows_received: mcp.raw_rows.length,
|
||||||
|
rows_after_account_scope: normalizedRows.length,
|
||||||
|
rows_after_recipe_filter: filterByAnchors.length,
|
||||||
|
rows_materialized: normalizedRows.length,
|
||||||
|
rows_matched: openItemsFallbackRows.length,
|
||||||
|
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||||
|
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||||
|
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||||
|
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||||
|
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||||
|
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||||
|
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||||
|
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||||
|
limited_reason_category: null,
|
||||||
|
response_type: fallbackFactual.responseType,
|
||||||
|
limitations: fallbackLimitations,
|
||||||
|
reasons: fallbackReasons
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (filteredRows.length === 0) {
|
if (filteredRows.length === 0) {
|
||||||
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
|
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
|
||||||
const hadAnchorMatchedRows = filterByAnchors.length > 0;
|
const hadAnchorMatchedRows = filterByAnchors.length > 0;
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,9 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
intent === "list_documents_by_contract" ||
|
intent === "list_documents_by_contract" ||
|
||||||
intent === "bank_operations_by_contract"
|
intent === "bank_operations_by_contract" ||
|
||||||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
intent === "list_open_contracts"
|
||||||
) {
|
) {
|
||||||
return ADDRESS_MAX_LIMIT_EXTENDED;
|
return ADDRESS_MAX_LIMIT_EXTENDED;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,12 @@ export function composeFactualReply(
|
||||||
`Строк движения: ${rows.length}.`,
|
`Строк движения: ${rows.length}.`,
|
||||||
`Договорных кандидатов: ${contracts.length}.`
|
`Договорных кандидатов: ${contracts.length}.`
|
||||||
];
|
];
|
||||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
if (contracts.length > 0) {
|
||||||
|
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||||
|
} else {
|
||||||
|
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
|
||||||
|
lines.push(...formatTopRows(rows, 6));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
|
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves unclosed contracts list query without specific anchor", () => {
|
||||||
|
const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31");
|
||||||
|
expect(result.intent).toBe("list_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
|
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
|
||||||
const result = resolveAddressIntent(
|
const result = resolveAddressIntent(
|
||||||
"Показать банковские операции (счета 51, 60, 62) связанные с договором 15/24."
|
"Показать банковские операции (счета 51, 60, 62) связанные с договором 15/24."
|
||||||
|
|
@ -268,6 +273,18 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(result.warnings).toContain("period_derived_from_month_phrase");
|
expect(result.warnings).toContain("period_derived_from_month_phrase");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("cuts period-end tail from counterparty anchor and keeps as_of for open-items query", () => {
|
||||||
|
const result = extractAddressFilters(
|
||||||
|
"Покажи хвосты по контрагенту СВК на конец периода 2020-12-31",
|
||||||
|
"open_items_by_counterparty_or_contract"
|
||||||
|
);
|
||||||
|
expect(result.extracted_filters.counterparty).toBe("СВК");
|
||||||
|
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
|
||||||
|
expect(result.extracted_filters.period_from).toBeUndefined();
|
||||||
|
expect(result.extracted_filters.period_to).toBeUndefined();
|
||||||
|
expect(result.warnings).toContain("period_window_cleared_for_as_of_intent");
|
||||||
|
});
|
||||||
|
|
||||||
it("derives month period for balance snapshot from 'на май 2020'", () => {
|
it("derives month period for balance snapshot from 'на май 2020'", () => {
|
||||||
const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot");
|
const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot");
|
||||||
expect(result.extracted_filters.account).toBe("60");
|
expect(result.extracted_filters.account).toBe("60");
|
||||||
|
|
@ -703,6 +720,34 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.limit).toBe(200);
|
expect(plan.limit).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows extended limit for open-items by contract intent", () => {
|
||||||
|
const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", {
|
||||||
|
contract: "15/24",
|
||||||
|
limit: 1000
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
contract: "15/24",
|
||||||
|
limit: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan.limit).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows extended limit for open-contracts intent", () => {
|
||||||
|
const selected = selectAddressRecipe("list_open_contracts", {
|
||||||
|
as_of_date: "2020-12-31",
|
||||||
|
limit: 1000
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
as_of_date: "2020-12-31",
|
||||||
|
limit: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan.limit).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
it("injects account condition into movements query for account snapshot", () => {
|
it("injects account condition into movements query for account snapshot", () => {
|
||||||
const filters = extractAddressFilters(
|
const filters = extractAddressFilters(
|
||||||
"Какой остаток по счету 60 на дату 2020-07-31",
|
"Какой остаток по счету 60 на дату 2020-07-31",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class QuestionCase:
|
||||||
text: str
|
text: str
|
||||||
expected_intent: str | None
|
expected_intent: str | None
|
||||||
expected_mode: str | None
|
expected_mode: str | None
|
||||||
|
expected_reply_type: str | None
|
||||||
session: str | None
|
session: str | None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -48,6 +49,12 @@ def parse_args() -> argparse.Namespace:
|
||||||
parser.add_argument("--max-output-tokens", type=int, default=900)
|
parser.add_argument("--max-output-tokens", type=int, default=900)
|
||||||
parser.add_argument("--timeout-sec", type=int, default=120)
|
parser.add_argument("--timeout-sec", type=int, default=120)
|
||||||
parser.add_argument("--run-id", default="")
|
parser.add_argument("--run-id", default="")
|
||||||
|
parser.add_argument(
|
||||||
|
"--strict-policy",
|
||||||
|
default="route",
|
||||||
|
choices=["semantic", "route", "factual"],
|
||||||
|
help="Pass policy: semantic=intent/mode only, route=semantic+non-blocked route, factual=semantic+factual reply",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output-root",
|
"--output-root",
|
||||||
default=str(PROJECT_ROOT / "docs" / "ADDRESS" / "runs"),
|
default=str(PROJECT_ROOT / "docs" / "ADDRESS" / "runs"),
|
||||||
|
|
@ -61,7 +68,7 @@ def now_stamp() -> str:
|
||||||
|
|
||||||
|
|
||||||
def load_cases(path: Path) -> list[QuestionCase]:
|
def load_cases(path: Path) -> list[QuestionCase]:
|
||||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
raw = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||||
if not isinstance(raw, list):
|
if not isinstance(raw, list):
|
||||||
raise ValueError("questions-file must contain JSON array")
|
raise ValueError("questions-file must contain JSON array")
|
||||||
|
|
||||||
|
|
@ -77,6 +84,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
|
||||||
text=text,
|
text=text,
|
||||||
expected_intent=None,
|
expected_intent=None,
|
||||||
expected_mode="address_query",
|
expected_mode="address_query",
|
||||||
|
expected_reply_type=None,
|
||||||
session=None,
|
session=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -91,6 +99,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
|
||||||
case_id = str(item.get("id", f"Q{idx:03d}")).strip() or f"Q{idx:03d}"
|
case_id = str(item.get("id", f"Q{idx:03d}")).strip() or f"Q{idx:03d}"
|
||||||
expected_intent = item.get("expected_intent")
|
expected_intent = item.get("expected_intent")
|
||||||
expected_mode = item.get("expected_mode", "address_query")
|
expected_mode = item.get("expected_mode", "address_query")
|
||||||
|
expected_reply_type = item.get("expected_reply_type")
|
||||||
session = item.get("session")
|
session = item.get("session")
|
||||||
cases.append(
|
cases.append(
|
||||||
QuestionCase(
|
QuestionCase(
|
||||||
|
|
@ -98,6 +107,7 @@ def load_cases(path: Path) -> list[QuestionCase]:
|
||||||
text=text,
|
text=text,
|
||||||
expected_intent=str(expected_intent).strip() if expected_intent else None,
|
expected_intent=str(expected_intent).strip() if expected_intent else None,
|
||||||
expected_mode=str(expected_mode).strip() if expected_mode else None,
|
expected_mode=str(expected_mode).strip() if expected_mode else None,
|
||||||
|
expected_reply_type=str(expected_reply_type).strip() if expected_reply_type else None,
|
||||||
session=str(session).strip() if session else None,
|
session=str(session).strip() if session else None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -124,6 +134,42 @@ def post_json(url: str, payload: dict[str, Any], timeout_sec: int) -> tuple[int,
|
||||||
return status, {"ok": False, "error": {"code": "HTTP_ERROR", "message": raw}}
|
return status, {"ok": False, "error": {"code": "HTTP_ERROR", "message": raw}}
|
||||||
|
|
||||||
|
|
||||||
|
def first_line(text: str | None) -> str:
|
||||||
|
value = str(text or "").strip()
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
return value.splitlines()[0].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def classify_route_health(
|
||||||
|
*,
|
||||||
|
status_code: int,
|
||||||
|
ok_flag: bool,
|
||||||
|
reply_type: str | None,
|
||||||
|
limited_reason_category: str | None,
|
||||||
|
mcp_call_status: str | None,
|
||||||
|
) -> str:
|
||||||
|
if status_code != 200 or not ok_flag:
|
||||||
|
return "http_or_backend_error"
|
||||||
|
if reply_type == "clarification_required":
|
||||||
|
return "blocked_clarification"
|
||||||
|
if reply_type == "backend_error":
|
||||||
|
return "blocked_backend_error"
|
||||||
|
if reply_type != "partial_coverage":
|
||||||
|
return "ok_or_factual"
|
||||||
|
if limited_reason_category == "missing_anchor":
|
||||||
|
return "blocked_missing_anchor"
|
||||||
|
if limited_reason_category == "unsupported":
|
||||||
|
return "blocked_unsupported"
|
||||||
|
if limited_reason_category == "recipe_visibility_gap":
|
||||||
|
return "blocked_recipe_visibility_gap"
|
||||||
|
if limited_reason_category == "execution_error":
|
||||||
|
return "blocked_execution_error"
|
||||||
|
if mcp_call_status in {"skipped", "materialized_but_not_anchor_matched", "materialized_but_filtered_out_by_recipe"}:
|
||||||
|
return "likely_blocked_route"
|
||||||
|
return "partial_non_blocking"
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
questions_path = Path(args.questions_file).resolve()
|
questions_path = Path(args.questions_file).resolve()
|
||||||
|
|
@ -179,7 +225,23 @@ def main() -> None:
|
||||||
|
|
||||||
intent_match = case.expected_intent is None or actual_intent == case.expected_intent
|
intent_match = case.expected_intent is None or actual_intent == case.expected_intent
|
||||||
mode_match = case.expected_mode is None or actual_mode == case.expected_mode
|
mode_match = case.expected_mode is None or actual_mode == case.expected_mode
|
||||||
|
reply_match = case.expected_reply_type is None or reply_type == case.expected_reply_type
|
||||||
semantic_pass = bool(intent_match and mode_match and status_code == 200 and ok_flag)
|
semantic_pass = bool(intent_match and mode_match and status_code == 200 and ok_flag)
|
||||||
|
route_health = classify_route_health(
|
||||||
|
status_code=status_code,
|
||||||
|
ok_flag=ok_flag,
|
||||||
|
reply_type=str(reply_type) if reply_type is not None else None,
|
||||||
|
limited_reason_category=debug.get("limited_reason_category"),
|
||||||
|
mcp_call_status=debug.get("mcp_call_status"),
|
||||||
|
)
|
||||||
|
route_pass = bool(semantic_pass and not route_health.startswith("blocked") and route_health != "likely_blocked_route")
|
||||||
|
if args.strict_policy == "semantic":
|
||||||
|
policy_pass = semantic_pass
|
||||||
|
elif args.strict_policy == "factual":
|
||||||
|
policy_pass = semantic_pass if case.expected_reply_type is not None else bool(semantic_pass and reply_type == "factual")
|
||||||
|
else:
|
||||||
|
policy_pass = route_pass
|
||||||
|
strict_pass = bool(policy_pass and reply_match)
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
"index": index,
|
"index": index,
|
||||||
|
|
@ -193,18 +255,32 @@ def main() -> None:
|
||||||
"reply_type": reply_type,
|
"reply_type": reply_type,
|
||||||
"trace_id": trace_id,
|
"trace_id": trace_id,
|
||||||
"assistant_reply": body.get("assistant_reply") if isinstance(body, dict) else None,
|
"assistant_reply": body.get("assistant_reply") if isinstance(body, dict) else None,
|
||||||
|
"assistant_reply_first_line": first_line(body.get("assistant_reply") if isinstance(body, dict) else None),
|
||||||
"expected_intent": case.expected_intent,
|
"expected_intent": case.expected_intent,
|
||||||
"actual_intent": actual_intent,
|
"actual_intent": actual_intent,
|
||||||
"intent_match": intent_match,
|
"intent_match": intent_match,
|
||||||
"expected_mode": case.expected_mode,
|
"expected_mode": case.expected_mode,
|
||||||
"actual_mode": actual_mode,
|
"actual_mode": actual_mode,
|
||||||
"mode_match": mode_match,
|
"mode_match": mode_match,
|
||||||
|
"expected_reply_type": case.expected_reply_type,
|
||||||
|
"reply_match": reply_match,
|
||||||
"semantic_pass": semantic_pass,
|
"semantic_pass": semantic_pass,
|
||||||
|
"route_pass": route_pass,
|
||||||
|
"route_health": route_health,
|
||||||
|
"strict_policy": args.strict_policy,
|
||||||
|
"strict_pass": strict_pass,
|
||||||
|
"selected_recipe": debug.get("selected_recipe"),
|
||||||
|
"missing_required_filters": debug.get("missing_required_filters"),
|
||||||
|
"match_failure_stage": debug.get("match_failure_stage"),
|
||||||
|
"match_failure_reason": debug.get("match_failure_reason"),
|
||||||
|
"rows_fetched": debug.get("rows_fetched"),
|
||||||
|
"rows_matched": debug.get("rows_matched"),
|
||||||
"mcp_call_status": debug.get("mcp_call_status"),
|
"mcp_call_status": debug.get("mcp_call_status"),
|
||||||
"limited_reason_category": debug.get("limited_reason_category"),
|
"limited_reason_category": debug.get("limited_reason_category"),
|
||||||
"llm_decomposition_applied": debug.get("llm_decomposition_applied"),
|
"llm_decomposition_applied": debug.get("llm_decomposition_applied"),
|
||||||
"llm_decomposition_reason": debug.get("llm_decomposition_reason"),
|
"llm_decomposition_reason": debug.get("llm_decomposition_reason"),
|
||||||
"fallback_rule_hit": debug.get("fallback_rule_hit"),
|
"fallback_rule_hit": debug.get("fallback_rule_hit"),
|
||||||
|
"debug_payload": debug,
|
||||||
"error_code": body.get("error", {}).get("code") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None,
|
"error_code": body.get("error", {}).get("code") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None,
|
||||||
"error_message": body.get("error", {}).get("message") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None,
|
"error_message": body.get("error", {}).get("message") if isinstance(body, dict) and isinstance(body.get("error"), dict) else None,
|
||||||
}
|
}
|
||||||
|
|
@ -212,7 +288,7 @@ def main() -> None:
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[{index:03d}/{len(cases):03d}] {case.id} | status={status_code} reply={reply_type} "
|
f"[{index:03d}/{len(cases):03d}] {case.id} | status={status_code} reply={reply_type} "
|
||||||
f"intent={actual_intent} mode={actual_mode} pass={semantic_pass}"
|
f"intent={actual_intent} mode={actual_mode} semantic={semantic_pass} route={route_pass} strict={strict_pass} health={route_health}"
|
||||||
)
|
)
|
||||||
|
|
||||||
reply_counter = Counter(str(r.get("reply_type")) for r in rows)
|
reply_counter = Counter(str(r.get("reply_type")) for r in rows)
|
||||||
|
|
@ -220,8 +296,11 @@ def main() -> None:
|
||||||
mode_counter = Counter(str(r.get("actual_mode")) for r in rows)
|
mode_counter = Counter(str(r.get("actual_mode")) for r in rows)
|
||||||
mcp_counter = Counter(str(r.get("mcp_call_status")) for r in rows)
|
mcp_counter = Counter(str(r.get("mcp_call_status")) for r in rows)
|
||||||
limited_counter = Counter(str(r.get("limited_reason_category")) for r in rows if r.get("limited_reason_category") is not None)
|
limited_counter = Counter(str(r.get("limited_reason_category")) for r in rows if r.get("limited_reason_category") is not None)
|
||||||
|
route_health_counter = Counter(str(r.get("route_health")) for r in rows)
|
||||||
|
|
||||||
semantic_pass_count = sum(1 for r in rows if r.get("semantic_pass"))
|
semantic_pass_count = sum(1 for r in rows if r.get("semantic_pass"))
|
||||||
|
route_pass_count = sum(1 for r in rows if r.get("route_pass"))
|
||||||
|
strict_pass_count = sum(1 for r in rows if r.get("strict_pass"))
|
||||||
factual_count = sum(1 for r in rows if r.get("reply_type") == "factual")
|
factual_count = sum(1 for r in rows if r.get("reply_type") == "factual")
|
||||||
ok_200_count = sum(1 for r in rows if r.get("status_code") == 200 and r.get("ok"))
|
ok_200_count = sum(1 for r in rows if r.get("status_code") == 200 and r.get("ok"))
|
||||||
llm_decomposition_applied_count = sum(1 for r in rows if r.get("llm_decomposition_applied") is True)
|
llm_decomposition_applied_count = sum(1 for r in rows if r.get("llm_decomposition_applied") is True)
|
||||||
|
|
@ -235,11 +314,16 @@ def main() -> None:
|
||||||
"llm_provider": args.llm_provider,
|
"llm_provider": args.llm_provider,
|
||||||
"llm_model": args.llm_model,
|
"llm_model": args.llm_model,
|
||||||
"llm_base_url": args.llm_base_url,
|
"llm_base_url": args.llm_base_url,
|
||||||
|
"strict_policy": args.strict_policy,
|
||||||
"totals": {
|
"totals": {
|
||||||
"questions_total": len(rows),
|
"questions_total": len(rows),
|
||||||
"ok_200_count": ok_200_count,
|
"ok_200_count": ok_200_count,
|
||||||
"semantic_pass_count": semantic_pass_count,
|
"semantic_pass_count": semantic_pass_count,
|
||||||
"semantic_pass_rate": round(semantic_pass_count / len(rows), 4) if rows else 0.0,
|
"semantic_pass_rate": round(semantic_pass_count / len(rows), 4) if rows else 0.0,
|
||||||
|
"route_pass_count": route_pass_count,
|
||||||
|
"route_pass_rate": round(route_pass_count / len(rows), 4) if rows else 0.0,
|
||||||
|
"strict_pass_count": strict_pass_count,
|
||||||
|
"strict_pass_rate": round(strict_pass_count / len(rows), 4) if rows else 0.0,
|
||||||
"factual_count": factual_count,
|
"factual_count": factual_count,
|
||||||
"partial_coverage_count": sum(1 for r in rows if r.get("reply_type") == "partial_coverage"),
|
"partial_coverage_count": sum(1 for r in rows if r.get("reply_type") == "partial_coverage"),
|
||||||
"clarification_required_count": sum(1 for r in rows if r.get("reply_type") == "clarification_required"),
|
"clarification_required_count": sum(1 for r in rows if r.get("reply_type") == "clarification_required"),
|
||||||
|
|
@ -253,13 +337,14 @@ def main() -> None:
|
||||||
"actual_mode": dict(mode_counter),
|
"actual_mode": dict(mode_counter),
|
||||||
"mcp_call_status": dict(mcp_counter),
|
"mcp_call_status": dict(mcp_counter),
|
||||||
"limited_reason_category": dict(limited_counter),
|
"limited_reason_category": dict(limited_counter),
|
||||||
|
"route_health": dict(route_health_counter),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
failures = [
|
failures = [
|
||||||
r
|
r
|
||||||
for r in rows
|
for r in rows
|
||||||
if not r.get("semantic_pass")
|
if not r.get("strict_pass")
|
||||||
or r.get("status_code") != 200
|
or r.get("status_code") != 200
|
||||||
or r.get("reply_type") in {"clarification_required", "backend_error"}
|
or r.get("reply_type") in {"clarification_required", "backend_error"}
|
||||||
]
|
]
|
||||||
|
|
@ -279,12 +364,17 @@ def main() -> None:
|
||||||
f"Questions file: {questions_path}",
|
f"Questions file: {questions_path}",
|
||||||
f"Backend URL: {args.backend_url}",
|
f"Backend URL: {args.backend_url}",
|
||||||
f"LLM: {args.llm_provider} / {args.llm_model} @ {args.llm_base_url}",
|
f"LLM: {args.llm_provider} / {args.llm_model} @ {args.llm_base_url}",
|
||||||
|
f"Strict policy: {args.strict_policy}",
|
||||||
"",
|
"",
|
||||||
"## Totals",
|
"## Totals",
|
||||||
f"- questions_total: {summary['totals']['questions_total']}",
|
f"- questions_total: {summary['totals']['questions_total']}",
|
||||||
f"- ok_200_count: {summary['totals']['ok_200_count']}",
|
f"- ok_200_count: {summary['totals']['ok_200_count']}",
|
||||||
f"- semantic_pass_count: {summary['totals']['semantic_pass_count']}",
|
f"- semantic_pass_count: {summary['totals']['semantic_pass_count']}",
|
||||||
f"- semantic_pass_rate: {summary['totals']['semantic_pass_rate']}",
|
f"- semantic_pass_rate: {summary['totals']['semantic_pass_rate']}",
|
||||||
|
f"- route_pass_count: {summary['totals']['route_pass_count']}",
|
||||||
|
f"- route_pass_rate: {summary['totals']['route_pass_rate']}",
|
||||||
|
f"- strict_pass_count: {summary['totals']['strict_pass_count']}",
|
||||||
|
f"- strict_pass_rate: {summary['totals']['strict_pass_rate']}",
|
||||||
f"- factual_count: {summary['totals']['factual_count']}",
|
f"- factual_count: {summary['totals']['factual_count']}",
|
||||||
f"- partial_coverage_count: {summary['totals']['partial_coverage_count']}",
|
f"- partial_coverage_count: {summary['totals']['partial_coverage_count']}",
|
||||||
f"- clarification_required_count: {summary['totals']['clarification_required_count']}",
|
f"- clarification_required_count: {summary['totals']['clarification_required_count']}",
|
||||||
|
|
@ -299,10 +389,25 @@ def main() -> None:
|
||||||
]
|
]
|
||||||
(run_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
(run_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
audit_lines = [
|
||||||
|
f"# Response Audit: {run_id}",
|
||||||
|
"",
|
||||||
|
"| id | strict | route_health | reply_type | intent | limited_reason | question | assistant_first_line |",
|
||||||
|
"|---|---|---|---|---|---|---|---|",
|
||||||
|
]
|
||||||
|
for row in rows:
|
||||||
|
audit_lines.append(
|
||||||
|
f"| {row.get('id')} | {row.get('strict_pass')} | {row.get('route_health')} | {row.get('reply_type')} | "
|
||||||
|
f"{row.get('actual_intent')} | {row.get('limited_reason_category')} | "
|
||||||
|
f"{str(row.get('question', '')).replace('|', '/')} | {str(row.get('assistant_reply_first_line', '')).replace('|', '/')} |"
|
||||||
|
)
|
||||||
|
(run_dir / "response_audit.md").write_text("\n".join(audit_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
print(f"\nRun directory: {run_dir}")
|
print(f"\nRun directory: {run_dir}")
|
||||||
print(f"Semantic pass: {semantic_pass_count}/{len(rows)}")
|
print(f"Semantic pass: {semantic_pass_count}/{len(rows)}")
|
||||||
|
print(f"Route pass: {route_pass_count}/{len(rows)}")
|
||||||
|
print(f"Strict pass ({args.strict_policy}): {strict_pass_count}/{len(rows)}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue