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