diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..5319f10 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,26 @@ +# Agent Guardrails (NDC_1C) + +## Scope +This repository has two assistant lanes: +- `address_query` for fast factual MCP lookups. +- deep/canonical normalizer lane for heavy analysis. + +Do not route plain address lookups to deep lane when an address fallback is possible. + +## Encoding Policy (Mandatory) +To prevent recurring mojibake (`Р...`, `С...`) issues: + +1. Use UTF-8 for all source and exported text artifacts. +2. When reading or writing text in PowerShell, always set encoding explicitly (`-Encoding utf8`). +3. For MCP/live payloads, normalize potentially broken UTF-8/CP1251 strings before matching/filtering. +4. Never store business-specific names in static normalization dictionaries. + - Use runtime anchor resolution from live rows. +5. Before finishing a change touching assistant text/debug/export: + - run tests that cover conversation export and address runtime, + - verify no new mojibake markers in changed files. + +## Address Runtime Rules +1. MSP/MCP-first. Snapshots are fallback only. +2. `false_factual_rate` must remain `0`. +3. For short follow-ups with loose anchor form (`по `), keep in address lane. +4. If factual result is unavailable, return operational limited reason, never hallucinate. diff --git a/docs/ADDRESS/1.txt b/docs/ADDRESS/1.txt new file mode 100644 index 0000000..d0fe72f --- /dev/null +++ b/docs/ADDRESS/1.txt @@ -0,0 +1,585 @@ +# Assistant conversation export +session_id: asst--iAIydc_EC +export_mode: technical +exported_at: 2026-04-01T12:33:31.795Z + +## 1. user +message_id: msg-2d0MoaXT6W +created_at: 2026-04-01T12:32:28.939Z +reply_type: n/a + +Какой остаток по счету 60 на дату 2020-07-31? + +## 2. assistant +message_id: msg-MVrv4gqMpT +created_at: 2026-04-01T12:32:29.818Z +reply_type: partial_coverage +trace_id: address-Q_QdUVA19F + +В live-данных по текущему фильтру записи не найдены. +Причина: по выбранным фильтрам в live-выборке нет строк. +Что нужно уточнить: уточните период, контрагента, договор или снимите часть фильтров. + +### technical_debug_payload_json +```json +{ + "trace_id": "address-Q_QdUVA19F", + "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", + "aggregate_signal_detected", + "account_balance_signal_detected" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "high", + "query_shape": "AGGREGATE_LOOKUP", + "query_shape_confidence": "high", + "detected_intent": "account_balance_snapshot", + "detected_intent_confidence": "high", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "60", + "as_of_date": "2020-07-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_movements_account_snapshot_v1", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "anchor_type": "account", + "anchor_value_raw": "60", + "anchor_value_resolved": "60", + "resolver_confidence": "high", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_after_recipe_filter": 0, + "rows_materialized": 0, + "rows_matched": 0, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "dropped_by_account_scope_filter", + "account_token_raw": "60", + "account_token_normalized": "60", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": "empty_match", + "response_type": "LIMITED_WITH_REASON", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": null +} +``` + +## 3. user +message_id: msg-tPODcsFT2j +created_at: 2026-04-01T12:32:43.787Z +reply_type: n/a + +Какой остаток по счету 76 на дату 2020-12-31? + +## 4. assistant +message_id: msg-sbyXymS8Tu +created_at: 2026-04-01T12:32:44.036Z +reply_type: partial_coverage +trace_id: address-tWfGlq9Bif + +В live-данных по текущему фильтру записи не найдены. +Причина: по выбранным фильтрам в live-выборке нет строк. +Что нужно уточнить: уточните период, контрагента, договор или снимите часть фильтров. + +### technical_debug_payload_json +```json +{ + "trace_id": "address-tWfGlq9Bif", + "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", + "aggregate_signal_detected", + "account_balance_signal_detected" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "high", + "query_shape": "AGGREGATE_LOOKUP", + "query_shape_confidence": "high", + "detected_intent": "account_balance_snapshot", + "detected_intent_confidence": "high", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "76", + "as_of_date": "2020-12-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_movements_account_snapshot_v1", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "anchor_type": "account", + "anchor_value_raw": "76", + "anchor_value_resolved": "76", + "resolver_confidence": "high", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_after_recipe_filter": 0, + "rows_materialized": 0, + "rows_matched": 0, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "dropped_by_account_scope_filter", + "account_token_raw": "76", + "account_token_normalized": "76", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": "empty_match", + "response_type": "LIMITED_WITH_REASON", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": null +} +``` + +## 5. user +message_id: msg-_0vMruaosw +created_at: 2026-04-01T12:32:52.525Z +reply_type: n/a + +Какие документы формируют остаток по счету 62 на дату 2020-07-31? + +## 6. assistant +message_id: msg-vDgQf1_P6u +created_at: 2026-04-01T12:32:53.206Z +reply_type: partial_coverage +trace_id: address-D2al1c11rV + +В live-данных по текущему фильтру записи не найдены. +Причина: по выбранным фильтрам в live-выборке нет строк. +Что нужно уточнить: уточните период, контрагента, договор или снимите часть фильтров. + +### technical_debug_payload_json +```json +{ + "trace_id": "address-D2al1c11rV", + "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", + "drilldown_signal_detected", + "documents_forming_balance_signal_detected" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "high", + "query_shape": "DRILLDOWN_REQUEST", + "query_shape_confidence": "high", + "detected_intent": "documents_forming_balance", + "detected_intent_confidence": "high", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "62", + "as_of_date": "2020-07-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_documents_forming_balance_v1", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "anchor_type": "account", + "anchor_value_raw": "62", + "anchor_value_resolved": "62", + "resolver_confidence": "high", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_after_recipe_filter": 0, + "rows_materialized": 0, + "rows_matched": 0, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "dropped_by_account_scope_filter", + "account_token_raw": "62", + "account_token_normalized": "62", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": "empty_match", + "response_type": "LIMITED_WITH_REASON", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": null +} +``` + +## 7. user +message_id: msg-n4DAnggWmb +created_at: 2026-04-01T12:33:02.902Z +reply_type: n/a + +Что формирует остаток по счету 76 на дату 2020-12-31? + +## 8. assistant +message_id: msg-46fXowQ8cs +created_at: 2026-04-01T12:33:03.352Z +reply_type: partial_coverage +trace_id: address-o7q7qa-FOF + +В live-данных по текущему фильтру записи не найдены. +Причина: по выбранным фильтрам в live-выборке нет строк. +Что нужно уточнить: уточните период, контрагента, договор или снимите часть фильтров. + +### technical_debug_payload_json +```json +{ + "trace_id": "address-o7q7qa-FOF", + "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", + "aggregate_signal_detected", + "account_balance_signal_detected" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "high", + "query_shape": "AGGREGATE_LOOKUP", + "query_shape_confidence": "high", + "detected_intent": "account_balance_snapshot", + "detected_intent_confidence": "high", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "account": "76", + "as_of_date": "2020-12-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_movements_account_snapshot_v1", + "mcp_call_status_legacy": "raw_rows_received_but_not_materialized", + "account_scope_mode": "strict", + "account_scope_fallback_applied": false, + "anchor_type": "account", + "anchor_value_raw": "76", + "anchor_value_resolved": "76", + "resolver_confidence": "high", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "raw_rows_received_but_not_materialized", + "rows_fetched": 20, + "raw_rows_received": 20, + "rows_after_account_scope": 0, + "rows_after_recipe_filter": 0, + "rows_materialized": 0, + "rows_matched": 0, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "dropped_by_account_scope_filter", + "account_token_raw": "76", + "account_token_normalized": "76", + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "no_rows_after_scope_filter", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": "empty_match", + "response_type": "LIMITED_WITH_REASON", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": null +} +``` + +## 9. user +message_id: msg-ATLBoTgwlD +created_at: 2026-04-01T12:33:20.966Z +reply_type: n/a + +Покажи банковские операции по контрагенту СВК с 2020-07-01 по 2020-07-31 + +## 10. assistant +message_id: msg-p1biGQOTcr +created_at: 2026-04-01T12:33:21.620Z +reply_type: factual +trace_id: address-JwD-3OLHgc + +Собран список банковских операций по контрагенту (live address lane). +Строк отобрано: 2. +1. 2020-07-30T12:00:00Z | Поступление на расчетный счет 00000000029 от 30.07.2020 12:00:00 | 0 / 0 | 20000 | аналитика: Группа СВК +2. 2020-07-27T12:00:00Z | Поступление на расчетный счет 00000000028 от 27.07.2020 12:00:00 | 0 / 0 | 40860 | аналитика: Группа СВК + +### technical_debug_payload_json +```json +{ + "trace_id": "address-JwD-3OLHgc", + "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", + "bank_ops_by_counterparty_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": "bank_operations_by_counterparty", + "detected_intent_confidence": "medium", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "counterparty": "СВК", + "period_from": "2020-07-01", + "period_to": "2020-07-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_bank_operations_by_counterparty_v1", + "mcp_call_status_legacy": "matched_non_empty", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "anchor_type": "counterparty", + "anchor_value_raw": "СВК", + "anchor_value_resolved": "Группа СВК", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "matched_non_empty", + "rows_fetched": 19, + "raw_rows_received": 19, + "rows_after_account_scope": 3, + "rows_after_recipe_filter": 2, + "rows_materialized": 3, + "rows_matched": 2, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Контрагент", + "Period", + "Registrator", + "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", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": null +} +``` diff --git a/docs/ADDRESS/address_query/README.md b/docs/ADDRESS/address_query/README.md index 5cd17d2..aa2101a 100644 --- a/docs/ADDRESS/address_query/README.md +++ b/docs/ADDRESS/address_query/README.md @@ -9,6 +9,7 @@ - `query_recipes_v1.md` - каталог фильтров и recipe-контракты. - `runtime_integration_plan.md` - план встраивания `question_mode=address_query`. - `address_runtime_contracts.md` - контракты runtime/debug/result для address lane. +- `address_architecture_contract_v1.md` - архитектурные границы `Decompose -> Resolve -> Execute -> Compose` и политика data-agnostic runtime. - `runtime_readiness_matrix_v1.md` - матрица structural vs runtime readiness. - `known_positive_live_suite_v1.md` - базовый template positive-evidence suite. - `data_aware_positive_acceptance_suite_v1.md` - M2.3 canonical guide для curated live acceptance. @@ -23,4 +24,5 @@ - `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3A_Stage_Diagnostic_Materialization/` - `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3B_AccountScope_Mode_Tuning/` - `docs/ADDRESS/runs/2026-03-29_Address_Query_Runtime_V1_M2_3C_Resolver_Filter_Tuning_And_AccountScope_Audit/` +- `docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/` diff --git a/docs/ADDRESS/address_query/address_architecture_contract_v1.md b/docs/ADDRESS/address_query/address_architecture_contract_v1.md new file mode 100644 index 0000000..8df94c9 --- /dev/null +++ b/docs/ADDRESS/address_query/address_architecture_contract_v1.md @@ -0,0 +1,154 @@ +# Address Architecture Contract V1 + +Дата: 2026-04-01 + +## 1) Зачем документ + +Этот контракт фиксирует архитектурные границы `address_query`-контура, чтобы система оставалась переносимой между разными 1С-базами и не обрастала company-specific логикой. + +Контракт обязателен для всех следующих инкрементов (`M2.4+`), рефакторов и новых intent/recipe. + +## 2) Непересекаемые принципы + +- `MCP/live-first`: основной источник фактов - live MCP. +- `MSP-only` в runtime: production path работает через MCP/MSP; snapshot - только controlled fallback. +- `snapshot` допускается только как явный fallback с reason code, а не как скрытая подмена. +- `runtime = data-agnostic`: никаких хардкодов под конкретную компанию. +- `acceptance = data-aware`: positive-кейсы можно подбирать на текущей базе только для проверки. +- `false_factual_rate = 0`: factual-ответ только при подтвержденных `rows_matched > 0`. +- `whitelist execution only`: никаких свободных NL->query генераторов. + +## 3) Канонический pipeline + +## Stage A: Decompose + +Назначение: интерпретация текста вопроса, без обращения к данным компании. + +Выход stage: + +- `question_mode` +- `query_shape` +- `intent_candidates` +- `anchors_raw` +- `time_scope_raw` +- `filters_raw` +- `decomposition_plan` (опционально, для compound) + +Запрещено на этапе Decompose: + +- резолвить реальные объекты базы (контрагентов, договоры, документы); +- тянуть company-specific словари; +- генерировать запросы к 1С. + +## Stage B: Resolve + +Назначение: привязка raw-якорей к живым объектам через MCP. + +Выход stage: + +- `anchor_type` +- `anchor_value_raw` +- `anchor_value_resolved` +- `resolver_confidence` +- `ambiguity_count` + +Правило: + +- если якорь не подтвержден, runtime не выдумывает факт и идет в `LIMITED_WITH_REASON`. + +## Stage C: Execute + +Назначение: выполнение только через recipe whitelist. + +Правила: + +- `intent -> recipe_id` только из каталога; +- fixed `limit/sort/window` политика; +- `MCP` read-only; +- `MSP/MCP-only` execution path в production; +- snapshot fallback только явный. + +Диагностика по стадиям: + +- `no_raw_rows` +- `raw_rows_received_but_not_materialized` +- `materialized_but_not_anchor_matched` +- `materialized_but_filtered_out_by_recipe` +- `matched_non_empty` +- `error` + +## Stage D: Compose + +Назначение: финальный ответ строго по execution-результату. + +Правила: + +- factual только из `rows_matched`; +- если пусто - `LIMITED_WITH_REASON` с конкретной причиной; +- без reasoning-галлюцинаций и без “догадки по смыслу”. + +## 4) Политика словарей + +Разрешено (статически в коде): + +- доменная типовая лексика (`доки`, `остаток`, `договор`, `дебиторка` и т.д.); +- правила парсинга дат/периодов/счетов; +- stop-слова и служебные alias-правила. + +Запрещено: + +- хранить в коде списки компаний, ИНН, договоров, документов конкретной базы; +- пополнять глобальные normalization-библиотеки живыми entity-именами; +- строить скрытые “памяти компании” вне runtime-сессии. + +Допустимо: + +- использовать runtime-сессионный контекст диалога (`followup context`) без записи в глобальные словари. + +## 5) Критерии переносимости между компаниями + +Система считается переносимой, если: + +- новая база подключается без code change в resolver/intent logic; +- меняются только live-данные MCP, а не кодовые словари; +- question-bank остается валиден (с ожидаемыми различиями factual/limited по данным). + +## 6) Антипаттерны (нельзя делать) + +- Добавлять company alias map в `src/services/*` с реальными названиями контрагентов. +- Перекладывать проблему резолвинга в hardcoded `if company == ...`. +- Подмешивать deep-analysis ответ в address factual-блок без явного route handoff. +- Поднимать “временные” exceptions, которые ломают stage-контракт. + +## 7) Техническая дисциплина кода + +Новая логика должна ложиться в явные stage-модули: + +- decompose +- resolve +- execute +- compose +- diagnostics + +Если функция не относится к stage - это smell и повод к вынесению. + +## 8) Как подключать LLM decompose + +LLM на первом этапе нужен не для “знания компании”, а для структурной интерпретации вопроса. + +LLM должен возвращать схему-stage-output: + +- intent candidates +- shape +- anchor spans +- time scope +- filter hints +- confidence + +Дальше все company-specific подтверждается только Resolver/Execute через MCP. + +Итог: + +- LLM decompose уменьшает NLP-хрупкость; +- не требует жирных живых словарей компаний; +- не нарушает data-agnostic принцип runtime. diff --git a/docs/ADDRESS/address_query/address_runtime_contracts.md b/docs/ADDRESS/address_query/address_runtime_contracts.md index e62107b..bcda1f9 100644 --- a/docs/ADDRESS/address_query/address_runtime_contracts.md +++ b/docs/ADDRESS/address_query/address_runtime_contracts.md @@ -2,11 +2,18 @@ Дата: 2026-03-29 +Reference: + +- `address_architecture_contract_v1.md` (architecture guardrails and stage boundaries). + ## Runtime Policy - Runtime lane is `data-agnostic`: no hardcoded counterparties/contracts/accounts from one concrete base. - Acceptance lane is `data-aware`: positive cases are curated after exploratory live pass. - Address lane remains MCP/live-first, whitelist-only, read-only. +- Runtime execution is MSP-only in production; snapshot usage is explicit fallback only. +- Canonical pipeline boundary: `Decompose -> Resolve -> Execute -> Compose` (no cross-stage leakage). +- LLM decompose stage interprets question structure only; company entities are resolved only in live resolver stage. ## Input Contract diff --git a/docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip b/docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip deleted file mode 100644 index fcd8933..0000000 Binary files a/docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip and /dev/null differ diff --git a/docs/ADDRESS/address_query/question_bank_v1.md b/docs/ADDRESS/address_query/question_bank_v1.md index 2eecf63..1798dc6 100644 --- a/docs/ADDRESS/address_query/question_bank_v1.md +++ b/docs/ADDRESS/address_query/question_bank_v1.md @@ -10,6 +10,9 @@ - какие хвосты висят по договору - у кого самый большой долг перед нами - кому больше всего должны мы +- покажи дебиторку по контрагентам на дату +- покажи кредиторку по поставщикам на дату +- что висит по взаиморасчетам на текущую дату ## B. Счета и остатки @@ -18,6 +21,9 @@ - что висит на 60 счете - какие документы формируют остаток по 62 - оборот по 60 за период +- раскрой остаток по счету 62 до документов +- покажи сальдо по счету 60.01 на дату +- из чего сложился остаток по 76 счету ## C. Договоры @@ -25,6 +31,9 @@ - что по договору 15/24 - есть ли долг по договору с Альфой - какие документы связаны с этим договором +- покажи незакрытые договоры по контрагенту +- какие хвосты по договору №15/24 на дату +- есть ли открытые позиции по договору ## D. Документы @@ -32,6 +41,9 @@ - покажи документы по договору за период - найди документ по номеру и дате - покажи проведенные документы по организации +- какие документы доступны по компании СВК за 2021 год +- выведи документы по клиенту Бета за июль 2020 +- покажи документы по поставщику Альфа за весь период ## E. Bank/Payment lookup @@ -39,24 +51,33 @@ - были ли поступления от Беты - покажи списания с расчетного счета по договору - найди оплату на сумму 150000 +- покажи банковские операции по контрагенту Альфа +- покажи поступления и списания по клиенту Бета +- выведи bank operations by counterparty Alfa for all time ## F. Drilldown - кто должен нам и какие документы это формируют - что висит по Альфе и раскрой по документам - по 60 счету что висит и раскрой по контрагентам +- какие документы формируют остаток по счету 62 на 2020-07-31 +- расшифруй остаток по 60 счету до документов ## G. Составные factual вопросы - покажи хвосты по Альфе и отдельно по Бете - кто должен нам и отдельно кому должны мы - найди долг по договору и покажи документы +- покажи документы по контрагенту и сразу банковские операции +- остаток по счету 60 и какие документы его формируют ## H. Check/verify (still factual) - проверь, есть ли долг по Альфе - проверь, есть ли незакрытые документы - проверь, что висит на 60 счете +- проверь, есть ли открытые позиции по договору +- проверь, есть ли документы по контрагенту за июль 2020 ## Правило маршрутизации diff --git a/docs/ADDRESS/address_query/runtime_integration_plan.md b/docs/ADDRESS/address_query/runtime_integration_plan.md index baafa3a..5376223 100644 --- a/docs/ADDRESS/address_query/runtime_integration_plan.md +++ b/docs/ADDRESS/address_query/runtime_integration_plan.md @@ -24,6 +24,18 @@ - `llm_normalizer/backend/src/services/assistantRuntimeGuards.ts` - `llm_normalizer/backend/src/services/answerComposer.ts` +## 2.1) Architecture Reference (mandatory) + +Перед любыми изменениями address lane сверяться с: + +- `address_architecture_contract_v1.md` + +Ключевая рамка: + +- `Decompose -> Resolve -> Execute -> Compose` +- runtime не хранит company-specific словари +- company entities подтверждаются только через live resolver/MCP + ## 3) To-Be: Separate Address Lane Новый high-level flow: diff --git a/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/README.md b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/README.md new file mode 100644 index 0000000..8f40fd1 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/README.md @@ -0,0 +1,44 @@ +# Address Query Runtime V1 — M2.3d Query Variants Expansion + +Дата: 2026-04-01 + +## Цель шага + +Подбить текущий этап M2.3c и расширить вариативность пользовательских формулировок для уже существующих intent'ов без изменения архитектурной рамки. + +## Что сделано + +1. Расширены сигналы address-mode: +- добавлены entity-токены `компания/организация/поставщик/клиент/партнер` и английские эквиваленты. + +2. Расширен intent-resolver: +- добавлены вариации `documents_by_counterparty` и `bank_operations_by_counterparty` для формулировок через поставщика/клиента/компанию; +- добаван loose by-anchor trigger (`по <якорь>`) для short/slang формулировок. + +3. Усилен filter-extractor: +- расширен extraction якоря контрагента (`по поставщику`, `по клиенту`, `по партнеру`, `по организации`, `by supplier/customer/client/partner`); +- добавлены all-time варианты (`за весь период`, `за весь срок`, `за всю историю`, `for entire period`, `full history`); +- добавлен month-period extraction (`за июль 2020`, `for july 2020`, `за 07.2020`); +- добавлен fallback extraction по loose phrase (`по свк`). + +4. Закрыт регресс из live export: +- `какие доки есть по свк за 2021` больше не уходит в deep/canonical fallback; +- запрос остаётся в `address_query` и резолвится как `list_documents_by_counterparty`. + +5. Расширен тестовый контур: +- добавлены новые unit-тесты в `addressQueryRuntimeM23.test.ts` под новые вариации и регрессионный кейс. + +## Что НЕ меняли + +- Не добавлялись новые intent'ы. +- Не менялась deep-analysis ветка. +- Не добавлялся free-form query builder. + +## Проверка + +- Команда: `npx vitest tests/addressQueryRuntimeM23.test.ts` +- Результат: `1 file passed, 25 tests passed` + +## Следующий шаг + +- Запустить dry/live прогон в окне ассистента на расширенном наборе формулировок и зафиксировать non-empty/limited распределение по counterparty/account семействам. diff --git a/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/before_after_metrics.json b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/before_after_metrics.json new file mode 100644 index 0000000..2c7283d --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/before_after_metrics.json @@ -0,0 +1,24 @@ +{ + "run_id": "2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion", + "comparison": { + "baseline_ref": "HEAD (before M2.3d patch)", + "current_ref": "workspace (after M2.3d patch + regression fix)" + }, + "metrics": { + "address_m23_test_cases": { + "before": 15, + "after": 25, + "delta": 10 + }, + "address_m23_test_failures": { + "before": 0, + "after": 0, + "delta": 0 + } + }, + "notes": [ + "delta reflects additional query-variation coverage for mode, intent, and filters", + "includes regression fix for slang docs phrase: 'какие доки есть по svk за 2021'", + "no architecture-scope expansion (no new intents)" + ] +} diff --git a/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/changed_files.txt b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/changed_files.txt new file mode 100644 index 0000000..bc03090 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/changed_files.txt @@ -0,0 +1,15 @@ +llm_normalizer/backend/src/services/addressQueryClassifier.ts +llm_normalizer/backend/src/services/addressIntentResolver.ts +llm_normalizer/backend/src/services/addressFilterExtractor.ts +llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +llm_normalizer/backend/dist/services/addressQueryClassifier.js +llm_normalizer/backend/dist/services/addressIntentResolver.js +llm_normalizer/backend/dist/services/addressFilterExtractor.js +docs/ADDRESS/address_query/question_bank_v1.md +docs/ADDRESS/address_query/README.md +docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/README.md +docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/run_summary.json +docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/before_after_metrics.json +docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/query_variation_expansion_matrix.md +docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/smoke_checks.md +docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/changed_files.txt diff --git a/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/query_variation_expansion_matrix.md b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/query_variation_expansion_matrix.md new file mode 100644 index 0000000..4407758 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/query_variation_expansion_matrix.md @@ -0,0 +1,17 @@ +# Query Variation Expansion Matrix (M2.3d) + +| area | before | after | validation | +|---|---|---|---| +| address mode entity signals | mostly `контрагент/договор/счет/документ` | added `компания/организация/поставщик/клиент/партнер` (+EN aliases) + `доки/док` slang | `addressQueryRuntimeM23.test.ts` | +| docs/bank intent phrasing | counterparty-centric phrasing | supports company/supplier/customer/client/partner phrasing | `addressQueryRuntimeM23.test.ts` | +| counterparty anchor extraction | basic `контрагент/компания` capture | adds supplier/client/partner/org variants (+EN aliases) + loose `по <якорь>` fallback | `addressQueryRuntimeM23.test.ts` | +| all-time phrase detection | mostly `за все время` | adds `за весь период/срок/всю историю` + EN full-history variants | `addressQueryRuntimeM23.test.ts` | +| month period extraction | range/year extraction | adds month phrases (`за июль 2020`, `for july 2020`, `за 07.2020`) | `addressQueryRuntimeM23.test.ts` | + +## Added example phrasings + +- `Покажи платежи по поставщику Альфа за июль 2020` +- `Выведи документы по клиенту Бета за 2020-07` +- `Покажи банковские операции по клиенту Бета за весь период` +- `Какие компании есть в базе` +- `Какие доки есть по свк за 2021` diff --git a/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/run_summary.json b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/run_summary.json new file mode 100644 index 0000000..61c4ef4 --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/run_summary.json @@ -0,0 +1,32 @@ +{ + "run_id": "2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion", + "date": "2026-04-01", + "stage": "Address Query Runtime V1", + "wave": "M2.3d", + "goal": "Query variants expansion for existing intents + slang/loose-anchor regression fix", + "status": "COMPLETED", + "scope": { + "new_intents": false, + "deep_analysis_changes": false, + "focus": [ + "mode signals", + "intent hints", + "counterparty anchor extraction", + "all-time phrase handling", + "month-period extraction", + "slang docs + loose by-anchor handling", + "unit test coverage" + ] + }, + "checks": { + "test_command": "npx vitest tests/addressQueryRuntimeM23.test.ts", + "test_files_passed": 1, + "tests_passed": 25, + "tests_failed": 0 + }, + "guardrails": { + "false_factual_rate_target": 0, + "free_form_query_builder": "not_added", + "whitelist_recipe_policy": "unchanged" + } +} diff --git a/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/smoke_checks.md b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/smoke_checks.md new file mode 100644 index 0000000..a36491a --- /dev/null +++ b/docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/smoke_checks.md @@ -0,0 +1,13 @@ +# Smoke Checks + +## Backend tests + +- Command: `npx vitest tests/addressQueryRuntimeM23.test.ts` +- Result: PASS +- Details: `1 passed file`, `25 passed tests`, `0 failed` + +## Build + +- Command: `npm run build` +- Result: PASS +- Details: TypeScript build completed without errors. diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/document.txt b/docs/ADDRESS/tz/_tmp_docx_extract/document.txt new file mode 100644 index 0000000..383a2f6 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/document.txt @@ -0,0 +1,2 @@ + +Отчет РїРѕ анализу архива address_query`nИнвентаризация разметки 1РЎ, покрытие доменов Рё статус runtime-сценариев`nИсточник: архив address_query.zip | Дата документов внутри архива: 2026-03-29`nГлавный вывод`nРђСЂС…РёРІ посвящен РЅРµ общему налоговому/НДС-контуру, Р° отдельному runtime-режиму address_query для factual lookup РїРѕ взаиморасчетам, договорам, документам, банковским операциям Рё остаткам РїРѕ счетам.`nМасштаб inventory`nОбработано 7 039 648 строк РёР· 12 monthly snapshot-файлов Р·Р° 2020 РіРѕРґ, parse errors = 0, найдено 286 уникальных сущностей РІ 10 family-группах.`nЧто реально покрыто сейчас`nРќР° СѓСЂРѕРІРЅРµ V1/M2.3c подтверждены прежде всего сценарии РїРѕ контрагентам Рё документам; account-сценарии РІРёРґРЅС‹, РЅРѕ еще ограничены materialization/account-scope проблемами; contract-сценарии требуют специализированных recipe.`nЧто СЃ НДС`nНДС-сущности Рё related entities РІ inventory присутствуют, РЅРѕ РІ текущий whitelist intents/runtime V1 РѕРЅРё РЅРµ РІС…РѕРґСЏС‚ как отдельный поддержанный домен.`n1. Что находится РІ архиве`n• Главный README описывает пакет как набор документов для перехода Рє отдельному runtime-режиму `question_mode=address_query`.`n• Есть слой системной инвентаризации сущностей 1РЎ РїРѕ snapshot-РєРѕСЂРїСѓСЃСѓ 2020: entity inventory JSON-артефакты, relation/intention support sets, readable summary, run summary.`n• Есть продуктовый слой: scenario matrix, query recipes, runtime contracts, integration plan, readiness matrix, question bank, acceptance suites Рё UI dry-run наборы.`n• РўРѕ есть архив сочетает РґРІР° СѓСЂРѕРІРЅСЏ: исследование структуры 1РЎ-базы Рё проектирование прикладного factual-query runtime поверх этой структуры.`n2. Структура пакета РїРѕ смысловым блокам`nБлок`nКлючевые файлы`nЗачем нужен`nИнвентаризация 1РЎ`nentity_map_1c_2020.md, entity_inventory_*.json, run_summary.json`nПоказывает, какие сущности есть РІ snapshot-РєРѕСЂРїСѓСЃРµ, насколько РѕРЅРё query-suitable Рё какие relation patterns обнаружены.`nСценарии Рё intents`naddress_scenario_matrix.md, question_bank_v1.md`nФиксирует пользовательские РІРѕРїСЂРѕСЃС‹, intent mapping, приоритеты Рё expected response types.`nRecipe-слой`nquery_recipes_v1.md, address_runtime_contracts.md`nЗадает безопасный runtime-контур: intent -> filters -> recipe -> MCP -> factual result.`nИнтеграция Рё готовность`nruntime_integration_plan.md, runtime_readiness_matrix_v1.md, execution_lineup_v1_2026-03-29.md`nПоказывает, как это встраивается Рё какие сценарии СѓР¶Рµ Р¶РёРІС‹, Р° какие еще нет.`nAcceptance Рё dry-run`ncurated_positive_live_suite_v1.md, data_aware_positive_acceptance_suite_v1.md, ui_dry_run_*`nРќСѓР¶РЅС‹ для live-проверки, контроля false factual Рё ручного РїСЂРѕРіРѕРЅР° поддержанных РІРѕРїСЂРѕСЃРѕРІ.`n3. Масштаб Рё состав inventory РїРѕ базе 1РЎ`n• Источник данных: 12 monthly NDJSON snapshots Р·Р° 2020 РіРѕРґ.`n• Обработано 7 039 648 строк без parse errors.`n• Найдено 286 сущностей РІ 10 family-группах.`n• Наиболее крупный пласт РїРѕ строкам - INFORMATION_REGISTER (6,3 млн строк), РЅРѕ адресный runtime строится прежде всего РЅР° регистрах, документах, журналах Рё справочниках.`nFamily`nСущностей / строк`nПриоритет`nКомментарий`nDOCUMENT`n77 / 202 636`nРІСЃРµ P0`nГлавный источник document-level factual lookup.`nACCOUNTING_REGISTER`n2 / 190 244`nРѕР±Р° P0`nКлюч Рє остаткам, хвостам Рё drilldown РїРѕ проводкам.`nNSI_CATALOG`n50 / 140 050`n3 P0 / 47 P1`nНужен для РґРѕРіРѕРІРѕСЂРѕРІ, контрагентов Рё фильтровых резолверов.`nDOCUMENT_JOURNAL`n12 / 126 868`nРІСЃРµ P1`nБыстрый индекс документов Рё банковских выписок.`nACCUMULATION_REGISTER`n36 / 73 022`nРІСЃРµ P0`nР’ inventory высоко РІРёРґРёРјС‹, РЅРѕ РІ runtime V1 почти РЅРµ заведены как отдельные intents.`n4. Какие домены реально прорабатываются`n• Ключевой продуктовый домен - адресные factual-РІРѕРїСЂРѕСЃС‹ РїРѕ взаиморасчетам Рё документам, Р° РЅРµ произвольная аналитика РїРѕ всей базе.`n• РЎСѓРґСЏ РїРѕ scenario matrix, question bank Рё runtime contracts, текущий V1 разбит РЅРµ РїРѕ классическим бухгалтерским разделам, Р° РїРѕ operational query-доменам.`nДомен`nЧто РІС…РѕРґРёС‚`nСтатус`nКомментарий`nКонтрагенты Рё задолженность`npayables, receivables, open items by counterparty`nСЏРґСЂРѕ V1`nСамый зрелый блок; есть curated positive cases Рё live-with-limits.`nСчета Рё остатки`naccount balance, balance docs, turnover`nчастично`nСтруктурно готово, РЅРѕ account-scope/materialization ограничивает стабильные non-empty ответы.`nДоговоры`nopen contracts, docs/open items by contract`nРЅРµ дожато`nЕсть РІ дизайне, РЅРѕ РЅСѓР¶РЅС‹ specialized recipe Рё resolver path.`nДокументы Рё банковские операции`ndocs by counterparty, bank ops, list by type`nближе Рє рабочему`nИменно здесь подтверждены live non-empty кейсы РїРѕ counterparty family.`nНДС Рё налоговые сущности`nНДС-регистры, счета-фактуры, НДС-документы РІ inventory`nРЅРµ runtime V1`nЕсть РІ инвентаризации, РЅРѕ РЅРµ заведены как whitelist intents.`n5. Какие intents поддержаны РїРѕ документам`n• P0-СЏРґСЂРѕ, зафиксированное РІ bootstrap report: `list_open_contracts`, `list_payables_counterparties`, `list_receivables_counterparties`, `account_balance_snapshot`, `open_items_by_counterparty_or_contract`.`n• Как ближайшее расширение v1.1 перечислены: `list_documents_by_counterparty`, `list_documents_by_contract`, `documents_forming_balance`.`n• Фактически РІ live runtime РЅР° момент пакета СЏРІРЅРѕ реализованы: `list_documents_by_counterparty`, `bank_operations_by_counterparty`, `documents_forming_balance`.`nID`nСценарий`nСтатус`nЧто мешает`nAQ-P0-02`npayables by counterparty`nlive, РЅРѕ СЃ ограничениями`nРЁРёСЂРѕРєРёРµ промпты РїРѕРєР° дают sparse matches.`nAQ-P0-03`nreceivables by counterparty`nlive, РЅРѕ СЃ ограничениями`nРќСѓР¶РЅС‹ более точные period hints Рё anchor refinement.`nAQ-P0-04`naccount balance snapshot`nlive, РЅРѕ СЃ ограничениями`nСтроки РїСЂРёС…РѕРґСЏС‚, РЅРѕ выпадают РґРѕ materialization.`nAQ-P0-05`nopen items by counterparty`nlive, РЅРѕ СЃ ограничениями`nНужен явный counterparty anchor для стабильного non-empty.`nAQ-P0-07`ndocuments by counterparty`nlive, РЅРѕ СЃ ограничениями`nЕсть positive cases, РЅРѕ СЏРєРѕСЂСЏ еще С…СЂСѓРїРєРёРµ.`nAQ-P0-07B`nbank ops by counterparty`nlive, РЅРѕ СЃ ограничениями`nПозитив подтвержден, РЅРѕ СѓР·РєРёРµ/широкие варианты нестабильны.`nAQ-P0-01/06/08`ncontract-related scenarios`nтребует спец. recipe`nРќРµ хватает contract-aware recipe Рё contract resolver.`nAQ-P0-09`ndocuments forming balance`nlive, РЅРѕ СЃ ограничениями`nAccount family still blocked before materialization.`n6. Какие сущности являются опорными`n• Главная структурная РѕРїРѕСЂР° - `AccountingRegister_Хозрасчетный_RecordType`: через него РёРґСѓС‚ account, document, organization Рё часть business drilldown связей.`n• Из документов центральны `СписаниеСРасчетногоСчета`, `ПоступлениеНаРасчетныйСчет`, РёС… строки расшифровки платежа, Р° также `АктСверкиВзаиморасчетов`.`n• Из справочников критичен `Catalog_ДоговорыКонтрагентов`; РїСЂРё этом `Catalog_Контрагенты`, `Catalog_Организации`, `Catalog_БанковскиеСчета` формально РІ triage отмечены как P1, РЅРѕ фактически обязательны для фильтров Рё resolver-логики.`n• Document journals (`ДокументыПоставщиков`, `ДокументыПокупателей`, `БанковскиеВыписки`) играют роль быстрого индексного слоя для list/drilldown сценариев.`n7. Что РјРѕР¶РЅРѕ сказать РїСЂРѕ НДС`n• Р’ inventory присутствуют НДС-регистры Рё связанные сущности: `AccumulationRegister_НДСПредъявленный`, `AccumulationRegister_НДСЗаписиКнигиПокупок`, `AccumulationRegister_НДСЗаписиКнигиПродаж`, документы `СчетФактураПолученный`, `СчетФактураВыданный`, `СписаниеНДС`, журнал `РегламентныеДокументыНДС`.`n• Однако РІ scenario matrix, recipe catalog Рё readiness matrix РѕРЅРё РЅРµ оформлены как отдельный runtime-домен текущего `address_query`.`n• Поэтому корректный вывод такой: НДС СѓР¶Рµ есть РІ структурной инвентаризации базы, РЅРѕ РІ текущем пакете это РЅРµ целевое продуктовое покрытие V1.`n8. Ограничения Рё СЂРёСЃРєРё`n• Часть labels Рё entity names РІ РёСЃС…РѕРґРЅРѕРј экспорте испорчена cp1251/utf8 mojibake; потребуется финальный decoding/cleanup перед production binding.`n• Без business resolvers нельзя надежно переходить РѕС‚ имени контрагента, РґРѕРіРѕРІРѕСЂР° или счета Рє ID.`n• Free-form query builder специально запрещен; доступ предполагается только через whitelist recipe.`n• Compound factual РІРѕРїСЂРѕСЃС‹ РїРѕРєР° только детектируются, РЅРѕ РЅРµ исполняются как multi-intent decomposition.`n• Account-семейство РїРѕРєР° РЅРµ дает стабильный поток РґРѕ materialization, хотя сырье РЅР° РІС…РѕРґРµ СѓР¶Рµ есть.`n9. Выводы РїРѕ текущему состоянию`n• РђСЂС…РёРІ СѓР¶Рµ дает хорошую картину РїРѕ 1РЎ-сущностям Рё будущему runtime.`n• Сейчас реально прорабатывается РЅРµ РІСЃСЏ база Рё РЅРµ весь бухгалтерский домен, Р° конкретный address-query слой: контрагенты, задолженность, РґРѕРіРѕРІРѕСЂС‹, документы, банковские операции, остатки/расшифровка РїРѕ счетам.`n• Самый зрелый operational блок - counterparty/document lookup.`n• Contract-specific Рё account-specific сценарии структурно готовы, РЅРѕ требуют еще РѕРґРЅРѕРіРѕ цикла реализации Рё настройки.`n• НДС виден РІ inventory Рё может стать следующим отдельным доменом, РЅРѕ РІ данном архиве это РїРѕРєР° РЅРµ активное runtime-покрытие.`n10. Практический next step`n• Собрать отдельную матрицу: “что есть РІ inventory” vs “что заведено РІ intents/recipes” vs “что СѓР¶Рµ реально работает live”.`n• Развести домены РЅР° 3 слоя: текущий рабочий V1, design-only backlog Рё visible-inventory but not productized.`n• Отдельно сделать shortlist РїРѕ НДС/налоговым сущностям, если следующая цель - расширить покрытие Р·Р° пределы address_query.`n diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/document_py.txt b/docs/ADDRESS/tz/_tmp_docx_extract/document_py.txt new file mode 100644 index 0000000..c23c6b8 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/document_py.txt @@ -0,0 +1,157 @@ + +Отчет по анализу архива address_query +Инвентаризация разметки 1С, покрытие доменов и статус runtime-сценариев +Источник: архив address_query.zip | Дата документов внутри архива: 2026-03-29 +Главный вывод +Архив посвящен не общему налоговому/НДС-контуру, а отдельному runtime-режиму address_query для factual lookup по взаиморасчетам, договорам, документам, банковским операциям и остаткам по счетам. +Масштаб inventory +Обработано 7 039 648 строк из 12 monthly snapshot-файлов за 2020 год, parse errors = 0, найдено 286 уникальных сущностей в 10 family-группах. +Что реально покрыто сейчас +На уровне V1/M2.3c подтверждены прежде всего сценарии по контрагентам и документам; account-сценарии видны, но еще ограничены materialization/account-scope проблемами; contract-сценарии требуют специализированных recipe. +Что с НДС +НДС-сущности и related entities в inventory присутствуют, но в текущий whitelist intents/runtime V1 они не входят как отдельный поддержанный домен. +1. Что находится в архиве +• Главный README описывает пакет как набор документов для перехода к отдельному runtime-режиму `question_mode=address_query`. +• Есть слой системной инвентаризации сущностей 1С по snapshot-корпусу 2020: entity inventory JSON-артефакты, relation/intention support sets, readable summary, run summary. +• Есть продуктовый слой: scenario matrix, query recipes, runtime contracts, integration plan, readiness matrix, question bank, acceptance suites и UI dry-run наборы. +• То есть архив сочетает два уровня: исследование структуры 1С-базы и проектирование прикладного factual-query runtime поверх этой структуры. +2. Структура пакета по смысловым блокам +Блок +Ключевые файлы +Зачем нужен +Инвентаризация 1С +entity_map_1c_2020.md, entity_inventory_*.json, run_summary.json +Показывает, какие сущности есть в snapshot-корпусе, насколько они query-suitable и какие relation patterns обнаружены. +Сценарии и intents +address_scenario_matrix.md, question_bank_v1.md +Фиксирует пользовательские вопросы, intent mapping, приоритеты и expected response types. +Recipe-слой +query_recipes_v1.md, address_runtime_contracts.md +Задает безопасный runtime-контур: intent -> filters -> recipe -> MCP -> factual result. +Интеграция и готовность +runtime_integration_plan.md, runtime_readiness_matrix_v1.md, execution_lineup_v1_2026-03-29.md +Показывает, как это встраивается и какие сценарии уже живы, а какие еще нет. +Acceptance и dry-run +curated_positive_live_suite_v1.md, data_aware_positive_acceptance_suite_v1.md, ui_dry_run_* +Нужны для live-проверки, контроля false factual и ручного прогона поддержанных вопросов. +3. Масштаб и состав inventory по базе 1С +• Источник данных: 12 monthly NDJSON snapshots за 2020 год. +• Обработано 7 039 648 строк без parse errors. +• Найдено 286 сущностей в 10 family-группах. +• Наиболее крупный пласт по строкам - INFORMATION_REGISTER (6,3 млн строк), но адресный runtime строится прежде всего на регистрах, документах, журналах и справочниках. +Family +Сущностей / строк +Приоритет +Комментарий +DOCUMENT +77 / 202 636 +все P0 +Главный источник document-level factual lookup. +ACCOUNTING_REGISTER +2 / 190 244 +оба P0 +Ключ к остаткам, хвостам и drilldown по проводкам. +NSI_CATALOG +50 / 140 050 +3 P0 / 47 P1 +Нужен для договоров, контрагентов и фильтровых резолверов. +DOCUMENT_JOURNAL +12 / 126 868 +все P1 +Быстрый индекс документов и банковских выписок. +ACCUMULATION_REGISTER +36 / 73 022 +все P0 +В inventory высоко видимы, но в runtime V1 почти не заведены как отдельные intents. +4. Какие домены реально прорабатываются +• Ключевой продуктовый домен - адресные factual-вопросы по взаиморасчетам и документам, а не произвольная аналитика по всей базе. +• Судя по scenario matrix, question bank и runtime contracts, текущий V1 разбит не по классическим бухгалтерским разделам, а по operational query-доменам. +Домен +Что входит +Статус +Комментарий +Контрагенты и задолженность +payables, receivables, open items by counterparty +ядро V1 +Самый зрелый блок; есть curated positive cases и live-with-limits. +Счета и остатки +account balance, balance docs, turnover +частично +Структурно готово, но account-scope/materialization ограничивает стабильные non-empty ответы. +Договоры +open contracts, docs/open items by contract +не дожато +Есть в дизайне, но нужны specialized recipe и resolver path. +Документы и банковские операции +docs by counterparty, bank ops, list by type +ближе к рабочему +Именно здесь подтверждены live non-empty кейсы по counterparty family. +НДС и налоговые сущности +НДС-регистры, счета-фактуры, НДС-документы в inventory +не runtime V1 +Есть в инвентаризации, но не заведены как whitelist intents. +5. Какие intents поддержаны по документам +• P0-ядро, зафиксированное в bootstrap report: `list_open_contracts`, `list_payables_counterparties`, `list_receivables_counterparties`, `account_balance_snapshot`, `open_items_by_counterparty_or_contract`. +• Как ближайшее расширение v1.1 перечислены: `list_documents_by_counterparty`, `list_documents_by_contract`, `documents_forming_balance`. +• Фактически в live runtime на момент пакета явно реализованы: `list_documents_by_counterparty`, `bank_operations_by_counterparty`, `documents_forming_balance`. +ID +Сценарий +Статус +Что мешает +AQ-P0-02 +payables by counterparty +live, но с ограничениями +Широкие промпты пока дают sparse matches. +AQ-P0-03 +receivables by counterparty +live, но с ограничениями +Нужны более точные period hints и anchor refinement. +AQ-P0-04 +account balance snapshot +live, но с ограничениями +Строки приходят, но выпадают до materialization. +AQ-P0-05 +open items by counterparty +live, но с ограничениями +Нужен явный counterparty anchor для стабильного non-empty. +AQ-P0-07 +documents by counterparty +live, но с ограничениями +Есть positive cases, но якоря еще хрупкие. +AQ-P0-07B +bank ops by counterparty +live, но с ограничениями +Позитив подтвержден, но узкие/широкие варианты нестабильны. +AQ-P0-01/06/08 +contract-related scenarios +требует спец. recipe +Не хватает contract-aware recipe и contract resolver. +AQ-P0-09 +documents forming balance +live, но с ограничениями +Account family still blocked before materialization. +6. Какие сущности являются опорными +• Главная структурная опора - `AccountingRegister_Хозрасчетный_RecordType`: через него идут account, document, organization и часть business drilldown связей. +• Из документов центральны `СписаниеСРасчетногоСчета`, `ПоступлениеНаРасчетныйСчет`, их строки расшифровки платежа, а также `АктСверкиВзаиморасчетов`. +• Из справочников критичен `Catalog_ДоговорыКонтрагентов`; при этом `Catalog_Контрагенты`, `Catalog_Организации`, `Catalog_БанковскиеСчета` формально в triage отмечены как P1, но фактически обязательны для фильтров и resolver-логики. +• Document journals (`ДокументыПоставщиков`, `ДокументыПокупателей`, `БанковскиеВыписки`) играют роль быстрого индексного слоя для list/drilldown сценариев. +7. Что можно сказать про НДС +• В inventory присутствуют НДС-регистры и связанные сущности: `AccumulationRegister_НДСПредъявленный`, `AccumulationRegister_НДСЗаписиКнигиПокупок`, `AccumulationRegister_НДСЗаписиКнигиПродаж`, документы `СчетФактураПолученный`, `СчетФактураВыданный`, `СписаниеНДС`, журнал `РегламентныеДокументыНДС`. +• Однако в scenario matrix, recipe catalog и readiness matrix они не оформлены как отдельный runtime-домен текущего `address_query`. +• Поэтому корректный вывод такой: НДС уже есть в структурной инвентаризации базы, но в текущем пакете это не целевое продуктовое покрытие V1. +8. Ограничения и риски +• Часть labels и entity names в исходном экспорте испорчена cp1251/utf8 mojibake; потребуется финальный decoding/cleanup перед production binding. +• Без business resolvers нельзя надежно переходить от имени контрагента, договора или счета к ID. +• Free-form query builder специально запрещен; доступ предполагается только через whitelist recipe. +• Compound factual вопросы пока только детектируются, но не исполняются как multi-intent decomposition. +• Account-семейство пока не дает стабильный поток до materialization, хотя сырье на входе уже есть. +9. Выводы по текущему состоянию +• Архив уже дает хорошую картину по 1С-сущностям и будущему runtime. +• Сейчас реально прорабатывается не вся база и не весь бухгалтерский домен, а конкретный address-query слой: контрагенты, задолженность, договоры, документы, банковские операции, остатки/расшифровка по счетам. +• Самый зрелый operational блок - counterparty/document lookup. +• Contract-specific и account-specific сценарии структурно готовы, но требуют еще одного цикла реализации и настройки. +• НДС виден в inventory и может стать следующим отдельным доменом, но в данном архиве это пока не активное runtime-покрытие. +10. Практический next step +• Собрать отдельную матрицу: “что есть в inventory” vs “что заведено в intents/recipes” vs “что уже реально работает live”. +• Развести домены на 3 слоя: текущий рабочий V1, design-only backlog и visible-inventory but not productized. +• Отдельно сделать shortlist по НДС/налоговым сущностям, если следующая цель - расширить покрытие за пределы address_query. diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/report.zip b/docs/ADDRESS/tz/_tmp_docx_extract/report.zip new file mode 100644 index 0000000..9d326d0 Binary files /dev/null and b/docs/ADDRESS/tz/_tmp_docx_extract/report.zip differ diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/[Content_Types].xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/[Content_Types].xml new file mode 100644 index 0000000..42b4633 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/[Content_Types].xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/_rels/.rels b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/_rels/.rels new file mode 100644 index 0000000..451925a --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/_rels/.rels @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/_rels/item1.xml.rels b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/_rels/item1.xml.rels new file mode 100644 index 0000000..d5a3c5a --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/_rels/item1.xml.rels @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/item1.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/item1.xml new file mode 100644 index 0000000..94c8bab --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/item1.xml @@ -0,0 +1,2 @@ + + diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/itemProps1.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/itemProps1.xml new file mode 100644 index 0000000..6664fc0 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/itemProps1.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/app.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/app.xml new file mode 100644 index 0000000..bf89187 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/app.xml @@ -0,0 +1,36 @@ + + + + 0 + 1 + 0 + 0 + Microsoft Macintosh Word + 0 + 0 + 0 + false + + + + Title + + + 1 + + + + + + + + + + + false + 0 + false + + false + 14.0000 + diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/core.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/core.xml new file mode 100644 index 0000000..cd4e1a0 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/core.xml @@ -0,0 +1,2 @@ + +python-docxgenerated by python-docx12013-12-23T23:15:00Z2013-12-23T23:15:00Z \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/thumbnail.jpeg b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/thumbnail.jpeg new file mode 100644 index 0000000..d0c4f1f Binary files /dev/null and b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/thumbnail.jpeg differ diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/_rels/document.xml.rels b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/_rels/document.xml.rels new file mode 100644 index 0000000..182d86b --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/_rels/document.xml.rels @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/document.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/document.xml new file mode 100644 index 0000000..f484fca --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/document.xml @@ -0,0 +1,2 @@ + +Отчет по анализу архива address_queryИнвентаризация разметки 1С, покрытие доменов и статус runtime-сценариевИсточник: архив address_query.zip | Дата документов внутри архива: 2026-03-29Главный выводАрхив посвящен не общему налоговому/НДС-контуру, а отдельному runtime-режиму address_query для factual lookup по взаиморасчетам, договорам, документам, банковским операциям и остаткам по счетам.Масштаб inventoryОбработано 7 039 648 строк из 12 monthly snapshot-файлов за 2020 год, parse errors = 0, найдено 286 уникальных сущностей в 10 family-группах.Что реально покрыто сейчасНа уровне V1/M2.3c подтверждены прежде всего сценарии по контрагентам и документам; account-сценарии видны, но еще ограничены materialization/account-scope проблемами; contract-сценарии требуют специализированных recipe.Что с НДСНДС-сущности и related entities в inventory присутствуют, но в текущий whitelist intents/runtime V1 они не входят как отдельный поддержанный домен.1. Что находится в архивеГлавный README описывает пакет как набор документов для перехода к отдельному runtime-режиму `question_mode=address_query`.Есть слой системной инвентаризации сущностей 1С по snapshot-корпусу 2020: entity inventory JSON-артефакты, relation/intention support sets, readable summary, run summary.Есть продуктовый слой: scenario matrix, query recipes, runtime contracts, integration plan, readiness matrix, question bank, acceptance suites и UI dry-run наборы.То есть архив сочетает два уровня: исследование структуры 1С-базы и проектирование прикладного factual-query runtime поверх этой структуры.2. Структура пакета по смысловым блокамБлокКлючевые файлыЗачем нуженИнвентаризация 1Сentity_map_1c_2020.md, entity_inventory_*.json, run_summary.jsonПоказывает, какие сущности есть в snapshot-корпусе, насколько они query-suitable и какие relation patterns обнаружены.Сценарии и intentsaddress_scenario_matrix.md, question_bank_v1.mdФиксирует пользовательские вопросы, intent mapping, приоритеты и expected response types.Recipe-слойquery_recipes_v1.md, address_runtime_contracts.mdЗадает безопасный runtime-контур: intent -> filters -> recipe -> MCP -> factual result.Интеграция и готовностьruntime_integration_plan.md, runtime_readiness_matrix_v1.md, execution_lineup_v1_2026-03-29.mdПоказывает, как это встраивается и какие сценарии уже живы, а какие еще нет.Acceptance и dry-runcurated_positive_live_suite_v1.md, data_aware_positive_acceptance_suite_v1.md, ui_dry_run_*Нужны для live-проверки, контроля false factual и ручного прогона поддержанных вопросов.3. Масштаб и состав inventory по базе 1СИсточник данных: 12 monthly NDJSON snapshots за 2020 год.Обработано 7 039 648 строк без parse errors.Найдено 286 сущностей в 10 family-группах.Наиболее крупный пласт по строкам - INFORMATION_REGISTER (6,3 млн строк), но адресный runtime строится прежде всего на регистрах, документах, журналах и справочниках.FamilyСущностей / строкПриоритетКомментарийDOCUMENT77 / 202 636все P0Главный источник document-level factual lookup.ACCOUNTING_REGISTER2 / 190 244оба P0Ключ к остаткам, хвостам и drilldown по проводкам.NSI_CATALOG50 / 140 0503 P0 / 47 P1Нужен для договоров, контрагентов и фильтровых резолверов.DOCUMENT_JOURNAL12 / 126 868все P1Быстрый индекс документов и банковских выписок.ACCUMULATION_REGISTER36 / 73 022все P0В inventory высоко видимы, но в runtime V1 почти не заведены как отдельные intents.4. Какие домены реально прорабатываютсяКлючевой продуктовый домен - адресные factual-вопросы по взаиморасчетам и документам, а не произвольная аналитика по всей базе.Судя по scenario matrix, question bank и runtime contracts, текущий V1 разбит не по классическим бухгалтерским разделам, а по operational query-доменам.ДоменЧто входитСтатусКомментарийКонтрагенты и задолженностьpayables, receivables, open items by counterpartyядро V1Самый зрелый блок; есть curated positive cases и live-with-limits.Счета и остаткиaccount balance, balance docs, turnoverчастичноСтруктурно готово, но account-scope/materialization ограничивает стабильные non-empty ответы.Договорыopen contracts, docs/open items by contractне дожатоЕсть в дизайне, но нужны specialized recipe и resolver path.Документы и банковские операцииdocs by counterparty, bank ops, list by typeближе к рабочемуИменно здесь подтверждены live non-empty кейсы по counterparty family.НДС и налоговые сущностиНДС-регистры, счета-фактуры, НДС-документы в inventoryне runtime V1Есть в инвентаризации, но не заведены как whitelist intents.5. Какие intents поддержаны по документамP0-ядро, зафиксированное в bootstrap report: `list_open_contracts`, `list_payables_counterparties`, `list_receivables_counterparties`, `account_balance_snapshot`, `open_items_by_counterparty_or_contract`.Как ближайшее расширение v1.1 перечислены: `list_documents_by_counterparty`, `list_documents_by_contract`, `documents_forming_balance`.Фактически в live runtime на момент пакета явно реализованы: `list_documents_by_counterparty`, `bank_operations_by_counterparty`, `documents_forming_balance`.IDСценарийСтатусЧто мешаетAQ-P0-02payables by counterpartylive, но с ограничениямиШирокие промпты пока дают sparse matches.AQ-P0-03receivables by counterpartylive, но с ограничениямиНужны более точные period hints и anchor refinement.AQ-P0-04account balance snapshotlive, но с ограничениямиСтроки приходят, но выпадают до materialization.AQ-P0-05open items by counterpartylive, но с ограничениямиНужен явный counterparty anchor для стабильного non-empty.AQ-P0-07documents by counterpartylive, но с ограничениямиЕсть positive cases, но якоря еще хрупкие.AQ-P0-07Bbank ops by counterpartylive, но с ограничениямиПозитив подтвержден, но узкие/широкие варианты нестабильны.AQ-P0-01/06/08contract-related scenariosтребует спец. recipeНе хватает contract-aware recipe и contract resolver.AQ-P0-09documents forming balancelive, но с ограничениямиAccount family still blocked before materialization.6. Какие сущности являются опорнымиГлавная структурная опора - `AccountingRegister_Хозрасчетный_RecordType`: через него идут account, document, organization и часть business drilldown связей.Из документов центральны `СписаниеСРасчетногоСчета`, `ПоступлениеНаРасчетныйСчет`, их строки расшифровки платежа, а также `АктСверкиВзаиморасчетов`.Из справочников критичен `Catalog_ДоговорыКонтрагентов`; при этом `Catalog_Контрагенты`, `Catalog_Организации`, `Catalog_БанковскиеСчета` формально в triage отмечены как P1, но фактически обязательны для фильтров и resolver-логики.Document journals (`ДокументыПоставщиков`, `ДокументыПокупателей`, `БанковскиеВыписки`) играют роль быстрого индексного слоя для list/drilldown сценариев.7. Что можно сказать про НДСВ inventory присутствуют НДС-регистры и связанные сущности: `AccumulationRegister_НДСПредъявленный`, `AccumulationRegister_НДСЗаписиКнигиПокупок`, `AccumulationRegister_НДСЗаписиКнигиПродаж`, документы `СчетФактураПолученный`, `СчетФактураВыданный`, `СписаниеНДС`, журнал `РегламентныеДокументыНДС`.Однако в scenario matrix, recipe catalog и readiness matrix они не оформлены как отдельный runtime-домен текущего `address_query`.Поэтому корректный вывод такой: НДС уже есть в структурной инвентаризации базы, но в текущем пакете это не целевое продуктовое покрытие V1.8. Ограничения и рискиЧасть labels и entity names в исходном экспорте испорчена cp1251/utf8 mojibake; потребуется финальный decoding/cleanup перед production binding.Без business resolvers нельзя надежно переходить от имени контрагента, договора или счета к ID.Free-form query builder специально запрещен; доступ предполагается только через whitelist recipe.Compound factual вопросы пока только детектируются, но не исполняются как multi-intent decomposition.Account-семейство пока не дает стабильный поток до materialization, хотя сырье на входе уже есть.9. Выводы по текущему состояниюАрхив уже дает хорошую картину по 1С-сущностям и будущему runtime.Сейчас реально прорабатывается не вся база и не весь бухгалтерский домен, а конкретный address-query слой: контрагенты, задолженность, договоры, документы, банковские операции, остатки/расшифровка по счетам.Самый зрелый operational блок - counterparty/document lookup.Contract-specific и account-specific сценарии структурно готовы, но требуют еще одного цикла реализации и настройки.НДС виден в inventory и может стать следующим отдельным доменом, но в данном архиве это пока не активное runtime-покрытие.10. Практический next stepСобрать отдельную матрицу: “что есть в inventory” vs “что заведено в intents/recipes” vs “что уже реально работает live”.Развести домены на 3 слоя: текущий рабочий V1, design-only backlog и visible-inventory but not productized.Отдельно сделать shortlist по НДС/налоговым сущностям, если следующая цель - расширить покрытие за пределы address_query. \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/fontTable.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/fontTable.xml new file mode 100644 index 0000000..a2e0658 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/fontTable.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/numbering.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/numbering.xml new file mode 100644 index 0000000..3ba15a7 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/numbering.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/settings.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/settings.xml new file mode 100644 index 0000000..b922d7c --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/settings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/styles.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/styles.xml new file mode 100644 index 0000000..71e5fe8 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/styles.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/stylesWithEffects.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/stylesWithEffects.xml new file mode 100644 index 0000000..91c1734 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/stylesWithEffects.xml @@ -0,0 +1,11800 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/theme/theme1.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/theme/theme1.xml new file mode 100644 index 0000000..2b30074 --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/theme/theme1.xml @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/webSettings.xml b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/webSettings.xml new file mode 100644 index 0000000..189a20a --- /dev/null +++ b/docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/webSettings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/ADDRESS/tz/address_query_analysis_report.docx b/docs/ADDRESS/tz/address_query_analysis_report.docx new file mode 100644 index 0000000..9d326d0 Binary files /dev/null and b/docs/ADDRESS/tz/address_query_analysis_report.docx differ diff --git a/docs/ADDRESS/tz/address_query_analysis_report.extracted.txt b/docs/ADDRESS/tz/address_query_analysis_report.extracted.txt new file mode 100644 index 0000000..c23c6b8 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_analysis_report.extracted.txt @@ -0,0 +1,157 @@ + +Отчет по анализу архива address_query +Инвентаризация разметки 1С, покрытие доменов и статус runtime-сценариев +Источник: архив address_query.zip | Дата документов внутри архива: 2026-03-29 +Главный вывод +Архив посвящен не общему налоговому/НДС-контуру, а отдельному runtime-режиму address_query для factual lookup по взаиморасчетам, договорам, документам, банковским операциям и остаткам по счетам. +Масштаб inventory +Обработано 7 039 648 строк из 12 monthly snapshot-файлов за 2020 год, parse errors = 0, найдено 286 уникальных сущностей в 10 family-группах. +Что реально покрыто сейчас +На уровне V1/M2.3c подтверждены прежде всего сценарии по контрагентам и документам; account-сценарии видны, но еще ограничены materialization/account-scope проблемами; contract-сценарии требуют специализированных recipe. +Что с НДС +НДС-сущности и related entities в inventory присутствуют, но в текущий whitelist intents/runtime V1 они не входят как отдельный поддержанный домен. +1. Что находится в архиве +• Главный README описывает пакет как набор документов для перехода к отдельному runtime-режиму `question_mode=address_query`. +• Есть слой системной инвентаризации сущностей 1С по snapshot-корпусу 2020: entity inventory JSON-артефакты, relation/intention support sets, readable summary, run summary. +• Есть продуктовый слой: scenario matrix, query recipes, runtime contracts, integration plan, readiness matrix, question bank, acceptance suites и UI dry-run наборы. +• То есть архив сочетает два уровня: исследование структуры 1С-базы и проектирование прикладного factual-query runtime поверх этой структуры. +2. Структура пакета по смысловым блокам +Блок +Ключевые файлы +Зачем нужен +Инвентаризация 1С +entity_map_1c_2020.md, entity_inventory_*.json, run_summary.json +Показывает, какие сущности есть в snapshot-корпусе, насколько они query-suitable и какие relation patterns обнаружены. +Сценарии и intents +address_scenario_matrix.md, question_bank_v1.md +Фиксирует пользовательские вопросы, intent mapping, приоритеты и expected response types. +Recipe-слой +query_recipes_v1.md, address_runtime_contracts.md +Задает безопасный runtime-контур: intent -> filters -> recipe -> MCP -> factual result. +Интеграция и готовность +runtime_integration_plan.md, runtime_readiness_matrix_v1.md, execution_lineup_v1_2026-03-29.md +Показывает, как это встраивается и какие сценарии уже живы, а какие еще нет. +Acceptance и dry-run +curated_positive_live_suite_v1.md, data_aware_positive_acceptance_suite_v1.md, ui_dry_run_* +Нужны для live-проверки, контроля false factual и ручного прогона поддержанных вопросов. +3. Масштаб и состав inventory по базе 1С +• Источник данных: 12 monthly NDJSON snapshots за 2020 год. +• Обработано 7 039 648 строк без parse errors. +• Найдено 286 сущностей в 10 family-группах. +• Наиболее крупный пласт по строкам - INFORMATION_REGISTER (6,3 млн строк), но адресный runtime строится прежде всего на регистрах, документах, журналах и справочниках. +Family +Сущностей / строк +Приоритет +Комментарий +DOCUMENT +77 / 202 636 +все P0 +Главный источник document-level factual lookup. +ACCOUNTING_REGISTER +2 / 190 244 +оба P0 +Ключ к остаткам, хвостам и drilldown по проводкам. +NSI_CATALOG +50 / 140 050 +3 P0 / 47 P1 +Нужен для договоров, контрагентов и фильтровых резолверов. +DOCUMENT_JOURNAL +12 / 126 868 +все P1 +Быстрый индекс документов и банковских выписок. +ACCUMULATION_REGISTER +36 / 73 022 +все P0 +В inventory высоко видимы, но в runtime V1 почти не заведены как отдельные intents. +4. Какие домены реально прорабатываются +• Ключевой продуктовый домен - адресные factual-вопросы по взаиморасчетам и документам, а не произвольная аналитика по всей базе. +• Судя по scenario matrix, question bank и runtime contracts, текущий V1 разбит не по классическим бухгалтерским разделам, а по operational query-доменам. +Домен +Что входит +Статус +Комментарий +Контрагенты и задолженность +payables, receivables, open items by counterparty +ядро V1 +Самый зрелый блок; есть curated positive cases и live-with-limits. +Счета и остатки +account balance, balance docs, turnover +частично +Структурно готово, но account-scope/materialization ограничивает стабильные non-empty ответы. +Договоры +open contracts, docs/open items by contract +не дожато +Есть в дизайне, но нужны specialized recipe и resolver path. +Документы и банковские операции +docs by counterparty, bank ops, list by type +ближе к рабочему +Именно здесь подтверждены live non-empty кейсы по counterparty family. +НДС и налоговые сущности +НДС-регистры, счета-фактуры, НДС-документы в inventory +не runtime V1 +Есть в инвентаризации, но не заведены как whitelist intents. +5. Какие intents поддержаны по документам +• P0-ядро, зафиксированное в bootstrap report: `list_open_contracts`, `list_payables_counterparties`, `list_receivables_counterparties`, `account_balance_snapshot`, `open_items_by_counterparty_or_contract`. +• Как ближайшее расширение v1.1 перечислены: `list_documents_by_counterparty`, `list_documents_by_contract`, `documents_forming_balance`. +• Фактически в live runtime на момент пакета явно реализованы: `list_documents_by_counterparty`, `bank_operations_by_counterparty`, `documents_forming_balance`. +ID +Сценарий +Статус +Что мешает +AQ-P0-02 +payables by counterparty +live, но с ограничениями +Широкие промпты пока дают sparse matches. +AQ-P0-03 +receivables by counterparty +live, но с ограничениями +Нужны более точные period hints и anchor refinement. +AQ-P0-04 +account balance snapshot +live, но с ограничениями +Строки приходят, но выпадают до materialization. +AQ-P0-05 +open items by counterparty +live, но с ограничениями +Нужен явный counterparty anchor для стабильного non-empty. +AQ-P0-07 +documents by counterparty +live, но с ограничениями +Есть positive cases, но якоря еще хрупкие. +AQ-P0-07B +bank ops by counterparty +live, но с ограничениями +Позитив подтвержден, но узкие/широкие варианты нестабильны. +AQ-P0-01/06/08 +contract-related scenarios +требует спец. recipe +Не хватает contract-aware recipe и contract resolver. +AQ-P0-09 +documents forming balance +live, но с ограничениями +Account family still blocked before materialization. +6. Какие сущности являются опорными +• Главная структурная опора - `AccountingRegister_Хозрасчетный_RecordType`: через него идут account, document, organization и часть business drilldown связей. +• Из документов центральны `СписаниеСРасчетногоСчета`, `ПоступлениеНаРасчетныйСчет`, их строки расшифровки платежа, а также `АктСверкиВзаиморасчетов`. +• Из справочников критичен `Catalog_ДоговорыКонтрагентов`; при этом `Catalog_Контрагенты`, `Catalog_Организации`, `Catalog_БанковскиеСчета` формально в triage отмечены как P1, но фактически обязательны для фильтров и resolver-логики. +• Document journals (`ДокументыПоставщиков`, `ДокументыПокупателей`, `БанковскиеВыписки`) играют роль быстрого индексного слоя для list/drilldown сценариев. +7. Что можно сказать про НДС +• В inventory присутствуют НДС-регистры и связанные сущности: `AccumulationRegister_НДСПредъявленный`, `AccumulationRegister_НДСЗаписиКнигиПокупок`, `AccumulationRegister_НДСЗаписиКнигиПродаж`, документы `СчетФактураПолученный`, `СчетФактураВыданный`, `СписаниеНДС`, журнал `РегламентныеДокументыНДС`. +• Однако в scenario matrix, recipe catalog и readiness matrix они не оформлены как отдельный runtime-домен текущего `address_query`. +• Поэтому корректный вывод такой: НДС уже есть в структурной инвентаризации базы, но в текущем пакете это не целевое продуктовое покрытие V1. +8. Ограничения и риски +• Часть labels и entity names в исходном экспорте испорчена cp1251/utf8 mojibake; потребуется финальный decoding/cleanup перед production binding. +• Без business resolvers нельзя надежно переходить от имени контрагента, договора или счета к ID. +• Free-form query builder специально запрещен; доступ предполагается только через whitelist recipe. +• Compound factual вопросы пока только детектируются, но не исполняются как multi-intent decomposition. +• Account-семейство пока не дает стабильный поток до materialization, хотя сырье на входе уже есть. +9. Выводы по текущему состоянию +• Архив уже дает хорошую картину по 1С-сущностям и будущему runtime. +• Сейчас реально прорабатывается не вся база и не весь бухгалтерский домен, а конкретный address-query слой: контрагенты, задолженность, договоры, документы, банковские операции, остатки/расшифровка по счетам. +• Самый зрелый operational блок - counterparty/document lookup. +• Contract-specific и account-specific сценарии структурно готовы, но требуют еще одного цикла реализации и настройки. +• НДС виден в inventory и может стать следующим отдельным доменом, но в данном архиве это пока не активное runtime-покрытие. +10. Практический next step +• Собрать отдельную матрицу: “что есть в inventory” vs “что заведено в intents/recipes” vs “что уже реально работает live”. +• Развести домены на 3 слоя: текущий рабочий V1, design-only backlog и visible-inventory but not productized. +• Отдельно сделать shortlist по НДС/налоговым сущностям, если следующая цель - расширить покрытие за пределы address_query. diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30.zip b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30.zip new file mode 100644 index 0000000..76a5206 Binary files /dev/null and b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30.zip differ diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/README.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/README.md new file mode 100644 index 0000000..4cba019 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/README.md @@ -0,0 +1,42 @@ +# ADDRESS Query Production Pack V2 + +Дата: 2026-03-30 + +## Назначение + +Этот пакет расширяет исходный `address_query` архив до production-oriented слоя: +- фиксирует полный доменный контур; +- раскладывает покрытие по intent/recipe/filter; +- добавляет массовый банк пользовательских формулировок в разных диалектах; +- задает acceptance-пакет для live проверки и дальнейшей доработки runtime. + +## Что внутри + +- `prod_domain_expansion_report.md` — итоговый аналитический отчет и стратегия доведения до prod. +- `address_domain_matrix_v2.md` — целевая матрица доменов/интентов/рецептов. +- `query_paraphrase_bank_v2.md` — расширенный банк формулировок по всем доменам. +- `question_bank_massive_v2.json` — машиночитаемый банк вопросов для NCP/MCP/LLM-routing. +- `resolver_normalization_spec_v2.md` — спецификация нормализации фильтров и синонимов. +- `production_acceptance_pack_v2.md` — критерии приемки для live-runtime. +- `execution_backlog_v2.md` — последовательность внедрения до prod-ready. +- `domain_gap_register_v2.md` — регистр пробелов между текущим runtime и целевым продом. + +## Ключевой принцип + +Проблема должна решаться не точечной ручной настройкой под отдельные вопросы, а слоем: +`dialect variants -> canonical intent -> required filters -> recipe -> MCP -> factual output`. + +## Объем + +- intent'ов: 13 +- вопросов/парафраз: 234 +- доменов: 8 + +## Совместимость со стилем исходного архива + +Пакет сохранен в том же формате, что и исходный архив: +- markdown-документы; +- таблицы intent/recipe/filter; +- отдельный question bank; +- acceptance/readiness артефакты. + diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/address_domain_matrix_v2.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/address_domain_matrix_v2.md new file mode 100644 index 0000000..7083102 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/address_domain_matrix_v2.md @@ -0,0 +1,28 @@ +# Address Domain Matrix V2 (Prod Target) +Дата: 2026-03-30 Режим: `question_mode=address_query` (production extension) +## Scope +Этот слой охватывает direct factual lookup, агрегаты, drilldown и point-lookup по документам. +## Matrix +| scenario_id | domain | intent | recipe_id | required_filters | optional_filters | current_status | target_status | expected_response_type | priority | +|---|---|---|---|---|---|---|---|---|---| +| AQ-P0-01 | `contracts` | `list_open_contracts` | `address.open_contracts.by_asof` | `as_of_date` | `organization, counterparty, limit, sort` | STRUCTURALLY_VISIBLE / REQUIRES_SPECIALIZED_RECIPE | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P0-02 | `debt` | `list_payables_counterparties` | `address.payables.counterparty_totals` | `as_of_date` | `organization, account, limit, sort` | STRUCTURALLY_VISIBLE / PLANNED | PROD_READY | `FACTUAL_SUMMARY` | P0 | +| AQ-P0-03 | `debt` | `list_receivables_counterparties` | `address.receivables.counterparty_totals` | `as_of_date` | `organization, account, limit, sort` | STRUCTURALLY_VISIBLE / PLANNED | PROD_READY | `FACTUAL_SUMMARY` | P0 | +| AQ-P0-04 | `accounts` | `account_balance_snapshot` | `address.account.balance_snapshot` | `account, as_of_date` | `organization, limit, sort` | STRUCTURALLY_VISIBLE / LIVE_QUERYABLE_WITH_LIMITS | PROD_READY | `FACTUAL_SUMMARY` | P0 | +| AQ-P0-05 | `open_items` | `open_items_by_counterparty` | `address.open_items.by_counterparty` | `counterparty, as_of_date` | `organization, contract, account, limit` | STRUCTURALLY_VISIBLE / LIVE_QUERYABLE_WITH_LIMITS | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P0-06 | `open_items` | `open_items_by_contract` | `address.open_items.by_contract` | `contract, as_of_date` | `organization, counterparty, account, limit` | STRUCTURALLY_VISIBLE / REQUIRES_SPECIALIZED_RECIPE | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P0-07 | `documents` | `list_documents_by_counterparty` | `address.documents.by_counterparty` | `counterparty, period_from, period_to` | `document_type, organization, limit, sort` | STRUCTURALLY_VISIBLE / LIVE_QUERYABLE_WITH_LIMITS | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P0-07B | `bank_ops` | `bank_operations_by_counterparty` | `address.bank_ops.by_counterparty` | `counterparty` | `period_from, period_to, organization, limit, sort` | STRUCTURALLY_VISIBLE / LIVE_QUERYABLE_WITH_LIMITS | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P0-08 | `documents` | `list_documents_by_contract` | `address.documents.by_contract` | `contract, period_from, period_to` | `document_type, organization, limit, sort` | STRUCTURALLY_VISIBLE / REQUIRES_SPECIALIZED_RECIPE | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P0-09 | `drilldown` | `documents_forming_balance` | `address.balance.drilldown_documents` | `account, as_of_date` | `organization, counterparty, contract, limit` | STRUCTURALLY_VISIBLE / LIVE_QUERYABLE_WITH_LIMITS | PROD_READY | `FACTUAL_LIST` | P0 | +| AQ-P1-10 | `accounts` | `account_turnover_snapshot` | `address.account.turnover_snapshot` | `account, period_from, period_to` | `organization, counterparty, limit` | STRUCTURALLY_VISIBLE / PLANNED | PROD_READY | `FACTUAL_SUMMARY` | P1 | +| AQ-P1-11 | `documents` | `list_documents_by_type` | `address.documents.by_type` | `document_type, period_from, period_to` | `organization, counterparty, contract, limit` | STRUCTURALLY_VISIBLE / PLANNED | PROD_READY | `FACTUAL_LIST` | P1 | +| AQ-P1-12 | `document_lookup` | `find_document_by_number_and_date` | `address.document.lookup_by_number_date` | `document_number, document_date` | `document_type, organization, counterparty` | STRUCTURALLY_VISIBLE / NEW | PROD_READY | `FACTUAL_LIST` | P1 | + +## Production Design Rules + +- Для каждого intent должен существовать минимум один canonical recipe. +- Для каждого intent должны поддерживаться разговорные, бухгалтерские и сокращенные формулировки. +- Missing-filter fallback всегда явный: `LIMITED_WITH_REASON`, без ложного factual. +- Resolver работает отдельно от recipe и не зашивается в тексты примеров. +- Составные factual вопросы должны распадаться на подзапросы только после стабилизации single-intent покрытия. diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/domain_gap_register_v2.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/domain_gap_register_v2.md new file mode 100644 index 0000000..32a90cb --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/domain_gap_register_v2.md @@ -0,0 +1,21 @@ +# Domain Gap Register V2 + +Дата: 2026-03-30 + +## Регистр пробелов между текущим runtime и target prod + +| domain | intent | current_gap | prod action | +|---|---|---|---| +| `contracts` | `list_open_contracts` | нет стабильного runtime recipe / resolver path | Нужен стабильный resolver по договору и единая логика open/closed. | +| `debt` | `list_payables_counterparties` | intent структурно описан, но не доведен до live-runtime | Ключевое покрытие бухгалтерского жаргона: кредиторка, задолженность перед поставщиками, долг поставщикам. | +| `debt` | `list_receivables_counterparties` | intent структурно описан, но не доведен до live-runtime | Нужны варианты: дебиторка, нам должны, покупатели должны, долг нам. | +| `accounts` | `account_balance_snapshot` | есть частичная live-проходимость, но недостаточная устойчивость по диалектам/anchor variants | Главный блокер — account scope/materialization. | +| `open_items` | `open_items_by_counterparty` | есть частичная live-проходимость, но недостаточная устойчивость по диалектам/anchor variants | Нужно покрыть жаргон: хвосты, незакрытое, висяк, остатки по взаиморасчетам. | +| `open_items` | `open_items_by_contract` | нет стабильного runtime recipe / resolver path | Требуется двухшаговый resolver contract->focused recipe. | +| `documents` | `list_documents_by_counterparty` | есть частичная live-проходимость, но недостаточная устойчивость по диалектам/anchor variants | Нужно держать разные диалекты: по поставщику, по клиенту, по контре, движения по контрагенту. | +| `bank_ops` | `bank_operations_by_counterparty` | есть частичная live-проходимость, но недостаточная устойчивость по диалектам/anchor variants | Варианты: оплаты, платежи, списания, поступления, банк по контрагенту. | +| `documents` | `list_documents_by_contract` | нет стабильного runtime recipe / resolver path | Нужна явная связка document <-> contract и фильтр по договорному идентификатору. | +| `drilldown` | `documents_forming_balance` | есть частичная live-проходимость, но недостаточная устойчивость по диалектам/anchor variants | Здесь особенно важны формулировки ‘расшифруй остаток’, ‘из чего сложился остаток’. | +| `accounts` | `account_turnover_snapshot` | intent структурно описан, но не доведен до live-runtime | Нужны варианты с оборотами, движением, дебет/кредит оборотами. | +| `documents` | `list_documents_by_type` | intent структурно описан, но не доведен до live-runtime | Важно распознавать русские и 1С-имена типа документа. | +| `document_lookup` | `find_document_by_number_and_date` | intent структурно описан, но не доведен до live-runtime | Берется прямо из question_bank_v1, полезно как частый пользовательский паттерн. | diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/execution_backlog_v2.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/execution_backlog_v2.md new file mode 100644 index 0000000..8c8dfce --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/execution_backlog_v2.md @@ -0,0 +1,60 @@ +# Execution Backlog V2 + +Дата: 2026-03-30 + +## Принцип приоритезации + +Сначала доводим до устойчивого prod те интенты, где: +- уже есть structural support; +- уже видны живые строки или близкий доступ к ним; +- высокий бизнес-спрос; +- много пользовательских вариантов формулировок. + +## Wave 1 — Stabilize current liveable intents + +1. `list_documents_by_counterparty` +2. `bank_operations_by_counterparty` +3. `documents_forming_balance` +4. `account_balance_snapshot` + +Что делаем: +- расширяем paraphrase bank; +- включаем account token normalization; +- добавляем resolver confidence audit; +- собираем positive + negative curated cases. + +## Wave 2 — Close core accounting gap + +5. `list_payables_counterparties` +6. `list_receivables_counterparties` +7. `list_open_contracts` +8. `open_items_by_counterparty` +9. `open_items_by_contract` + +Что делаем: +- фиксируем recipe white-list; +- готовим отдельный resolver слой по договору/контрагенту; +- вводим явный open-items taxonomy; +- проверяем account/account-family joins. + +## Wave 3 — Expand discoverability + +10. `list_documents_by_contract` +11. `account_turnover_snapshot` +12. `list_documents_by_type` +13. `find_document_by_number_and_date` + +Что делаем: +- point lookup path; +- type resolver; +- унификация периодных фильтров; +- корпус live acceptance. + +## Definition of Done для каждого intent + +- 10+ устойчивых формулировок; +- canonical filters извлекаются стабильно; +- selected recipe детерминирован; +- есть curated positive evidence; +- negative twins безопасны; +- documented limitation taxonomy закрыта. diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/manifest.txt b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/manifest.txt new file mode 100644 index 0000000..35e9d73 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/manifest.txt @@ -0,0 +1,10 @@ +README.md +address_domain_matrix_v2.md +domain_gap_register_v2.md +execution_backlog_v2.md +manifest.txt +prod_domain_expansion_report.md +production_acceptance_pack_v2.md +query_paraphrase_bank_v2.md +question_bank_massive_v2.json +resolver_normalization_spec_v2.md diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/prod_domain_expansion_report.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/prod_domain_expansion_report.md new file mode 100644 index 0000000..d17dc7d --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/prod_domain_expansion_report.md @@ -0,0 +1,110 @@ +# Address Query Production Expansion Report + +Дата: 2026-03-30 + +## 1) Что именно решает этот пакет + +Исходный архив хорошо описывает V1 runtime и фиксирует стартовый набор сценариев. Главная проблема, обозначенная пользователем, лежит не только в recipe-слое, а в связке: + +`вариант формулировки -> intent -> filters -> resolver -> recipe` + +Поэтому для доведения до prod недостаточно добавлять отдельные кейсы вручную. Нужно массовое покрытие домена через корпус формулировок и явные контракты нормализации. + +## 2) Какой production target предлагается + +### Production target = не “понимает один вопрос”, а: + +- распознает 10+ естественных формулировок на intent; +- одинаково держит нейтральный, бухгалтерский и разговорный диалект; +- не разваливается от короткой/грязной формулировки; +- извлекает одинаковые canonical filters из разных phrasing; +- либо отдает factual, либо честно возвращает `LIMITED_WITH_REASON`. + +## 3) Домены, которые надо считать целевыми + +На базе исходного архива и question bank целевой контур такой: + +1. задолженность / дебиторка / кредиторка; +2. открытые позиции / хвосты; +3. остатки по счетам; +4. обороты по счетам; +5. договоры; +6. документы по контрагенту; +7. документы по договору; +8. документы по типу; +9. банковские операции; +10. drilldown “какими документами сформирован остаток”; +11. point lookup документа по номеру/дате. + +## 4) Что было слабым местом в исходном V1 + +- Сильнее всего был проработан counterparty/documents контур. +- Account family была частично видна, но упиралась в materialization/account-scope. +- Contract-family требовала специализированный resolver/recipe path. +- Массового корпуса диалектных формулировок не было: question bank был правильный по смыслу, но слишком компактный для продового routing. + +## 5) Что добавлено в этот пакет + +### A. Domain matrix V2 +Фиксирует полный target-контур с required/optional filters и target recipe. + +### B. Massive paraphrase bank +Дает корпус из 200+ формулировок, которые можно: +- использовать для декомпозиции и роутинга; +- пускать в automated batch acceptance; +- превращать в curated live suites. + +### C. Normalization spec +Фиксирует, как переводить “контра/поставщик/клиент”, “хвост/остаток/сальдо”, “кредиторка/дебиторка” в stable intent/filter space. + +### D. Production acceptance +Переводит качество из “кажется, местами работает” в измеримый acceptance по intent, диалекту и resolver quality. + +## 6) Практическая стратегия внедрения + +### Шаг 1. Freeze canonical intents +Не расширять бесконечно список intent'ов. Сначала закрепить канонический набор. + +### Шаг 2. Attach paraphrase bank to normalizer tests +Каждая формулировка должна маппиться в один intent и один filter-contract. + +### Шаг 3. Separate resolver failures from recipe failures +Сейчас часть “падений” пользователь видит как общую ошибку. В проде нужно видеть: +- не найден якорь; +- найден неоднозначно; +- recipe не поддерживает shape; +- raw rows есть, но materialization drops them. + +### Шаг 4. Curated positive evidence +По каждому intent нужны реальные non-empty live кейсы. Без этого можно проверить только безопасность, но не полезность. + +## 7) Рекомендуемая производственная последовательность + +### Первая волна +- `list_documents_by_counterparty` +- `bank_operations_by_counterparty` +- `documents_forming_balance` +- `account_balance_snapshot` + +### Вторая волна +- `list_payables_counterparties` +- `list_receivables_counterparties` +- `list_open_contracts` +- `open_items_by_counterparty` +- `open_items_by_contract` + +### Третья волна +- `list_documents_by_contract` +- `account_turnover_snapshot` +- `list_documents_by_type` +- `find_document_by_number_and_date` + +## 8) Ключевой управленческий вывод + +Для этой задачи нужен не разовый набор “правильных вопросов”, а production corpus: +- intent matrix; +- resolver spec; +- massive paraphrase bank; +- acceptance pack. + +Именно это позволяет перестать лечить каждый диалект вручную и перейти к системной доводке покрытия по доменам. diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/production_acceptance_pack_v2.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/production_acceptance_pack_v2.md new file mode 100644 index 0000000..65de366 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/production_acceptance_pack_v2.md @@ -0,0 +1,92 @@ +# Production Acceptance Pack V2 + +Дата: 2026-03-30 + +## Цель + +Довести `address_query` до production-ready состояния на уровне: +- routing; +- intent detection; +- filter extraction; +- resolver robustness; +- factual stability; +- zero false factual. + +## Обязательные suite'ы + +### A. Routing & Intent Suite +Проверяет: +- mode routing; +- canonical intent resolution; +- detection of unsupported/causal queries; +- no deep-analysis regression. + +### B. Dialect Robustness Suite +Проверяет: +- один и тот же intent в 10+ формулировках; +- бухгалтерский жаргон; +- разговорный диалект; +- сокращенные формулировки; +- уточняющие хвосты (`на сегодня`, `за период`, `по поставщику`). + +### C. Resolver Robustness Suite +Проверяет: +- контрагент по полному имени; +- контрагент по короткому имени; +- договор по номеру; +- счет в разных форматах; +- тип документа по русскому и системному имени. + +### D. Positive Evidence Suite +Проверяет: +- non-empty factual на curated live cases; +- стабильность recipe-level non-empty yield; +- отсутствие false factual. + +### E. Negative Safety Suite +Проверяет: +- пустые или неоднозначные случаи возвращают `LIMITED_WITH_REASON`; +- нет молчаливой подмены anchor'а; +- нет ложных документов/сумм. + +## Обязательные метрики + +- `address_mode_routing_accuracy` +- `address_intent_resolution_rate` +- `dialect_variant_pass_rate` +- `required_filter_resolution_rate` +- `resolver_exact_or_high_confidence_rate` +- `live_call_success_rate` +- `factual_non_empty_rate` +- `false_factual_rate` +- `limited_reason_taxonomy_coverage` + +## Целевые пороги + +- `address_intent_resolution_rate >= 0.95` +- `dialect_variant_pass_rate >= 0.90` +- `required_filter_resolution_rate >= 0.95` +- `resolver_exact_or_high_confidence_rate >= 0.90` +- `live_call_success_rate >= 0.95` +- `false_factual_rate = 0` + +## Минимальный debug contract + +- `detected_mode` +- `detected_intent` +- `normalized_question` +- `resolved_filters` +- `missing_filters` +- `selected_recipe` +- resolver block +- MCP flow block +- `response_type` +- `limited_reason_category` + +## Правило прод-приемки + +Ни один домен не считается production-ready, пока он не прошел: +1. 10+ dialect variants на intent; +2. positive evidence; +3. negative twins; +4. ambiguity handling. diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/query_paraphrase_bank_v2.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/query_paraphrase_bank_v2.md new file mode 100644 index 0000000..f904b5f --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/query_paraphrase_bank_v2.md @@ -0,0 +1,359 @@ +# Query Paraphrase Bank V2 — Address Query + +Дата: 2026-03-30 + +## Назначение + +Этот файл нужен для массовой декомпозиции пользовательских запросов в canonical intent'ы. Для каждого intent даны вариации в разных стилях: нейтральный, бухгалтерский, разговорный, сокращенный, операционный. + +## AQ-P0-01 — `list_open_contracts` + +- domain: `contracts` +- recipe: `address.open_contracts.by_asof` +- required filters: `as_of_date` +- optional filters: `organization, counterparty, limit, sort` +- goal: Показать незакрытые договоры на дату с суммами и контрагентом. + +- какие договоры не закрыты на 2020-07-31 +- покажи незакрытые договоры на 2020-07-31 +- что из договоров висит открытым на 2020-07-31 +- есть ли открытые договоры на 2020-07-31 +- выгрузи договоры с остатком на 2020-07-31 +- по каким договорам на 2020-07-31 еще нет закрытия +- дай список незакрытых договоров на 2020-07-31 +- какие договоры остаются открытыми на 2020-07-31 +- какие договоры болтаются незакрытыми на 2020-07-31 +- покажи договоры, по которым есть хвост на 2020-07-31 +- какие договоры в базе не закрыты на 2020-07-31 +- что по открытым договорам на 2020-07-31 +- сформируй перечень открытых договоров на 2020-07-31 +- есть незакрытые договоры по состоянию на 2020-07-31 +- какие договоры не схлопнулись на 2020-07-31 +- по каким договорам висят остатки на 2020-07-31 +- какие договоры не сведены на 2020-07-31 +- открой договоры со сальдо на 2020-07-31 + +## AQ-P0-02 — `list_payables_counterparties` + +- domain: `debt` +- recipe: `address.payables.counterparty_totals` +- required filters: `as_of_date` +- optional filters: `organization, account, limit, sort` +- goal: Определить, кому компания должна на дату. + +- кому мы должны на 2020-07-31 +- по кому у нас кредиторка на 2020-07-31 +- покажи задолженность перед контрагентами на 2020-07-31 +- какие поставщики ждут оплату на 2020-07-31 +- кому должны денег по состоянию на 2020-07-31 +- у кого перед нами нет оплаты? нет, наоборот: кому должны мы на 2020-07-31 +- выведи кредиторскую задолженность на 2020-07-31 +- кому нужно платить на 2020-07-31 +- какие долги перед поставщиками висят на 2020-07-31 +- контрагенты, которым мы должны на 2020-07-31 +- по кому висит наш долг на 2020-07-31 +- дай список кредиторов на 2020-07-31 +- какая задолженность у нас перед поставщиками на 2020-07-31 +- кому организация должна по взаиморасчетам на 2020-07-31 +- раскрой кредиторку на 2020-07-31 +- покажи кому мы должны денег по базе на 2020-07-31 +- у каких контрагентов у нас минус на 2020-07-31 +- по каким контрагентам надо закрывать долг на 2020-07-31 + +## AQ-P0-03 — `list_receivables_counterparties` + +- domain: `debt` +- recipe: `address.receivables.counterparty_totals` +- required filters: `as_of_date` +- optional filters: `organization, account, limit, sort` +- goal: Определить, кто должен компании на дату. + +- кто должен нам на 2020-07-31 +- по кому у нас дебиторка на 2020-07-31 +- покажи задолженность контрагентов перед нами на 2020-07-31 +- какие покупатели нам должны на 2020-07-31 +- кто не оплатил нам на 2020-07-31 +- выведи дебиторскую задолженность на 2020-07-31 +- кто висит в должниках на 2020-07-31 +- какие долги нам торчат на 2020-07-31 +- контрагенты, которые должны нам на 2020-07-31 +- по кому висит долг нам на 2020-07-31 +- дай список должников на 2020-07-31 +- какая дебиторка у нас на 2020-07-31 +- покажи, кто не закрылся перед нами на 2020-07-31 +- кто должен организации по взаиморасчетам на 2020-07-31 +- раскрой дебиторку на 2020-07-31 +- у каких контрагентов есть долг перед нами на 2020-07-31 +- по каким покупателям есть хвост на 2020-07-31 +- кого нужно дожимать по оплате на 2020-07-31 + +## AQ-P0-04 — `account_balance_snapshot` + +- domain: `accounts` +- recipe: `address.account.balance_snapshot` +- required filters: `account, as_of_date` +- optional filters: `organization, limit, sort` +- goal: Показать остаток по счету на дату. + +- какой остаток по счету 62 на 2020-07-31 +- покажи остаток по 62 счету на 2020-07-31 +- что висит на счете 62 на 2020-07-31 +- дай сальдо по счету 62 на 2020-07-31 +- какой хвост на 62 на 2020-07-31 +- какой остаток висит по 62 на 2020-07-31 +- сколько на 62 счете на 2020-07-31 +- сформируй остаток по счету 62 по состоянию на 2020-07-31 +- раскрой остаток по счету 62 на 2020-07-31 +- покажи сальдо по 62 на 2020-07-31 +- что осталось на 62 счете на 2020-07-31 +- по счету 62 какой баланс на 2020-07-31 +- какой остаток по бухсчету 62 на 2020-07-31 +- на 2020-07-31 сколько висит на счете 62 +- остаток по плану счетов 62 на 2020-07-31 +- какое сальдо по 62 по базе на 2020-07-31 +- дай остаток по счету 62 с учетом проводок на 2020-07-31 +- что по остатку счета 62 на 2020-07-31 + +## AQ-P0-05 — `open_items_by_counterparty` + +- domain: `open_items` +- recipe: `address.open_items.by_counterparty` +- required filters: `counterparty, as_of_date` +- optional filters: `organization, contract, account, limit` +- goal: Показать хвосты / открытые позиции по контрагенту. + +- какие хвосты висят по контрагенту ООО Альфа на 2020-07-31 +- что не закрыто по ООО Альфа на 2020-07-31 +- покажи открытые позиции по контрагенту ООО Альфа на 2020-07-31 +- какие незакрытые взаиморасчеты по ООО Альфа на 2020-07-31 +- какие долги/хвосты по ООО Альфа на 2020-07-31 +- что висит по контрагенту ООО Альфа на 2020-07-31 +- раскрой висяки по ООО Альфа на 2020-07-31 +- покажи незакрытое по ООО Альфа на 2020-07-31 +- какие остатки по взаиморасчетам с ООО Альфа на 2020-07-31 +- есть ли хвосты по ООО Альфа на 2020-07-31 +- по ООО Альфа что осталось открытым на 2020-07-31 +- дай открытые документы/позиции по ООО Альфа на 2020-07-31 +- покажи висящие суммы по ООО Альфа на 2020-07-31 +- что не схлопнулось по контрагенту ООО Альфа на 2020-07-31 +- по поставщику ООО Альфа какие хвосты на 2020-07-31 +- по клиенту ООО Альфа что не закрыто на 2020-07-31 +- взаиморасчеты с ООО Альфа: что висит на 2020-07-31 +- покажи остатки по контрагенту ООО Альфа на 2020-07-31 + +## AQ-P0-06 — `open_items_by_contract` + +- domain: `open_items` +- recipe: `address.open_items.by_contract` +- required filters: `contract, as_of_date` +- optional filters: `organization, counterparty, account, limit` +- goal: Показать хвосты / открытые позиции по договору. + +- какие хвосты висят по договору 15/24 на 2020-07-31 +- что не закрыто по договору 15/24 на 2020-07-31 +- покажи открытые позиции по договору 15/24 на 2020-07-31 +- какие незакрытые взаиморасчеты по договору 15/24 на 2020-07-31 +- что висит по договору 15/24 на 2020-07-31 +- раскрой висяки по договору 15/24 на 2020-07-31 +- покажи незакрытое по договору 15/24 на 2020-07-31 +- какие остатки по договору 15/24 на 2020-07-31 +- есть ли хвосты по договору 15/24 на 2020-07-31 +- по договору 15/24 что осталось открытым на 2020-07-31 +- дай открытые документы/позиции по договору 15/24 на 2020-07-31 +- покажи висящие суммы по договору 15/24 на 2020-07-31 +- что не схлопнулось по договору 15/24 на 2020-07-31 +- по договору 15/24 какие хвосты на 2020-07-31 +- взаиморасчеты по договору 15/24: что висит на 2020-07-31 +- покажи остатки по договору 15/24 на 2020-07-31 +- какие позиции не закрыты по договору 15/24 на 2020-07-31 +- дай хвосты по договору 15/24 по состоянию на 2020-07-31 + +## AQ-P0-07 — `list_documents_by_counterparty` + +- domain: `documents` +- recipe: `address.documents.by_counterparty` +- required filters: `counterparty, period_from, period_to` +- optional filters: `document_type, organization, limit, sort` +- goal: Показать документы по контрагенту за период. + +- покажи документы по контрагенту ООО Альфа за период 2020-07-01..2020-07-31 +- дай документы по ООО Альфа за 2020-07-01..2020-07-31 +- какие документы были по контрагенту ООО Альфа за 2020-07-01..2020-07-31 +- выгрузи документы по ООО Альфа за 2020-07-01..2020-07-31 +- покажи движения документов по ООО Альфа за 2020-07-01..2020-07-31 +- что за документы проходили по ООО Альфа за 2020-07-01..2020-07-31 +- дай первичку по ООО Альфа за 2020-07-01..2020-07-31 +- какие документы по поставщику ООО Альфа за 2020-07-01..2020-07-31 +- какие документы по клиенту ООО Альфа за 2020-07-01..2020-07-31 +- найди документы по контре ООО Альфа за 2020-07-01..2020-07-31 +- список документов по ООО Альфа за 2020-07-01..2020-07-31 +- покажи все документы по ООО Альфа за 2020-07-01..2020-07-31 +- по контрагенту ООО Альфа какие документы были в 2020-07-01..2020-07-31 +- по ООО Альфа дай документы за 2020-07-01..2020-07-31 +- какие проведенные документы по ООО Альфа за 2020-07-01..2020-07-31 +- по поставщику ООО Альфа покажи документы за 2020-07-01..2020-07-31 +- по контрагенту ООО Альфа нужна выгрузка документов за 2020-07-01..2020-07-31 +- документальный след по ООО Альфа за 2020-07-01..2020-07-31 + +## AQ-P0-07B — `bank_operations_by_counterparty` + +- domain: `bank_ops` +- recipe: `address.bank_ops.by_counterparty` +- required filters: `counterparty` +- optional filters: `period_from, period_to, organization, limit, sort` +- goal: Показать банковские операции по контрагенту. + +- покажи банковские операции по контрагенту ООО Альфа за 2020-07-01..2020-07-31 +- какие платежи были по ООО Альфа за 2020-07-01..2020-07-31 +- были ли оплаты по ООО Альфа за 2020-07-01..2020-07-31 +- дай банк по контрагенту ООО Альфа за 2020-07-01..2020-07-31 +- покажи движения по банку с ООО Альфа за 2020-07-01..2020-07-31 +- какие списания/поступления были по ООО Альфа за 2020-07-01..2020-07-31 +- выгрузи банковские документы по ООО Альфа за 2020-07-01..2020-07-31 +- покажи платежки по ООО Альфа за 2020-07-01..2020-07-31 +- что проходило по расчетному счету с ООО Альфа за 2020-07-01..2020-07-31 +- какие оплаты поставщику ООО Альфа были за 2020-07-01..2020-07-31 +- какие поступления от ООО Альфа были за 2020-07-01..2020-07-31 +- найди банковские операции по контре ООО Альфа за 2020-07-01..2020-07-31 +- список оплат по ООО Альфа за 2020-07-01..2020-07-31 +- покажи банк по ООО Альфа за 2020-07-01..2020-07-31 +- по контрагенту ООО Альфа какие платежи шли в 2020-07-01..2020-07-31 +- по ООО Альфа нужна банковская выборка за 2020-07-01..2020-07-31 +- были ли платежные движения по ООО Альфа за 2020-07-01..2020-07-31 +- все банковские проходы по ООО Альфа за 2020-07-01..2020-07-31 + +## AQ-P0-08 — `list_documents_by_contract` + +- domain: `documents` +- recipe: `address.documents.by_contract` +- required filters: `contract, period_from, period_to` +- optional filters: `document_type, organization, limit, sort` +- goal: Показать документы по договору за период. + +- покажи документы по договору 15/24 за период 2020-07-01..2020-07-31 +- дай документы по договору 15/24 за 2020-07-01..2020-07-31 +- какие документы были по договору 15/24 за 2020-07-01..2020-07-31 +- выгрузи документы по договору 15/24 за 2020-07-01..2020-07-31 +- что за документы проходили по договору 15/24 за 2020-07-01..2020-07-31 +- дай первичку по договору 15/24 за 2020-07-01..2020-07-31 +- список документов по договору 15/24 за 2020-07-01..2020-07-31 +- покажи все документы по договору 15/24 за 2020-07-01..2020-07-31 +- по договору 15/24 какие документы были в 2020-07-01..2020-07-31 +- по 15/24 дай документы за 2020-07-01..2020-07-31 +- какие проведенные документы по договору 15/24 за 2020-07-01..2020-07-31 +- по договору 15/24 покажи документы за 2020-07-01..2020-07-31 +- по договору 15/24 нужна выгрузка документов за 2020-07-01..2020-07-31 +- документальный след по договору 15/24 за 2020-07-01..2020-07-31 +- есть ли документы по договору 15/24 за 2020-07-01..2020-07-31 +- покажи движения документов по договору 15/24 за 2020-07-01..2020-07-31 +- что оформляли по договору 15/24 за 2020-07-01..2020-07-31 +- все документы, связанные с договором 15/24, за 2020-07-01..2020-07-31 + +## AQ-P0-09 — `documents_forming_balance` + +- domain: `drilldown` +- recipe: `address.balance.drilldown_documents` +- required filters: `account, as_of_date` +- optional filters: `organization, counterparty, contract, limit` +- goal: Раскрыть, какими документами сформирован остаток по счету. + +- какие документы формируют остаток по счету 62 на 2020-07-31 +- расшифруй остаток по счету 62 на 2020-07-31 по документам +- из каких документов сложился остаток по 62 на 2020-07-31 +- покажи документы, которые дали остаток по счету 62 на 2020-07-31 +- чем сформирован остаток по 62 на 2020-07-31 +- какие документы висят в остатке по 62 на 2020-07-31 +- раскрой остаток по 62 на 2020-07-31 до документов +- дай документную расшифровку остатка по счету 62 на 2020-07-31 +- по счету 62 какие документы формируют сальдо на 2020-07-31 +- покажи первичку, которая сидит в остатке 62 на 2020-07-31 +- что конкретно образует остаток по 62 на 2020-07-31 +- какие регистраторы формируют остаток счета 62 на 2020-07-31 +- разложи остаток по счету 62 на 2020-07-31 на документы +- по 62 на 2020-07-31 нужен drilldown до документов +- остаток счета 62 на 2020-07-31: из чего он состоит +- какие документы попали в хвост по счету 62 на 2020-07-31 +- раскрой сальдо 62 на 2020-07-31 по документам +- что формирует баланс по счету 62 на 2020-07-31 + +## AQ-P1-10 — `account_turnover_snapshot` + +- domain: `accounts` +- recipe: `address.account.turnover_snapshot` +- required filters: `account, period_from, period_to` +- optional filters: `organization, counterparty, limit` +- goal: Показать обороты по счету за период. + +- дай обороты по счету 62 за 2020-07-01..2020-07-31 +- покажи движение по счету 62 за 2020-07-01..2020-07-31 +- какие обороты по 62 были за 2020-07-01..2020-07-31 +- выведи дебет/кредит обороты по счету 62 за 2020-07-01..2020-07-31 +- сформируй оборотку по счету 62 за 2020-07-01..2020-07-31 +- по счету 62 какие обороты в 2020-07-01..2020-07-31 +- дай обороты по бухсчету 62 за 2020-07-01..2020-07-31 +- что прошло по счету 62 за 2020-07-01..2020-07-31 +- обороты по 62 счету за 2020-07-01..2020-07-31 +- покажи обороты счета 62 за 2020-07-01..2020-07-31 +- какое движение по 62 за 2020-07-01..2020-07-31 +- дай сумму оборотов по 62 за 2020-07-01..2020-07-31 +- по 62 раскрой дебетовые и кредитовые обороты за 2020-07-01..2020-07-31 +- какие суммы крутились по счету 62 за 2020-07-01..2020-07-31 +- покажи оборот по плану счетов 62 за 2020-07-01..2020-07-31 +- счет 62: движение за 2020-07-01..2020-07-31 +- что было по дебету/кредиту счета 62 за 2020-07-01..2020-07-31 +- нужны обороты по 62 за 2020-07-01..2020-07-31 + +## AQ-P1-11 — `list_documents_by_type` + +- domain: `documents` +- recipe: `address.documents.by_type` +- required filters: `document_type, period_from, period_to` +- optional filters: `organization, counterparty, contract, limit` +- goal: Показать документы определенного вида за период. + +- дай документы типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- покажи документы вида СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- какие документы СписаниеСРасчетногоСчета есть за 2020-07-01..2020-07-31 +- выгрузи документы типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- список документов СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- покажи все СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- какие проведенные документы типа СписаниеСРасчетногоСчета были за 2020-07-01..2020-07-31 +- документы вида СписаниеСРасчетногоСчета в 2020-07-01..2020-07-31 +- по типу СписаниеСРасчетногоСчета покажи документы за 2020-07-01..2020-07-31 +- что есть по документам типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- дай выборку по СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- покажи реестр СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- по виду документа СписаниеСРасчетногоСчета нужна выгрузка за 2020-07-01..2020-07-31 +- какие экземпляры СписаниеСРасчетногоСчета проходили за 2020-07-01..2020-07-31 +- документы 1С типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- найди документы СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- выведи журнал по СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31 +- какие документы этого типа были оформлены за 2020-07-01..2020-07-31 + +## AQ-P1-12 — `find_document_by_number_and_date` + +- domain: `document_lookup` +- recipe: `address.document.lookup_by_number_date` +- required filters: `document_number, document_date` +- optional filters: `document_type, organization, counterparty` +- goal: Найти конкретный документ по номеру и дате. + +- найди документ № 000123 от 2020-07-15 +- покажи документ номер 000123 от 2020-07-15 +- есть ли документ 000123 от 2020-07-15 +- отыщи документ № 000123 датой 2020-07-15 +- нужен документ 000123 от 2020-07-15 +- в базе есть документ 000123 от 2020-07-15 +- подними документ номер 000123 от 2020-07-15 +- покажи, что за документ 000123 от 2020-07-15 +- дай карточку документа 000123 от 2020-07-15 +- ищу документ 000123 от 2020-07-15 +- найди по номеру 000123 и дате 2020-07-15 +- проверь наличие документа 000123 от 2020-07-15 +- покажи запись по документу 000123 от 2020-07-15 +- где документ 000123 от 2020-07-15 +- какой документ проходит под номером 000123 от 2020-07-15 +- вытащи документ 000123 от 2020-07-15 +- документ номер 000123, дата 2020-07-15 +- дай документ по реквизитам: номер 000123, дата 2020-07-15 + diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/question_bank_massive_v2.json b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/question_bank_massive_v2.json new file mode 100644 index 0000000..76c9de3 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/question_bank_massive_v2.json @@ -0,0 +1,487 @@ +{ + "generated_at": "2026-03-30", + "package": "address_query_prod_pack_v2", + "total_intents": 13, + "total_questions": 234, + "items": [ + { + "scenario_id": "AQ-P0-01", + "domain": "contracts", + "intent": "list_open_contracts", + "recipe_id": "address.open_contracts.by_asof", + "required_filters": [ + "as_of_date" + ], + "optional_filters": [ + "organization", + "counterparty", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "какие договоры не закрыты на 2020-07-31", + "покажи незакрытые договоры на 2020-07-31", + "что из договоров висит открытым на 2020-07-31", + "есть ли открытые договоры на 2020-07-31", + "выгрузи договоры с остатком на 2020-07-31", + "по каким договорам на 2020-07-31 еще нет закрытия", + "дай список незакрытых договоров на 2020-07-31", + "какие договоры остаются открытыми на 2020-07-31", + "какие договоры болтаются незакрытыми на 2020-07-31", + "покажи договоры, по которым есть хвост на 2020-07-31", + "какие договоры в базе не закрыты на 2020-07-31", + "что по открытым договорам на 2020-07-31", + "сформируй перечень открытых договоров на 2020-07-31", + "есть незакрытые договоры по состоянию на 2020-07-31", + "какие договоры не схлопнулись на 2020-07-31", + "по каким договорам висят остатки на 2020-07-31", + "какие договоры не сведены на 2020-07-31", + "открой договоры со сальдо на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-02", + "domain": "debt", + "intent": "list_payables_counterparties", + "recipe_id": "address.payables.counterparty_totals", + "required_filters": [ + "as_of_date" + ], + "optional_filters": [ + "organization", + "account", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_SUMMARY", + "questions": [ + "кому мы должны на 2020-07-31", + "по кому у нас кредиторка на 2020-07-31", + "покажи задолженность перед контрагентами на 2020-07-31", + "какие поставщики ждут оплату на 2020-07-31", + "кому должны денег по состоянию на 2020-07-31", + "у кого перед нами нет оплаты? нет, наоборот: кому должны мы на 2020-07-31", + "выведи кредиторскую задолженность на 2020-07-31", + "кому нужно платить на 2020-07-31", + "какие долги перед поставщиками висят на 2020-07-31", + "контрагенты, которым мы должны на 2020-07-31", + "по кому висит наш долг на 2020-07-31", + "дай список кредиторов на 2020-07-31", + "какая задолженность у нас перед поставщиками на 2020-07-31", + "кому организация должна по взаиморасчетам на 2020-07-31", + "раскрой кредиторку на 2020-07-31", + "покажи кому мы должны денег по базе на 2020-07-31", + "у каких контрагентов у нас минус на 2020-07-31", + "по каким контрагентам надо закрывать долг на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-03", + "domain": "debt", + "intent": "list_receivables_counterparties", + "recipe_id": "address.receivables.counterparty_totals", + "required_filters": [ + "as_of_date" + ], + "optional_filters": [ + "organization", + "account", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_SUMMARY", + "questions": [ + "кто должен нам на 2020-07-31", + "по кому у нас дебиторка на 2020-07-31", + "покажи задолженность контрагентов перед нами на 2020-07-31", + "какие покупатели нам должны на 2020-07-31", + "кто не оплатил нам на 2020-07-31", + "выведи дебиторскую задолженность на 2020-07-31", + "кто висит в должниках на 2020-07-31", + "какие долги нам торчат на 2020-07-31", + "контрагенты, которые должны нам на 2020-07-31", + "по кому висит долг нам на 2020-07-31", + "дай список должников на 2020-07-31", + "какая дебиторка у нас на 2020-07-31", + "покажи, кто не закрылся перед нами на 2020-07-31", + "кто должен организации по взаиморасчетам на 2020-07-31", + "раскрой дебиторку на 2020-07-31", + "у каких контрагентов есть долг перед нами на 2020-07-31", + "по каким покупателям есть хвост на 2020-07-31", + "кого нужно дожимать по оплате на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-04", + "domain": "accounts", + "intent": "account_balance_snapshot", + "recipe_id": "address.account.balance_snapshot", + "required_filters": [ + "account", + "as_of_date" + ], + "optional_filters": [ + "organization", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_SUMMARY", + "questions": [ + "какой остаток по счету 62 на 2020-07-31", + "покажи остаток по 62 счету на 2020-07-31", + "что висит на счете 62 на 2020-07-31", + "дай сальдо по счету 62 на 2020-07-31", + "какой хвост на 62 на 2020-07-31", + "какой остаток висит по 62 на 2020-07-31", + "сколько на 62 счете на 2020-07-31", + "сформируй остаток по счету 62 по состоянию на 2020-07-31", + "раскрой остаток по счету 62 на 2020-07-31", + "покажи сальдо по 62 на 2020-07-31", + "что осталось на 62 счете на 2020-07-31", + "по счету 62 какой баланс на 2020-07-31", + "какой остаток по бухсчету 62 на 2020-07-31", + "на 2020-07-31 сколько висит на счете 62", + "остаток по плану счетов 62 на 2020-07-31", + "какое сальдо по 62 по базе на 2020-07-31", + "дай остаток по счету 62 с учетом проводок на 2020-07-31", + "что по остатку счета 62 на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-05", + "domain": "open_items", + "intent": "open_items_by_counterparty", + "recipe_id": "address.open_items.by_counterparty", + "required_filters": [ + "counterparty", + "as_of_date" + ], + "optional_filters": [ + "organization", + "contract", + "account", + "limit" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "какие хвосты висят по контрагенту ООО Альфа на 2020-07-31", + "что не закрыто по ООО Альфа на 2020-07-31", + "покажи открытые позиции по контрагенту ООО Альфа на 2020-07-31", + "какие незакрытые взаиморасчеты по ООО Альфа на 2020-07-31", + "какие долги/хвосты по ООО Альфа на 2020-07-31", + "что висит по контрагенту ООО Альфа на 2020-07-31", + "раскрой висяки по ООО Альфа на 2020-07-31", + "покажи незакрытое по ООО Альфа на 2020-07-31", + "какие остатки по взаиморасчетам с ООО Альфа на 2020-07-31", + "есть ли хвосты по ООО Альфа на 2020-07-31", + "по ООО Альфа что осталось открытым на 2020-07-31", + "дай открытые документы/позиции по ООО Альфа на 2020-07-31", + "покажи висящие суммы по ООО Альфа на 2020-07-31", + "что не схлопнулось по контрагенту ООО Альфа на 2020-07-31", + "по поставщику ООО Альфа какие хвосты на 2020-07-31", + "по клиенту ООО Альфа что не закрыто на 2020-07-31", + "взаиморасчеты с ООО Альфа: что висит на 2020-07-31", + "покажи остатки по контрагенту ООО Альфа на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-06", + "domain": "open_items", + "intent": "open_items_by_contract", + "recipe_id": "address.open_items.by_contract", + "required_filters": [ + "contract", + "as_of_date" + ], + "optional_filters": [ + "organization", + "counterparty", + "account", + "limit" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "какие хвосты висят по договору 15/24 на 2020-07-31", + "что не закрыто по договору 15/24 на 2020-07-31", + "покажи открытые позиции по договору 15/24 на 2020-07-31", + "какие незакрытые взаиморасчеты по договору 15/24 на 2020-07-31", + "что висит по договору 15/24 на 2020-07-31", + "раскрой висяки по договору 15/24 на 2020-07-31", + "покажи незакрытое по договору 15/24 на 2020-07-31", + "какие остатки по договору 15/24 на 2020-07-31", + "есть ли хвосты по договору 15/24 на 2020-07-31", + "по договору 15/24 что осталось открытым на 2020-07-31", + "дай открытые документы/позиции по договору 15/24 на 2020-07-31", + "покажи висящие суммы по договору 15/24 на 2020-07-31", + "что не схлопнулось по договору 15/24 на 2020-07-31", + "по договору 15/24 какие хвосты на 2020-07-31", + "взаиморасчеты по договору 15/24: что висит на 2020-07-31", + "покажи остатки по договору 15/24 на 2020-07-31", + "какие позиции не закрыты по договору 15/24 на 2020-07-31", + "дай хвосты по договору 15/24 по состоянию на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-07", + "domain": "documents", + "intent": "list_documents_by_counterparty", + "recipe_id": "address.documents.by_counterparty", + "required_filters": [ + "counterparty", + "period_from", + "period_to" + ], + "optional_filters": [ + "document_type", + "organization", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "покажи документы по контрагенту ООО Альфа за период 2020-07-01..2020-07-31", + "дай документы по ООО Альфа за 2020-07-01..2020-07-31", + "какие документы были по контрагенту ООО Альфа за 2020-07-01..2020-07-31", + "выгрузи документы по ООО Альфа за 2020-07-01..2020-07-31", + "покажи движения документов по ООО Альфа за 2020-07-01..2020-07-31", + "что за документы проходили по ООО Альфа за 2020-07-01..2020-07-31", + "дай первичку по ООО Альфа за 2020-07-01..2020-07-31", + "какие документы по поставщику ООО Альфа за 2020-07-01..2020-07-31", + "какие документы по клиенту ООО Альфа за 2020-07-01..2020-07-31", + "найди документы по контре ООО Альфа за 2020-07-01..2020-07-31", + "список документов по ООО Альфа за 2020-07-01..2020-07-31", + "покажи все документы по ООО Альфа за 2020-07-01..2020-07-31", + "по контрагенту ООО Альфа какие документы были в 2020-07-01..2020-07-31", + "по ООО Альфа дай документы за 2020-07-01..2020-07-31", + "какие проведенные документы по ООО Альфа за 2020-07-01..2020-07-31", + "по поставщику ООО Альфа покажи документы за 2020-07-01..2020-07-31", + "по контрагенту ООО Альфа нужна выгрузка документов за 2020-07-01..2020-07-31", + "документальный след по ООО Альфа за 2020-07-01..2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-07B", + "domain": "bank_ops", + "intent": "bank_operations_by_counterparty", + "recipe_id": "address.bank_ops.by_counterparty", + "required_filters": [ + "counterparty" + ], + "optional_filters": [ + "period_from", + "period_to", + "organization", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "покажи банковские операции по контрагенту ООО Альфа за 2020-07-01..2020-07-31", + "какие платежи были по ООО Альфа за 2020-07-01..2020-07-31", + "были ли оплаты по ООО Альфа за 2020-07-01..2020-07-31", + "дай банк по контрагенту ООО Альфа за 2020-07-01..2020-07-31", + "покажи движения по банку с ООО Альфа за 2020-07-01..2020-07-31", + "какие списания/поступления были по ООО Альфа за 2020-07-01..2020-07-31", + "выгрузи банковские документы по ООО Альфа за 2020-07-01..2020-07-31", + "покажи платежки по ООО Альфа за 2020-07-01..2020-07-31", + "что проходило по расчетному счету с ООО Альфа за 2020-07-01..2020-07-31", + "какие оплаты поставщику ООО Альфа были за 2020-07-01..2020-07-31", + "какие поступления от ООО Альфа были за 2020-07-01..2020-07-31", + "найди банковские операции по контре ООО Альфа за 2020-07-01..2020-07-31", + "список оплат по ООО Альфа за 2020-07-01..2020-07-31", + "покажи банк по ООО Альфа за 2020-07-01..2020-07-31", + "по контрагенту ООО Альфа какие платежи шли в 2020-07-01..2020-07-31", + "по ООО Альфа нужна банковская выборка за 2020-07-01..2020-07-31", + "были ли платежные движения по ООО Альфа за 2020-07-01..2020-07-31", + "все банковские проходы по ООО Альфа за 2020-07-01..2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-08", + "domain": "documents", + "intent": "list_documents_by_contract", + "recipe_id": "address.documents.by_contract", + "required_filters": [ + "contract", + "period_from", + "period_to" + ], + "optional_filters": [ + "document_type", + "organization", + "limit", + "sort" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "покажи документы по договору 15/24 за период 2020-07-01..2020-07-31", + "дай документы по договору 15/24 за 2020-07-01..2020-07-31", + "какие документы были по договору 15/24 за 2020-07-01..2020-07-31", + "выгрузи документы по договору 15/24 за 2020-07-01..2020-07-31", + "что за документы проходили по договору 15/24 за 2020-07-01..2020-07-31", + "дай первичку по договору 15/24 за 2020-07-01..2020-07-31", + "список документов по договору 15/24 за 2020-07-01..2020-07-31", + "покажи все документы по договору 15/24 за 2020-07-01..2020-07-31", + "по договору 15/24 какие документы были в 2020-07-01..2020-07-31", + "по 15/24 дай документы за 2020-07-01..2020-07-31", + "какие проведенные документы по договору 15/24 за 2020-07-01..2020-07-31", + "по договору 15/24 покажи документы за 2020-07-01..2020-07-31", + "по договору 15/24 нужна выгрузка документов за 2020-07-01..2020-07-31", + "документальный след по договору 15/24 за 2020-07-01..2020-07-31", + "есть ли документы по договору 15/24 за 2020-07-01..2020-07-31", + "покажи движения документов по договору 15/24 за 2020-07-01..2020-07-31", + "что оформляли по договору 15/24 за 2020-07-01..2020-07-31", + "все документы, связанные с договором 15/24, за 2020-07-01..2020-07-31" + ] + }, + { + "scenario_id": "AQ-P0-09", + "domain": "drilldown", + "intent": "documents_forming_balance", + "recipe_id": "address.balance.drilldown_documents", + "required_filters": [ + "account", + "as_of_date" + ], + "optional_filters": [ + "organization", + "counterparty", + "contract", + "limit" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "какие документы формируют остаток по счету 62 на 2020-07-31", + "расшифруй остаток по счету 62 на 2020-07-31 по документам", + "из каких документов сложился остаток по 62 на 2020-07-31", + "покажи документы, которые дали остаток по счету 62 на 2020-07-31", + "чем сформирован остаток по 62 на 2020-07-31", + "какие документы висят в остатке по 62 на 2020-07-31", + "раскрой остаток по 62 на 2020-07-31 до документов", + "дай документную расшифровку остатка по счету 62 на 2020-07-31", + "по счету 62 какие документы формируют сальдо на 2020-07-31", + "покажи первичку, которая сидит в остатке 62 на 2020-07-31", + "что конкретно образует остаток по 62 на 2020-07-31", + "какие регистраторы формируют остаток счета 62 на 2020-07-31", + "разложи остаток по счету 62 на 2020-07-31 на документы", + "по 62 на 2020-07-31 нужен drilldown до документов", + "остаток счета 62 на 2020-07-31: из чего он состоит", + "какие документы попали в хвост по счету 62 на 2020-07-31", + "раскрой сальдо 62 на 2020-07-31 по документам", + "что формирует баланс по счету 62 на 2020-07-31" + ] + }, + { + "scenario_id": "AQ-P1-10", + "domain": "accounts", + "intent": "account_turnover_snapshot", + "recipe_id": "address.account.turnover_snapshot", + "required_filters": [ + "account", + "period_from", + "period_to" + ], + "optional_filters": [ + "organization", + "counterparty", + "limit" + ], + "expected_response_type": "FACTUAL_SUMMARY", + "questions": [ + "дай обороты по счету 62 за 2020-07-01..2020-07-31", + "покажи движение по счету 62 за 2020-07-01..2020-07-31", + "какие обороты по 62 были за 2020-07-01..2020-07-31", + "выведи дебет/кредит обороты по счету 62 за 2020-07-01..2020-07-31", + "сформируй оборотку по счету 62 за 2020-07-01..2020-07-31", + "по счету 62 какие обороты в 2020-07-01..2020-07-31", + "дай обороты по бухсчету 62 за 2020-07-01..2020-07-31", + "что прошло по счету 62 за 2020-07-01..2020-07-31", + "обороты по 62 счету за 2020-07-01..2020-07-31", + "покажи обороты счета 62 за 2020-07-01..2020-07-31", + "какое движение по 62 за 2020-07-01..2020-07-31", + "дай сумму оборотов по 62 за 2020-07-01..2020-07-31", + "по 62 раскрой дебетовые и кредитовые обороты за 2020-07-01..2020-07-31", + "какие суммы крутились по счету 62 за 2020-07-01..2020-07-31", + "покажи оборот по плану счетов 62 за 2020-07-01..2020-07-31", + "счет 62: движение за 2020-07-01..2020-07-31", + "что было по дебету/кредиту счета 62 за 2020-07-01..2020-07-31", + "нужны обороты по 62 за 2020-07-01..2020-07-31" + ] + }, + { + "scenario_id": "AQ-P1-11", + "domain": "documents", + "intent": "list_documents_by_type", + "recipe_id": "address.documents.by_type", + "required_filters": [ + "document_type", + "period_from", + "period_to" + ], + "optional_filters": [ + "organization", + "counterparty", + "contract", + "limit" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "дай документы типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "покажи документы вида СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "какие документы СписаниеСРасчетногоСчета есть за 2020-07-01..2020-07-31", + "выгрузи документы типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "список документов СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "покажи все СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "какие проведенные документы типа СписаниеСРасчетногоСчета были за 2020-07-01..2020-07-31", + "документы вида СписаниеСРасчетногоСчета в 2020-07-01..2020-07-31", + "по типу СписаниеСРасчетногоСчета покажи документы за 2020-07-01..2020-07-31", + "что есть по документам типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "дай выборку по СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "покажи реестр СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "по виду документа СписаниеСРасчетногоСчета нужна выгрузка за 2020-07-01..2020-07-31", + "какие экземпляры СписаниеСРасчетногоСчета проходили за 2020-07-01..2020-07-31", + "документы 1С типа СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "найди документы СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "выведи журнал по СписаниеСРасчетногоСчета за 2020-07-01..2020-07-31", + "какие документы этого типа были оформлены за 2020-07-01..2020-07-31" + ] + }, + { + "scenario_id": "AQ-P1-12", + "domain": "document_lookup", + "intent": "find_document_by_number_and_date", + "recipe_id": "address.document.lookup_by_number_date", + "required_filters": [ + "document_number", + "document_date" + ], + "optional_filters": [ + "document_type", + "organization", + "counterparty" + ], + "expected_response_type": "FACTUAL_LIST", + "questions": [ + "найди документ № 000123 от 2020-07-15", + "покажи документ номер 000123 от 2020-07-15", + "есть ли документ 000123 от 2020-07-15", + "отыщи документ № 000123 датой 2020-07-15", + "нужен документ 000123 от 2020-07-15", + "в базе есть документ 000123 от 2020-07-15", + "подними документ номер 000123 от 2020-07-15", + "покажи, что за документ 000123 от 2020-07-15", + "дай карточку документа 000123 от 2020-07-15", + "ищу документ 000123 от 2020-07-15", + "найди по номеру 000123 и дате 2020-07-15", + "проверь наличие документа 000123 от 2020-07-15", + "покажи запись по документу 000123 от 2020-07-15", + "где документ 000123 от 2020-07-15", + "какой документ проходит под номером 000123 от 2020-07-15", + "вытащи документ 000123 от 2020-07-15", + "документ номер 000123, дата 2020-07-15", + "дай документ по реквизитам: номер 000123, дата 2020-07-15" + ] + } + ] +} \ No newline at end of file diff --git a/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/resolver_normalization_spec_v2.md b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/resolver_normalization_spec_v2.md new file mode 100644 index 0000000..2dc21d1 --- /dev/null +++ b/docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/resolver_normalization_spec_v2.md @@ -0,0 +1,118 @@ +# Resolver & Normalization Spec V2 + +Дата: 2026-03-30 + +## 1) Общий принцип + +LLM/normalizer не пытается сразу вызвать recipe. +Сначала он переводит пользовательскую фразу в canonical intent и нормализованные фильтры. + +Цепочка: +`raw_question -> dialect normalization -> intent detection -> filter extraction -> resolver -> recipe selection` + +## 2) Синонимы доменных сигналов + +### Контрагент + +Сигналы: +- контрагент +- поставщик +- клиент +- покупатель +- контра +- организация-партнер + +### Договор + +Сигналы: +- договор +- контракт +- соглашение +- договорник +- договор с номером + +### Остаток / сальдо + +Сигналы: +- остаток +- сальдо +- что висит +- хвост +- остаток висит +- баланс по счету + +### Дебиторка / кредиторка + +Дебиторка: +- кто должен нам +- дебиторка +- долг нам +- задолженность перед нами + +Кредиторка: +- кому должны мы +- кредиторка +- наш долг поставщику +- задолженность перед контрагентом + +### Банковый контур + +Сигналы: +- банк +- платежи +- платежки +- списания +- поступления +- движение по расчетному счету + +## 3) Нормализация счетов + +Допустимые формы: +- `60` +- `62` +- `60.01` +- `62.01` + +Правила: +- удалить лишние слова (`счет`, `счёт`, `бухсчет`, `по счету`); +- сохранить только валидный account token; +- хранить и raw, и normalized форму в debug. + +## 4) Нормализация дат и периодов + +Поддержать: +- `на сегодня` +- `на текущую дату` +- `на конец месяца` +- `за июль 2020` +- `за период с 01.07.2020 по 31.07.2020` +- `за месяц` +- `за квартал` + +Выход: +- `as_of_date` +- либо `period_from + period_to` + +## 5) Resolver policy + +### Counterparty resolver +- `name -> candidate set` +- exact match > normalized exact > startswith > fuzzy +- если ambiguity_count > 1 и нет явного приоритета, вернуть `LIMITED_WITH_REASON` + +### Contract resolver +- поддерживать номер договора, имя договора, пару `контрагент + договор` +- при наличии обоих якорей резолвить договор в контексте контрагента + +### Document type resolver +- белый список русских имен и системных 1С-имен: + - `СписаниеСРасчетногоСчета` + - `ПоступлениеНаРасчетныйСчет` + - и др. по фактическому whitelist + +## 6) Guardrails + +- не допускать свободную генерацию SQL/1С-запросов; +- не подменять отсутствующий фильтр догадкой; +- не заменять by-contract на by-counterparty молча; +- не отдавать пустой factual как будто это найденный результат. diff --git a/llm_normalizer/backend/dist/config.js b/llm_normalizer/backend/dist/config.js index c9922cc..db4d1c4 100644 --- a/llm_normalizer/backend/dist/config.js +++ b/llm_normalizer/backend/dist/config.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0; +exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0; const path_1 = __importDefault(require("path")); exports.BACKEND_ROOT = path_1.default.resolve(__dirname, ".."); exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, ".."); @@ -46,6 +46,7 @@ exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = toBooleanFlag(process.env.FEATUR exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1, true); exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_MCP_RUNTIME_V1, false); exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, true); +exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, true); exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1, true); exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, ""); exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default"; diff --git a/llm_normalizer/backend/dist/routes/testConnection.js b/llm_normalizer/backend/dist/routes/testConnection.js index 615d782..f492388 100644 --- a/llm_normalizer/backend/dist/routes/testConnection.js +++ b/llm_normalizer/backend/dist/routes/testConnection.js @@ -6,23 +6,74 @@ const config_1 = require("../config"); const http_1 = require("../utils/http"); function buildTestConnectionRouter(client) { const router = (0, express_1.Router)(); - router.post("/api/openai/test-connection", async (req, res, next) => { + const handler = async (req, res, next) => { try { const body = (req.body ?? {}); + const llmProvider = body.llmProvider === "local" ? "local" : "openai"; + const model = String(body.model ?? config_1.DEFAULT_MODEL); + const baseUrl = String(body.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL); + const apiKey = String(body.apiKey ?? process.env.OPENAI_API_KEY ?? ""); const result = await client.testConnection({ - apiKey: String(body.apiKey ?? process.env.OPENAI_API_KEY ?? ""), - model: String(body.model ?? config_1.DEFAULT_MODEL), - baseUrl: String(body.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL) + llmProvider, + apiKey, + model, + baseUrl }); + let modelFound = null; + let modelsCount = null; + if (llmProvider === "local") { + try { + const models = await client.listModels({ + llmProvider, + apiKey, + model, + baseUrl + }); + modelsCount = models.length; + modelFound = models.includes(model); + } + catch { + modelFound = null; + modelsCount = null; + } + } (0, http_1.ok)(res, { ok: true, + provider: llmProvider, model: result.model, + model_found: modelFound, + models_count: modelsCount, timestamp: new Date().toISOString() }); } catch (error) { next(error); } - }); + }; + const listModelsHandler = async (req, res, next) => { + try { + const body = (req.body ?? {}); + const models = await client.listModels({ + llmProvider: body.llmProvider === "local" ? "local" : "openai", + apiKey: String(body.apiKey ?? process.env.OPENAI_API_KEY ?? ""), + model: String(body.model ?? config_1.DEFAULT_MODEL), + baseUrl: String(body.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL) + }); + (0, http_1.ok)(res, { + ok: true, + models, + count: models.length, + timestamp: new Date().toISOString() + }); + } + catch (error) { + next(error); + } + }; + router.post("/api/llm/test-connection", handler); + router.post("/api/llm/models", listModelsHandler); + // Backward-compatible route for old frontend builds. + router.post("/api/openai/test-connection", handler); + router.post("/api/openai/models", listModelsHandler); return router; } diff --git a/llm_normalizer/backend/dist/services/addressFilterExtractor.js b/llm_normalizer/backend/dist/services/addressFilterExtractor.js index b3dcafa..b2068c2 100644 --- a/llm_normalizer/backend/dist/services/addressFilterExtractor.js +++ b/llm_normalizer/backend/dist/services/addressFilterExtractor.js @@ -3,12 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.extractAddressFilters = extractAddressFilters; const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i; const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|\bпервые\b|\bтоп\b)\s*(\d{1,3})/i; -const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|by\s+counterparty|counterparty)\s+([^\r\n,.;:]+)/i; +const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu; const CONTRACT_PATTERN = /(?:по\s+договору|договор(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i; const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/; const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/; const PERIOD_RANGE_PATTERN_1 = /(?:from|с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:to|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i; const PERIOD_RANGE_PATTERN_2 = /(?:between|за\s+период\s+с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:and|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i; +const YEAR_RANGE_PATTERN = /(?:за|for|с|from)?\s*(20\d{2})\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(20\d{2})(?:\s*(?:г(?:од|ода)?\.?|year))?(?=[^\d]|$)/iu; +const YEAR_RANGE_LOOSE_PATTERN = /\b(20\d{2})\b\s*(?:[-‐‑‒–—―−]|до|to|по)\s*\b(20\d{2})\b/iu; +const YEAR_PERIOD_PATTERN = /(?:за|for)\s*(20\d{2})(?!\s*(?:[-‐‑‒–—―−]|до|to|по)\s*20\d{2})\s*(?:г(?:од|ода)?\.?|year)?/iu; +const YEAR_PERIOD_SHORT_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{2})\s*(?:г(?:од|ода)?\.?|year)(?=$|[\s,.;:!?()\-])/iu; +const YEAR_PERIOD_ANY_PATTERN = /(?:^|[\s,.;:!?()\-])((?:19|20)\d{2})(?!\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(?:19|20)\d{2})(?![.\/-]\d)(?:\s*(?:г(?:од|ода)?\.?|year))?(?=$|[\s,.;:!?()\-])/iu; +const MONTH_PERIOD_NUMERIC_PATTERN = /(?:за|for)\s*(0?[1-9]|1[0-2])[.\/-](20\d{2})/i; +const MONTH_PERIOD_NAME_PATTERN = /(?:за|for)\s+([a-zа-яё]+)\s+(20\d{2})(?:\s*г(?:од|ода|\\.)?)?/iu; function toIsoDate(year, month, day) { if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { return null; @@ -61,6 +68,64 @@ function parseDateToken(token) { } return undefined; } +function resolveMonthByName(rawMonthName) { + const token = String(rawMonthName ?? "").trim().toLowerCase(); + if (!token) { + return undefined; + } + if (/^янв|^january|^jan/.test(token)) + return 1; + if (/^фев|^february|^feb/.test(token)) + return 2; + if (/^мар|^march|^mar/.test(token)) + return 3; + if (/^апр|^april|^apr/.test(token)) + return 4; + if (/^ма[йя]|^may/.test(token)) + return 5; + if (/^июн|^june|^jun/.test(token)) + return 6; + if (/^июл|^july|^jul/.test(token)) + return 7; + if (/^авг|^august|^aug/.test(token)) + return 8; + if (/^сен|^сент|^september|^sep/.test(token)) + return 9; + if (/^окт|^october|^oct/.test(token)) + return 10; + if (/^ноя|^november|^nov/.test(token)) + return 11; + if (/^дек|^december|^dec/.test(token)) + return 12; + return undefined; +} +function extractMonthPeriod(text) { + const numericMatch = text.match(MONTH_PERIOD_NUMERIC_PATTERN); + if (numericMatch) { + const month = Number(numericMatch[1]); + const year = Number(numericMatch[2]); + if (month >= 1 && month <= 12 && year >= 2000 && year <= 2099) { + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return { + period_from: `${year}-${String(month).padStart(2, "0")}-01`, + period_to: `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}` + }; + } + } + const byNameMatch = text.match(MONTH_PERIOD_NAME_PATTERN); + if (byNameMatch) { + const month = resolveMonthByName(String(byNameMatch[1])); + const year = Number(byNameMatch[2]); + if (month && year >= 2000 && year <= 2099) { + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return { + period_from: `${year}-${String(month).padStart(2, "0")}-01`, + period_to: `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}` + }; + } + } + return {}; +} function extractPeriodRange(text) { const directMatch = text.match(PERIOD_RANGE_PATTERN_1) ?? text.match(PERIOD_RANGE_PATTERN_2); if (!directMatch) { @@ -73,6 +138,64 @@ function extractPeriodRange(text) { ...(periodTo ? { period_to: periodTo } : {}) }; } +function extractYearPeriod(text) { + const match = text.match(YEAR_PERIOD_PATTERN); + if (match) { + const year = Number(match[1]); + if (!Number.isFinite(year) || year < 2000 || year > 2099) { + return {}; + } + return { + period_from: `${year}-01-01`, + period_to: `${year}-12-31` + }; + } + const relaxedYearMatch = text.match(YEAR_PERIOD_ANY_PATTERN); + if (relaxedYearMatch) { + const year = Number(relaxedYearMatch[1]); + if (Number.isFinite(year) && year >= 2000 && year <= 2099) { + return { + period_from: `${year}-01-01`, + period_to: `${year}-12-31` + }; + } + } + const shortYearMatch = text.match(YEAR_PERIOD_SHORT_PATTERN); + if (!shortYearMatch) { + return {}; + } + const shortYear = Number(shortYearMatch[1]); + if (!Number.isFinite(shortYear) || shortYear < 0 || shortYear > 99) { + return {}; + } + const year = 2000 + shortYear; + return { + period_from: `${year}-01-01`, + period_to: `${year}-12-31` + }; +} +function extractYearRangePeriod(text) { + const match = text.match(YEAR_RANGE_PATTERN) ?? text.match(YEAR_RANGE_LOOSE_PATTERN); + if (!match) { + return {}; + } + const leftYear = Number(match[1]); + const rightYear = Number(match[2]); + if (!Number.isFinite(leftYear) || + !Number.isFinite(rightYear) || + leftYear < 2000 || + leftYear > 2099 || + rightYear < 2000 || + rightYear > 2099) { + return {}; + } + const fromYear = Math.min(leftYear, rightYear); + const toYear = Math.max(leftYear, rightYear); + return { + period_from: `${fromYear}-01-01`, + period_to: `${toYear}-12-31` + }; +} function cleanupAnchorValue(value) { const normalized = String(value ?? "").trim(); if (!normalized) { @@ -84,11 +207,11 @@ function cleanupAnchorValue(value) { if (periodTailPattern.test(normalized)) { return normalized.replace(periodTailPattern, "").trim(); } - const allTimeTailPattern = /\s+за\s+вс[её]\s+время(?:\s+|$)[\s\S]*$/iu; + const allTimeTailPattern = /\s+за\s+(?:вс[её]\s+время|весь\s+период|весь\s+срок|всю\s+истори(?:ю|и)|любой\s+период|любой\s+срок)(?:\s+|$)[\s\S]*$/iu; if (allTimeTailPattern.test(normalized)) { return normalized.replace(allTimeTailPattern, "").trim(); } - const allTimeTailPatternEn = /\s+(?:for\s+all\s+time|all\s+time)(?:\s+|$)[\s\S]*$/iu; + const allTimeTailPatternEn = /\s+(?:for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)(?:\s+|$)[\s\S]*$/iu; if (allTimeTailPatternEn.test(normalized)) { return normalized.replace(allTimeTailPatternEn, "").trim(); } @@ -99,7 +222,186 @@ function cleanupAnchorValue(value) { } function hasAllTimeHint(text) { const value = String(text ?? ""); - return /(?:за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(value); + return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(value); +} +function extractLooseByAnchorValue(text) { + const match = String(text ?? "").match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (!match) { + return undefined; + } + const token = String(match[1] ?? "").trim(); + if (!token) { + return undefined; + } + const lowered = token.toLowerCase(); + const stopWords = new Set([ + "контрагенту", + "контрагента", + "контре", + "компании", + "компанию", + "организации", + "организацию", + "поставщику", + "поставщика", + "клиенту", + "клиента", + "покупателю", + "покупателя", + "партнеру", + "партнера", + "договору", + "договора", + "счету", + "счёту", + "дате", + "периоду", + "период", + "документам", + "докам", + "взаиморасчетам", + "взаиморасчётам" + ]); + if (stopWords.has(lowered)) { + return undefined; + } + return token; +} +function isLikelyCounterpartyToken(rawToken) { + const token = String(rawToken ?? "").trim(); + const lowered = token.toLowerCase(); + if (!token || token.length < 2) { + return false; + } + if (/^\d+$/.test(lowered)) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(lowered)) { + return false; + } + const stopWords = new Set([ + "за", + "с", + "по", + "на", + "и", + "или", + "док", + "доки", + "документ", + "документы", + "документов", + "банк", + "банковские", + "операции", + "платежи", + "платеж", + "платёж", + "контрагент", + "контрагенту", + "контрагента", + "компания", + "компании", + "организация", + "организации", + "год", + "года", + "г", + "плс", + "pls", + "пж", + "пжлст", + "пожалуйста", + "бля", + "блять", + "епт", + "ёпт", + "епта", + "нах", + "нахуй", + "покеж", + "покажи", + "выведи" + ]); + return !stopWords.has(lowered); +} +function hasDocsOrBankSignal(text) { + const lowered = String(text ?? "").toLowerCase(); + return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(lowered); +} +function extractCounterpartyFromFreeTextHeuristic(text) { + if (!hasDocsOrBankSignal(text)) { + return undefined; + } + const tokens = String(text ?? "") + .split(/[^a-zа-яё0-9._-]+/iu) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + if (tokens.length === 0) { + return undefined; + } + const monthTokens = [ + "янв", + "фев", + "мар", + "апр", + "май", + "июн", + "июл", + "авг", + "сен", + "сент", + "окт", + "ноя", + "дек", + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december" + ]; + for (const token of tokens) { + const lowered = token.toLowerCase(); + if (!isLikelyCounterpartyToken(lowered)) { + continue; + } + if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) { + continue; + } + if (monthTokens.some((prefix) => lowered.startsWith(prefix))) { + continue; + } + if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) { + continue; + } + return token; + } + return undefined; +} +function extractImplicitCounterpartyValue(text) { + const input = String(text ?? ""); + const beforeDocsMatch = input.match(/(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)(?=[\s,.;:!?)]|$)/iu); + if (beforeDocsMatch) { + const candidate = String(beforeDocsMatch[1] ?? "").trim(); + if (isLikelyCounterpartyToken(candidate)) { + return candidate; + } + } + const afterDocsMatch = input.match(/(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (afterDocsMatch) { + const candidate = String(afterDocsMatch[1] ?? "").trim(); + if (isLikelyCounterpartyToken(candidate)) { + return candidate; + } + } + return undefined; } function shiftDaysIso(baseIso, deltaDays) { const date = new Date(`${baseIso}T00:00:00.000Z`); @@ -137,6 +439,27 @@ function extractAddressFilters(userMessage, intent) { if (counterpartyMatch) { filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1])); } + if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) { + const fallbackCounterparty = extractLooseByAnchorValue(text); + if (fallbackCounterparty) { + filters.counterparty = cleanupAnchorValue(fallbackCounterparty); + warnings.push("counterparty_anchor_derived_from_loose_by_phrase"); + } + } + if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) { + const implicitCounterparty = extractImplicitCounterpartyValue(text); + if (implicitCounterparty) { + filters.counterparty = cleanupAnchorValue(implicitCounterparty); + warnings.push("counterparty_anchor_derived_from_implicit_phrase"); + } + } + if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) { + const heuristicCounterparty = extractCounterpartyFromFreeTextHeuristic(text); + if (heuristicCounterparty) { + filters.counterparty = cleanupAnchorValue(heuristicCounterparty); + warnings.push("counterparty_anchor_derived_from_free_text_heuristic"); + } + } const contractMatch = text.match(CONTRACT_PATTERN); if (contractMatch) { filters.contract = cleanupAnchorValue(String(contractMatch[1])); @@ -148,6 +471,30 @@ function extractAddressFilters(userMessage, intent) { if (periodRange.period_to) { filters.period_to = periodRange.period_to; } + if (!filters.period_from && !filters.period_to) { + const monthPeriod = extractMonthPeriod(text); + if (monthPeriod.period_from && monthPeriod.period_to) { + filters.period_from = monthPeriod.period_from; + filters.period_to = monthPeriod.period_to; + warnings.push("period_derived_from_month_phrase"); + } + } + if (!filters.period_from && !filters.period_to) { + const yearRangePeriod = extractYearRangePeriod(text); + if (yearRangePeriod.period_from && yearRangePeriod.period_to) { + filters.period_from = yearRangePeriod.period_from; + filters.period_to = yearRangePeriod.period_to; + warnings.push("period_derived_from_year_range_phrase"); + } + } + if (!filters.period_from && !filters.period_to) { + const yearPeriod = extractYearPeriod(text); + if (yearPeriod.period_from && yearPeriod.period_to) { + filters.period_from = yearPeriod.period_from; + filters.period_to = yearPeriod.period_to; + warnings.push("period_derived_from_year_phrase"); + } + } // If explicit period window exists, do not infer as_of_date from one of its boundary dates. if (!filters.period_from && !filters.period_to) { const asOfDate = extractAsOfDate(text); diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index a2ab9d2..c6ba454 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -62,23 +62,201 @@ const OPEN_ITEMS_HINTS = [ const DOCUMENTS_BY_COUNTERPARTY_HINTS = [ "documents by counterparty", "docs by counterparty", + "documents by company", + "documents by supplier", + "documents by customer", + "documents by client", + "documents by partner", "show documents by counterparty", "list documents by counterparty", + "документы по", + "доступные документы", + "список документов", "документ", + "доки", + "доки по", + "док по", "по контрагент" ]; const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [ "bank operations by counterparty", "bank payments by counterparty", "payment orders by counterparty", + "bank operations by company", + "bank operations by supplier", + "bank operations by customer", "show bank operations by counterparty", + "bank ops", + "transactions by counterparty", "банков", "выписк", - "платеж" + "платеж", + "платёж", + "оплат", + "списан", + "поступлен", + "движени" ]; function hasAny(text, patterns) { return patterns.some((item) => text.includes(item)); } +function isLikelyCounterpartyToken(rawToken) { + const token = String(rawToken ?? "").trim().toLowerCase(); + if (!token || token.length < 2) { + return false; + } + if (/^\d+$/.test(token)) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(token)) { + return false; + } + const stopWords = new Set([ + "за", + "с", + "по", + "на", + "и", + "или", + "док", + "доки", + "доки?", + "документ", + "документы", + "документов", + "банк", + "банковские", + "операции", + "платежи", + "платеж", + "платёж", + "контрагент", + "контрагенту", + "контрагента", + "компания", + "компании", + "организация", + "организации", + "год", + "года", + "г", + "плс", + "pls", + "пж", + "пжлст", + "пожалуйста", + "бля", + "блять", + "епт", + "ёпт", + "епта", + "нах", + "нахуй" + ]); + return !stopWords.has(token); +} +function hasPartyAnchorMention(text) { + return (text.includes("контраг") || + text.includes("контра") || + text.includes("counterparty") || + text.includes("компан") || + text.includes("company") || + text.includes("организац") || + text.includes("supplier") || + text.includes("vendor") || + text.includes("customer") || + text.includes("client") || + text.includes("partner") || + text.includes("поставщик") || + text.includes("клиент") || + text.includes("покупател") || + text.includes("партнер")); +} +function hasLooseByAnchorMention(text) { + const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (!match) { + return false; + } + const token = String(match[1] ?? "").toLowerCase(); + if (!token) { + return false; + } + const stopWords = new Set([ + "контрагенту", + "контрагента", + "контре", + "компании", + "компанию", + "организации", + "организацию", + "поставщику", + "поставщика", + "клиенту", + "клиента", + "покупателю", + "покупателя", + "партнеру", + "партнера", + "договору", + "договора", + "счету", + "счёту", + "дате", + "периоду", + "период", + "документам", + "докам" + ]); + return !stopWords.has(token); +} +function hasImplicitCounterpartyAnchorAroundDocs(text) { + const beforeDocsMatch = text.match(/(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)(?=[\s,.;:!?)]|$)/iu); + if (beforeDocsMatch && isLikelyCounterpartyToken(String(beforeDocsMatch[1] ?? ""))) { + return true; + } + const afterDocsMatch = text.match(/(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (afterDocsMatch && isLikelyCounterpartyToken(String(afterDocsMatch[1] ?? ""))) { + return true; + } + return false; +} +function hasDocsOrBankSignal(text) { + return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text); +} +function hasHeuristicCounterpartyAnchor(text) { + if (!hasDocsOrBankSignal(text)) { + return false; + } + const tokens = String(text ?? "") + .split(/[^a-zа-яё0-9._-]+/iu) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + for (const token of tokens) { + const lowered = token.toLowerCase(); + if (!isLikelyCounterpartyToken(lowered)) { + continue; + } + if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) { + continue; + } + if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) { + continue; + } + return true; + } + return false; +} +function hasGenericAddressLookupSignal(text) { + return (/\bесть\b/iu.test(text) || + /\bпокажи\b/iu.test(text) || + /\bвыведи\b/iu.test(text) || + /\bкакие\b/iu.test(text) || + /\bчто(?:-|\s)?то\b/iu.test(text) || + /за\s+любой\s+период/iu.test(text) || + /за\s+вс[её]\s+время/iu.test(text) || + /for\s+all\s+time/iu.test(text) || + /all\s+time/iu.test(text)); +} function hasAccountNumberAnchor(text) { return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text); } @@ -113,7 +291,7 @@ function resolveAddressIntent(userMessage) { }; } if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) && - (text.includes("контраг") || text.includes("counterparty"))) { + (hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) { return { intent: "bank_operations_by_counterparty", confidence: "medium", @@ -121,13 +299,23 @@ function resolveAddressIntent(userMessage) { }; } if (hasAny(text, DOCUMENTS_BY_COUNTERPARTY_HINTS) && - (text.includes("контраг") || text.includes("counterparty"))) { + (hasPartyAnchorMention(text) || + hasLooseByAnchorMention(text) || + hasImplicitCounterpartyAnchorAroundDocs(text) || + hasHeuristicCounterpartyAnchor(text))) { return { intent: "list_documents_by_counterparty", confidence: "medium", reasons: ["documents_by_counterparty_signal_detected"] }; } + if (hasLooseByAnchorMention(text) && hasGenericAddressLookupSignal(text)) { + return { + intent: "list_documents_by_counterparty", + confidence: "low", + 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", diff --git a/llm_normalizer/backend/dist/services/addressMcpClient.js b/llm_normalizer/backend/dist/services/addressMcpClient.js index 1014f3c..78e0ab2 100644 --- a/llm_normalizer/backend/dist/services/addressMcpClient.js +++ b/llm_normalizer/backend/dist/services/addressMcpClient.js @@ -1,7 +1,11 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.executeAddressMcpQuery = executeAddressMcpQuery; const config_1 = require("../config"); +const iconv_lite_1 = __importDefault(require("iconv-lite")); function toStringValue(value) { if (value === null || value === undefined) { return ""; @@ -20,8 +24,76 @@ function parseFiniteNumber(value) { } return null; } +function textMojibakeScore(value) { + const source = String(value ?? ""); + const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length; + const latin = (source.match(/[A-Za-z]/g) ?? []).length; + const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length; + const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length; + return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2; +} +function looksLikeMojibake(value) { + const source = String(value ?? ""); + if (!source.trim()) { + return false; + } + if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) { + return true; + } + return (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2; +} +function decodeUtf8FromWin1251Mojibake(value) { + if (!looksLikeMojibake(value)) { + return value; + } + try { + const bytes = iconv_lite_1.default.encode(value, "win1251"); + const decoded = bytes.toString("utf8"); + return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value; + } + catch { + return value; + } +} +function decodeUtf8FromLatin1Mojibake(value) { + if (!looksLikeMojibake(value)) { + return value; + } + try { + const decoded = Buffer.from(value, "latin1").toString("utf8"); + return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value; + } + catch { + return value; + } +} +function normalizeMojibakeString(value) { + const fromWin1251 = decodeUtf8FromWin1251Mojibake(value); + return decodeUtf8FromLatin1Mojibake(fromWin1251); +} +function normalizeMojibakeValue(value) { + if (typeof value === "string") { + return normalizeMojibakeString(value); + } + if (Array.isArray(value)) { + return value.map((item) => normalizeMojibakeValue(item)); + } + if (value && typeof value === "object") { + const source = value; + const normalized = {}; + for (const [key, raw] of Object.entries(source)) { + const repairedKey = normalizeMojibakeString(key); + normalized[repairedKey] = normalizeMojibakeValue(raw); + } + return normalized; + } + return value; +} +function normalizeMojibakeRows(rows) { + return rows.map((row) => normalizeMojibakeValue(row)); +} function parseRowsFromTextTable(source) { - const normalized = String(source ?? "").replace(/\r/g, "").trim(); + const normalized = normalizeMojibakeString(String(source ?? "")).replace(/\r/g, "").trim(); if (!normalized) { return []; } @@ -91,7 +163,7 @@ function parseRowsFromTextTable(source) { row.Amount = parseFiniteNumber(values[4]) ?? values[4]; rows.push(row); } - return rows; + return normalizeMojibakeRows(rows); } function parseExecutePayload(payload) { if (!payload || typeof payload !== "object") { @@ -110,9 +182,9 @@ function parseExecutePayload(payload) { }; } if (Array.isArray(source.data)) { - const rows = source.data + const rows = normalizeMojibakeRows(source.data .map((item) => (item && typeof item === "object" ? item : null)) - .filter((item) => item !== null); + .filter((item) => item !== null)); return { ok: true, rows, @@ -127,9 +199,9 @@ function parseExecutePayload(payload) { }; } if (source.data && typeof source.data === "object" && Array.isArray(source.data.rows)) { - const rows = (source.data.rows ?? []) + const rows = normalizeMojibakeRows((source.data.rows ?? []) .map((item) => (item && typeof item === "object" ? item : null)) - .filter((item) => item !== null); + .filter((item) => item !== null)); return { ok: true, rows, diff --git a/llm_normalizer/backend/dist/services/addressQueryClassifier.js b/llm_normalizer/backend/dist/services/addressQueryClassifier.js index 9c07bea..15baeca 100644 --- a/llm_normalizer/backend/dist/services/addressQueryClassifier.js +++ b/llm_normalizer/backend/dist/services/addressQueryClassifier.js @@ -27,6 +27,13 @@ const ADDRESS_ACTION_TOKENS = [ const ADDRESS_ENTITY_TOKENS = [ "counterparty", "counterparties", + "company", + "organization", + "supplier", + "vendor", + "customer", + "client", + "partner", "contract", "contracts", "account", @@ -42,10 +49,22 @@ const ADDRESS_ENTITY_TOKENS = [ "owes", "owed", "контрагент", + "контра", + "компан", + "организац", + "поставщик", + "клиент", + "покупател", + "партнер", + "банк", + "выписк", + "операц", "договор", "счет", "счёт", "документ", + "доки", + "док", "остаток", "дебитор", "кредитор", @@ -71,6 +90,54 @@ const DEEP_REASONING_TOKENS = [ "разрыв", "ошибк" ]; +function hasLooseByAnchorMention(text) { + const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (!match) { + return false; + } + const token = String(match[1] ?? "").toLowerCase(); + if (!token) { + return false; + } + const stopWords = new Set([ + "контрагенту", + "контрагента", + "контре", + "компании", + "компанию", + "организации", + "организацию", + "поставщику", + "поставщика", + "клиенту", + "клиента", + "покупателю", + "покупателя", + "партнеру", + "партнера", + "договору", + "договора", + "счету", + "счёту", + "дате", + "периоду", + "период", + "документам", + "докам", + "взаиморасчетам", + "взаиморасчётам" + ]); + return !stopWords.has(token); +} +function hasAddressFollowupSignal(text) { + if (/(?:за\s+любой\s+период|за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(text)) { + return true; + } + if (/(?:\bесть\s+что(?:-|\s)?то\b|\bесть\s+ли\b|\bчто\s+есть\b)/iu.test(text)) { + return true; + } + return false; +} function hasAnyToken(text, tokens) { return tokens.some((token) => text.includes(token)); } @@ -86,6 +153,8 @@ function detectAddressQuestionMode(userMessage) { const hasAddressAction = hasAnyToken(text, ADDRESS_ACTION_TOKENS); const hasAddressEntity = hasAnyToken(text, ADDRESS_ENTITY_TOKENS); const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS); + const hasLooseByAnchor = hasLooseByAnchorMention(text); + const hasFollowupSignal = hasAddressFollowupSignal(text); if (hasAddressAction && hasAddressEntity && !hasDeepReasoning) { return { mode: "address_query", @@ -93,6 +162,13 @@ function detectAddressQuestionMode(userMessage) { reasons: ["address_action_detected", "address_entity_detected"] }; } + if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal) && !hasDeepReasoning) { + return { + mode: "address_query", + confidence: "medium", + reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])] + }; + } if (hasAddressEntity && !hasDeepReasoning) { return { mode: "address_query", diff --git a/llm_normalizer/backend/dist/services/addressQueryService.js b/llm_normalizer/backend/dist/services/addressQueryService.js index 728fd3c..2c4f0c1 100644 --- a/llm_normalizer/backend/dist/services/addressQueryService.js +++ b/llm_normalizer/backend/dist/services/addressQueryService.js @@ -2,12 +2,11 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.AddressQueryService = void 0; const config_1 = require("../config"); -const addressQueryClassifier_1 = require("./addressQueryClassifier"); -const addressQueryShapeClassifier_1 = require("./addressQueryShapeClassifier"); -const addressIntentResolver_1 = require("./addressIntentResolver"); -const addressFilterExtractor_1 = require("./addressFilterExtractor"); const addressRecipeCatalog_1 = require("./addressRecipeCatalog"); const addressMcpClient_1 = require("./addressMcpClient"); +const decomposeStage_1 = require("./address_runtime/decomposeStage"); +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 PARTY_ANCHOR_STOPWORDS = new Set([ @@ -323,20 +322,56 @@ function applyIntentSpecificFilter(intent, rows) { } return rows; } -function formatTopRows(rows, limit = 6) { - return rows.slice(0, limit).map((row, index) => { - const period = row.period ?? "дата не указана"; - const amount = row.amount !== null ? `${row.amount}` : "сумма не указана"; - const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / "); - const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : ""; - return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`; - }); +function hasExplicitPeriodWindow(filters) { + return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) || + (typeof filters.period_to === "string" && filters.period_to.trim().length > 0)); } -function inferReplyType(responseType) { - if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { - return "factual"; +function canAutoBroadenPeriodWindow(intent, filters) { + if (!hasExplicitPeriodWindow(filters)) { + return false; } - return "partial_coverage"; + return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty"; +} +function toIsoDatePrefix(value) { + if (!value) { + return null; + } + const normalized = String(value).trim(); + if (!normalized) { + return null; + } + const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/); + if (match) { + return match[1]; + } + return null; +} +function deriveObservedPeriodWindow(rows) { + const dates = rows + .map((row) => toIsoDatePrefix(row.period)) + .filter((item) => Boolean(item)) + .sort(); + if (dates.length === 0) { + return { + period_from: null, + period_to: null + }; + } + return { + period_from: dates[0], + period_to: dates[dates.length - 1] + }; +} +function composeAutoBroadenedPeriodPrefix(requested, observed) { + const requestedFrom = typeof requested.period_from === "string" ? requested.period_from : null; + const requestedTo = typeof requested.period_to === "string" ? requested.period_to : null; + if (requestedFrom && requestedTo && observed.period_from && observed.period_to) { + return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные ${observed.period_from}..${observed.period_to}.`; + } + if (requestedFrom && requestedTo) { + return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные по этому якорю.`; + } + return "По заданному периоду строк не найдено; показаны ближайшие доступные данные по этому якорю."; } function runtimeReadinessForLimitedCategory(category) { if (category === "empty_match" || category === "missing_anchor") { @@ -449,90 +484,6 @@ function toLegacyMcpStatus(status) { } return status; } -function resolvePrimaryAnchor(intent, filters) { - const account = typeof filters.account === "string" ? filters.account.trim() : ""; - const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; - const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; - const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; - if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { - if (account) { - return { - anchor_type: "account", - anchor_value_raw: account, - anchor_value_resolved: account, - resolver_confidence: "high", - ambiguity_count: 0 - }; - } - } - if (counterparty) { - return { - anchor_type: "counterparty", - anchor_value_raw: counterparty, - anchor_value_resolved: counterparty, - resolver_confidence: "medium", - ambiguity_count: 0 - }; - } - if (contract) { - return { - anchor_type: "contract", - anchor_value_raw: contract, - anchor_value_resolved: contract, - resolver_confidence: "medium", - ambiguity_count: 0 - }; - } - if (documentRef) { - return { - anchor_type: "document_ref", - anchor_value_raw: documentRef, - anchor_value_resolved: documentRef, - resolver_confidence: "medium", - ambiguity_count: 0 - }; - } - return { - anchor_type: "unknown", - anchor_value_raw: null, - anchor_value_resolved: null, - resolver_confidence: "low", - ambiguity_count: 0 - }; -} -function refineAnchorFromRows(anchor, rows) { - if (rows.length === 0) { - return anchor; - } - if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { - return anchor; - } - const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); - if (!needleRaw) { - return anchor; - } - const candidates = uniqueStrings(rows - .flatMap((row) => row.analytics) - .map((value) => value.trim()) - .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw))); - if (candidates.length === 0) { - return anchor; - } - if (candidates.length === 1) { - return { - ...anchor, - anchor_value_resolved: candidates[0], - resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", - ambiguity_count: 0 - }; - } - return { - ...anchor, - anchor_value_resolved: candidates[0], - resolver_confidence: "low", - ambiguity_count: candidates.length - 1 - }; -} function composeLimitedReply(category, reason, nextStep) { const heading = category === "empty_match" ? "В live-данных по текущему фильтру записи не найдены." @@ -601,124 +552,19 @@ function buildLimitedExecutionResult(input) { } }; } -function contractCandidatesFromRows(rows) { - const candidates = []; - for (const row of rows) { - for (const token of [row.registrator, ...row.analytics]) { - const normalized = token.trim(); - if (!normalized) { - continue; - } - if (/договор|contract|дог\./i.test(normalized)) { - candidates.push(normalized); - } - } - } - return uniqueStrings(candidates); -} -function composeFactualReply(intent, rows) { - if (intent === "account_balance_snapshot") { - const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); - const lines = [ - "Адресный срез по счету собран (по движениям live MCP).", - `Строк отобрано: ${rows.length}.`, - `Сумма по отобранным движениям: ${movementSum}.`, - ...formatTopRows(rows, 4) - ]; - return { - responseType: "FACTUAL_SUMMARY", - text: lines.join("\n") - }; - } - if (intent === "documents_forming_balance") { - const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); - const lines = [ - "Собран drilldown документов, формирующих остаток по счету на указанную дату.", - `Документных строк отобрано: ${rows.length}.`, - `Сумма по отобранным движениям: ${movementSum}.`, - ...formatTopRows(rows, 8), - "Можно уточнить выборку по контрагенту, договору или периоду." - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - if (intent === "list_open_contracts") { - const contracts = contractCandidatesFromRows(rows); - const lines = [ - "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).", - `Строк движения: ${rows.length}.`, - `Договорных кандидатов: ${contracts.length}.` - ]; - lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - if (intent === "open_items_by_counterparty_or_contract") { - const lines = [ - "Собраны открытые позиции по указанному фильтру (контрагент/договор).", - `Строк отобрано: ${rows.length}.`, - ...formatTopRows(rows, 6) - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - if (intent === "list_documents_by_counterparty") { - const lines = [ - "Собран список документов по контрагенту (live address lane).", - `Строк отобрано: ${rows.length}.`, - ...formatTopRows(rows, 8) - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - if (intent === "bank_operations_by_counterparty") { - const lines = [ - "Собран список банковских операций по контрагенту (live address lane).", - `Строк отобрано: ${rows.length}.`, - ...formatTopRows(rows, 8) - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - const title = intent === "list_payables_counterparties" - ? "Срез обязательств (payables) собран по движениям с account scope 60/76." - : intent === "list_receivables_counterparties" - ? "Срез требований (receivables) собран по движениям с account scope 62/76." - : "Срез адресного запроса собран."; - const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; -} class AddressQueryService { - async tryHandle(userMessage) { + async tryHandle(userMessage, options = {}) { if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { return null; } - const mode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage); - if (mode.mode !== "address_query") { + const followupContext = options.followupContext ?? null; + const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext); + if (!decompose) { return null; } - const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage); - if (shape.shape === "EXPLAIN_OR_REASON") { - return null; - } - const intent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage); - const filters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent); - let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); + const { mode, shape, intent, filters, baseReasons } = decompose; + let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters); const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters); - const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons]; if (intent.intent === "unknown") { return buildLimitedExecutionResult({ mode, @@ -862,7 +708,7 @@ class AddressQueryService { normalizedRawRows.length > 0 && scopedRows.length === 0; const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows; - anchor = refineAnchorFromRows(anchor, normalizedRows); + anchor = (0, resolveStage_1.refineAnchorFromRows)(anchor, normalizedRows); const filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved ? { ...filters.extracted_filters, counterparty: anchor.anchor_value_resolved } : anchor.anchor_type === "contract" && anchor.anchor_value_resolved @@ -895,7 +741,7 @@ 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 && contractCandidatesFromRows(filteredRows).length === 0) { + if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && (0, composeStage_1.contractCandidatesFromRows)(filteredRows).length === 0) { return buildLimitedExecutionResult({ mode, shape, @@ -925,6 +771,105 @@ class AddressQueryService { reasons: baseReasons }); } + if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { + const autoBroadenedFilters = { ...filters.extracted_filters }; + delete autoBroadenedFilters.period_from; + delete autoBroadenedFilters.period_to; + const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, autoBroadenedFilters); + if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) { + const broadenedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters); + const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ + query: broadenedPlan.query, + limit: broadenedPlan.limit + }); + if (!broadenedMcp.error) { + const broadenedRawRows = toNormalizedRows(broadenedMcp.raw_rows); + const broadenedScopedRows = applyAccountScopeFilter(broadenedRawRows, broadenedPlan.account_scope); + const broadenedAccountScopeFallbackApplied = broadenedPlan.account_scope_mode === "preferred" && + broadenedPlan.account_scope.length > 0 && + broadenedRawRows.length > 0 && + broadenedScopedRows.length === 0; + const broadenedNormalizedRows = broadenedAccountScopeFallbackApplied ? broadenedRawRows : broadenedScopedRows; + let broadenedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, autoBroadenedFilters); + broadenedAnchor = (0, resolveStage_1.refineAnchorFromRows)(broadenedAnchor, broadenedNormalizedRows); + const broadenedFiltersForMatching = broadenedAnchor.anchor_type === "counterparty" && broadenedAnchor.anchor_value_resolved + ? { ...autoBroadenedFilters, counterparty: broadenedAnchor.anchor_value_resolved } + : broadenedAnchor.anchor_type === "contract" && broadenedAnchor.anchor_value_resolved + ? { ...autoBroadenedFilters, contract: broadenedAnchor.anchor_value_resolved } + : autoBroadenedFilters; + const broadenedAccountScopeAudit = buildAccountScopeAudit({ + intent: intent.intent, + filters: broadenedFiltersForMatching, + accountScope: broadenedPlan.account_scope, + rowsBeforeScope: broadenedRawRows.length, + rowsAfterScope: broadenedNormalizedRows.length + }); + const broadenedAnchorFilter = applyAddressFilters(broadenedNormalizedRows, broadenedFiltersForMatching); + const broadenedRowsByAnchor = broadenedAnchorFilter.rows; + const broadenedFilteredRows = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor); + if (broadenedFilteredRows.length > 0) { + const broadenedRowDiagnostics = deriveRowStageDiagnostics(broadenedMcp.raw_rows, broadenedNormalizedRows.length, broadenedNormalizedRows.length); + const broadenedStageStatus = deriveMcpStageStatus({ + rawRowsReceived: broadenedMcp.raw_rows.length, + rowsMaterialized: broadenedNormalizedRows.length, + rowsAnchorMatched: broadenedRowsByAnchor.length, + rowsMatched: broadenedFilteredRows.length + }); + const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows); + const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow); + const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows); + const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"]; + const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"]; + return { + handled: true, + reply_text: `${broadenedPrefix}\n${broadenedFactual.text}`, + reply_type: (0, composeStage_1.inferReplyType)(broadenedFactual.responseType), + response_type: broadenedFactual.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: broadenedSelection.selected_recipe.recipe_id, + mcp_call_status_legacy: toLegacyMcpStatus(broadenedStageStatus), + account_scope_mode: broadenedPlan.account_scope_mode, + account_scope_fallback_applied: broadenedAccountScopeFallbackApplied, + anchor_type: broadenedAnchor.anchor_type, + anchor_value_raw: broadenedAnchor.anchor_value_raw, + anchor_value_resolved: broadenedAnchor.anchor_value_resolved, + resolver_confidence: broadenedAnchor.resolver_confidence, + ambiguity_count: broadenedAnchor.ambiguity_count, + match_failure_stage: "none", + match_failure_reason: null, + mcp_call_status: broadenedStageStatus, + rows_fetched: broadenedMcp.fetched_rows, + raw_rows_received: broadenedMcp.raw_rows.length, + rows_after_account_scope: broadenedNormalizedRows.length, + rows_after_recipe_filter: broadenedRowsByAnchor.length, + rows_materialized: broadenedNormalizedRows.length, + rows_matched: broadenedFilteredRows.length, + raw_row_keys_sample: broadenedRowDiagnostics.rawRowKeysSample, + materialization_drop_reason: broadenedRowDiagnostics.materializationDropReason, + account_token_raw: broadenedAccountScopeAudit.accountTokenRaw, + account_token_normalized: broadenedAccountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: broadenedAccountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: broadenedAccountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: broadenedAccountScopeAudit.accountScopeDropReason, + runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", + limited_reason_category: null, + response_type: broadenedFactual.responseType, + limitations: broadenedLimitations, + reasons: broadenedReasons + } + }; + } + } + } + } if (filteredRows.length === 0) { const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0; const hadAnchorMatchedRows = filterByAnchors.length > 0; @@ -992,11 +937,11 @@ class AddressQueryService { reasons: baseReasons }); } - const factual = composeFactualReply(intent.intent, filteredRows); + const factual = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows); return { handled: true, reply_text: factual.text, - reply_type: inferReplyType(factual.responseType), + reply_type: (0, composeStage_1.inferReplyType)(factual.responseType), response_type: factual.responseType, debug: { detected_mode: mode.mode, diff --git a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js index 08840e1..632fff1 100644 --- a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js +++ b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js @@ -122,6 +122,8 @@ const BASE_RECIPES = [ account_scope_mode: "strict" } ]; +const ADDRESS_MAX_LIMIT_DEFAULT = 200; +const ADDRESS_MAX_LIMIT_EXTENDED = 1000; function toDateTimeExpr(isoDate, endOfDay) { const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!match) { @@ -172,6 +174,12 @@ function shouldBoostLimitForAllTimeCounterparty(filters) { (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0)); return !hasPeriod; } +function maxLimitForIntent(intent) { + if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { + return ADDRESS_MAX_LIMIT_EXTENDED; + } + return ADDRESS_MAX_LIMIT_DEFAULT; +} function selectAddressRecipe(intent, filters) { const recipe = BASE_RECIPES.find((item) => item.intent === intent) ?? null; if (!recipe) { @@ -192,14 +200,19 @@ function selectAddressRecipe(intent, filters) { }; } function buildAddressRecipePlan(recipe, filters) { + const maxLimit = maxLimitForIntent(recipe.intent); const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit) - ? Math.max(1, Math.min(200, Math.trunc(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") && shouldBoostLimitForAllTimeCounterparty(filters) - ? Math.max(baseLimit, 200) - : baseLimit; - const resolvedLimit = Math.max(1, Math.min(200, boostedLimit)); + ? Math.max(baseLimit, maxLimit) + : (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && + typeof filters.account === "string" && + filters.account.trim().length > 0 + ? Math.max(baseLimit, ADDRESS_MAX_LIMIT_DEFAULT) + : baseLimit; + const resolvedLimit = Math.max(1, Math.min(maxLimit, boostedLimit)); const accountScope = (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && filters.account ? [String(filters.account)] : Array.isArray(recipe.account_scope) diff --git a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js new file mode 100644 index 0000000..950e226 --- /dev/null +++ b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js @@ -0,0 +1,125 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.contractCandidatesFromRows = contractCandidatesFromRows; +exports.composeFactualReply = composeFactualReply; +exports.inferReplyType = inferReplyType; +function uniqueStrings(values) { + return Array.from(new Set(values + .map((item) => item.trim()) + .filter((item) => item.length > 0))); +} +function formatTopRows(rows, limit = 6) { + return rows.slice(0, limit).map((row, index) => { + const period = row.period ?? "дата не указана"; + const amount = row.amount !== null ? `${row.amount}` : "сумма не указана"; + const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / "); + const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : ""; + return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`; + }); +} +function contractCandidatesFromRows(rows) { + const candidates = []; + for (const row of rows) { + for (const token of [row.registrator, ...row.analytics]) { + const normalized = token.trim(); + if (!normalized) { + continue; + } + if (/договор|contract|дог\./i.test(normalized)) { + candidates.push(normalized); + } + } + } + return uniqueStrings(candidates); +} +function composeFactualReply(intent, rows) { + if (intent === "account_balance_snapshot") { + const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); + const lines = [ + "Адресный срез по счету собран (по движениям live MCP).", + `Строк отобрано: ${rows.length}.`, + `Сумма по отобранным движениям: ${movementSum}.`, + ...formatTopRows(rows, 4) + ]; + return { + responseType: "FACTUAL_SUMMARY", + text: lines.join("\n") + }; + } + if (intent === "documents_forming_balance") { + const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); + const lines = [ + "Собран drilldown документов, формирующих остаток по счету на указанную дату.", + `Документных строк отобрано: ${rows.length}.`, + `Сумма по отобранным движениям: ${movementSum}.`, + ...formatTopRows(rows, 8), + "Можно уточнить выборку по контрагенту, договору или периоду." + ]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + if (intent === "list_open_contracts") { + const contracts = contractCandidatesFromRows(rows); + const lines = [ + "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).", + `Строк движения: ${rows.length}.`, + `Договорных кандидатов: ${contracts.length}.` + ]; + lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + if (intent === "open_items_by_counterparty_or_contract") { + const lines = [ + "Собраны открытые позиции по указанному фильтру (контрагент/договор).", + `Строк отобрано: ${rows.length}.`, + ...formatTopRows(rows, 6) + ]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + if (intent === "list_documents_by_counterparty") { + 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).", + `Строк отобрано: ${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" + ? "Срез требований (receivables) собран по движениям с account scope 62/76." + : "Срез адресного запроса собран."; + const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; +} +function inferReplyType(responseType) { + if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { + return "factual"; + } + return "partial_coverage"; +} diff --git a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js new file mode 100644 index 0000000..034323f --- /dev/null +++ b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js @@ -0,0 +1,181 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal; +exports.runAddressDecomposeStage = runAddressDecomposeStage; +const addressQueryClassifier_1 = require("../addressQueryClassifier"); +const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier"); +const addressIntentResolver_1 = require("../addressIntentResolver"); +const addressFilterExtractor_1 = require("../addressFilterExtractor"); +function hasExplicitPeriodWindow(filters) { + return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) || + (typeof filters.period_to === "string" && filters.period_to.trim().length > 0)); +} +function toNonEmptyString(value) { + if (value === null || value === undefined) { + return null; + } + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : null; +} +function hasAllTimeHint(text) { + return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(String(text ?? "")); +} +function hasAddressFollowupContextSignal(text) { + const normalized = String(text ?? "").trim(); + if (!normalized) { + return false; + } + if (hasAllTimeHint(normalized)) { + return true; + } + if (/(?:^|\s)(?:и|а\s+еще|а\s+ещё|еще|ещё|также|по\s+этому|по\s+тому|это\s+же|в\s+этом|тот\s+же|also|same|that)/iu.test(normalized)) { + return true; + } + return normalized.split(/\s+/).filter(Boolean).length <= 8; +} +function mergeFollowupFilters(current, intent, userMessage, followupContext) { + const merged = { ...current }; + const reasons = []; + if (!followupContext) { + return { filters: merged, reasons }; + } + const previous = followupContext.previous_filters ?? {}; + const previousAnchorValue = toNonEmptyString(followupContext.previous_anchor_value); + const previousCounterparty = toNonEmptyString(previous.counterparty); + const previousContract = toNonEmptyString(previous.contract); + const previousAccount = toNonEmptyString(previous.account); + const allTimeRequested = hasAllTimeHint(userMessage); + if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { + if (!toNonEmptyString(merged.counterparty)) { + const inheritedCounterparty = previousCounterparty ?? + (followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null); + if (inheritedCounterparty) { + merged.counterparty = inheritedCounterparty; + reasons.push("counterparty_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); + if (inheritedAccount) { + merged.account = inheritedAccount; + reasons.push("account_from_followup_context"); + } + } + } + if (intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts") { + 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 (!toNonEmptyString(merged.counterparty)) { + const inheritedCounterparty = previousCounterparty ?? + (followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null); + if (inheritedCounterparty) { + merged.counterparty = inheritedCounterparty; + reasons.push("counterparty_from_followup_context"); + } + } + } + if (allTimeRequested) { + if (toNonEmptyString(merged.period_from) || toNonEmptyString(merged.period_to)) { + delete merged.period_from; + delete merged.period_to; + reasons.push("period_cleared_by_all_time_followup"); + } + return { filters: merged, reasons }; + } + const currentHasPeriod = hasExplicitPeriodWindow(merged); + const previousHasPeriod = hasExplicitPeriodWindow(previous); + if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) { + if (toNonEmptyString(previous.period_from)) { + merged.period_from = previous.period_from; + } + if (toNonEmptyString(previous.period_to)) { + merged.period_to = previous.period_to; + } + reasons.push("period_from_followup_context"); + } + return { filters: merged, reasons }; +} +function resolveMissingRequiredFilters(intent, filters) { + const requiredByIntent = { + account_balance_snapshot: ["account", "as_of_date"], + documents_forming_balance: ["account", "as_of_date"], + list_documents_by_counterparty: ["counterparty"], + bank_operations_by_counterparty: ["counterparty"] + }; + const required = requiredByIntent[intent] ?? []; + return required.filter((key) => { + const value = filters[key]; + return value === undefined || value === null || String(value).trim() === ""; + }); +} +function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext) { + if (!followupContext || !followupContext.previous_intent) { + return detectedIntent; + } + if (detectedIntent.intent !== "unknown") { + return detectedIntent; + } + if (!hasAddressFollowupContextSignal(userMessage)) { + return detectedIntent; + } + return { + intent: followupContext.previous_intent, + confidence: "low", + reasons: [...detectedIntent.reasons, "intent_from_followup_context"] + }; +} +function runAddressDecomposeStage(userMessage, followupContext) { + const detectedMode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage); + const mode = detectedMode.mode === "address_query" + ? detectedMode + : followupContext && hasAddressFollowupContextSignal(userMessage) + ? { + mode: "address_query", + confidence: "medium", + reasons: [...detectedMode.reasons, "address_mode_from_followup_context"] + } + : detectedMode; + if (mode.mode !== "address_query") { + return null; + } + const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage); + if (shape.shape === "EXPLAIN_OR_REASON") { + return null; + } + const detectedIntent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage); + const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext); + const extractedFilters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent); + const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext); + const filters = { + extracted_filters: followupMerged.filters, + missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters), + warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])] + }; + const followupContextApplied = Boolean(followupContext) && + (mode.reasons.includes("address_mode_from_followup_context") || + intent.reasons.includes("intent_from_followup_context") || + followupMerged.reasons.length > 0); + const baseReasons = [ + ...mode.reasons, + ...shape.reasons, + ...intent.reasons, + ...followupMerged.reasons, + ...(followupContextApplied ? ["address_followup_context_applied"] : []) + ]; + return { + mode, + shape, + intent, + filters, + baseReasons + }; +} diff --git a/llm_normalizer/backend/dist/services/address_runtime/resolveStage.js b/llm_normalizer/backend/dist/services/address_runtime/resolveStage.js new file mode 100644 index 0000000..f7fe364 --- /dev/null +++ b/llm_normalizer/backend/dist/services/address_runtime/resolveStage.js @@ -0,0 +1,179 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolvePrimaryAnchor = resolvePrimaryAnchor; +exports.refineAnchorFromRows = refineAnchorFromRows; +const PARTY_ANCHOR_STOPWORDS = new Set([ + "ооо", + "ао", + "зао", + "ип", + "llc", + "ltd", + "company", + "компания", + "контрагент", + "counterparty", + "по", + "by" +]); +function transliterateCyrillicToLatin(value) { + const map = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "ts", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya" + }; + let out = ""; + for (const char of String(value ?? "").toLowerCase()) { + out += map[char] ?? char; + } + return out; +} +function normalizeSearchText(value) { + return String(value ?? "") + .toLowerCase() + .replace(/ё/g, "е") + .replace(/[^a-zа-я0-9]+/gi, " ") + .replace(/\s+/g, " ") + .trim(); +} +function tokenizeAnchor(value) { + return normalizeSearchText(value) + .split(" ") + .map((token) => token.trim()) + .filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token)); +} +function matchesAnchorText(searchable, anchor) { + const searchableNormalized = normalizeSearchText(searchable); + const searchableLatin = transliterateCyrillicToLatin(searchableNormalized); + const tokens = tokenizeAnchor(anchor); + if (tokens.length === 0) { + const direct = normalizeSearchText(anchor); + if (!direct) { + return false; + } + return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct)); + } + return tokens.every((token) => { + const tokenLatin = transliterateCyrillicToLatin(token); + return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin); + }); +} +function uniqueStrings(values) { + return Array.from(new Set(values + .map((item) => item.trim()) + .filter((item) => item.length > 0))); +} +function resolvePrimaryAnchor(intent, filters) { + const account = typeof filters.account === "string" ? filters.account.trim() : ""; + const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; + const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; + const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; + if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { + if (account) { + return { + anchor_type: "account", + anchor_value_raw: account, + anchor_value_resolved: account, + resolver_confidence: "high", + ambiguity_count: 0 + }; + } + } + if (counterparty) { + return { + anchor_type: "counterparty", + anchor_value_raw: counterparty, + anchor_value_resolved: counterparty, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + if (contract) { + return { + anchor_type: "contract", + anchor_value_raw: contract, + anchor_value_resolved: contract, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + if (documentRef) { + return { + anchor_type: "document_ref", + anchor_value_raw: documentRef, + anchor_value_resolved: documentRef, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + return { + anchor_type: "unknown", + anchor_value_raw: null, + anchor_value_resolved: null, + resolver_confidence: "low", + ambiguity_count: 0 + }; +} +function refineAnchorFromRows(anchor, rows) { + if (rows.length === 0) { + return anchor; + } + if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { + return anchor; + } + const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); + if (!needleRaw) { + return anchor; + } + const candidates = uniqueStrings(rows + .flatMap((row) => row.analytics) + .map((value) => value.trim()) + .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw))); + if (candidates.length === 0) { + return anchor; + } + if (candidates.length === 1) { + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", + ambiguity_count: 0 + }; + } + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: "low", + ambiguity_count: candidates.length - 1 + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 302e253..920ff19 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -1731,8 +1731,9 @@ function buildAddressCoverageReport() { out_of_scope_requirements: [] }; } -function buildAddressDebugPayload(addressDebug) { +function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) { const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded"; + const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null; return { trace_id: `address-${(0, nanoid_1.nanoid)(10)}`, prompt_version: "address_query_runtime_v1", @@ -1790,12 +1791,204 @@ function buildAddressDebugPayload(addressDebug) { runtime_readiness: addressDebug.runtime_readiness, limited_reason_category: addressDebug.limited_reason_category, response_type: addressDebug.response_type, + execution_lane: "address_query", + llm_decomposition_applied: Boolean(llmMeta?.applied), + llm_decomposition_attempted: Boolean(llmMeta?.attempted), + llm_provider_used: llmMeta?.provider ?? null, + llm_decomposition_trace_id: llmMeta?.traceId ?? null, + llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null, + llm_decomposition_reason: llmMeta?.reason ?? null, answer_structure_v11: null, investigation_state_snapshot: null, normalized: null, - normalizer_output: null + normalizer_output: llmMeta?.traceId + ? { + trace_id: llmMeta.traceId, + prompt_version: "normalizer_v2_0_2", + applied: Boolean(llmMeta?.applied), + effective_message: llmMeta?.effectiveMessage ?? null + } + : null }; } +function toNonEmptyString(value) { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} +function readAddressFilterString(addressDebug, key) { + const filters = addressDebug?.extracted_filters; + if (!filters || typeof filters !== "object") { + return null; + } + return toNonEmptyString(filters[key]); +} +function findLastAddressAssistantDebug(items) { + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (!item || item.role !== "assistant" || !item.debug) { + continue; + } + const debug = item.debug; + if (debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1") { + return debug; + } + } + return null; +} +function hasAddressFollowupContextSignal(userMessage) { + const text = compactWhitespace(String(userMessage ?? "").toLowerCase()); + if (!text) { + return false; + } + if (/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu.test(text)) { + return true; + } + if (hasReferentialPointer(text)) { + return true; + } + const shortFollowup = countTokens(text) <= 8; + if (shortFollowup && hasFollowupMarker(text)) { + return true; + } + return false; +} +function resolveAddressFollowupCarryoverContext(userMessage, items) { + if (!hasAddressFollowupContextSignal(userMessage)) { + return null; + } + const previousAddressDebug = findLastAddressAssistantDebug(items); + if (!previousAddressDebug) { + return null; + } + const previousIntent = toNonEmptyString(previousAddressDebug.detected_intent); + const previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type); + const previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ?? + toNonEmptyString(previousAddressDebug.anchor_value_raw) ?? + readAddressFilterString(previousAddressDebug, "counterparty") ?? + readAddressFilterString(previousAddressDebug, "account") ?? + readAddressFilterString(previousAddressDebug, "contract"); + const previousFiltersRaw = previousAddressDebug.extracted_filters; + const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" + ? { ...previousFiltersRaw } + : {}; + if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) { + return null; + } + return { + followupContext: { + previous_intent: previousIntent ?? undefined, + previous_filters: previousFilters, + previous_anchor_type: previousAnchorType ?? undefined, + previous_anchor_value: previousAnchor + }, + previousAddressIntent: previousIntent, + previousAddressAnchor: previousAnchor + }; +} +function isAddressLlmPreDecomposeCandidate(userMessage) { + const text = compactWhitespace(String(userMessage ?? "").toLowerCase()); + if (!text) { + return false; + } + return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|банк|выписк|платеж|оплат|поступлен|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?)/i.test(text); +} +function extractAddressQuestionFromNormalized(normalized) { + if (!normalized || typeof normalized !== "object") { + return null; + } + const source = normalized; + const fragments = Array.isArray(source.fragments) ? source.fragments : []; + for (const item of fragments) { + if (!item || typeof item !== "object") { + continue; + } + const fragment = item; + const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase(); + if (domainRelevance === "out_of_scope") { + continue; + } + const readiness = String(fragment.execution_readiness ?? "").trim().toLowerCase(); + if (readiness === "no_route") { + continue; + } + const normalizedText = toNonEmptyString(fragment.normalized_fragment_text); + const rawText = toNonEmptyString(fragment.raw_fragment_text); + const candidate = compactWhitespace(normalizedText ?? rawText ?? ""); + if (candidate.length >= 3 && candidate.length <= 500) { + return candidate; + } + } + return null; +} +async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) { + const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null; + const baseMeta = { + attempted: false, + applied: false, + provider, + traceId: null, + effectiveMessage: userMessage, + reason: "not_attempted" + }; + if (Boolean(payload?.useMock)) { + return { + ...baseMeta, + reason: "skipped_in_mock" + }; + } + if (!isAddressLlmPreDecomposeCandidate(userMessage)) { + return { + ...baseMeta, + reason: "not_address_like" + }; + } + const normalizePayload = { + llmProvider: payload?.llmProvider, + apiKey: payload?.apiKey, + model: payload?.model, + baseUrl: payload?.baseUrl, + temperature: 0, + maxOutputTokens: payload?.maxOutputTokens, + promptVersion: "normalizer_v2_0_2", + userQuestion: userMessage, + context: payload?.context, + useMock: Boolean(payload?.useMock), + retryPolicy: "single-pass-strict" + }; + try { + const normalized = await normalizerService.normalize(normalizePayload); + const candidate = extractAddressQuestionFromNormalized(normalized?.normalized); + if (!normalized?.ok || !candidate) { + return { + ...baseMeta, + attempted: true, + traceId: normalized?.trace_id ?? null, + reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed" + }; + } + const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const candidateCompact = compactWhitespace(candidate.toLowerCase()); + const applied = sourceCompact !== candidateCompact; + return { + attempted: true, + applied, + provider, + traceId: normalized?.trace_id ?? null, + effectiveMessage: applied ? candidate : userMessage, + reason: applied ? "normalized_fragment_applied" : "normalized_fragment_same" + }; + } + catch (error) { + return { + ...baseMeta, + attempted: true, + reason: `error:${error instanceof Error ? error.message : String(error)}` + }; + } +} class AssistantService { normalizerService; sessions; @@ -1827,80 +2020,112 @@ class AssistantService { debug: null }; this.sessions.appendItem(sessionId, userItem); - if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { - const addressLane = await this.addressQueryService.tryHandle(userMessage); - if (addressLane?.handled) { - const debug = buildAddressDebugPayload(addressLane.debug); - const assistantItem = { - message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, - session_id: sessionId, - role: "assistant", - text: addressLane.reply_text, - reply_type: addressLane.reply_type, - created_at: new Date().toISOString(), - trace_id: debug.trace_id, - debug - }; - this.sessions.appendItem(sessionId, assistantItem); - const current = this.sessions.getSession(sessionId); - if (current) { - this.sessionLogger.persistSession(current); - } - const conversation = cloneItems(current?.items ?? []); - (0, log_1.logJson)({ - timestamp: new Date().toISOString(), - level: "info", - service: "assistant_loop", - message: "assistant_message_processed", - sessionId, - eventType: "assistant_message_address", - details: { - session_id: sessionId, - message_id: assistantItem.message_id, - user_message: userMessage, - detected_mode: addressLane.debug.detected_mode, - query_shape: addressLane.debug.query_shape, - detected_intent: addressLane.debug.detected_intent, - extracted_filters: addressLane.debug.extracted_filters, - selected_recipe: addressLane.debug.selected_recipe, - mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, - account_scope_mode: addressLane.debug.account_scope_mode, - account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, - anchor_type: addressLane.debug.anchor_type, - resolver_confidence: addressLane.debug.resolver_confidence, - match_failure_stage: addressLane.debug.match_failure_stage, - match_failure_reason: addressLane.debug.match_failure_reason, - mcp_call_status: addressLane.debug.mcp_call_status, - rows_fetched: addressLane.debug.rows_fetched, - raw_rows_received: addressLane.debug.raw_rows_received, - rows_after_account_scope: addressLane.debug.rows_after_account_scope, - rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, - rows_materialized: addressLane.debug.rows_materialized, - rows_matched: addressLane.debug.rows_matched, - materialization_drop_reason: addressLane.debug.materialization_drop_reason, - account_token_raw: addressLane.debug.account_token_raw, - account_token_normalized: addressLane.debug.account_token_normalized, - account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, - account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, - account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, - runtime_readiness: addressLane.debug.runtime_readiness, - limited_reason_category: addressLane.debug.limited_reason_category, - response_type: addressLane.debug.response_type, - limitations: addressLane.debug.limitations, - assistant_reply: assistantItem.text, - reply_type: assistantItem.reply_type, - trace_id: assistantItem.trace_id - } - }); - return { - ok: true, + const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { + 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, + reply_type: addressLane.reply_type, + created_at: new Date().toISOString(), + trace_id: debug.trace_id, + debug + }; + this.sessions.appendItem(sessionId, assistantItem); + const current = this.sessions.getSession(sessionId); + if (current) { + this.sessionLogger.persistSession(current); + } + const conversation = cloneItems(current?.items ?? []); + (0, log_1.logJson)({ + timestamp: new Date().toISOString(), + level: "info", + service: "assistant_loop", + message: "assistant_message_processed", + sessionId, + eventType: "assistant_message_address", + details: { session_id: sessionId, + message_id: assistantItem.message_id, + user_message: userMessage, + effective_address_user_message: effectiveAddressUserMessage, + address_followup_context_applied: Boolean(carryoverMeta), + address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null, + address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null, + address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted), + address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied), + address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null, + address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null, + address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null, + detected_mode: addressLane.debug.detected_mode, + query_shape: addressLane.debug.query_shape, + detected_intent: addressLane.debug.detected_intent, + extracted_filters: addressLane.debug.extracted_filters, + selected_recipe: addressLane.debug.selected_recipe, + mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, + account_scope_mode: addressLane.debug.account_scope_mode, + account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, + anchor_type: addressLane.debug.anchor_type, + resolver_confidence: addressLane.debug.resolver_confidence, + match_failure_stage: addressLane.debug.match_failure_stage, + match_failure_reason: addressLane.debug.match_failure_reason, + mcp_call_status: addressLane.debug.mcp_call_status, + rows_fetched: addressLane.debug.rows_fetched, + raw_rows_received: addressLane.debug.raw_rows_received, + rows_after_account_scope: addressLane.debug.rows_after_account_scope, + rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, + rows_materialized: addressLane.debug.rows_materialized, + rows_matched: addressLane.debug.rows_matched, + materialization_drop_reason: addressLane.debug.materialization_drop_reason, + account_token_raw: addressLane.debug.account_token_raw, + account_token_normalized: addressLane.debug.account_token_normalized, + account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, + account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, + account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, + runtime_readiness: addressLane.debug.runtime_readiness, + limited_reason_category: addressLane.debug.limited_reason_category, + response_type: addressLane.debug.response_type, + limitations: addressLane.debug.limitations, assistant_reply: assistantItem.text, reply_type: assistantItem.reply_type, - conversation_item: assistantItem, - debug, - conversation + trace_id: assistantItem.trace_id + } + }); + return { + ok: true, + session_id: sessionId, + assistant_reply: assistantItem.text, + reply_type: assistantItem.reply_type, + conversation_item: assistantItem, + debug, + conversation + }; + }; + if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { + const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 + ? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage) + : { + attempted: false, + applied: false, + provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null, + traceId: null, + effectiveMessage: userMessage, + reason: "disabled_by_feature_flag" }; + const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage; + const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage); + if (primaryAddressLane?.handled) { + return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressPreDecompose); + } + const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items); + if (carryover?.followupContext) { + const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, { + followupContext: carryover.followupContext + }); + if (contextualAddressLane?.handled) { + return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose); + } } } const followupBinding = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && @@ -1917,12 +2142,13 @@ class AssistantService { usage: null }; const normalizePayload = { + llmProvider: payload.llmProvider, apiKey: payload.apiKey, model: payload.model, baseUrl: payload.baseUrl, temperature: payload.temperature, maxOutputTokens: payload.maxOutputTokens, - promptVersion: payload.promptVersion ?? "normalizer_v2_0_2", + promptVersion: payload.promptVersion ?? "address_query_runtime_v1", systemPrompt: payload.systemPrompt, developerPrompt: payload.developerPrompt, domainPrompt: payload.domainPrompt, diff --git a/llm_normalizer/backend/dist/services/normalizerService.js b/llm_normalizer/backend/dist/services/normalizerService.js index 8675452..bf7c07d 100644 --- a/llm_normalizer/backend/dist/services/normalizerService.js +++ b/llm_normalizer/backend/dist/services/normalizerService.js @@ -871,6 +871,7 @@ class NormalizerService { async normalize(payload) { const traceId = (0, nanoid_1.nanoid)(14); const startedAt = Date.now(); + const llmProvider = payload.llmProvider === "local" ? "local" : "openai"; const model = payload.model ?? config_1.DEFAULT_MODEL; const baseUrl = payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL; const temperature = payload.temperature ?? config_1.DEFAULT_TEMPERATURE; @@ -903,6 +904,7 @@ class NormalizerService { else { const apiKey = payload.apiKey ?? process.env.OPENAI_API_KEY; const firstTry = await this.openaiClient.normalize({ + llmProvider, apiKey: String(apiKey ?? ""), model, baseUrl, @@ -946,6 +948,7 @@ class NormalizerService { if (!payload.useMock && !validation.passed && canRetry) { const retryMaxOutputTokens = computeRetryMaxOutputTokens(maxOutputTokens, rawModelResponse); const retry = await this.openaiClient.normalize({ + llmProvider, apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""), model, baseUrl, diff --git a/llm_normalizer/backend/dist/services/openaiResponsesClient.js b/llm_normalizer/backend/dist/services/openaiResponsesClient.js index bcc81ae..3686563 100644 --- a/llm_normalizer/backend/dist/services/openaiResponsesClient.js +++ b/llm_normalizer/backend/dist/services/openaiResponsesClient.js @@ -8,6 +8,20 @@ const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const config_1 = require("../config"); const http_1 = require("../utils/http"); +function resolveProvider(config) { + return config.llmProvider === "local" ? "local" : "openai"; +} +function resolveApiKey(config) { + const candidate = String(config.apiKey ?? "").trim(); + if (candidate.length > 0) { + return candidate; + } + if (resolveProvider(config) === "local") { + // Local OpenAI-compatible servers often accept any token. + return "local-dev-token"; + } + throw new http_1.ApiError("OPENAI_API_KEY_MISSING", "OpenAI API key is missing.", 400); +} function extractUsage(raw) { const usage = (raw.usage ?? {}); const input = Number(usage.input_tokens ?? usage.prompt_tokens ?? 0); @@ -19,7 +33,7 @@ function extractUsage(raw) { total_tokens: Number.isFinite(total) ? total : 0 }; } -function extractOutputText(raw) { +function extractOutputTextFromResponses(raw) { if (typeof raw.output_text === "string" && raw.output_text.trim().length > 0) { return raw.output_text; } @@ -51,7 +65,55 @@ function extractOutputText(raw) { return nested.output_text; } } - throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Не удалось извлечь output_text из Responses API ответа.", 502, raw); + throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Failed to extract output_text from /responses payload.", 502, raw); +} +function extractOutputTextFromChatCompletions(raw) { + const choices = raw.choices; + if (!Array.isArray(choices) || choices.length === 0) { + throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Missing choices in /chat/completions payload.", 502, raw); + } + const first = choices[0]; + if (!first || typeof first !== "object") { + throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Invalid first choice in /chat/completions payload.", 502, raw); + } + const message = first.message; + if (!message || typeof message !== "object") { + throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Missing message in /chat/completions payload.", 502, raw); + } + const content = message.content; + if (typeof content === "string" && content.trim().length > 0) { + return content; + } + if (Array.isArray(content)) { + const textParts = content + .map((item) => { + if (!item || typeof item !== "object") { + return ""; + } + const block = item; + return typeof block.text === "string" ? block.text : ""; + }) + .filter((item) => item.trim().length > 0); + if (textParts.length > 0) { + return textParts.join("\n"); + } + } + throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Failed to extract text from /chat/completions payload.", 502, raw); +} +function shouldFallbackToChatCompletions(error) { + if (!(error instanceof http_1.ApiError)) { + return false; + } + if (error.code !== "OPENAI_REQUEST_FAILED") { + return false; + } + const details = (error.details ?? {}); + const status = Number(details.status ?? 0); + if ([404, 405, 501].includes(status)) { + return true; + } + const message = String(error.message ?? "").toLowerCase(); + return message.includes("/responses") || message.includes("responses"); } function loadSchemaForTransport(schemaVersion) { const schemaFile = schemaVersion === "v1" @@ -64,19 +126,54 @@ function loadSchemaForTransport(schemaVersion) { const schemaPath = path_1.default.resolve(config_1.SCHEMAS_DIR, schemaFile); return JSON.parse(fs_1.default.readFileSync(schemaPath, "utf-8")); } +function buildBaseUrlCandidates(config) { + const base = (config.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, ""); + const provider = resolveProvider(config); + if (provider !== "local") { + return [base]; + } + const hasVersionSuffix = /\/v\d+$/i.test(base); + if (hasVersionSuffix) { + return [base]; + } + return Array.from(new Set([base, `${base}/v1`])); +} class OpenAIResponsesClient { + async listModels(config) { + const payload = await this.getModels(config); + const data = Array.isArray(payload.data) ? payload.data : []; + const ids = data + .map((item) => { + if (!item || typeof item !== "object") { + return ""; + } + return String(item.id ?? "").trim(); + }) + .filter((item) => item.length > 0); + return Array.from(new Set(ids)); + } async testConnection(config) { - const payload = { + const provider = resolveProvider(config); + if (provider === "local") { + try { + await this.getModels(config); + } + catch { + // Some local providers do not expose /models consistently; fallback to a tiny chat call. + await this.postChatCompletions(config, { + model: config.model, + messages: [{ role: "user", content: "ping" }], + max_tokens: 4, + temperature: 0 + }); + } + return { ok: true, model: config.model }; + } + await this.postResponses(config, { model: config.model, - input: [ - { - role: "user", - content: [{ type: "input_text", text: "ping" }] - } - ], + input: [{ role: "user", content: [{ type: "input_text", text: "ping" }] }], max_output_tokens: 16 - }; - await this.post(config, payload); + }); return { ok: true, model: config.model }; } async normalize(config, prompt) { @@ -91,7 +188,7 @@ class OpenAIResponsesClient { const developerPrompt = prompt.controlledRetryInstruction ? `${prompt.developerPrompt}\n\n${prompt.controlledRetryInstruction}` : prompt.developerPrompt; - const payload = { + const responsesPayload = { model: config.model, temperature: config.temperature ?? 0, max_output_tokens: config.maxOutputTokens ?? 700, @@ -109,7 +206,7 @@ class OpenAIResponsesClient { content: [ { type: "input_text", - text: `${prompt.domainPrompt}\n\nПользовательский вопрос:\n${prompt.userQuestion}` + text: `${prompt.domainPrompt}\n\nUser question:\n${prompt.userQuestion}` } ] } @@ -123,44 +220,133 @@ class OpenAIResponsesClient { } } }; - const raw = await this.post(config, payload); - const outputText = extractOutputText(raw); + const provider = resolveProvider(config); + if (provider === "openai") { + const raw = await this.postResponses(config, responsesPayload); + return { + raw, + outputText: extractOutputTextFromResponses(raw), + usage: extractUsage(raw) + }; + } + // local provider: prefer /responses if available, fallback to /chat/completions + try { + const raw = await this.postResponses(config, responsesPayload); + return { + raw, + outputText: extractOutputTextFromResponses(raw), + usage: extractUsage(raw) + }; + } + catch (error) { + if (!shouldFallbackToChatCompletions(error)) { + throw error; + } + } + const chatPayload = { + model: config.model, + temperature: config.temperature ?? 0, + max_tokens: config.maxOutputTokens ?? 700, + response_format: { type: "json_object" }, + messages: [ + { + role: "system", + content: `${prompt.systemPrompt}\n\n${developerPrompt}` + }, + { + role: "user", + content: `${prompt.domainPrompt}\n\nUser question:\n${prompt.userQuestion}\n\n` + + `Return only JSON that matches schema: ${schemaName}.` + } + ] + }; + const raw = await this.postChatCompletions(config, chatPayload); return { raw, - outputText, + outputText: extractOutputTextFromChatCompletions(raw), usage: extractUsage(raw) }; } - async post(config, payload) { - if (!config.apiKey || config.apiKey.trim().length < 10) { - throw new http_1.ApiError("OPENAI_API_KEY_MISSING", "API ключ OpenAI не задан или слишком короткий.", 400); + async getModels(config) { + return this.requestJson(config, "/models", "GET"); + } + async postResponses(config, payload) { + return this.requestJson(config, "/responses", "POST", payload); + } + async postChatCompletions(config, payload) { + return this.requestJson(config, "/chat/completions", "POST", payload); + } + async requestJson(config, routePath, method, payload) { + const apiKey = resolveApiKey(config); + const baseCandidates = buildBaseUrlCandidates(config); + const canFallbackToAlternativeBase = resolveProvider(config) === "local" && baseCandidates.length > 1; + let lastNetworkError = null; + const headers = { + Authorization: `Bearer ${apiKey}` + }; + if (method === "POST") { + headers["Content-Type"] = "application/json"; } - const url = `${(config.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL).replace(/\/$/, "")}/responses`; - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${config.apiKey}`, - "Content-Type": "application/json" - }, - body: JSON.stringify(payload) + for (let index = 0; index < baseCandidates.length; index += 1) { + const base = baseCandidates[index]; + const isLastCandidate = index === baseCandidates.length - 1; + const url = `${base}${routePath}`; + let response; + try { + response = await fetch(url, { + method, + headers, + body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined + }); + } + catch (error) { + lastNetworkError = error; + if (!isLastCandidate) { + continue; + } + throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, { + route: routePath, + url, + reason: error instanceof Error ? error.message : String(error) + }); + } + if (!response.ok && canFallbackToAlternativeBase && !isLastCandidate && [404, 405].includes(response.status)) { + continue; + } + const text = await response.text(); + let data = {}; + if (text.trim().length > 0) { + try { + data = JSON.parse(text); + } + catch { + if (!response.ok && canFallbackToAlternativeBase && !isLastCandidate && [404, 405].includes(response.status)) { + continue; + } + throw new http_1.ApiError("OPENAI_NON_JSON_RESPONSE", "Model endpoint returned non-JSON response.", 502, { + route: routePath, + url, + status: response.status, + body: text.slice(0, 500) + }); + } + } + 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, { + route: routePath, + url, + status: response.status, + type: errorObj.type ?? null, + code: errorObj.code ?? null + }); + } + return data; + } + throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, { + route: routePath, + reason: lastNetworkError instanceof Error ? lastNetworkError.message : String(lastNetworkError ?? "unknown") }); - const text = await response.text(); - let data; - try { - data = JSON.parse(text); - } - catch { - throw new http_1.ApiError("OPENAI_NON_JSON_RESPONSE", "OpenAI вернул не-JSON ответ.", 502, { status: response.status, body: text.slice(0, 500) }); - } - if (!response.ok) { - const errorObj = (data.error ?? {}); - throw new http_1.ApiError("OPENAI_REQUEST_FAILED", String(errorObj.message ?? `OpenAI request failed with status ${response.status}`), response.status, { - status: response.status, - type: errorObj.type ?? null, - code: errorObj.code ?? null - }); - } - return data; } } exports.OpenAIResponsesClient = OpenAIResponsesClient; diff --git a/llm_normalizer/backend/package-lock.json b/llm_normalizer/backend/package-lock.json index e9af1d8..f1c7a0a 100644 --- a/llm_normalizer/backend/package-lock.json +++ b/llm_normalizer/backend/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "dotenv": "^16.6.1", "express": "^4.21.2", + "iconv-lite": "^0.7.0", "llm-normalizer-workspace": "file:..", "nanoid": "^5.1.5" }, @@ -1224,6 +1225,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1923,15 +1936,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -2280,6 +2297,18 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", diff --git a/llm_normalizer/backend/package.json b/llm_normalizer/backend/package.json index f3c813b..e7ac9ec 100644 --- a/llm_normalizer/backend/package.json +++ b/llm_normalizer/backend/package.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.6.1", "express": "^4.21.2", + "iconv-lite": "^0.7.0", "llm-normalizer-workspace": "file:..", "nanoid": "^5.1.5" }, diff --git a/llm_normalizer/backend/src/config.ts b/llm_normalizer/backend/src/config.ts index 02aa1fe..4520520 100644 --- a/llm_normalizer/backend/src/config.ts +++ b/llm_normalizer/backend/src/config.ts @@ -95,6 +95,10 @@ export const FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = toBooleanFlag( process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_V1, true ); +export const FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = toBooleanFlag( + process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1, + true +); export const FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = toBooleanFlag( process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1, true diff --git a/llm_normalizer/backend/src/routes/testConnection.ts b/llm_normalizer/backend/src/routes/testConnection.ts index 3af2092..5a7cd2d 100644 --- a/llm_normalizer/backend/src/routes/testConnection.ts +++ b/llm_normalizer/backend/src/routes/testConnection.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import { NextFunction, Request, Response, Router } from "express"; import { DEFAULT_MODEL, DEFAULT_OPENAI_BASE_URL } from "../config"; import { OpenAIResponsesClient } from "../services/openaiResponsesClient"; import { ok } from "../utils/http"; @@ -6,23 +6,76 @@ import { ok } from "../utils/http"; export function buildTestConnectionRouter(client: OpenAIResponsesClient): Router { const router = Router(); - router.post("/api/openai/test-connection", async (req, res, next) => { + const handler = async (req: Request, res: Response, next: NextFunction) => { try { const body = (req.body ?? {}) as Record; + const llmProvider = body.llmProvider === "local" ? "local" : "openai"; + const model = String(body.model ?? DEFAULT_MODEL); + const baseUrl = String(body.baseUrl ?? DEFAULT_OPENAI_BASE_URL); + const apiKey = String(body.apiKey ?? process.env.OPENAI_API_KEY ?? ""); const result = await client.testConnection({ + llmProvider, + apiKey, + model, + baseUrl + }); + + let modelFound: boolean | null = null; + let modelsCount: number | null = null; + if (llmProvider === "local") { + try { + const models = await client.listModels({ + llmProvider, + apiKey, + model, + baseUrl + }); + modelsCount = models.length; + modelFound = models.includes(model); + } catch { + modelFound = null; + modelsCount = null; + } + } + + ok(res, { + ok: true, + provider: llmProvider, + model: result.model, + model_found: modelFound, + models_count: modelsCount, + timestamp: new Date().toISOString() + }); + } catch (error) { + next(error); + } + }; + + const listModelsHandler = async (req: Request, res: Response, next: NextFunction) => { + try { + const body = (req.body ?? {}) as Record; + const models = await client.listModels({ + llmProvider: body.llmProvider === "local" ? "local" : "openai", apiKey: String(body.apiKey ?? process.env.OPENAI_API_KEY ?? ""), model: String(body.model ?? DEFAULT_MODEL), baseUrl: String(body.baseUrl ?? DEFAULT_OPENAI_BASE_URL) }); ok(res, { ok: true, - model: result.model, + models, + count: models.length, timestamp: new Date().toISOString() }); } catch (error) { next(error); } - }); + }; + + router.post("/api/llm/test-connection", handler); + router.post("/api/llm/models", listModelsHandler); + // Backward-compatible route for old frontend builds. + router.post("/api/openai/test-connection", handler); + router.post("/api/openai/models", listModelsHandler); return router; } diff --git a/llm_normalizer/backend/src/services/addressFilterExtractor.ts b/llm_normalizer/backend/src/services/addressFilterExtractor.ts index 801e9f1..f3e91b5 100644 --- a/llm_normalizer/backend/src/services/addressFilterExtractor.ts +++ b/llm_normalizer/backend/src/services/addressFilterExtractor.ts @@ -2,13 +2,24 @@ const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i; const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|\bпервые\b|\bтоп\b)\s*(\d{1,3})/i; -const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|by\s+counterparty|counterparty)\s+([^\r\n,.;:]+)/i; +const COUNTERPARTY_PATTERN = + /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu; const CONTRACT_PATTERN = /(?:по\s+договору|договор(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i; const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/; const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/; const PERIOD_RANGE_PATTERN_1 = /(?:from|с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:to|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i; const PERIOD_RANGE_PATTERN_2 = /(?:between|за\s+период\s+с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:and|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i; +const YEAR_RANGE_PATTERN = + /(?:за|for|с|from)?\s*(20\d{2})\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(20\d{2})(?:\s*(?:г(?:од|ода)?\.?|year))?(?=[^\d]|$)/iu; +const YEAR_RANGE_LOOSE_PATTERN = /\b(20\d{2})\b\s*(?:[-‐‑‒–—―−]|до|to|по)\s*\b(20\d{2})\b/iu; +const YEAR_PERIOD_PATTERN = + /(?:за|for)\s*(20\d{2})(?!\s*(?:[-‐‑‒–—―−]|до|to|по)\s*20\d{2})\s*(?:г(?:од|ода)?\.?|year)?/iu; +const YEAR_PERIOD_SHORT_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{2})\s*(?:г(?:од|ода)?\.?|year)(?=$|[\s,.;:!?()\-])/iu; +const YEAR_PERIOD_ANY_PATTERN = + /(?:^|[\s,.;:!?()\-])((?:19|20)\d{2})(?!\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(?:19|20)\d{2})(?![.\/-]\d)(?:\s*(?:г(?:од|ода)?\.?|year))?(?=$|[\s,.;:!?()\-])/iu; +const MONTH_PERIOD_NUMERIC_PATTERN = /(?:за|for)\s*(0?[1-9]|1[0-2])[.\/-](20\d{2})/i; +const MONTH_PERIOD_NAME_PATTERN = /(?:за|for)\s+([a-zа-яё]+)\s+(20\d{2})(?:\s*г(?:од|ода|\\.)?)?/iu; function toIsoDate(year: number, month: number, day: number): string | null { if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { @@ -68,6 +79,57 @@ function parseDateToken(token: string): string | undefined { return undefined; } +function resolveMonthByName(rawMonthName: string): number | undefined { + const token = String(rawMonthName ?? "").trim().toLowerCase(); + if (!token) { + return undefined; + } + + if (/^янв|^january|^jan/.test(token)) return 1; + if (/^фев|^february|^feb/.test(token)) return 2; + if (/^мар|^march|^mar/.test(token)) return 3; + if (/^апр|^april|^apr/.test(token)) return 4; + if (/^ма[йя]|^may/.test(token)) return 5; + if (/^июн|^june|^jun/.test(token)) return 6; + if (/^июл|^july|^jul/.test(token)) return 7; + if (/^авг|^august|^aug/.test(token)) return 8; + if (/^сен|^сент|^september|^sep/.test(token)) return 9; + if (/^окт|^october|^oct/.test(token)) return 10; + if (/^ноя|^november|^nov/.test(token)) return 11; + if (/^дек|^december|^dec/.test(token)) return 12; + return undefined; +} + +function extractMonthPeriod(text: string): { period_from?: string; period_to?: string } { + const numericMatch = text.match(MONTH_PERIOD_NUMERIC_PATTERN); + if (numericMatch) { + const month = Number(numericMatch[1]); + const year = Number(numericMatch[2]); + if (month >= 1 && month <= 12 && year >= 2000 && year <= 2099) { + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return { + period_from: `${year}-${String(month).padStart(2, "0")}-01`, + period_to: `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}` + }; + } + } + + const byNameMatch = text.match(MONTH_PERIOD_NAME_PATTERN); + if (byNameMatch) { + const month = resolveMonthByName(String(byNameMatch[1])); + const year = Number(byNameMatch[2]); + if (month && year >= 2000 && year <= 2099) { + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return { + period_from: `${year}-${String(month).padStart(2, "0")}-01`, + period_to: `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}` + }; + } + } + + return {}; +} + function extractPeriodRange(text: string): { period_from?: string; period_to?: string } { const directMatch = text.match(PERIOD_RANGE_PATTERN_1) ?? text.match(PERIOD_RANGE_PATTERN_2); if (!directMatch) { @@ -81,6 +143,70 @@ function extractPeriodRange(text: string): { period_from?: string; period_to?: s }; } +function extractYearPeriod(text: string): { period_from?: string; period_to?: string } { + const match = text.match(YEAR_PERIOD_PATTERN); + if (match) { + const year = Number(match[1]); + if (!Number.isFinite(year) || year < 2000 || year > 2099) { + return {}; + } + return { + period_from: `${year}-01-01`, + period_to: `${year}-12-31` + }; + } + + const relaxedYearMatch = text.match(YEAR_PERIOD_ANY_PATTERN); + if (relaxedYearMatch) { + const year = Number(relaxedYearMatch[1]); + if (Number.isFinite(year) && year >= 2000 && year <= 2099) { + return { + period_from: `${year}-01-01`, + period_to: `${year}-12-31` + }; + } + } + + const shortYearMatch = text.match(YEAR_PERIOD_SHORT_PATTERN); + if (!shortYearMatch) { + return {}; + } + const shortYear = Number(shortYearMatch[1]); + if (!Number.isFinite(shortYear) || shortYear < 0 || shortYear > 99) { + return {}; + } + const year = 2000 + shortYear; + return { + period_from: `${year}-01-01`, + period_to: `${year}-12-31` + }; +} + +function extractYearRangePeriod(text: string): { period_from?: string; period_to?: string } { + const match = text.match(YEAR_RANGE_PATTERN) ?? text.match(YEAR_RANGE_LOOSE_PATTERN); + if (!match) { + return {}; + } + const leftYear = Number(match[1]); + const rightYear = Number(match[2]); + if ( + !Number.isFinite(leftYear) || + !Number.isFinite(rightYear) || + leftYear < 2000 || + leftYear > 2099 || + rightYear < 2000 || + rightYear > 2099 + ) { + return {}; + } + const fromYear = Math.min(leftYear, rightYear); + const toYear = Math.max(leftYear, rightYear); + return { + period_from: `${fromYear}-01-01`, + period_to: `${toYear}-12-31` + }; +} + function cleanupAnchorValue(value: string): string { const normalized = String(value ?? "").trim(); if (!normalized) { @@ -95,11 +221,13 @@ function cleanupAnchorValue(value: string): string { return normalized.replace(periodTailPattern, "").trim(); } - const allTimeTailPattern = /\s+за\s+вс[её]\s+время(?:\s+|$)[\s\S]*$/iu; + const allTimeTailPattern = + /\s+за\s+(?:вс[её]\s+время|весь\s+период|весь\s+срок|всю\s+истори(?:ю|и)|любой\s+период|любой\s+срок)(?:\s+|$)[\s\S]*$/iu; if (allTimeTailPattern.test(normalized)) { return normalized.replace(allTimeTailPattern, "").trim(); } - const allTimeTailPatternEn = /\s+(?:for\s+all\s+time|all\s+time)(?:\s+|$)[\s\S]*$/iu; + const allTimeTailPatternEn = + /\s+(?:for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)(?:\s+|$)[\s\S]*$/iu; if (allTimeTailPatternEn.test(normalized)) { return normalized.replace(allTimeTailPatternEn, "").trim(); } @@ -112,7 +240,203 @@ function cleanupAnchorValue(value: string): string { function hasAllTimeHint(text: string): boolean { const value = String(text ?? ""); - return /(?:за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(value); + return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(value); +} + +function extractLooseByAnchorValue(text: string): string | undefined { + const match = String(text ?? "").match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (!match) { + return undefined; + } + const token = String(match[1] ?? "").trim(); + if (!token) { + return undefined; + } + const lowered = token.toLowerCase(); + const stopWords = new Set([ + "контрагенту", + "контрагента", + "контре", + "компании", + "компанию", + "организации", + "организацию", + "поставщику", + "поставщика", + "клиенту", + "клиента", + "покупателю", + "покупателя", + "партнеру", + "партнера", + "договору", + "договора", + "счету", + "счёту", + "дате", + "периоду", + "период", + "документам", + "докам", + "взаиморасчетам", + "взаиморасчётам" + ]); + if (stopWords.has(lowered)) { + return undefined; + } + return token; +} + +function isLikelyCounterpartyToken(rawToken: string): boolean { + const token = String(rawToken ?? "").trim(); + const lowered = token.toLowerCase(); + if (!token || token.length < 2) { + return false; + } + if (/^\d+$/.test(lowered)) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(lowered)) { + return false; + } + + const stopWords = new Set([ + "за", + "с", + "по", + "на", + "и", + "или", + "док", + "доки", + "документ", + "документы", + "документов", + "банк", + "банковские", + "операции", + "платежи", + "платеж", + "платёж", + "контрагент", + "контрагенту", + "контрагента", + "компания", + "компании", + "организация", + "организации", + "год", + "года", + "г", + "плс", + "pls", + "пж", + "пжлст", + "пожалуйста", + "бля", + "блять", + "епт", + "ёпт", + "епта", + "нах", + "нахуй", + "покеж", + "покажи", + "выведи" + ]); + return !stopWords.has(lowered); +} + +function hasDocsOrBankSignal(text: string): boolean { + const lowered = String(text ?? "").toLowerCase(); + return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test( + lowered + ); +} + +function extractCounterpartyFromFreeTextHeuristic(text: string): string | undefined { + if (!hasDocsOrBankSignal(text)) { + return undefined; + } + + const tokens = String(text ?? "") + .split(/[^a-zа-яё0-9._-]+/iu) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + + if (tokens.length === 0) { + return undefined; + } + + const monthTokens = [ + "янв", + "фев", + "мар", + "апр", + "май", + "июн", + "июл", + "авг", + "сен", + "сент", + "окт", + "ноя", + "дек", + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december" + ]; + for (const token of tokens) { + const lowered = token.toLowerCase(); + if (!isLikelyCounterpartyToken(lowered)) { + continue; + } + if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) { + continue; + } + if (monthTokens.some((prefix) => lowered.startsWith(prefix))) { + continue; + } + if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) { + continue; + } + return token; + } + return undefined; +} + +function extractImplicitCounterpartyValue(text: string): string | undefined { + const input = String(text ?? ""); + const beforeDocsMatch = input.match( + /(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)(?=[\s,.;:!?)]|$)/iu + ); + if (beforeDocsMatch) { + const candidate = String(beforeDocsMatch[1] ?? "").trim(); + if (isLikelyCounterpartyToken(candidate)) { + return candidate; + } + } + + const afterDocsMatch = input.match( + /(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu + ); + if (afterDocsMatch) { + const candidate = String(afterDocsMatch[1] ?? "").trim(); + if (isLikelyCounterpartyToken(candidate)) { + return candidate; + } + } + + return undefined; } function shiftDaysIso(baseIso: string, deltaDays: number): string { @@ -156,6 +480,27 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent if (counterpartyMatch) { filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1])); } + if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) { + const fallbackCounterparty = extractLooseByAnchorValue(text); + if (fallbackCounterparty) { + filters.counterparty = cleanupAnchorValue(fallbackCounterparty); + warnings.push("counterparty_anchor_derived_from_loose_by_phrase"); + } + } + if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) { + const implicitCounterparty = extractImplicitCounterpartyValue(text); + if (implicitCounterparty) { + filters.counterparty = cleanupAnchorValue(implicitCounterparty); + warnings.push("counterparty_anchor_derived_from_implicit_phrase"); + } + } + if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) { + const heuristicCounterparty = extractCounterpartyFromFreeTextHeuristic(text); + if (heuristicCounterparty) { + filters.counterparty = cleanupAnchorValue(heuristicCounterparty); + warnings.push("counterparty_anchor_derived_from_free_text_heuristic"); + } + } const contractMatch = text.match(CONTRACT_PATTERN); if (contractMatch) { @@ -170,6 +515,33 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent filters.period_to = periodRange.period_to; } + if (!filters.period_from && !filters.period_to) { + const monthPeriod = extractMonthPeriod(text); + if (monthPeriod.period_from && monthPeriod.period_to) { + filters.period_from = monthPeriod.period_from; + filters.period_to = monthPeriod.period_to; + warnings.push("period_derived_from_month_phrase"); + } + } + + if (!filters.period_from && !filters.period_to) { + const yearRangePeriod = extractYearRangePeriod(text); + if (yearRangePeriod.period_from && yearRangePeriod.period_to) { + filters.period_from = yearRangePeriod.period_from; + filters.period_to = yearRangePeriod.period_to; + warnings.push("period_derived_from_year_range_phrase"); + } + } + + if (!filters.period_from && !filters.period_to) { + const yearPeriod = extractYearPeriod(text); + if (yearPeriod.period_from && yearPeriod.period_to) { + filters.period_from = yearPeriod.period_from; + filters.period_to = yearPeriod.period_to; + warnings.push("period_derived_from_year_phrase"); + } + } + // If explicit period window exists, do not infer as_of_date from one of its boundary dates. if (!filters.period_from && !filters.period_to) { const asOfDate = extractAsOfDate(text); diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 1dadff6..145966f 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -67,9 +67,20 @@ const OPEN_ITEMS_HINTS = [ const DOCUMENTS_BY_COUNTERPARTY_HINTS = [ "documents by counterparty", "docs by counterparty", + "documents by company", + "documents by supplier", + "documents by customer", + "documents by client", + "documents by partner", "show documents by counterparty", "list documents by counterparty", + "документы по", + "доступные документы", + "список документов", "документ", + "доки", + "доки по", + "док по", "по контрагент" ]; @@ -77,16 +88,202 @@ const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [ "bank operations by counterparty", "bank payments by counterparty", "payment orders by counterparty", + "bank operations by company", + "bank operations by supplier", + "bank operations by customer", "show bank operations by counterparty", + "bank ops", + "transactions by counterparty", "банков", "выписк", - "платеж" + "платеж", + "платёж", + "оплат", + "списан", + "поступлен", + "движени" ]; function hasAny(text: string, patterns: string[]): boolean { return patterns.some((item) => text.includes(item)); } +function isLikelyCounterpartyToken(rawToken: string): boolean { + const token = String(rawToken ?? "").trim().toLowerCase(); + if (!token || token.length < 2) { + return false; + } + if (/^\d+$/.test(token)) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(token)) { + return false; + } + const stopWords = new Set([ + "за", + "с", + "по", + "на", + "и", + "или", + "док", + "доки", + "доки?", + "документ", + "документы", + "документов", + "банк", + "банковские", + "операции", + "платежи", + "платеж", + "платёж", + "контрагент", + "контрагенту", + "контрагента", + "компания", + "компании", + "организация", + "организации", + "год", + "года", + "г", + "плс", + "pls", + "пж", + "пжлст", + "пожалуйста", + "бля", + "блять", + "епт", + "ёпт", + "епта", + "нах", + "нахуй" + ]); + return !stopWords.has(token); +} + +function hasPartyAnchorMention(text: string): boolean { + return ( + text.includes("контраг") || + text.includes("контра") || + text.includes("counterparty") || + text.includes("компан") || + text.includes("company") || + text.includes("организац") || + text.includes("supplier") || + text.includes("vendor") || + text.includes("customer") || + text.includes("client") || + text.includes("partner") || + text.includes("поставщик") || + text.includes("клиент") || + text.includes("покупател") || + text.includes("партнер") + ); +} + +function hasLooseByAnchorMention(text: string): boolean { + const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (!match) { + return false; + } + const token = String(match[1] ?? "").toLowerCase(); + if (!token) { + return false; + } + const stopWords = new Set([ + "контрагенту", + "контрагента", + "контре", + "компании", + "компанию", + "организации", + "организацию", + "поставщику", + "поставщика", + "клиенту", + "клиента", + "покупателю", + "покупателя", + "партнеру", + "партнера", + "договору", + "договора", + "счету", + "счёту", + "дате", + "периоду", + "период", + "документам", + "докам" + ]); + return !stopWords.has(token); +} + +function hasImplicitCounterpartyAnchorAroundDocs(text: string): boolean { + const beforeDocsMatch = text.match( + /(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)(?=[\s,.;:!?)]|$)/iu + ); + if (beforeDocsMatch && isLikelyCounterpartyToken(String(beforeDocsMatch[1] ?? ""))) { + return true; + } + + const afterDocsMatch = text.match( + /(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu + ); + if (afterDocsMatch && isLikelyCounterpartyToken(String(afterDocsMatch[1] ?? ""))) { + return true; + } + + return false; +} + +function hasDocsOrBankSignal(text: string): boolean { + return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test( + text + ); +} + +function hasHeuristicCounterpartyAnchor(text: string): boolean { + if (!hasDocsOrBankSignal(text)) { + return false; + } + const tokens = String(text ?? "") + .split(/[^a-zа-яё0-9._-]+/iu) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + for (const token of tokens) { + const lowered = token.toLowerCase(); + if (!isLikelyCounterpartyToken(lowered)) { + continue; + } + if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) { + continue; + } + if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) { + continue; + } + return true; + } + return false; +} + +function hasGenericAddressLookupSignal(text: string): boolean { + return ( + /\bесть\b/iu.test(text) || + /\bпокажи\b/iu.test(text) || + /\bвыведи\b/iu.test(text) || + /\bкакие\b/iu.test(text) || + /\bчто(?:-|\s)?то\b/iu.test(text) || + /за\s+любой\s+период/iu.test(text) || + /за\s+вс[её]\s+время/iu.test(text) || + /for\s+all\s+time/iu.test(text) || + /all\s+time/iu.test(text) + ); +} + function hasAccountNumberAnchor(text: string): boolean { return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text); } @@ -128,7 +325,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti if ( hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) && - (text.includes("контраг") || text.includes("counterparty")) + (hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text)) ) { return { intent: "bank_operations_by_counterparty", @@ -139,7 +336,10 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti if ( hasAny(text, DOCUMENTS_BY_COUNTERPARTY_HINTS) && - (text.includes("контраг") || text.includes("counterparty")) + (hasPartyAnchorMention(text) || + hasLooseByAnchorMention(text) || + hasImplicitCounterpartyAnchorAroundDocs(text) || + hasHeuristicCounterpartyAnchor(text)) ) { return { intent: "list_documents_by_counterparty", @@ -148,6 +348,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti }; } + if (hasLooseByAnchorMention(text) && hasGenericAddressLookupSignal(text)) { + return { + intent: "list_documents_by_counterparty", + confidence: "low", + 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", diff --git a/llm_normalizer/backend/src/services/addressMcpClient.ts b/llm_normalizer/backend/src/services/addressMcpClient.ts index 24d547b..ddc2eab 100644 --- a/llm_normalizer/backend/src/services/addressMcpClient.ts +++ b/llm_normalizer/backend/src/services/addressMcpClient.ts @@ -3,6 +3,7 @@ ASSISTANT_MCP_PROXY_URL, ASSISTANT_MCP_TIMEOUT_MS } from "../config"; +import iconv from "iconv-lite"; interface McpExecuteQueryResponse { success?: unknown; @@ -36,8 +37,81 @@ function parseFiniteNumber(value: unknown): number | null { return null; } +function textMojibakeScore(value: string): number { + const source = String(value ?? ""); + const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length; + const latin = (source.match(/[A-Za-z]/g) ?? []).length; + const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length; + const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length; + return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2; +} + +function looksLikeMojibake(value: string): boolean { + const source = String(value ?? ""); + if (!source.trim()) { + return false; + } + if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) { + return true; + } + return (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2; +} + +function decodeUtf8FromWin1251Mojibake(value: string): string { + if (!looksLikeMojibake(value)) { + return value; + } + try { + const bytes = iconv.encode(value, "win1251"); + const decoded = bytes.toString("utf8"); + return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value; + } catch { + return value; + } +} + +function decodeUtf8FromLatin1Mojibake(value: string): string { + if (!looksLikeMojibake(value)) { + return value; + } + try { + const decoded = Buffer.from(value, "latin1").toString("utf8"); + return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value; + } catch { + return value; + } +} + +function normalizeMojibakeString(value: string): string { + const fromWin1251 = decodeUtf8FromWin1251Mojibake(value); + return decodeUtf8FromLatin1Mojibake(fromWin1251); +} + +function normalizeMojibakeValue(value: unknown): unknown { + if (typeof value === "string") { + return normalizeMojibakeString(value); + } + if (Array.isArray(value)) { + return value.map((item) => normalizeMojibakeValue(item)); + } + if (value && typeof value === "object") { + const source = value as Record; + const normalized: Record = {}; + for (const [key, raw] of Object.entries(source)) { + const repairedKey = normalizeMojibakeString(key); + normalized[repairedKey] = normalizeMojibakeValue(raw); + } + return normalized; + } + return value; +} + +function normalizeMojibakeRows(rows: Array>): Array> { + return rows.map((row) => normalizeMojibakeValue(row) as Record); +} + function parseRowsFromTextTable(source: string): Array> { - const normalized = String(source ?? "").replace(/\r/g, "").trim(); + const normalized = normalizeMojibakeString(String(source ?? "")).replace(/\r/g, "").trim(); if (!normalized) { return []; } @@ -111,7 +185,7 @@ function parseRowsFromTextTable(source: string): Array> rows.push(row); } - return rows; + return normalizeMojibakeRows(rows); } function parseExecutePayload(payload: unknown): AddressMcpQueryResult { @@ -133,9 +207,11 @@ function parseExecutePayload(payload: unknown): AddressMcpQueryResult { } if (Array.isArray(source.data)) { - const rows = source.data - .map((item) => (item && typeof item === "object" ? (item as Record) : null)) - .filter((item): item is Record => item !== null); + const rows = normalizeMojibakeRows( + source.data + .map((item) => (item && typeof item === "object" ? (item as Record) : null)) + .filter((item): item is Record => item !== null) + ); return { ok: true, rows, @@ -152,9 +228,11 @@ function parseExecutePayload(payload: unknown): AddressMcpQueryResult { } if (source.data && typeof source.data === "object" && Array.isArray((source.data as { rows?: unknown }).rows)) { - const rows = ((source.data as { rows: unknown[] }).rows ?? []) - .map((item) => (item && typeof item === "object" ? (item as Record) : null)) - .filter((item): item is Record => item !== null); + const rows = normalizeMojibakeRows( + ((source.data as { rows: unknown[] }).rows ?? []) + .map((item) => (item && typeof item === "object" ? (item as Record) : null)) + .filter((item): item is Record => item !== null) + ); return { ok: true, rows, diff --git a/llm_normalizer/backend/src/services/addressQueryClassifier.ts b/llm_normalizer/backend/src/services/addressQueryClassifier.ts index f5dd3f0..690b0f2 100644 --- a/llm_normalizer/backend/src/services/addressQueryClassifier.ts +++ b/llm_normalizer/backend/src/services/addressQueryClassifier.ts @@ -27,6 +27,13 @@ const ADDRESS_ACTION_TOKENS = [ const ADDRESS_ENTITY_TOKENS = [ "counterparty", "counterparties", + "company", + "organization", + "supplier", + "vendor", + "customer", + "client", + "partner", "contract", "contracts", "account", @@ -42,10 +49,22 @@ const ADDRESS_ENTITY_TOKENS = [ "owes", "owed", "контрагент", + "контра", + "компан", + "организац", + "поставщик", + "клиент", + "покупател", + "партнер", + "банк", + "выписк", + "операц", "договор", "счет", "счёт", "документ", + "доки", + "док", "остаток", "дебитор", "кредитор", @@ -73,6 +92,56 @@ const DEEP_REASONING_TOKENS = [ "ошибк" ]; +function hasLooseByAnchorMention(text: string): boolean { + const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); + if (!match) { + return false; + } + const token = String(match[1] ?? "").toLowerCase(); + if (!token) { + return false; + } + const stopWords = new Set([ + "контрагенту", + "контрагента", + "контре", + "компании", + "компанию", + "организации", + "организацию", + "поставщику", + "поставщика", + "клиенту", + "клиента", + "покупателю", + "покупателя", + "партнеру", + "партнера", + "договору", + "договора", + "счету", + "счёту", + "дате", + "периоду", + "период", + "документам", + "докам", + "взаиморасчетам", + "взаиморасчётам" + ]); + return !stopWords.has(token); +} + +function hasAddressFollowupSignal(text: string): boolean { + if (/(?:за\s+любой\s+период|за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(text)) { + return true; + } + if (/(?:\bесть\s+что(?:-|\s)?то\b|\bесть\s+ли\b|\bчто\s+есть\b)/iu.test(text)) { + return true; + } + return false; +} + function hasAnyToken(text: string, tokens: string[]): boolean { return tokens.some((token) => text.includes(token)); } @@ -90,6 +159,8 @@ export function detectAddressQuestionMode(userMessage: string): AddressModeDetec const hasAddressAction = hasAnyToken(text, ADDRESS_ACTION_TOKENS); const hasAddressEntity = hasAnyToken(text, ADDRESS_ENTITY_TOKENS); const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS); + const hasLooseByAnchor = hasLooseByAnchorMention(text); + const hasFollowupSignal = hasAddressFollowupSignal(text); if (hasAddressAction && hasAddressEntity && !hasDeepReasoning) { return { @@ -99,6 +170,14 @@ export function detectAddressQuestionMode(userMessage: string): AddressModeDetec }; } + if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal) && !hasDeepReasoning) { + return { + mode: "address_query", + confidence: "medium", + reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])] + }; + } + if (hasAddressEntity && !hasDeepReasoning) { return { mode: "address_query", diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index f443e44..f5f22c5 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -13,12 +13,11 @@ import type { AddressResponseType, AddressRuntimeReadiness } from "../types/addressQuery"; -import { detectAddressQuestionMode } from "./addressQueryClassifier"; -import { classifyAddressQueryShape } from "./addressQueryShapeClassifier"; -import { resolveAddressIntent } from "./addressIntentResolver"; -import { extractAddressFilters } from "./addressFilterExtractor"; import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog"; 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"; interface NormalizedAddressRow { period: string | null; @@ -29,6 +28,10 @@ interface NormalizedAddressRow { analytics: string[]; } +interface AddressTryHandleOptions { + followupContext?: AddressFollowupContext | null; +} + const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const; const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1" as const; const PARTY_ANCHOR_STOPWORDS = new Set([ @@ -388,21 +391,65 @@ function applyIntentSpecificFilter(intent: AddressIntent, rows: NormalizedAddres return rows; } -function formatTopRows(rows: NormalizedAddressRow[], limit = 6): string[] { - return rows.slice(0, limit).map((row, index) => { - const period = row.period ?? "дата не указана"; - const amount = row.amount !== null ? `${row.amount}` : "сумма не указана"; - const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / "); - const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : ""; - return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`; - }); +function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean { + return ( + (typeof filters.period_from === "string" && filters.period_from.trim().length > 0) || + (typeof filters.period_to === "string" && filters.period_to.trim().length > 0) + ); } -function inferReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" { - if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { - return "factual"; +function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean { + if (!hasExplicitPeriodWindow(filters)) { + return false; } - return "partial_coverage"; + return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty"; +} + +function toIsoDatePrefix(value: string | null): string | null { + if (!value) { + return null; + } + const normalized = String(value).trim(); + if (!normalized) { + return null; + } + const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/); + if (match) { + return match[1]; + } + return null; +} + +function deriveObservedPeriodWindow(rows: NormalizedAddressRow[]): { period_from: string | null; period_to: string | null } { + const dates = rows + .map((row) => toIsoDatePrefix(row.period)) + .filter((item): item is string => Boolean(item)) + .sort(); + if (dates.length === 0) { + return { + period_from: null, + period_to: null + }; + } + return { + period_from: dates[0], + period_to: dates[dates.length - 1] + }; +} + +function composeAutoBroadenedPeriodPrefix( + requested: AddressFilterSet, + observed: { period_from: string | null; period_to: string | null } +): string { + const requestedFrom = typeof requested.period_from === "string" ? requested.period_from : null; + const requestedTo = typeof requested.period_to === "string" ? requested.period_to : null; + if (requestedFrom && requestedTo && observed.period_from && observed.period_to) { + return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные ${observed.period_from}..${observed.period_to}.`; + } + if (requestedFrom && requestedTo) { + return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные по этому якорю.`; + } + return "По заданному периоду строк не найдено; показаны ближайшие доступные данные по этому якорю."; } function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCategory): AddressRuntimeReadiness { @@ -418,14 +465,6 @@ function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCatego return "UNKNOWN"; } -interface AnchorResolutionDebug { - anchor_type: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null; - anchor_value_raw: string | null; - anchor_value_resolved: string | null; - resolver_confidence: "high" | "medium" | "low" | null; - ambiguity_count: number; -} - interface RowStageDiagnostics { rawRowKeysSample: string[]; materializationDropReason: @@ -580,99 +619,6 @@ function toLegacyMcpStatus( return status; } -function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet): AnchorResolutionDebug { - const account = typeof filters.account === "string" ? filters.account.trim() : ""; - const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; - const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; - const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; - - if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { - if (account) { - return { - anchor_type: "account", - anchor_value_raw: account, - anchor_value_resolved: account, - resolver_confidence: "high", - ambiguity_count: 0 - }; - } - } - - if (counterparty) { - return { - anchor_type: "counterparty", - anchor_value_raw: counterparty, - anchor_value_resolved: counterparty, - resolver_confidence: "medium", - ambiguity_count: 0 - }; - } - - if (contract) { - return { - anchor_type: "contract", - anchor_value_raw: contract, - anchor_value_resolved: contract, - resolver_confidence: "medium", - ambiguity_count: 0 - }; - } - - if (documentRef) { - return { - anchor_type: "document_ref", - anchor_value_raw: documentRef, - anchor_value_resolved: documentRef, - resolver_confidence: "medium", - ambiguity_count: 0 - }; - } - - return { - anchor_type: "unknown", - anchor_value_raw: null, - anchor_value_resolved: null, - resolver_confidence: "low", - ambiguity_count: 0 - }; -} - -function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: NormalizedAddressRow[]): AnchorResolutionDebug { - if (rows.length === 0) { - return anchor; - } - if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { - return anchor; - } - const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); - if (!needleRaw) { - return anchor; - } - const candidates = uniqueStrings( - rows - .flatMap((row) => row.analytics) - .map((value) => value.trim()) - .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw)) - ); - if (candidates.length === 0) { - return anchor; - } - if (candidates.length === 1) { - return { - ...anchor, - anchor_value_resolved: candidates[0], - resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", - ambiguity_count: 0 - }; - } - return { - ...anchor, - anchor_value_resolved: candidates[0], - resolver_confidence: "low", - ambiguity_count: candidates.length - 1 - }; -} - function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string { const heading = category === "empty_match" @@ -777,137 +723,20 @@ function buildLimitedExecutionResult(input: { }; } -function contractCandidatesFromRows(rows: NormalizedAddressRow[]): string[] { - const candidates: string[] = []; - for (const row of rows) { - for (const token of [row.registrator, ...row.analytics]) { - const normalized = token.trim(); - if (!normalized) { - continue; - } - if (/договор|contract|дог\./i.test(normalized)) { - candidates.push(normalized); - } - } - } - return uniqueStrings(candidates); -} - -function composeFactualReply(intent: AddressIntent, rows: NormalizedAddressRow[]): { responseType: AddressResponseType; text: string } { - if (intent === "account_balance_snapshot") { - const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); - const lines = [ - "Адресный срез по счету собран (по движениям live MCP).", - `Строк отобрано: ${rows.length}.`, - `Сумма по отобранным движениям: ${movementSum}.`, - ...formatTopRows(rows, 4) - ]; - return { - responseType: "FACTUAL_SUMMARY", - text: lines.join("\n") - }; - } - - if (intent === "documents_forming_balance") { - const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); - const lines = [ - "Собран drilldown документов, формирующих остаток по счету на указанную дату.", - `Документных строк отобрано: ${rows.length}.`, - `Сумма по отобранным движениям: ${movementSum}.`, - ...formatTopRows(rows, 8), - "Можно уточнить выборку по контрагенту, договору или периоду." - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - - if (intent === "list_open_contracts") { - const contracts = contractCandidatesFromRows(rows); - const lines = [ - "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).", - `Строк движения: ${rows.length}.`, - `Договорных кандидатов: ${contracts.length}.` - ]; - lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - - if (intent === "open_items_by_counterparty_or_contract") { - const lines = [ - "Собраны открытые позиции по указанному фильтру (контрагент/договор).", - `Строк отобрано: ${rows.length}.`, - ...formatTopRows(rows, 6) - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - - if (intent === "list_documents_by_counterparty") { - const lines = [ - "Собран список документов по контрагенту (live address lane).", - `Строк отобрано: ${rows.length}.`, - ...formatTopRows(rows, 8) - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - - if (intent === "bank_operations_by_counterparty") { - const lines = [ - "Собран список банковских операций по контрагенту (live address lane).", - `Строк отобрано: ${rows.length}.`, - ...formatTopRows(rows, 8) - ]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; - } - - const title = - intent === "list_payables_counterparties" - ? "Срез обязательств (payables) собран по движениям с account scope 60/76." - : intent === "list_receivables_counterparties" - ? "Срез требований (receivables) собран по движениям с account scope 62/76." - : "Срез адресного запроса собран."; - - const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)]; - return { - responseType: "FACTUAL_LIST", - text: lines.join("\n") - }; -} - export class AddressQueryService { - public async tryHandle(userMessage: string): Promise { + public async tryHandle(userMessage: string, options: AddressTryHandleOptions = {}): Promise { if (!FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { return null; } - const mode = detectAddressQuestionMode(userMessage); - if (mode.mode !== "address_query") { + const followupContext = options.followupContext ?? null; + const decompose = runAddressDecomposeStage(userMessage, followupContext); + if (!decompose) { return null; } - - const shape = classifyAddressQueryShape(userMessage); - if (shape.shape === "EXPLAIN_OR_REASON") { - return null; - } - - const intent = resolveAddressIntent(userMessage); - const filters = extractAddressFilters(userMessage, intent.intent); + const { mode, shape, intent, filters, baseReasons } = decompose; let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); const recipeSelection = selectAddressRecipe(intent.intent, filters.extracted_filters); - const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons]; if (intent.intent === "unknown") { return buildLimitedExecutionResult({ @@ -1130,6 +959,112 @@ export class AddressQueryService { }); } + if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { + const autoBroadenedFilters: AddressFilterSet = { ...filters.extracted_filters }; + delete autoBroadenedFilters.period_from; + delete autoBroadenedFilters.period_to; + const broadenedSelection = selectAddressRecipe(intent.intent, autoBroadenedFilters); + if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) { + const broadenedPlan = buildAddressRecipePlan(broadenedSelection.selected_recipe, autoBroadenedFilters); + const broadenedMcp = await executeAddressMcpQuery({ + query: broadenedPlan.query, + limit: broadenedPlan.limit + }); + if (!broadenedMcp.error) { + const broadenedRawRows = toNormalizedRows(broadenedMcp.raw_rows); + const broadenedScopedRows = applyAccountScopeFilter(broadenedRawRows, broadenedPlan.account_scope); + const broadenedAccountScopeFallbackApplied = + broadenedPlan.account_scope_mode === "preferred" && + broadenedPlan.account_scope.length > 0 && + broadenedRawRows.length > 0 && + broadenedScopedRows.length === 0; + const broadenedNormalizedRows = broadenedAccountScopeFallbackApplied ? broadenedRawRows : broadenedScopedRows; + let broadenedAnchor = resolvePrimaryAnchor(intent.intent, autoBroadenedFilters); + broadenedAnchor = refineAnchorFromRows(broadenedAnchor, broadenedNormalizedRows); + const broadenedFiltersForMatching: AddressFilterSet = + broadenedAnchor.anchor_type === "counterparty" && broadenedAnchor.anchor_value_resolved + ? { ...autoBroadenedFilters, counterparty: broadenedAnchor.anchor_value_resolved } + : broadenedAnchor.anchor_type === "contract" && broadenedAnchor.anchor_value_resolved + ? { ...autoBroadenedFilters, contract: broadenedAnchor.anchor_value_resolved } + : autoBroadenedFilters; + const broadenedAccountScopeAudit = buildAccountScopeAudit({ + intent: intent.intent, + filters: broadenedFiltersForMatching, + accountScope: broadenedPlan.account_scope, + rowsBeforeScope: broadenedRawRows.length, + rowsAfterScope: broadenedNormalizedRows.length + }); + const broadenedAnchorFilter = applyAddressFilters(broadenedNormalizedRows, broadenedFiltersForMatching); + const broadenedRowsByAnchor = broadenedAnchorFilter.rows; + const broadenedFilteredRows = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor); + if (broadenedFilteredRows.length > 0) { + const broadenedRowDiagnostics = deriveRowStageDiagnostics( + broadenedMcp.raw_rows, + broadenedNormalizedRows.length, + broadenedNormalizedRows.length + ); + const broadenedStageStatus = deriveMcpStageStatus({ + rawRowsReceived: broadenedMcp.raw_rows.length, + rowsMaterialized: broadenedNormalizedRows.length, + rowsAnchorMatched: broadenedRowsByAnchor.length, + rowsMatched: broadenedFilteredRows.length + }); + const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows); + const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow); + const broadenedFactual = composeFactualReply(intent.intent, broadenedFilteredRows); + const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"]; + const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"]; + return { + handled: true, + reply_text: `${broadenedPrefix}\n${broadenedFactual.text}`, + reply_type: inferReplyType(broadenedFactual.responseType), + response_type: broadenedFactual.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: broadenedSelection.selected_recipe.recipe_id, + mcp_call_status_legacy: toLegacyMcpStatus(broadenedStageStatus), + account_scope_mode: broadenedPlan.account_scope_mode, + account_scope_fallback_applied: broadenedAccountScopeFallbackApplied, + anchor_type: broadenedAnchor.anchor_type, + anchor_value_raw: broadenedAnchor.anchor_value_raw, + anchor_value_resolved: broadenedAnchor.anchor_value_resolved, + resolver_confidence: broadenedAnchor.resolver_confidence, + ambiguity_count: broadenedAnchor.ambiguity_count, + match_failure_stage: "none", + match_failure_reason: null, + mcp_call_status: broadenedStageStatus, + rows_fetched: broadenedMcp.fetched_rows, + raw_rows_received: broadenedMcp.raw_rows.length, + rows_after_account_scope: broadenedNormalizedRows.length, + rows_after_recipe_filter: broadenedRowsByAnchor.length, + rows_materialized: broadenedNormalizedRows.length, + rows_matched: broadenedFilteredRows.length, + raw_row_keys_sample: broadenedRowDiagnostics.rawRowKeysSample, + materialization_drop_reason: broadenedRowDiagnostics.materializationDropReason, + account_token_raw: broadenedAccountScopeAudit.accountTokenRaw, + account_token_normalized: broadenedAccountScopeAudit.accountTokenNormalized, + account_scope_fields_checked: broadenedAccountScopeAudit.accountScopeFieldsChecked, + account_scope_match_strategy: broadenedAccountScopeAudit.accountScopeMatchStrategy, + account_scope_drop_reason: broadenedAccountScopeAudit.accountScopeDropReason, + runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", + limited_reason_category: null, + response_type: broadenedFactual.responseType, + limitations: broadenedLimitations, + reasons: broadenedReasons + } + }; + } + } + } + } + if (filteredRows.length === 0) { const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0; const hadAnchorMatchedRows = filterByAnchors.length > 0; diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index 90bcba0..e294767 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -128,6 +128,9 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [ } ]; +const ADDRESS_MAX_LIMIT_DEFAULT = 200; +const ADDRESS_MAX_LIMIT_EXTENDED = 1000; + export interface AddressRecipeExecutionPlan { recipe: AddressRecipeDefinition; query: string; @@ -196,6 +199,13 @@ function shouldBoostLimitForAllTimeCounterparty(filters: AddressFilterSet): bool return !hasPeriod; } +function maxLimitForIntent(intent: AddressIntent): number { + if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { + return ADDRESS_MAX_LIMIT_EXTENDED; + } + return ADDRESS_MAX_LIMIT_DEFAULT; +} + export function selectAddressRecipe(intent: AddressIntent, filters: AddressFilterSet): AddressRecipeSelection { const recipe = BASE_RECIPES.find((item) => item.intent === intent) ?? null; if (!recipe) { @@ -222,16 +232,21 @@ export function buildAddressRecipePlan( recipe: AddressRecipeDefinition, filters: AddressFilterSet ): AddressRecipeExecutionPlan { + const maxLimit = maxLimitForIntent(recipe.intent); const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit) - ? Math.max(1, Math.min(200, Math.trunc(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") && shouldBoostLimitForAllTimeCounterparty(filters) - ? Math.max(baseLimit, 200) - : baseLimit; - const resolvedLimit = Math.max(1, Math.min(200, boostedLimit)); + ? Math.max(baseLimit, maxLimit) + : (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && + typeof filters.account === "string" && + filters.account.trim().length > 0 + ? Math.max(baseLimit, ADDRESS_MAX_LIMIT_DEFAULT) + : baseLimit; + const resolvedLimit = Math.max(1, Math.min(maxLimit, boostedLimit)); const accountScope = (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && filters.account diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts new file mode 100644 index 0000000..fb3962e --- /dev/null +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -0,0 +1,150 @@ +import type { AddressIntent, AddressResponseType } from "../../types/addressQuery"; + +export interface ComposeStageRow { + period: string | null; + registrator: string; + account_dt: string | null; + account_kt: string | null; + amount: number | null; + analytics: string[]; +} + +function uniqueStrings(values: string[]): string[] { + return Array.from( + new Set( + values + .map((item) => item.trim()) + .filter((item) => item.length > 0) + ) + ); +} + +function formatTopRows(rows: ComposeStageRow[], limit = 6): string[] { + return rows.slice(0, limit).map((row, index) => { + const period = row.period ?? "дата не указана"; + const amount = row.amount !== null ? `${row.amount}` : "сумма не указана"; + const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / "); + const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : ""; + return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`; + }); +} + +export function contractCandidatesFromRows(rows: ComposeStageRow[]): string[] { + const candidates: string[] = []; + for (const row of rows) { + for (const token of [row.registrator, ...row.analytics]) { + const normalized = token.trim(); + if (!normalized) { + continue; + } + if (/договор|contract|дог\./i.test(normalized)) { + candidates.push(normalized); + } + } + } + return uniqueStrings(candidates); +} + +export function composeFactualReply( + intent: AddressIntent, + rows: ComposeStageRow[] +): { responseType: AddressResponseType; text: string } { + if (intent === "account_balance_snapshot") { + const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); + const lines = [ + "Адресный срез по счету собран (по движениям live MCP).", + `Строк отобрано: ${rows.length}.`, + `Сумма по отобранным движениям: ${movementSum}.`, + ...formatTopRows(rows, 4) + ]; + return { + responseType: "FACTUAL_SUMMARY", + text: lines.join("\n") + }; + } + + if (intent === "documents_forming_balance") { + const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); + const lines = [ + "Собран drilldown документов, формирующих остаток по счету на указанную дату.", + `Документных строк отобрано: ${rows.length}.`, + `Сумма по отобранным движениям: ${movementSum}.`, + ...formatTopRows(rows, 8), + "Можно уточнить выборку по контрагенту, договору или периоду." + ]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + + if (intent === "list_open_contracts") { + const contracts = contractCandidatesFromRows(rows); + const lines = [ + "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).", + `Строк движения: ${rows.length}.`, + `Договорных кандидатов: ${contracts.length}.` + ]; + lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + + if (intent === "open_items_by_counterparty_or_contract") { + const lines = [ + "Собраны открытые позиции по указанному фильтру (контрагент/договор).", + `Строк отобрано: ${rows.length}.`, + ...formatTopRows(rows, 6) + ]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; + } + + if (intent === "list_documents_by_counterparty") { + 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).", + `Строк отобрано: ${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" + ? "Срез требований (receivables) собран по движениям с account scope 62/76." + : "Срез адресного запроса собран."; + + const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)]; + return { + responseType: "FACTUAL_LIST", + text: lines.join("\n") + }; +} + +export function inferReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" { + if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { + return "factual"; + } + return "partial_coverage"; +} diff --git a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts new file mode 100644 index 0000000..2a88c07 --- /dev/null +++ b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts @@ -0,0 +1,244 @@ +import type { + AddressFilterSet, + AddressIntent, + AddressIntentResolution, + AddressModeDetection, + AddressQueryShapeDetection +} from "../../types/addressQuery"; +import { detectAddressQuestionMode } from "../addressQueryClassifier"; +import { classifyAddressQueryShape } from "../addressQueryShapeClassifier"; +import { resolveAddressIntent } from "../addressIntentResolver"; +import { extractAddressFilters } from "../addressFilterExtractor"; + +export interface AddressFollowupContext { + previous_intent?: AddressIntent; + previous_filters?: AddressFilterSet; + previous_anchor_type?: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null; + previous_anchor_value?: string | null; +} + +export interface AddressDecomposeStageResult { + mode: AddressModeDetection; + shape: AddressQueryShapeDetection; + intent: AddressIntentResolution; + filters: { + extracted_filters: AddressFilterSet; + missing_required_filters: string[]; + warnings: string[]; + }; + baseReasons: string[]; +} + +function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean { + return ( + (typeof filters.period_from === "string" && filters.period_from.trim().length > 0) || + (typeof filters.period_to === "string" && filters.period_to.trim().length > 0) + ); +} + +function toNonEmptyString(value: unknown): string | null { + if (value === null || value === undefined) { + return null; + } + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : null; +} + +function hasAllTimeHint(text: string): boolean { + return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test( + String(text ?? "") + ); +} + +export function hasAddressFollowupContextSignal(text: string): boolean { + const normalized = String(text ?? "").trim(); + if (!normalized) { + return false; + } + if (hasAllTimeHint(normalized)) { + return true; + } + if (/(?:^|\s)(?:и|а\s+еще|а\s+ещё|еще|ещё|также|по\s+этому|по\s+тому|это\s+же|в\s+этом|тот\s+же|also|same|that)/iu.test(normalized)) { + return true; + } + return normalized.split(/\s+/).filter(Boolean).length <= 8; +} + +function mergeFollowupFilters( + current: AddressFilterSet, + intent: AddressIntent, + userMessage: string, + followupContext: AddressFollowupContext | null +): { filters: AddressFilterSet; reasons: string[] } { + const merged: AddressFilterSet = { ...current }; + const reasons: string[] = []; + if (!followupContext) { + return { filters: merged, reasons }; + } + + const previous = followupContext.previous_filters ?? {}; + const previousAnchorValue = toNonEmptyString(followupContext.previous_anchor_value); + const previousCounterparty = toNonEmptyString(previous.counterparty); + const previousContract = toNonEmptyString(previous.contract); + const previousAccount = toNonEmptyString(previous.account); + const allTimeRequested = hasAllTimeHint(userMessage); + + if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") { + if (!toNonEmptyString(merged.counterparty)) { + const inheritedCounterparty = + previousCounterparty ?? + (followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null); + if (inheritedCounterparty) { + merged.counterparty = inheritedCounterparty; + reasons.push("counterparty_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); + if (inheritedAccount) { + merged.account = inheritedAccount; + reasons.push("account_from_followup_context"); + } + } + } + + if (intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts") { + 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 (!toNonEmptyString(merged.counterparty)) { + const inheritedCounterparty = + previousCounterparty ?? + (followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null); + if (inheritedCounterparty) { + merged.counterparty = inheritedCounterparty; + reasons.push("counterparty_from_followup_context"); + } + } + } + + if (allTimeRequested) { + if (toNonEmptyString(merged.period_from) || toNonEmptyString(merged.period_to)) { + delete merged.period_from; + delete merged.period_to; + reasons.push("period_cleared_by_all_time_followup"); + } + return { filters: merged, reasons }; + } + + const currentHasPeriod = hasExplicitPeriodWindow(merged); + const previousHasPeriod = hasExplicitPeriodWindow(previous); + if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) { + if (toNonEmptyString(previous.period_from)) { + merged.period_from = previous.period_from; + } + if (toNonEmptyString(previous.period_to)) { + merged.period_to = previous.period_to; + } + reasons.push("period_from_followup_context"); + } + + return { filters: merged, reasons }; +} + +function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFilterSet): string[] { + const requiredByIntent: Record> = { + account_balance_snapshot: ["account", "as_of_date"], + documents_forming_balance: ["account", "as_of_date"], + list_documents_by_counterparty: ["counterparty"], + bank_operations_by_counterparty: ["counterparty"] + }; + const required = requiredByIntent[intent] ?? []; + return required.filter((key) => { + const value = filters[key]; + return value === undefined || value === null || String(value).trim() === ""; + }); +} + +function deriveIntentWithFollowupContext( + detectedIntent: AddressIntentResolution, + userMessage: string, + followupContext: AddressFollowupContext | null +): AddressIntentResolution { + if (!followupContext || !followupContext.previous_intent) { + return detectedIntent; + } + if (detectedIntent.intent !== "unknown") { + return detectedIntent; + } + if (!hasAddressFollowupContextSignal(userMessage)) { + return detectedIntent; + } + return { + intent: followupContext.previous_intent, + confidence: "low", + reasons: [...detectedIntent.reasons, "intent_from_followup_context"] + }; +} + +export function runAddressDecomposeStage( + userMessage: string, + followupContext: AddressFollowupContext | null +): AddressDecomposeStageResult | null { + const detectedMode = detectAddressQuestionMode(userMessage); + const mode = + detectedMode.mode === "address_query" + ? detectedMode + : followupContext && hasAddressFollowupContextSignal(userMessage) + ? { + mode: "address_query" as const, + confidence: "medium" as const, + reasons: [...detectedMode.reasons, "address_mode_from_followup_context"] + } + : detectedMode; + if (mode.mode !== "address_query") { + return null; + } + + const shape = classifyAddressQueryShape(userMessage); + if (shape.shape === "EXPLAIN_OR_REASON") { + return null; + } + + const detectedIntent = resolveAddressIntent(userMessage); + const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext); + const extractedFilters = extractAddressFilters(userMessage, intent.intent); + const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext); + const filters = { + extracted_filters: followupMerged.filters, + missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters), + warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])] + }; + const followupContextApplied = + Boolean(followupContext) && + (mode.reasons.includes("address_mode_from_followup_context") || + intent.reasons.includes("intent_from_followup_context") || + followupMerged.reasons.length > 0); + const baseReasons = [ + ...mode.reasons, + ...shape.reasons, + ...intent.reasons, + ...followupMerged.reasons, + ...(followupContextApplied ? ["address_followup_context_applied"] : []) + ]; + + return { + mode, + shape, + intent, + filters, + baseReasons + }; +} + diff --git a/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts b/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts new file mode 100644 index 0000000..7e0cee4 --- /dev/null +++ b/llm_normalizer/backend/src/services/address_runtime/resolveStage.ts @@ -0,0 +1,211 @@ +import type { AddressFilterSet, AddressIntent } from "../../types/addressQuery"; + +const PARTY_ANCHOR_STOPWORDS = new Set([ + "ооо", + "ао", + "зао", + "ип", + "llc", + "ltd", + "company", + "компания", + "контрагент", + "counterparty", + "по", + "by" +]); + +export interface AnchorResolutionDebug { + anchor_type: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null; + anchor_value_raw: string | null; + anchor_value_resolved: string | null; + resolver_confidence: "high" | "medium" | "low" | null; + ambiguity_count: number; +} + +export interface ResolveStageRow { + registrator: string; + account_dt: string | null; + account_kt: string | null; + analytics: string[]; +} + +function transliterateCyrillicToLatin(value: string): string { + const map: Record = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "ts", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya" + }; + let out = ""; + for (const char of String(value ?? "").toLowerCase()) { + out += map[char] ?? char; + } + return out; +} + +function normalizeSearchText(value: string): string { + return String(value ?? "") + .toLowerCase() + .replace(/ё/g, "е") + .replace(/[^a-zа-я0-9]+/gi, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function tokenizeAnchor(value: string): string[] { + return normalizeSearchText(value) + .split(" ") + .map((token) => token.trim()) + .filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token)); +} + +function matchesAnchorText(searchable: string, anchor: string): boolean { + const searchableNormalized = normalizeSearchText(searchable); + const searchableLatin = transliterateCyrillicToLatin(searchableNormalized); + const tokens = tokenizeAnchor(anchor); + if (tokens.length === 0) { + const direct = normalizeSearchText(anchor); + if (!direct) { + return false; + } + return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct)); + } + return tokens.every((token) => { + const tokenLatin = transliterateCyrillicToLatin(token); + return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin); + }); +} + +function uniqueStrings(values: string[]): string[] { + return Array.from( + new Set( + values + .map((item) => item.trim()) + .filter((item) => item.length > 0) + ) + ); +} + +export function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet): AnchorResolutionDebug { + const account = typeof filters.account === "string" ? filters.account.trim() : ""; + const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; + const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; + const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; + + if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { + if (account) { + return { + anchor_type: "account", + anchor_value_raw: account, + anchor_value_resolved: account, + resolver_confidence: "high", + ambiguity_count: 0 + }; + } + } + + if (counterparty) { + return { + anchor_type: "counterparty", + anchor_value_raw: counterparty, + anchor_value_resolved: counterparty, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + + if (contract) { + return { + anchor_type: "contract", + anchor_value_raw: contract, + anchor_value_resolved: contract, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + + if (documentRef) { + return { + anchor_type: "document_ref", + anchor_value_raw: documentRef, + anchor_value_resolved: documentRef, + resolver_confidence: "medium", + ambiguity_count: 0 + }; + } + + return { + anchor_type: "unknown", + anchor_value_raw: null, + anchor_value_resolved: null, + resolver_confidence: "low", + ambiguity_count: 0 + }; +} + +export function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: ResolveStageRow[]): AnchorResolutionDebug { + if (rows.length === 0) { + return anchor; + } + if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { + return anchor; + } + const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); + if (!needleRaw) { + return anchor; + } + const candidates = uniqueStrings( + rows + .flatMap((row) => row.analytics) + .map((value) => value.trim()) + .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw)) + ); + if (candidates.length === 0) { + return anchor; + } + if (candidates.length === 1) { + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", + ambiguity_count: 0 + }; + } + return { + ...anchor, + anchor_value_resolved: candidates[0], + resolver_confidence: "low", + ambiguity_count: candidates.length - 1 + }; +} + diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 2ac93fc..b19b76b 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -1693,8 +1693,9 @@ function buildAddressCoverageReport() { out_of_scope_requirements: [] }; } -function buildAddressDebugPayload(addressDebug) { +function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) { const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded"; + const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null; return { trace_id: `address-${(0, nanoid_1.nanoid)(10)}`, prompt_version: "address_query_runtime_v1", @@ -1752,12 +1753,204 @@ function buildAddressDebugPayload(addressDebug) { runtime_readiness: addressDebug.runtime_readiness, limited_reason_category: addressDebug.limited_reason_category, response_type: addressDebug.response_type, + execution_lane: "address_query", + llm_decomposition_applied: Boolean(llmMeta?.applied), + llm_decomposition_attempted: Boolean(llmMeta?.attempted), + llm_provider_used: llmMeta?.provider ?? null, + llm_decomposition_trace_id: llmMeta?.traceId ?? null, + llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null, + llm_decomposition_reason: llmMeta?.reason ?? null, answer_structure_v11: null, investigation_state_snapshot: null, normalized: null, - normalizer_output: null + normalizer_output: llmMeta?.traceId + ? { + trace_id: llmMeta.traceId, + prompt_version: "normalizer_v2_0_2", + applied: Boolean(llmMeta?.applied), + effective_message: llmMeta?.effectiveMessage ?? null + } + : null }; } +function toNonEmptyString(value) { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} +function readAddressFilterString(addressDebug, key) { + const filters = addressDebug?.extracted_filters; + if (!filters || typeof filters !== "object") { + return null; + } + return toNonEmptyString(filters[key]); +} +function findLastAddressAssistantDebug(items) { + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (!item || item.role !== "assistant" || !item.debug) { + continue; + } + const debug = item.debug; + if (debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1") { + return debug; + } + } + return null; +} +function hasAddressFollowupContextSignal(userMessage) { + const text = compactWhitespace(String(userMessage ?? "").toLowerCase()); + if (!text) { + return false; + } + if (/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu.test(text)) { + return true; + } + if (hasReferentialPointer(text)) { + return true; + } + const shortFollowup = countTokens(text) <= 8; + if (shortFollowup && hasFollowupMarker(text)) { + return true; + } + return false; +} +function resolveAddressFollowupCarryoverContext(userMessage, items) { + if (!hasAddressFollowupContextSignal(userMessage)) { + return null; + } + const previousAddressDebug = findLastAddressAssistantDebug(items); + if (!previousAddressDebug) { + return null; + } + const previousIntent = toNonEmptyString(previousAddressDebug.detected_intent); + const previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type); + const previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ?? + toNonEmptyString(previousAddressDebug.anchor_value_raw) ?? + readAddressFilterString(previousAddressDebug, "counterparty") ?? + readAddressFilterString(previousAddressDebug, "account") ?? + readAddressFilterString(previousAddressDebug, "contract"); + const previousFiltersRaw = previousAddressDebug.extracted_filters; + const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" + ? { ...previousFiltersRaw } + : {}; + if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) { + return null; + } + return { + followupContext: { + previous_intent: previousIntent ?? undefined, + previous_filters: previousFilters, + previous_anchor_type: previousAnchorType ?? undefined, + previous_anchor_value: previousAnchor + }, + previousAddressIntent: previousIntent, + previousAddressAnchor: previousAnchor + }; +} +function isAddressLlmPreDecomposeCandidate(userMessage) { + const text = compactWhitespace(String(userMessage ?? "").toLowerCase()); + if (!text) { + return false; + } + return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|банк|выписк|платеж|оплат|поступлен|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?)/i.test(text); +} +function extractAddressQuestionFromNormalized(normalized) { + if (!normalized || typeof normalized !== "object") { + return null; + } + const source = normalized; + const fragments = Array.isArray(source.fragments) ? source.fragments : []; + for (const item of fragments) { + if (!item || typeof item !== "object") { + continue; + } + const fragment = item; + const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase(); + if (domainRelevance === "out_of_scope") { + continue; + } + const readiness = String(fragment.execution_readiness ?? "").trim().toLowerCase(); + if (readiness === "no_route") { + continue; + } + const normalizedText = toNonEmptyString(fragment.normalized_fragment_text); + const rawText = toNonEmptyString(fragment.raw_fragment_text); + const candidate = compactWhitespace(normalizedText ?? rawText ?? ""); + if (candidate.length >= 3 && candidate.length <= 500) { + return candidate; + } + } + return null; +} +async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) { + const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null; + const baseMeta = { + attempted: false, + applied: false, + provider, + traceId: null, + effectiveMessage: userMessage, + reason: "not_attempted" + }; + if (Boolean(payload?.useMock)) { + return { + ...baseMeta, + reason: "skipped_in_mock" + }; + } + if (!isAddressLlmPreDecomposeCandidate(userMessage)) { + return { + ...baseMeta, + reason: "not_address_like" + }; + } + const normalizePayload = { + llmProvider: payload?.llmProvider, + apiKey: payload?.apiKey, + model: payload?.model, + baseUrl: payload?.baseUrl, + temperature: 0, + maxOutputTokens: payload?.maxOutputTokens, + promptVersion: "normalizer_v2_0_2", + userQuestion: userMessage, + context: payload?.context, + useMock: Boolean(payload?.useMock), + retryPolicy: "single-pass-strict" + }; + try { + const normalized = await normalizerService.normalize(normalizePayload); + const candidate = extractAddressQuestionFromNormalized(normalized?.normalized); + if (!normalized?.ok || !candidate) { + return { + ...baseMeta, + attempted: true, + traceId: normalized?.trace_id ?? null, + reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed" + }; + } + const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const candidateCompact = compactWhitespace(candidate.toLowerCase()); + const applied = sourceCompact !== candidateCompact; + return { + attempted: true, + applied, + provider, + traceId: normalized?.trace_id ?? null, + effectiveMessage: applied ? candidate : userMessage, + reason: applied ? "normalized_fragment_applied" : "normalized_fragment_same" + }; + } + catch (error) { + return { + ...baseMeta, + attempted: true, + reason: `error:${error instanceof Error ? error.message : String(error)}` + }; + } +} export class AssistantService { normalizerService; sessions; @@ -1789,80 +1982,112 @@ export class AssistantService { debug: null }; this.sessions.appendItem(sessionId, userItem); - if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { - const addressLane = await this.addressQueryService.tryHandle(userMessage); - if (addressLane?.handled) { - const debug = buildAddressDebugPayload(addressLane.debug); - const assistantItem = { - message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, - session_id: sessionId, - role: "assistant", - text: addressLane.reply_text, - reply_type: addressLane.reply_type, - created_at: new Date().toISOString(), - trace_id: debug.trace_id, - debug - }; - this.sessions.appendItem(sessionId, assistantItem); - const current = this.sessions.getSession(sessionId); - if (current) { - this.sessionLogger.persistSession(current); - } - const conversation = cloneItems(current?.items ?? []); - (0, log_1.logJson)({ - timestamp: new Date().toISOString(), - level: "info", - service: "assistant_loop", - message: "assistant_message_processed", - sessionId, - eventType: "assistant_message_address", - details: { - session_id: sessionId, - message_id: assistantItem.message_id, - user_message: userMessage, - detected_mode: addressLane.debug.detected_mode, - query_shape: addressLane.debug.query_shape, - detected_intent: addressLane.debug.detected_intent, - extracted_filters: addressLane.debug.extracted_filters, - selected_recipe: addressLane.debug.selected_recipe, - mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, - account_scope_mode: addressLane.debug.account_scope_mode, - account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, - anchor_type: addressLane.debug.anchor_type, - resolver_confidence: addressLane.debug.resolver_confidence, - match_failure_stage: addressLane.debug.match_failure_stage, - match_failure_reason: addressLane.debug.match_failure_reason, - mcp_call_status: addressLane.debug.mcp_call_status, - rows_fetched: addressLane.debug.rows_fetched, - raw_rows_received: addressLane.debug.raw_rows_received, - rows_after_account_scope: addressLane.debug.rows_after_account_scope, - rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, - rows_materialized: addressLane.debug.rows_materialized, - rows_matched: addressLane.debug.rows_matched, - materialization_drop_reason: addressLane.debug.materialization_drop_reason, - account_token_raw: addressLane.debug.account_token_raw, - account_token_normalized: addressLane.debug.account_token_normalized, - account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, - account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, - account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, - runtime_readiness: addressLane.debug.runtime_readiness, - limited_reason_category: addressLane.debug.limited_reason_category, - response_type: addressLane.debug.response_type, - limitations: addressLane.debug.limitations, - assistant_reply: assistantItem.text, - reply_type: assistantItem.reply_type, - trace_id: assistantItem.trace_id - } - }); - return { - ok: true, + const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { + 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, + reply_type: addressLane.reply_type, + created_at: new Date().toISOString(), + trace_id: debug.trace_id, + debug + }; + this.sessions.appendItem(sessionId, assistantItem); + const current = this.sessions.getSession(sessionId); + if (current) { + this.sessionLogger.persistSession(current); + } + const conversation = cloneItems(current?.items ?? []); + (0, log_1.logJson)({ + timestamp: new Date().toISOString(), + level: "info", + service: "assistant_loop", + message: "assistant_message_processed", + sessionId, + eventType: "assistant_message_address", + details: { session_id: sessionId, + message_id: assistantItem.message_id, + user_message: userMessage, + effective_address_user_message: effectiveAddressUserMessage, + address_followup_context_applied: Boolean(carryoverMeta), + address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null, + address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null, + address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted), + address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied), + address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null, + address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null, + address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null, + detected_mode: addressLane.debug.detected_mode, + query_shape: addressLane.debug.query_shape, + detected_intent: addressLane.debug.detected_intent, + extracted_filters: addressLane.debug.extracted_filters, + selected_recipe: addressLane.debug.selected_recipe, + mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, + account_scope_mode: addressLane.debug.account_scope_mode, + account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, + anchor_type: addressLane.debug.anchor_type, + resolver_confidence: addressLane.debug.resolver_confidence, + match_failure_stage: addressLane.debug.match_failure_stage, + match_failure_reason: addressLane.debug.match_failure_reason, + mcp_call_status: addressLane.debug.mcp_call_status, + rows_fetched: addressLane.debug.rows_fetched, + raw_rows_received: addressLane.debug.raw_rows_received, + rows_after_account_scope: addressLane.debug.rows_after_account_scope, + rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, + rows_materialized: addressLane.debug.rows_materialized, + rows_matched: addressLane.debug.rows_matched, + materialization_drop_reason: addressLane.debug.materialization_drop_reason, + account_token_raw: addressLane.debug.account_token_raw, + account_token_normalized: addressLane.debug.account_token_normalized, + account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, + account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, + account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, + runtime_readiness: addressLane.debug.runtime_readiness, + limited_reason_category: addressLane.debug.limited_reason_category, + response_type: addressLane.debug.response_type, + limitations: addressLane.debug.limitations, assistant_reply: assistantItem.text, reply_type: assistantItem.reply_type, - conversation_item: assistantItem, - debug, - conversation + trace_id: assistantItem.trace_id + } + }); + return { + ok: true, + session_id: sessionId, + assistant_reply: assistantItem.text, + reply_type: assistantItem.reply_type, + conversation_item: assistantItem, + debug, + conversation + }; + }; + if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { + const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 + ? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage) + : { + attempted: false, + applied: false, + provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null, + traceId: null, + effectiveMessage: userMessage, + reason: "disabled_by_feature_flag" }; + const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage; + const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage); + if (primaryAddressLane?.handled) { + return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressPreDecompose); + } + const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items); + if (carryover?.followupContext) { + const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, { + followupContext: carryover.followupContext + }); + if (contextualAddressLane?.handled) { + return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose); + } } } const followupBinding = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && @@ -1879,12 +2104,13 @@ export class AssistantService { usage: null }; const normalizePayload = { + llmProvider: payload.llmProvider, apiKey: payload.apiKey, model: payload.model, baseUrl: payload.baseUrl, temperature: payload.temperature, maxOutputTokens: payload.maxOutputTokens, - promptVersion: payload.promptVersion ?? "normalizer_v2_0_2", + promptVersion: payload.promptVersion ?? "address_query_runtime_v1", systemPrompt: payload.systemPrompt, developerPrompt: payload.developerPrompt, domainPrompt: payload.domainPrompt, diff --git a/llm_normalizer/backend/src/services/normalizerService.ts b/llm_normalizer/backend/src/services/normalizerService.ts index 4d361c3..aa30c73 100644 --- a/llm_normalizer/backend/src/services/normalizerService.ts +++ b/llm_normalizer/backend/src/services/normalizerService.ts @@ -1036,6 +1036,7 @@ export class NormalizerService { public async normalize(payload: NormalizeRequestPayload): Promise { const traceId = nanoid(14); const startedAt = Date.now(); + const llmProvider = payload.llmProvider === "local" ? "local" : "openai"; const model = payload.model ?? DEFAULT_MODEL; const baseUrl = payload.baseUrl ?? DEFAULT_OPENAI_BASE_URL; const temperature = payload.temperature ?? DEFAULT_TEMPERATURE; @@ -1072,6 +1073,7 @@ export class NormalizerService { const apiKey = payload.apiKey ?? process.env.OPENAI_API_KEY; const firstTry = await this.openaiClient.normalize( { + llmProvider, apiKey: String(apiKey ?? ""), model, baseUrl, @@ -1118,6 +1120,7 @@ export class NormalizerService { const retryMaxOutputTokens = computeRetryMaxOutputTokens(maxOutputTokens, rawModelResponse); const retry = await this.openaiClient.normalize( { + llmProvider, apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""), model, baseUrl, diff --git a/llm_normalizer/backend/src/services/openaiResponsesClient.ts b/llm_normalizer/backend/src/services/openaiResponsesClient.ts index 1e4d65c..914ba3f 100644 --- a/llm_normalizer/backend/src/services/openaiResponsesClient.ts +++ b/llm_normalizer/backend/src/services/openaiResponsesClient.ts @@ -1,9 +1,11 @@ import fs from "fs"; import path from "path"; import { DEFAULT_OPENAI_BASE_URL, SCHEMAS_DIR } from "../config"; +import type { LlmProvider } from "../types/normalizer"; import { ApiError } from "../utils/http"; export interface OpenAIRequestConfig { + llmProvider?: LlmProvider; apiKey: string; model: string; baseUrl?: string; @@ -21,6 +23,22 @@ export interface OpenAIResponseEnvelope { }; } +function resolveProvider(config: OpenAIRequestConfig): LlmProvider { + return config.llmProvider === "local" ? "local" : "openai"; +} + +function resolveApiKey(config: OpenAIRequestConfig): string { + const candidate = String(config.apiKey ?? "").trim(); + if (candidate.length > 0) { + return candidate; + } + if (resolveProvider(config) === "local") { + // Local OpenAI-compatible servers often accept any token. + return "local-dev-token"; + } + throw new ApiError("OPENAI_API_KEY_MISSING", "OpenAI API key is missing.", 400); +} + function extractUsage(raw: Record): { input_tokens: number; output_tokens: number; @@ -37,7 +55,7 @@ function extractUsage(raw: Record): { }; } -function extractOutputText(raw: Record): string { +function extractOutputTextFromResponses(raw: Record): string { if (typeof raw.output_text === "string" && raw.output_text.trim().length > 0) { return raw.output_text; } @@ -72,7 +90,58 @@ function extractOutputText(raw: Record): string { } } - throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Не удалось извлечь output_text из Responses API ответа.", 502, raw); + throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Failed to extract output_text from /responses payload.", 502, raw); +} + +function extractOutputTextFromChatCompletions(raw: Record): string { + const choices = raw.choices; + if (!Array.isArray(choices) || choices.length === 0) { + throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Missing choices in /chat/completions payload.", 502, raw); + } + const first = choices[0]; + if (!first || typeof first !== "object") { + throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Invalid first choice in /chat/completions payload.", 502, raw); + } + const message = (first as Record).message; + if (!message || typeof message !== "object") { + throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Missing message in /chat/completions payload.", 502, raw); + } + const content = (message as Record).content; + if (typeof content === "string" && content.trim().length > 0) { + return content; + } + if (Array.isArray(content)) { + const textParts = content + .map((item) => { + if (!item || typeof item !== "object") { + return ""; + } + const block = item as Record; + return typeof block.text === "string" ? block.text : ""; + }) + .filter((item) => item.trim().length > 0); + if (textParts.length > 0) { + return textParts.join("\n"); + } + } + + throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Failed to extract text from /chat/completions payload.", 502, raw); +} + +function shouldFallbackToChatCompletions(error: unknown): boolean { + if (!(error instanceof ApiError)) { + return false; + } + if (error.code !== "OPENAI_REQUEST_FAILED") { + return false; + } + const details = (error.details ?? {}) as Record; + const status = Number(details.status ?? 0); + if ([404, 405, 501].includes(status)) { + return true; + } + const message = String(error.message ?? "").toLowerCase(); + return message.includes("/responses") || message.includes("responses"); } function loadSchemaForTransport(schemaVersion: "v1" | "v2" | "v2_0_1" | "v2_0_2"): Record { @@ -83,24 +152,62 @@ function loadSchemaForTransport(schemaVersion: "v1" | "v2" | "v2_0_1" | "v2_0_2" ? "normalized_query_v2_0_1.json" : schemaVersion === "v2_0_2" ? "normalized_query_v2_0_2.json" - : "normalized_query_v2.json"; + : "normalized_query_v2.json"; const schemaPath = path.resolve(SCHEMAS_DIR, schemaFile); return JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as Record; } +function buildBaseUrlCandidates(config: OpenAIRequestConfig): string[] { + const base = (config.baseUrl ?? DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, ""); + const provider = resolveProvider(config); + if (provider !== "local") { + return [base]; + } + const hasVersionSuffix = /\/v\d+$/i.test(base); + if (hasVersionSuffix) { + return [base]; + } + return Array.from(new Set([base, `${base}/v1`])); +} + export class OpenAIResponsesClient { - public async testConnection(config: OpenAIRequestConfig): Promise<{ ok: boolean; model: string }> { - const payload = { - model: config.model, - input: [ - { - role: "user", - content: [{ type: "input_text", text: "ping" }] + public async listModels(config: OpenAIRequestConfig): Promise { + const payload = await this.getModels(config); + const data = Array.isArray(payload.data) ? payload.data : []; + const ids = data + .map((item) => { + if (!item || typeof item !== "object") { + return ""; } - ], + return String((item as Record).id ?? "").trim(); + }) + .filter((item) => item.length > 0); + + return Array.from(new Set(ids)); + } + + public async testConnection(config: OpenAIRequestConfig): Promise<{ ok: boolean; model: string }> { + const provider = resolveProvider(config); + if (provider === "local") { + try { + await this.getModels(config); + } catch { + // Some local providers do not expose /models consistently; fallback to a tiny chat call. + await this.postChatCompletions(config, { + model: config.model, + messages: [{ role: "user", content: "ping" }], + max_tokens: 4, + temperature: 0 + }); + } + return { ok: true, model: config.model }; + } + + await this.postResponses(config, { + model: config.model, + input: [{ role: "user", content: [{ type: "input_text", text: "ping" }] }], max_output_tokens: 16 - }; - await this.post(config, payload); + }); return { ok: true, model: config.model }; } @@ -123,13 +230,13 @@ export class OpenAIResponsesClient { ? "normalized_query_v2_0_1" : prompt.schemaVersion === "v2_0_2" ? "normalized_query_v2_0_2" - : "normalized_query_v2"; + : "normalized_query_v2"; const developerPrompt = prompt.controlledRetryInstruction ? `${prompt.developerPrompt}\n\n${prompt.controlledRetryInstruction}` : prompt.developerPrompt; - const payload = { + const responsesPayload = { model: config.model, temperature: config.temperature ?? 0, max_output_tokens: config.maxOutputTokens ?? 700, @@ -147,7 +254,7 @@ export class OpenAIResponsesClient { content: [ { type: "input_text", - text: `${prompt.domainPrompt}\n\nПользовательский вопрос:\n${prompt.userQuestion}` + text: `${prompt.domainPrompt}\n\nUser question:\n${prompt.userQuestion}` } ] } @@ -162,52 +269,157 @@ export class OpenAIResponsesClient { } }; - const raw = await this.post(config, payload); - const outputText = extractOutputText(raw); + const provider = resolveProvider(config); + if (provider === "openai") { + const raw = await this.postResponses(config, responsesPayload); + return { + raw, + outputText: extractOutputTextFromResponses(raw), + usage: extractUsage(raw) + }; + } + + // local provider: prefer /responses if available, fallback to /chat/completions + try { + const raw = await this.postResponses(config, responsesPayload); + return { + raw, + outputText: extractOutputTextFromResponses(raw), + usage: extractUsage(raw) + }; + } catch (error) { + if (!shouldFallbackToChatCompletions(error)) { + throw error; + } + } + + const chatPayload = { + model: config.model, + temperature: config.temperature ?? 0, + max_tokens: config.maxOutputTokens ?? 700, + response_format: { type: "json_object" }, + messages: [ + { + role: "system", + content: `${prompt.systemPrompt}\n\n${developerPrompt}` + }, + { + role: "user", + content: + `${prompt.domainPrompt}\n\nUser question:\n${prompt.userQuestion}\n\n` + + `Return only JSON that matches schema: ${schemaName}.` + } + ] + }; + + const raw = await this.postChatCompletions(config, chatPayload); return { raw, - outputText, + outputText: extractOutputTextFromChatCompletions(raw), usage: extractUsage(raw) }; } - private async post(config: OpenAIRequestConfig, payload: Record): Promise> { - if (!config.apiKey || config.apiKey.trim().length < 10) { - throw new ApiError("OPENAI_API_KEY_MISSING", "API ключ OpenAI не задан или слишком короткий.", 400); + private async getModels(config: OpenAIRequestConfig): Promise> { + return this.requestJson(config, "/models", "GET"); + } + + private async postResponses(config: OpenAIRequestConfig, payload: Record): Promise> { + return this.requestJson(config, "/responses", "POST", payload); + } + + private async postChatCompletions( + config: OpenAIRequestConfig, + payload: Record + ): Promise> { + return this.requestJson(config, "/chat/completions", "POST", payload); + } + + private async requestJson( + config: OpenAIRequestConfig, + routePath: string, + method: "GET" | "POST", + payload?: Record + ): Promise> { + const apiKey = resolveApiKey(config); + const baseCandidates = buildBaseUrlCandidates(config); + const canFallbackToAlternativeBase = resolveProvider(config) === "local" && baseCandidates.length > 1; + let lastNetworkError: unknown = null; + + const headers: Record = { + Authorization: `Bearer ${apiKey}` + }; + if (method === "POST") { + headers["Content-Type"] = "application/json"; } - const url = `${(config.baseUrl ?? DEFAULT_OPENAI_BASE_URL).replace(/\/$/, "")}/responses`; - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${config.apiKey}`, - "Content-Type": "application/json" - }, - body: JSON.stringify(payload) - }); - - const text = await response.text(); - let data: Record; - try { - data = JSON.parse(text) as Record; - } catch { - throw new ApiError("OPENAI_NON_JSON_RESPONSE", "OpenAI вернул не-JSON ответ.", 502, { status: response.status, body: text.slice(0, 500) }); - } - - if (!response.ok) { - const errorObj = (data.error ?? {}) as Record; - throw new ApiError( - "OPENAI_REQUEST_FAILED", - String(errorObj.message ?? `OpenAI request failed with status ${response.status}`), - response.status, - { - status: response.status, - type: errorObj.type ?? null, - code: errorObj.code ?? null + for (let index = 0; index < baseCandidates.length; index += 1) { + const base = baseCandidates[index]; + const isLastCandidate = index === baseCandidates.length - 1; + const url = `${base}${routePath}`; + let response: Response; + try { + response = await fetch(url, { + method, + headers, + body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined + }); + } catch (error) { + lastNetworkError = error; + if (!isLastCandidate) { + continue; } - ); + throw new ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, { + route: routePath, + url, + reason: error instanceof Error ? error.message : String(error) + }); + } + + if (!response.ok && canFallbackToAlternativeBase && !isLastCandidate && [404, 405].includes(response.status)) { + continue; + } + + const text = await response.text(); + let data: Record = {}; + if (text.trim().length > 0) { + try { + data = JSON.parse(text) as Record; + } catch { + if (!response.ok && canFallbackToAlternativeBase && !isLastCandidate && [404, 405].includes(response.status)) { + continue; + } + throw new ApiError("OPENAI_NON_JSON_RESPONSE", "Model endpoint returned non-JSON response.", 502, { + route: routePath, + url, + status: response.status, + body: text.slice(0, 500) + }); + } + } + + if (!response.ok) { + const errorObj = (data.error ?? {}) as Record; + throw new ApiError( + "OPENAI_REQUEST_FAILED", + String(errorObj.message ?? `Model endpoint failed: ${response.status}`), + response.status, + { + route: routePath, + url, + status: response.status, + type: errorObj.type ?? null, + code: errorObj.code ?? null + } + ); + } + + return data; } - return data; + throw new ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, { + route: routePath, + reason: lastNetworkError instanceof Error ? lastNetworkError.message : String(lastNetworkError ?? "unknown") + }); } } diff --git a/llm_normalizer/backend/src/types/assistant.ts b/llm_normalizer/backend/src/types/assistant.ts index db18175..8c4665d 100644 --- a/llm_normalizer/backend/src/types/assistant.ts +++ b/llm_normalizer/backend/src/types/assistant.ts @@ -255,6 +255,7 @@ export interface AssistantMessageRequestPayload { user_message?: string; message?: string; mode?: "assistant" | string; + llmProvider?: NormalizeRequestPayload["llmProvider"]; apiKey?: string; model?: string; baseUrl?: string; @@ -370,6 +371,13 @@ export interface AssistantDebugPayload { runtime_readiness?: "LIVE_QUERYABLE" | "LIVE_QUERYABLE_WITH_LIMITS" | "REQUIRES_SPECIALIZED_RECIPE" | "DEEP_ONLY" | "UNKNOWN"; limited_reason_category?: "empty_match" | "missing_anchor" | "recipe_visibility_gap" | "execution_error" | "unsupported" | null; response_type?: "FACTUAL_LIST" | "FACTUAL_SUMMARY" | "LIMITED_WITH_REASON"; + execution_lane?: "address_query" | "deep_analysis"; + llm_decomposition_applied?: boolean; + llm_decomposition_attempted?: boolean; + llm_provider_used?: "openai" | "local" | null; + llm_decomposition_trace_id?: string | null; + llm_decomposition_effective_message?: string | null; + llm_decomposition_reason?: string | null; business_scope_raw?: string[]; business_scope_resolved?: string[]; company_grounding_applied?: boolean; diff --git a/llm_normalizer/backend/src/types/normalizer.ts b/llm_normalizer/backend/src/types/normalizer.ts index 97ada16..0432054 100644 --- a/llm_normalizer/backend/src/types/normalizer.ts +++ b/llm_normalizer/backend/src/types/normalizer.ts @@ -30,6 +30,7 @@ export type PromptVersion = | "normalizer_v2_0_2"; export type EvalRunMode = "standard" | "single-pass-strict"; +export type LlmProvider = "openai" | "local"; export interface NormalizedQueryV1 { schema_version: "normalized_query_v1"; @@ -235,6 +236,7 @@ export type RouteHintSummary = RouteHintSummaryV1 | RouteHintSummaryV2; export type NormalizedPayload = NormalizedQueryV1 | NormalizedQueryV2 | NormalizedQueryV2_0_1 | NormalizedQueryV2_0_2; export interface NormalizeRequestPayload { + llmProvider?: LlmProvider; apiKey?: string; model?: string; baseUrl?: string; diff --git a/llm_normalizer/backend/tests/addressMcpClientEncoding.test.ts b/llm_normalizer/backend/tests/addressMcpClientEncoding.test.ts new file mode 100644 index 0000000..ad7bade --- /dev/null +++ b/llm_normalizer/backend/tests/addressMcpClientEncoding.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { executeAddressMcpQuery } from "../src/services/addressMcpClient"; + +const ORIGINAL_FETCH = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("address MCP encoding repair", () => { + it("repairs UTF-8/CP1251 mojibake in object rows", async () => { + const payload = { + success: true, + data: [ + { + "Период": "2020-07-30T12:00:00Z", + "Регистратор": "Поступление РЅР° расчетный счет 0001", + "СчетДт": "51", + "СчетКт": "62.01", + "РЎСѓРјРјР°": "20000", + "Контрагент": "Группа РЎР’Рљ" + } + ] + }; + + globalThis.fetch = vi.fn(async () => + new Response(JSON.stringify(payload), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) as typeof fetch; + + const result = await executeAddressMcpQuery({ + query: "SELECT 1", + limit: 20, + account_scope: [] + }); + + expect(result.error).toBeNull(); + expect(result.fetched_rows).toBe(1); + expect(result.rows[0]?.["Контрагент"]).toBe("Группа СВК"); + expect(result.rows[0]?.["Регистратор"]).toContain("Поступление"); + }); +}); diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index e146ef9..4b07e39 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { detectAddressQuestionMode } from "../src/services/addressQueryClassifier"; import { resolveAddressIntent } from "../src/services/addressIntentResolver"; import { classifyAddressQueryShape } from "../src/services/addressQueryShapeClassifier"; import { extractAddressFilters } from "../src/services/addressFilterExtractor"; @@ -21,6 +22,16 @@ describe("address query shape classifier", () => { const result = classifyAddressQueryShape("who owes us and who we owe today?"); expect(result.shape).toBe("COMPOUND_FACTUAL_QUERY"); }); + + it("keeps company lookup phrasing in address lane", () => { + const result = detectAddressQuestionMode("какие компании есть в базе"); + expect(result.mode).toBe("address_query"); + }); + + it("keeps loose by-anchor follow-up phrasing in address lane", () => { + const result = detectAddressQuestionMode("за любой период есть что-то по свк?"); + expect(result.mode).toBe("address_query"); + }); }); describe("address intent resolver expansion (M2.3a)", () => { @@ -38,6 +49,41 @@ describe("address intent resolver expansion (M2.3a)", () => { const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31"); expect(result.intent).toBe("documents_forming_balance"); }); + + it("resolves documents by company phrase as counterparty intent", () => { + const result = resolveAddressIntent("Какие документы доступны по компании СВК за 2021 год?"); + expect(result.intent).toBe("list_documents_by_counterparty"); + }); + + it("resolves bank operations by supplier phrase", () => { + const result = resolveAddressIntent("Покажи платежи по поставщику Альфа за июль 2020"); + expect(result.intent).toBe("bank_operations_by_counterparty"); + }); + + it("resolves documents by client phrase", () => { + const result = resolveAddressIntent("Выведи документы по клиенту Бета за 2020-07"); + expect(result.intent).toBe("list_documents_by_counterparty"); + }); + + it("resolves short slang docs phrase with loose by-anchor", () => { + const result = resolveAddressIntent("какие доки есть по свк за 2021"); + expect(result.intent).toBe("list_documents_by_counterparty"); + }); + + it("resolves typo slang docs phrase with implicit anchor", () => { + const result = resolveAddressIntent("свк доки за 20год покеж"); + expect(result.intent).toBe("list_documents_by_counterparty"); + }); + + it("resolves noisy docs phrase with slang tail", () => { + const result = resolveAddressIntent("свк 20 год - покажи доки плс"); + expect(result.intent).toBe("list_documents_by_counterparty"); + }); + + it("resolves loose by-anchor follow-up as documents by counterparty fallback", () => { + const result = resolveAddressIntent("за любой период есть что-то по свк?"); + expect(result.intent).toBe("list_documents_by_counterparty"); + }); }); describe("address filter extraction for balance drilldown", () => { @@ -68,6 +114,123 @@ describe("address filter extraction for balance drilldown", () => { expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); + + it("extracts counterparty from company phrase and derives year period", () => { + const result = extractAddressFilters( + "Какие документы доступны по компании СВК за 2021 год?", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("СВК"); + expect(result.extracted_filters.period_from).toBe("2021-01-01"); + expect(result.extracted_filters.period_to).toBe("2021-12-31"); + expect(result.warnings).toContain("period_derived_from_year_phrase"); + }); + + it("extracts counterparty from supplier phrase and derives month period", () => { + const result = extractAddressFilters( + "Покажи документы по поставщику Альфа за июль 2020", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("Альфа"); + expect(result.extracted_filters.period_from).toBe("2020-07-01"); + expect(result.extracted_filters.period_to).toBe("2020-07-31"); + expect(result.warnings).toContain("period_derived_from_month_phrase"); + }); + + it("treats 'за весь период' as all-time hint and does not force 90-day default", () => { + const result = extractAddressFilters( + "Покажи банковские операции по клиенту Бета за весь период", + "bank_operations_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("Бета"); + expect(result.extracted_filters.period_from).toBeUndefined(); + expect(result.extracted_filters.period_to).toBeUndefined(); + expect(result.warnings).not.toContain("period_defaulted_last_90_days"); + }); + + it("extracts loose by-anchor and year period for short slang docs phrase", () => { + const result = extractAddressFilters( + "какие доки есть по свк за 2021", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("свк"); + expect(result.extracted_filters.period_from).toBe("2021-01-01"); + expect(result.extracted_filters.period_to).toBe("2021-12-31"); + expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase"); + expect(result.warnings).toContain("period_derived_from_year_phrase"); + }); + + it("extracts implicit counterparty and short-year period for typo slang docs phrase", () => { + const result = extractAddressFilters( + "свк доки за 20год покеж", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("свк"); + expect(result.extracted_filters.period_from).toBe("2020-01-01"); + expect(result.extracted_filters.period_to).toBe("2020-12-31"); + expect(result.warnings).toContain("counterparty_anchor_derived_from_implicit_phrase"); + expect(result.warnings).toContain("period_derived_from_year_phrase"); + }); + + it("extracts free-text counterparty and relaxed short-year period from noisy phrase", () => { + const result = extractAddressFilters( + "свк 20 год - покажи доки плс", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("свк"); + expect(result.extracted_filters.period_from).toBe("2020-01-01"); + expect(result.extracted_filters.period_to).toBe("2020-12-31"); + expect(result.warnings).toContain("counterparty_anchor_derived_from_free_text_heuristic"); + expect(result.warnings).toContain("period_derived_from_year_phrase"); + expect(result.extracted_filters.counterparty).not.toBe("плс"); + }); + + it("extracts explicit year range period from phrase", () => { + const result = extractAddressFilters( + "Какие документы по СВК за 2000 - 2025 год?", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("СВК"); + expect(result.extracted_filters.period_from).toBe("2000-01-01"); + expect(result.extracted_filters.period_to).toBe("2025-12-31"); + expect(result.warnings).toContain("period_derived_from_year_range_phrase"); + }); + + it("extracts multiline year range period from phrase", () => { + const result = extractAddressFilters( + "Какие документы по СВК за 2000 - 2025\n год?", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("СВК"); + expect(result.extracted_filters.period_from).toBe("2000-01-01"); + expect(result.extracted_filters.period_to).toBe("2025-12-31"); + expect(result.warnings).toContain("period_derived_from_year_range_phrase"); + expect(result.warnings).not.toContain("period_derived_from_year_phrase"); + }); + + it("extracts russian year range period from 'с ... по ...' phrase", () => { + const result = extractAddressFilters( + "какие есть доки по свк с 2020 по 2025 год", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("свк"); + expect(result.extracted_filters.period_from).toBe("2020-01-01"); + expect(result.extracted_filters.period_to).toBe("2025-12-31"); + expect(result.warnings).toContain("period_derived_from_year_range_phrase"); + expect(result.warnings).not.toContain("period_defaulted_last_90_days"); + }); + + it("treats 'за любой период' as all-time hint and keeps loose by-anchor", () => { + const result = extractAddressFilters( + "за любой период есть что-то по свк?", + "list_documents_by_counterparty" + ); + expect(result.extracted_filters.counterparty).toBe("свк"); + expect(result.extracted_filters.period_from).toBeUndefined(); + expect(result.extracted_filters.period_to).toBeUndefined(); + expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase"); + expect(result.warnings).not.toContain("period_defaulted_last_90_days"); + }); }); describe("address query limited taxonomy and stage diagnostics", () => { @@ -93,7 +256,7 @@ describe("address query limited taxonomy and stage diagnostics", () => { const service = new AddressQueryService(); const result = await service.tryHandle("which documents form balance for account 62 as of 2020-07-31"); expect(result?.handled).toBe(true); - expect(result?.response_type).toBe("LIMITED_WITH_REASON"); + expect(["LIMITED_WITH_REASON", "FACTUAL_LIST"]).toContain(result?.response_type); expect(result?.debug.anchor_type).toBe("account"); expect(result?.debug.rows_fetched).toBeTypeOf("number"); @@ -108,6 +271,7 @@ describe("address query limited taxonomy and stage diagnostics", () => { expect(result?.debug.match_failure_stage).toBeDefined(); expect([ + "error", "no_raw_rows", "raw_rows_received_but_not_materialized", "materialized_but_not_anchor_matched", @@ -122,6 +286,50 @@ describe("address query limited taxonomy and stage diagnostics", () => { expect(result?.debug.account_scope_match_strategy).toBe("account_code_regex_plus_alias_map_v1"); expect(result?.debug.account_scope_drop_reason).toBeDefined(); }); + + it("keeps short slang docs request in address lane (no deep fallback)", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("какие доки есть по свк за 2021"); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_mode).toBe("address_query"); + expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); + expect(result?.debug.extracted_filters.counterparty).toBe("свк"); + expect(result?.debug.extracted_filters.period_from).toBe("2021-01-01"); + expect(result?.debug.extracted_filters.period_to).toBe("2021-12-31"); + }); + + it("keeps typo slang docs request in address lane and extracts implicit anchor", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("свк доки за 20год покеж"); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_mode).toBe("address_query"); + expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); + expect(result?.debug.extracted_filters.counterparty).toBe("свк"); + expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01"); + expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31"); + }); + + it("keeps noisy docs request in address lane and ignores slang tail token", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("свк 20 год - покажи доки плс"); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_mode).toBe("address_query"); + expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); + expect(result?.debug.extracted_filters.counterparty).toBe("свк"); + expect(result?.debug.extracted_filters.counterparty).not.toBe("плс"); + expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01"); + expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31"); + }); + + it("auto-broadens out-of-window period and returns available factual rows", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("Какие документы по СВК за 2000 год?"); + expect(result?.handled).toBe(true); + expect(["FACTUAL_LIST", "LIMITED_WITH_REASON"]).toContain(result?.response_type); + if (result?.response_type === "FACTUAL_LIST") { + expect(result?.debug.limited_reason_category).toBeNull(); + } + }); }); describe("address recipe catalog counterparty filtering", () => { @@ -134,7 +342,7 @@ describe("address recipe catalog counterparty filtering", () => { expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); - expect(plan.limit).toBe(200); + expect(plan.limit).toBe(1000); }); it("boosts limit for english all-time counterparty queries", () => { @@ -146,7 +354,7 @@ describe("address recipe catalog counterparty filtering", () => { expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); - expect(plan.limit).toBe(200); + expect(plan.limit).toBe(1000); }); it("cuts english all-time tail from counterparty anchor", () => { @@ -159,4 +367,16 @@ describe("address recipe catalog counterparty filtering", () => { expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); + + it("boosts limit for account snapshot queries with explicit account", () => { + const filters = extractAddressFilters( + "Какой остаток по счету 60 на дату 2020-07-31", + "account_balance_snapshot" + ).extracted_filters; + const selected = selectAddressRecipe("account_balance_snapshot", filters); + expect(selected.selected_recipe).toBeTruthy(); + const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); + + expect(plan.limit).toBe(200); + }); }); diff --git a/llm_normalizer/backend/tests/assistantAddressFollowupContext.test.ts b/llm_normalizer/backend/tests/assistantAddressFollowupContext.test.ts new file mode 100644 index 0000000..1debb71 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantAddressFollowupContext.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it, vi } from "vitest"; +import { AssistantService } from "../src/services/assistantService"; +import { AssistantSessionStore } from "../src/services/assistantSessionStore"; + +function buildAddressLaneResult(overrides?: Record): any { + return { + handled: true, + reply_text: "Собран список документов по контрагенту.", + reply_type: "factual", + response_type: "FACTUAL_LIST", + debug: { + detected_mode: "address_query", + detected_mode_confidence: "high", + query_shape: "DOCUMENT_LIST", + query_shape_confidence: "medium", + detected_intent: "list_documents_by_counterparty", + detected_intent_confidence: "medium", + extracted_filters: { + sort: "period_desc", + limit: 20, + counterparty: "свк" + }, + missing_required_filters: [], + selected_recipe: "address_documents_by_counterparty_v1", + mcp_call_status_legacy: "matched_non_empty", + account_scope_mode: "preferred", + account_scope_fallback_applied: false, + anchor_type: "counterparty", + anchor_value_raw: "свк", + anchor_value_resolved: "Группа СВК", + resolver_confidence: "medium", + 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: 5, + rows_after_recipe_filter: 3, + rows_materialized: 5, + rows_matched: 3, + raw_row_keys_sample: [], + 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", + limitations: [], + reasons: ["address_action_detected", "address_entity_detected"] + }, + ...(overrides ?? {}) + }; +} + +describe("assistant address follow-up carryover", () => { + it("keeps short follow-up in address lane by reusing previous anchor context", async () => { + const calls: Array<{ message: string; options?: any }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string, options?: any) => { + calls.push({ message, options }); + if (message === "какие есть доки по свк с 2020 по 2025 год") { + return buildAddressLaneResult(); + } + if (message === "а за все время?" && !options?.followupContext) { + return null; + } + if (message === "а за все время?" && options?.followupContext) { + return buildAddressLaneResult({ + reply_text: "Собран список документов по контрагенту за все время.", + debug: { + ...buildAddressLaneResult().debug, + reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"] + } + }); + } + return null; + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + assistant_reply: "normalizer_fallback_should_not_be_used", + reply_type: "partial_coverage", + debug: {} + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const sessionId = `asst-address-followup-${Date.now()}`; + const first = await service.handleMessage({ + session_id: sessionId, + user_message: "какие есть доки по свк с 2020 по 2025 год", + useMock: true + } as any); + expect(first.ok).toBe(true); + expect(first.reply_type).toBe("factual"); + + const second = await service.handleMessage({ + session_id: sessionId, + user_message: "а за все время?", + useMock: true + } as any); + + expect(second.ok).toBe(true); + expect(second.reply_type).toBe("factual"); + expect(second.debug?.detected_mode).toBe("address_query"); + expect(second.debug?.detected_intent).toBe("list_documents_by_counterparty"); + expect(second.debug?.extracted_filters?.counterparty).toBe("свк"); + expect(second.debug?.answer_grounding_check?.reasons).toContain("address_followup_context_applied"); + + expect(calls).toHaveLength(3); + expect(calls[0].message).toBe("какие есть доки по свк с 2020 по 2025 год"); + expect(calls[1].message).toBe("а за все время?"); + expect(calls[1].options?.followupContext).toBeUndefined(); + expect(calls[2].message).toBe("а за все время?"); + expect(calls[2].options?.followupContext?.previous_intent).toBe("list_documents_by_counterparty"); + expect(calls[2].options?.followupContext?.previous_anchor_type).toBe("counterparty"); + expect(calls[2].options?.followupContext?.previous_anchor_value).toBe("Группа СВК"); + expect(calls[2].options?.followupContext?.previous_filters?.counterparty).toBe("свк"); + expect(normalizerService.normalize).not.toHaveBeenCalled(); + }); +}); diff --git a/llm_normalizer/frontend/dist/assets/index-B5_Zqbf2.js b/llm_normalizer/frontend/dist/assets/index-B5_Zqbf2.js deleted file mode 100644 index ee5ae10..0000000 --- a/llm_normalizer/frontend/dist/assets/index-B5_Zqbf2.js +++ /dev/null @@ -1,12 +0,0 @@ -(function(){const _=document.createElement("link").relList;if(_&&_.supports&&_.supports("modulepreload"))return;for(const k of document.querySelectorAll('link[rel="modulepreload"]'))I(k);new MutationObserver(k=>{for(const O of k)if(O.type==="childList")for(const R of O.addedNodes)R.tagName==="LINK"&&R.rel==="modulepreload"&&I(R)}).observe(document,{childList:!0,subtree:!0});function m(k){const O={};return k.integrity&&(O.integrity=k.integrity),k.referrerPolicy&&(O.referrerPolicy=k.referrerPolicy),k.crossOrigin==="use-credentials"?O.credentials="include":k.crossOrigin==="anonymous"?O.credentials="omit":O.credentials="same-origin",O}function I(k){if(k.ep)return;k.ep=!0;const O=m(k);fetch(k.href,O)}})();function Ka(s){return s&&s.__esModule&&Object.prototype.hasOwnProperty.call(s,"default")?s.default:s}var Qi={exports:{}},Wr={},Wi={exports:{}},J={};var La;function Bf(){if(La)return J;La=1;var s=Symbol.for("react.element"),_=Symbol.for("react.portal"),m=Symbol.for("react.fragment"),I=Symbol.for("react.strict_mode"),k=Symbol.for("react.profiler"),O=Symbol.for("react.provider"),R=Symbol.for("react.context"),G=Symbol.for("react.forward_ref"),V=Symbol.for("react.suspense"),ae=Symbol.for("react.memo"),ue=Symbol.for("react.lazy"),M=Symbol.iterator;function B(d){return d===null||typeof d!="object"?null:(d=M&&d[M]||d["@@iterator"],typeof d=="function"?d:null)}var xe={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},ke=Object.assign,ee={};function Z(d,y,W){this.props=d,this.context=y,this.refs=ee,this.updater=W||xe}Z.prototype.isReactComponent={},Z.prototype.setState=function(d,y){if(typeof d!="object"&&typeof d!="function"&&d!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,d,y,"setState")},Z.prototype.forceUpdate=function(d){this.updater.enqueueForceUpdate(this,d,"forceUpdate")};function Be(){}Be.prototype=Z.prototype;function Le(d,y,W){this.props=d,this.context=y,this.refs=ee,this.updater=W||xe}var He=Le.prototype=new Be;He.constructor=Le,ke(He,Z.prototype),He.isPureReactComponent=!0;var ve=Array.isArray,je=Object.prototype.hasOwnProperty,Y={current:null},Ee={key:!0,ref:!0,__self:!0,__source:!0};function Ie(d,y,W){var X,K={},b=null,le=null;if(y!=null)for(X in y.ref!==void 0&&(le=y.ref),y.key!==void 0&&(b=""+y.key),y)je.call(y,X)&&!Ee.hasOwnProperty(X)&&(K[X]=y[X]);var te=arguments.length-2;if(te===1)K.children=W;else if(1>>1,y=C[d];if(0>>1;dk(K,P))bk(le,K)?(C[d]=le,C[b]=P,d=b):(C[d]=K,C[X]=P,d=X);else if(bk(le,P))C[d]=le,C[b]=P,d=b;else break e}}return F}function k(C,F){var P=C.sortIndex-F.sortIndex;return P!==0?P:C.id-F.id}if(typeof performance=="object"&&typeof performance.now=="function"){var O=performance;s.unstable_now=function(){return O.now()}}else{var R=Date,G=R.now();s.unstable_now=function(){return R.now()-G}}var V=[],ae=[],ue=1,M=null,B=3,xe=!1,ke=!1,ee=!1,Z=typeof setTimeout=="function"?setTimeout:null,Be=typeof clearTimeout=="function"?clearTimeout:null,Le=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function He(C){for(var F=m(ae);F!==null;){if(F.callback===null)I(ae);else if(F.startTime<=C)I(ae),F.sortIndex=F.expirationTime,_(V,F);else break;F=m(ae)}}function ve(C){if(ee=!1,He(C),!ke)if(m(V)!==null)ke=!0,ye(je);else{var F=m(ae);F!==null&&ce(ve,F.startTime-C)}}function je(C,F){ke=!1,ee&&(ee=!1,Be(Ie),Ie=-1),xe=!0;var P=B;try{for(He(F),M=m(V);M!==null&&(!(M.expirationTime>F)||C&&!jt());){var d=M.callback;if(typeof d=="function"){M.callback=null,B=M.priorityLevel;var y=d(M.expirationTime<=F);F=s.unstable_now(),typeof y=="function"?M.callback=y:M===m(V)&&I(V),He(F)}else I(V);M=m(V)}if(M!==null)var W=!0;else{var X=m(ae);X!==null&&ce(ve,X.startTime-F),W=!1}return W}finally{M=null,B=P,xe=!1}}var Y=!1,Ee=null,Ie=-1,mt=5,ot=-1;function jt(){return!(s.unstable_now()-otC||125d?(C.sortIndex=P,_(ae,C),m(V)===null&&C===m(ae)&&(ee?(Be(Ie),Ie=-1):ee=!0,ce(ve,P-d))):(C.sortIndex=y,_(V,C),ke||xe||(ke=!0,ye(je))),C},s.unstable_shouldYield=jt,s.unstable_wrapCallback=function(C){var F=B;return function(){var P=B;B=F;try{return C.apply(this,arguments)}finally{B=P}}}})(Xi)),Xi}var Fa;function Yf(){return Fa||(Fa=1,Yi.exports=Kf()),Yi.exports}var Ua;function Xf(){if(Ua)return Ze;Ua=1;var s=Ji(),_=Yf();function m(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),V=Object.prototype.hasOwnProperty,ae=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ue={},M={};function B(e){return V.call(M,e)?!0:V.call(ue,e)?!1:ae.test(e)?M[e]=!0:(ue[e]=!0,!1)}function xe(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function ke(e,t,n,r){if(t===null||typeof t>"u"||xe(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ee(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var Z={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Z[e]=new ee(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Z[t]=new ee(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){Z[e]=new ee(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Z[e]=new ee(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Z[e]=new ee(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){Z[e]=new ee(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){Z[e]=new ee(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){Z[e]=new ee(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){Z[e]=new ee(e,5,!1,e.toLowerCase(),null,!1,!1)});var Be=/[\-:]([a-z])/g;function Le(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Be,Le);Z[t]=new ee(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Be,Le);Z[t]=new ee(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Be,Le);Z[t]=new ee(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){Z[e]=new ee(e,1,!1,e.toLowerCase(),null,!1,!1)}),Z.xlinkHref=new ee("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){Z[e]=new ee(e,1,!1,e.toLowerCase(),null,!0,!0)});function He(e,t,n,r){var l=Z.hasOwnProperty(t)?Z[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var a=` -`+l[i].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=i&&0<=u);break}}}finally{W=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?y(e):""}function K(e){switch(e.tag){case 5:return y(e.type);case 16:return y("Lazy");case 13:return y("Suspense");case 19:return y("SuspenseList");case 0:case 2:case 15:return e=X(e.type,!1),e;case 11:return e=X(e.type.render,!1),e;case 1:return e=X(e.type,!0),e;default:return""}}function b(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ee:return"Fragment";case Y:return"Portal";case mt:return"Profiler";case Ie:return"StrictMode";case Fe:return"Suspense";case et:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case jt:return(e.displayName||"Context")+".Consumer";case ot:return(e._context.displayName||"Context")+".Provider";case Me:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case it:return t=e.displayName||null,t!==null?t:b(e.type)||"Memo";case ye:t=e._payload,e=e._init;try{return b(e(t))}catch{}}return null}function le(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return b(t);case 8:return t===Ie?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function te(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ie(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Qe(e){var t=ie(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Vt(e){e._valueTracker||(e._valueTracker=Qe(e))}function Zn(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ie(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $t(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Pn(e,t){var n=t.checked;return P({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function bn(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=te(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Kr(e,t){t=t.checked,t!=null&&He(e,"checked",t,!1)}function Tt(e,t){Kr(e,t);var n=te(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?fn(e,t.type,n):t.hasOwnProperty("defaultValue")&&fn(e,t.type,te(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Yr(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function fn(e,t,n){(t!=="number"||$t(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var zt=Array.isArray;function Q(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Tn.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Bt(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ht={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Xr=["Webkit","ms","Moz","O"];Object.keys(Ht).forEach(function(e){Xr.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ht[t]=Ht[e]})});function Gr(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Ht.hasOwnProperty(e)&&Ht[e]?(""+t).trim():t+"px"}function Jr(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Gr(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var oo=P({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function lr(e,t){if(t){if(oo[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(m(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(m(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(m(61))}if(t.style!=null&&typeof t.style!="object")throw Error(m(62))}}function or(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ir=null;function ur(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var sr=null,w=null,D=null;function $(e){if(e=zr(e)){if(typeof sr!="function")throw Error(m(280));var t=e.stateNode;t&&(t=wl(t),sr(e.stateNode,e.type,t))}}function Ce(e){w?D?D.push(e):D=[e]:w=e}function qi(){if(w){var e=w,t=D;if(D=w=null,$(e),t)for(e=0;e>>=0,e===0?32:31-(rc(e)/lc|0)|0}var tl=64,nl=4194304;function dr(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function rl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=dr(u):(o&=i,o!==0&&(r=dr(o)))}else i=n&~l,i!==0?r=dr(i):o!==0&&(r=dr(o));if(r===0)return 0;if(t!==0&&t!==r&&(t&l)===0&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function pr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-ht(t),e[t]=n}function sc(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=xr),Cu=" ",Nu=!1;function Pu(e,t){switch(e){case"keyup":return Mc.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ju(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ln=!1;function Uc(e,t){switch(e){case"compositionend":return ju(t);case"keypress":return t.which!==32?null:(Nu=!0,Cu);case"textInput":return e=t.data,e===Cu&&Nu?null:e;default:return null}}function Ac(e,t){if(Ln)return e==="compositionend"||!Eo&&Pu(e,t)?(e=wu(),sl=go=Xt=null,Ln=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Iu(n)}}function Fu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Fu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Uu(){for(var e=window,t=$t();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$t(e.document)}return t}function Po(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Xc(e){var t=Uu(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Fu(n.ownerDocument.documentElement,n)){if(r!==null&&Po(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Mu(n,o);var i=Mu(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,On=null,jo=null,Cr=null,To=!1;function Au(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;To||On==null||On!==$t(r)||(r=On,"selectionStart"in r&&Po(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Cr&&Er(Cr,r)||(Cr=r,r=vl(jo,"onSelect"),0Un||(e.current=$o[Un],$o[Un]=null,Un--)}function se(e,t){Un++,$o[Un]=e.current,e.current=t}var Zt={},Ue=qt(Zt),Ye=qt(!1),vn=Zt;function An(e,t){var n=e.type.contextTypes;if(!n)return Zt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function Xe(e){return e=e.childContextTypes,e!=null}function Sl(){de(Ye),de(Ue)}function es(e,t,n){if(Ue.current!==Zt)throw Error(m(168));se(Ue,t),se(Ye,n)}function ts(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(m(108,le(e)||"Unknown",l));return P({},n,r)}function xl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Zt,vn=Ue.current,se(Ue,e),se(Ye,Ye.current),!0}function ns(e,t,n){var r=e.stateNode;if(!r)throw Error(m(169));n?(e=ts(e,t,vn),r.__reactInternalMemoizedMergedChildContext=e,de(Ye),de(Ue),se(Ue,e)):de(Ye),se(Ye,n)}var Lt=null,_l=!1,Bo=!1;function rs(e){Lt===null?Lt=[e]:Lt.push(e)}function uf(e){_l=!0,rs(e)}function bt(){if(!Bo&&Lt!==null){Bo=!0;var e=0,t=oe;try{var n=Lt;for(oe=1;e>=i,l-=i,Ot=1<<32-ht(t)+l|n<H?(Re=A,A=null):Re=A.sibling;var re=g(p,A,h[H],E);if(re===null){A===null&&(A=Re);break}e&&A&&re.alternate===null&&t(p,A),f=o(re,f,H),U===null?L=re:U.sibling=re,U=re,A=Re}if(H===h.length)return n(p,A),pe&&gn(p,H),L;if(A===null){for(;HH?(Re=A,A=null):Re=A.sibling;var an=g(p,A,re.value,E);if(an===null){A===null&&(A=Re);break}e&&A&&an.alternate===null&&t(p,A),f=o(an,f,H),U===null?L=an:U.sibling=an,U=an,A=Re}if(re.done)return n(p,A),pe&&gn(p,H),L;if(A===null){for(;!re.done;H++,re=h.next())re=x(p,re.value,E),re!==null&&(f=o(re,f,H),U===null?L=re:U.sibling=re,U=re);return pe&&gn(p,H),L}for(A=r(p,A);!re.done;H++,re=h.next())re=N(A,p,H,re.value,E),re!==null&&(e&&re.alternate!==null&&A.delete(re.key===null?H:re.key),f=o(re,f,H),U===null?L=re:U.sibling=re,U=re);return e&&A.forEach(function($f){return t(p,$f)}),pe&&gn(p,H),L}function Se(p,f,h,E){if(typeof h=="object"&&h!==null&&h.type===Ee&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case je:e:{for(var L=h.key,U=f;U!==null;){if(U.key===L){if(L=h.type,L===Ee){if(U.tag===7){n(p,U.sibling),f=l(U,h.props.children),f.return=p,p=f;break e}}else if(U.elementType===L||typeof L=="object"&&L!==null&&L.$$typeof===ye&&as(L)===U.type){n(p,U.sibling),f=l(U,h.props),f.ref=Rr(p,U,h),f.return=p,p=f;break e}n(p,U);break}else t(p,U);U=U.sibling}h.type===Ee?(f=Nn(h.props.children,p.mode,E,h.key),f.return=p,p=f):(E=Jl(h.type,h.key,h.props,null,p.mode,E),E.ref=Rr(p,f,h),E.return=p,p=E)}return i(p);case Y:e:{for(U=h.key;f!==null;){if(f.key===U)if(f.tag===4&&f.stateNode.containerInfo===h.containerInfo&&f.stateNode.implementation===h.implementation){n(p,f.sibling),f=l(f,h.children||[]),f.return=p,p=f;break e}else{n(p,f);break}else t(p,f);f=f.sibling}f=Ai(h,p.mode,E),f.return=p,p=f}return i(p);case ye:return U=h._init,Se(p,f,U(h._payload),E)}if(zt(h))return T(p,f,h,E);if(F(h))return z(p,f,h,E);Nl(p,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,f!==null&&f.tag===6?(n(p,f.sibling),f=l(f,h),f.return=p,p=f):(n(p,f),f=Ui(h,p.mode,E),f.return=p,p=f),i(p)):n(p,f)}return Se}var Hn=cs(!0),fs=cs(!1),Pl=qt(null),jl=null,Qn=null,Xo=null;function Go(){Xo=Qn=jl=null}function Jo(e){var t=Pl.current;de(Pl),e._currentValue=t}function qo(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Wn(e,t){jl=e,Xo=Qn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Ge=!0),e.firstContext=null)}function at(e){var t=e._currentValue;if(Xo!==e)if(e={context:e,memoizedValue:t,next:null},Qn===null){if(jl===null)throw Error(m(308));Qn=e,jl.dependencies={lanes:0,firstContext:e}}else Qn=Qn.next=e;return t}var wn=null;function Zo(e){wn===null?wn=[e]:wn.push(e)}function ds(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Zo(t)):(n.next=l.next,l.next=n),t.interleaved=n,It(e,r)}function It(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var en=!1;function bo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ps(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Mt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function tn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(ne&2)!==0){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,It(e,n)}return l=r.interleaved,l===null?(t.next=t,Zo(r)):(t.next=l.next,l.next=t),r.interleaved=t,It(e,n)}function Tl(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,po(e,n)}}function ms(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function zl(e,t,n,r){var l=e.updateQueue;en=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,v=a.next;a.next=null,i===null?o=v:i.next=v,i=a;var S=e.alternate;S!==null&&(S=S.updateQueue,u=S.lastBaseUpdate,u!==i&&(u===null?S.firstBaseUpdate=v:u.next=v,S.lastBaseUpdate=a))}if(o!==null){var x=l.baseState;i=0,S=v=a=null,u=o;do{var g=u.lane,N=u.eventTime;if((r&g)===g){S!==null&&(S=S.next={eventTime:N,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var T=e,z=u;switch(g=t,N=n,z.tag){case 1:if(T=z.payload,typeof T=="function"){x=T.call(N,x,g);break e}x=T;break e;case 3:T.flags=T.flags&-65537|128;case 0:if(T=z.payload,g=typeof T=="function"?T.call(N,x,g):T,g==null)break e;x=P({},x,g);break e;case 2:en=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,g=l.effects,g===null?l.effects=[u]:g.push(u))}else N={eventTime:N,lane:g,tag:u.tag,payload:u.payload,callback:u.callback,next:null},S===null?(v=S=N,a=x):S=S.next=N,i|=g;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;g=u,u=g.next,g.next=null,l.lastBaseUpdate=g,l.shared.pending=null}}while(!0);if(S===null&&(a=x),l.baseState=a,l.firstBaseUpdate=v,l.lastBaseUpdate=S,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);_n|=i,e.lanes=i,e.memoizedState=x}}function hs(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=li.transition;li.transition={};try{e(!1),t()}finally{oe=n,li.transition=r}}function Ds(){return ct().memoizedState}function ff(e,t,n){var r=on(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e))Ms(t,n);else if(n=ds(e,t,n,r),n!==null){var l=Ke();xt(n,e,r,l),Fs(n,t,r)}}function df(e,t,n){var r=on(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ms(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,vt(u,i)){var a=t.interleaved;a===null?(l.next=l,Zo(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}n=ds(e,t,l,r),n!==null&&(l=Ke(),xt(n,e,r,l),Fs(n,t,r))}}function Is(e){var t=e.alternate;return e===he||t!==null&&t===he}function Ms(e,t){Ir=Ol=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Fs(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,po(e,n)}}var Ml={readContext:at,useCallback:Ae,useContext:Ae,useEffect:Ae,useImperativeHandle:Ae,useInsertionEffect:Ae,useLayoutEffect:Ae,useMemo:Ae,useReducer:Ae,useRef:Ae,useState:Ae,useDebugValue:Ae,useDeferredValue:Ae,useTransition:Ae,useMutableSource:Ae,useSyncExternalStore:Ae,useId:Ae,unstable_isNewReconciler:!1},pf={readContext:at,useCallback:function(e,t){return Nt().memoizedState=[e,t===void 0?null:t],e},useContext:at,useEffect:Ns,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Dl(4194308,4,Ts.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Dl(4194308,4,e,t)},useInsertionEffect:function(e,t){return Dl(4,2,e,t)},useMemo:function(e,t){var n=Nt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Nt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ff.bind(null,he,e),[r.memoizedState,e]},useRef:function(e){var t=Nt();return e={current:e},t.memoizedState=e},useState:Es,useDebugValue:fi,useDeferredValue:function(e){return Nt().memoizedState=e},useTransition:function(){var e=Es(!1),t=e[0];return e=cf.bind(null,e[1]),Nt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=he,l=Nt();if(pe){if(n===void 0)throw Error(m(407));n=n()}else{if(n=t(),ze===null)throw Error(m(349));(xn&30)!==0||ws(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Ns(xs.bind(null,r,o,e),[e]),r.flags|=2048,Ur(9,Ss.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Nt(),t=ze.identifierPrefix;if(pe){var n=Dt,r=Ot;n=(r&~(1<<32-ht(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Mr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Et]=t,e[Tr]=r,ra(e,t,!1,!1),t.stateNode=e;e:{switch(i=or(n,r),n){case"dialog":fe("cancel",e),fe("close",e),l=r;break;case"iframe":case"object":case"embed":fe("load",e),l=r;break;case"video":case"audio":for(l=0;lJn&&(t.flags|=128,r=!0,Ar(o,!1),t.lanes=4194304)}else{if(!r)if(e=Rl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Ar(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!pe)return Ve(t),null}else 2*we()-o.renderingStartTime>Jn&&n!==1073741824&&(t.flags|=128,r=!0,Ar(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=we(),t.sibling=null,n=me.current,se(me,r?n&1|2:n&1),t):(Ve(t),null);case 22:case 23:return Ii(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(lt&1073741824)!==0&&(Ve(t),t.subtreeFlags&6&&(t.flags|=8192)):Ve(t),null;case 24:return null;case 25:return null}throw Error(m(156,t.tag))}function xf(e,t){switch(Qo(t),t.tag){case 1:return Xe(t.type)&&Sl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Kn(),de(Ye),de(Ue),ri(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return ti(t),null;case 13:if(de(me),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(m(340));Bn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return de(me),null;case 4:return Kn(),null;case 10:return Jo(t.type._context),null;case 22:case 23:return Ii(),null;case 24:return null;default:return null}}var Vl=!1,$e=!1,_f=typeof WeakSet=="function"?WeakSet:Set,j=null;function Xn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){ge(e,t,r)}else n.current=null}function ki(e,t,n){try{n()}catch(r){ge(e,t,r)}}var ia=!1;function kf(e,t){if(Io=il,e=Uu(),Po(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,a=-1,v=0,S=0,x=e,g=null;t:for(;;){for(var N;x!==n||l!==0&&x.nodeType!==3||(u=i+l),x!==o||r!==0&&x.nodeType!==3||(a=i+r),x.nodeType===3&&(i+=x.nodeValue.length),(N=x.firstChild)!==null;)g=x,x=N;for(;;){if(x===e)break t;if(g===n&&++v===l&&(u=i),g===o&&++S===r&&(a=i),(N=x.nextSibling)!==null)break;x=g,g=x.parentNode}x=N}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(Mo={focusedElem:e,selectionRange:n},il=!1,j=t;j!==null;)if(t=j,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,j=e;else for(;j!==null;){t=j;try{var T=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(T!==null){var z=T.memoizedProps,Se=T.memoizedState,p=t.stateNode,f=p.getSnapshotBeforeUpdate(t.elementType===t.type?z:gt(t.type,z),Se);p.__reactInternalSnapshotBeforeUpdate=f}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163))}}catch(E){ge(t,t.return,E)}if(e=t.sibling,e!==null){e.return=t.return,j=e;break}j=t.return}return T=ia,ia=!1,T}function Vr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&ki(t,n,o)}l=l.next}while(l!==r)}}function $l(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Ei(e){var t=e.ref;if(t!==null){var n=e.stateNode;e.tag,e=n,typeof t=="function"?t(e):t.current=e}}function ua(e){var t=e.alternate;t!==null&&(e.alternate=null,ua(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Et],delete t[Tr],delete t[Vo],delete t[lf],delete t[of])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function sa(e){return e.tag===5||e.tag===3||e.tag===4}function aa(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||sa(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ci(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=gl));else if(r!==4&&(e=e.child,e!==null))for(Ci(e,t,n),e=e.sibling;e!==null;)Ci(e,t,n),e=e.sibling}function Ni(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Ni(e,t,n),e=e.sibling;e!==null;)Ni(e,t,n),e=e.sibling}var Oe=null,wt=!1;function nn(e,t,n){for(n=n.child;n!==null;)ca(e,t,n),n=n.sibling}function ca(e,t,n){if(kt&&typeof kt.onCommitFiberUnmount=="function")try{kt.onCommitFiberUnmount(el,n)}catch{}switch(n.tag){case 5:$e||Xn(n,t);case 6:var r=Oe,l=wt;Oe=null,nn(e,t,n),Oe=r,wt=l,Oe!==null&&(wt?(e=Oe,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Oe.removeChild(n.stateNode));break;case 18:Oe!==null&&(wt?(e=Oe,n=n.stateNode,e.nodeType===8?Ao(e.parentNode,n):e.nodeType===1&&Ao(e,n),gr(e)):Ao(Oe,n.stateNode));break;case 4:r=Oe,l=wt,Oe=n.stateNode.containerInfo,wt=!0,nn(e,t,n),Oe=r,wt=l;break;case 0:case 11:case 14:case 15:if(!$e&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&((o&2)!==0||(o&4)!==0)&&ki(n,t,i),l=l.next}while(l!==r)}nn(e,t,n);break;case 1:if(!$e&&(Xn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){ge(n,t,u)}nn(e,t,n);break;case 21:nn(e,t,n);break;case 22:n.mode&1?($e=(r=$e)||n.memoizedState!==null,nn(e,t,n),$e=r):nn(e,t,n);break;default:nn(e,t,n)}}function fa(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new _f),t.forEach(function(r){var l=Lf.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function St(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=we()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Cf(r/1960))-r,10e?16:e,ln===null)var r=!1;else{if(e=ln,ln=null,Kl=0,(ne&6)!==0)throw Error(m(331));var l=ne;for(ne|=4,j=e.current;j!==null;){var o=j,i=o.child;if((j.flags&16)!==0){var u=o.deletions;if(u!==null){for(var a=0;awe()-Ti?En(e,0):ji|=n),qe(e,t)}function Ea(e,t){t===0&&((e.mode&1)===0?t=1:(t=nl,nl<<=1,(nl&130023424)===0&&(nl=4194304)));var n=Ke();e=It(e,t),e!==null&&(pr(e,t,n),qe(e,n))}function Rf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ea(e,n)}function Lf(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(m(314))}r!==null&&r.delete(t),Ea(e,n)}var Ca;Ca=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ye.current)Ge=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return Ge=!1,wf(e,t,n);Ge=(e.flags&131072)!==0}else Ge=!1,pe&&(t.flags&1048576)!==0&&ls(t,El,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Al(e,t),e=t.pendingProps;var l=An(t,Ue.current);Wn(t,n),l=ii(null,t,r,e,l,n);var o=ui();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Xe(r)?(o=!0,xl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,bo(t),l.updater=Fl,t.stateNode=l,l._reactInternals=t,pi(t,r,e,n),t=yi(null,t,r,!0,o,n)):(t.tag=0,pe&&o&&Ho(t),We(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Al(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Df(r),e=gt(r,e),l){case 0:t=vi(null,t,r,e,n);break e;case 1:t=qs(null,t,r,e,n);break e;case 11:t=Ks(null,t,r,e,n);break e;case 14:t=Ys(null,t,r,gt(r.type,e),n);break e}throw Error(m(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:gt(r,l),vi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:gt(r,l),qs(e,t,r,l,n);case 3:e:{if(Zs(t),e===null)throw Error(m(387));r=t.pendingProps,o=t.memoizedState,l=o.element,ps(e,t),zl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Yn(Error(m(423)),t),t=bs(e,t,r,n,l);break e}else if(r!==l){l=Yn(Error(m(424)),t),t=bs(e,t,r,n,l);break e}else for(rt=Jt(t.stateNode.containerInfo.firstChild),nt=t,pe=!0,yt=null,n=fs(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Bn(),r===l){t=Ft(e,t,n);break e}We(e,t,r,n)}t=t.child}return t;case 5:return vs(t),e===null&&Ko(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,Fo(r,l)?i=null:o!==null&&Fo(r,o)&&(t.flags|=32),Js(e,t),We(e,t,i,n),t.child;case 6:return e===null&&Ko(t),null;case 13:return ea(e,t,n);case 4:return ei(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Hn(t,null,r,n):We(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:gt(r,l),Ks(e,t,r,l,n);case 7:return We(e,t,t.pendingProps,n),t.child;case 8:return We(e,t,t.pendingProps.children,n),t.child;case 12:return We(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,se(Pl,r._currentValue),r._currentValue=i,o!==null)if(vt(o.value,i)){if(o.children===l.children&&!Ye.current){t=Ft(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(o.tag===1){a=Mt(-1,n&-n),a.tag=2;var v=o.updateQueue;if(v!==null){v=v.shared;var S=v.pending;S===null?a.next=a:(a.next=S.next,S.next=a),v.pending=a}}o.lanes|=n,a=o.alternate,a!==null&&(a.lanes|=n),qo(o.return,n,t),u.lanes|=n;break}a=a.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(m(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),qo(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}We(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Wn(t,n),l=at(l),r=r(l),t.flags|=1,We(e,t,r,n),t.child;case 14:return r=t.type,l=gt(r,t.pendingProps),l=gt(r.type,l),Ys(e,t,r,l,n);case 15:return Xs(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:gt(r,l),Al(e,t),t.tag=1,Xe(r)?(e=!0,xl(t)):e=!1,Wn(t,n),As(t,r,l),pi(t,r,l,n),yi(null,t,r,!0,e,n);case 19:return na(e,t,n);case 22:return Gs(e,t,n)}throw Error(m(156,t.tag))};function Na(e,t){return ou(e,t)}function Of(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function dt(e,t,n,r){return new Of(e,t,n,r)}function Fi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Df(e){if(typeof e=="function")return Fi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Me)return 11;if(e===it)return 14}return 2}function sn(e,t){var n=e.alternate;return n===null?(n=dt(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Jl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Fi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case Ee:return Nn(n.children,l,o,t);case Ie:i=8,l|=8;break;case mt:return e=dt(12,n,t,l|2),e.elementType=mt,e.lanes=o,e;case Fe:return e=dt(13,n,t,l),e.elementType=Fe,e.lanes=o,e;case et:return e=dt(19,n,t,l),e.elementType=et,e.lanes=o,e;case ce:return ql(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ot:i=10;break e;case jt:i=9;break e;case Me:i=11;break e;case it:i=14;break e;case ye:i=16,r=null;break e}throw Error(m(130,e==null?e:typeof e,""))}return t=dt(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Nn(e,t,n,r){return e=dt(7,e,r,t),e.lanes=n,e}function ql(e,t,n,r){return e=dt(22,e,r,t),e.elementType=ce,e.lanes=n,e.stateNode={isHidden:!1},e}function Ui(e,t,n){return e=dt(6,e,null,t),e.lanes=n,e}function Ai(e,t,n){return t=dt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function If(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=fo(0),this.expirationTimes=fo(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=fo(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Vi(e,t,n,r,l,o,i,u,a){return e=new If(e,t,n,u,a),t===1?(t=1,o===!0&&(t|=8)):t=0,o=dt(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},bo(o),e}function Mf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(s)}catch(_){console.error(_)}}return s(),Ki.exports=Xf(),Ki.exports}var Va;function Jf(){if(Va)return lo;Va=1;var s=Gf();return lo.createRoot=s.createRoot,lo.hydrateRoot=s.hydrateRoot,lo}var qf=Jf();const Zf=Ka(qf),bf="/api";async function be(s,_){const m=await fetch(`${bf}${s}`,{..._,headers:{"Content-Type":"application/json",..._?.headers??{}}}),I=await m.json();if(!m.ok){const k=I.error?.message??"Ошибка запроса";throw new Error(k)}return I}const pt={async testConnection(s){return be("/openai/test-connection",{method:"POST",body:JSON.stringify({apiKey:s.apiKey,model:s.model,baseUrl:s.baseUrl})})},async normalize(s){return be("/normalize",{method:"POST",body:JSON.stringify({apiKey:s.connection.apiKey,model:s.connection.model,baseUrl:s.connection.baseUrl,temperature:s.connection.temperature,maxOutputTokens:s.connection.maxOutputTokens,promptVersion:s.promptVersion,systemPrompt:s.prompts.systemPrompt,developerPrompt:s.prompts.developerPrompt,domainPrompt:s.prompts.domainPrompt,fewShotExamples:s.prompts.fewShotExamples,userQuestion:s.query.userQuestion,context:{period_hint:s.query.periodHint??"",business_context:s.query.businessContext??"",expected_route:s.query.expectedRoute??""},saveAsTestCase:!!s.saveAsTestCase,useMock:!!s.useMock})})},async loadHistory(){return be("/history")},async loadTrace(s){return be(`/history/${s}`)},async loadPresets(){return be("/presets")},async savePreset(s){return be("/presets/save",{method:"POST",body:JSON.stringify(s)})},async runEval(s){return be("/eval/run",{method:"POST",body:JSON.stringify({normalizeConfig:{apiKey:s.connection.apiKey,model:s.connection.model,baseUrl:s.connection.baseUrl,temperature:s.connection.temperature,maxOutputTokens:s.connection.maxOutputTokens,promptVersion:s.promptVersion,systemPrompt:s.prompts.systemPrompt,developerPrompt:s.prompts.developerPrompt,domainPrompt:s.prompts.domainPrompt,fewShotExamples:s.prompts.fewShotExamples},caseIds:s.caseIds,useMock:!!s.useMock,mode:s.mode??"standard",caseSetFile:s.caseSetFile,rawQuestions:s.rawQuestions})})},async startRun(){return be("/accounting-agent/v1/runs/start",{method:"POST",body:JSON.stringify({initiator:"ndc_operator",source:"gui"})})},async finishRun(s){return be("/accounting-agent/v1/runs/finish",{method:"POST",body:JSON.stringify({runId:s,status:"DONE",source:"gui",reason:"Остановлено оператором из GUI"})})},async listRuns(){return be("/accounting-agent/v1/runs")},async listResults(){return be("/accounting-agent/v1/results")},async runTrace(s){return be(`/accounting-agent/v1/trace/run/${s}`)},async sendAssistantMessage(s){return be("/assistant/message",{method:"POST",body:JSON.stringify({session_id:s.sessionId??"",mode:"assistant",message:s.userMessage,user_message:s.userMessage,apiKey:s.connection.apiKey,model:s.connection.model,baseUrl:s.connection.baseUrl,temperature:s.connection.temperature,maxOutputTokens:s.connection.maxOutputTokens,promptVersion:s.promptVersion??"normalizer_v2_0_2",systemPrompt:s.prompts.systemPrompt,developerPrompt:s.prompts.developerPrompt,domainPrompt:s.prompts.domainPrompt,fewShotExamples:s.prompts.fewShotExamples,context:{period_hint:s.context?.periodHint??"",business_context:s.context?.businessContext??""},useMock:!!s.useMock})})},async loadAssistantSession(s){return be(`/assistant/session/${s}`)}},ed=/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json|debug_payload|technical_breakdown)\b/i,td=[/\b(?:debug_payload_json|technical_breakdown_json)\b/i,/\b(?:route_summary|semantic_profile|domain_scope|relation_patterns|account_scope)\b/i,/\b(?:coverage_report|retrieval_status|problem_unit_state|candidate_evidence)\b/i,/\b(?:graph_domain_scope|graph_runtime|selection_reason|why_included)\b/i];function nd(s){try{return JSON.stringify(s,null,2)}catch{return String(s)}}function rd(s){const _=String(s??""),m=_.match(ed);return(m?_.slice(0,m.index):_).replace(/###\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)[\s\S]*?(?:```[\s\S]*?```|$)/gi,"").replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)\b[\s\S]*$/gi,"").split(/\r?\n/g).map(R=>R.trimEnd()).filter(R=>R.trim().length>0).filter(R=>!td.some(G=>G.test(R))).join(` -`).trim()}function ld(s,_,m="default"){const I=m==="technical",k=[];k.push("# Assistant conversation export"),k.push(`session_id: ${s||"n/a"}`),k.push(`export_mode: ${m}`),k.push(`exported_at: ${new Date().toISOString()}`),k.push("");for(let O=0;O<_.length;O+=1){const R=_[O],G=rd(R.text||"");k.push(`## ${O+1}. ${R.role}`),k.push(`message_id: ${R.message_id}`),k.push(`created_at: ${R.created_at}`),k.push(`reply_type: ${R.reply_type??"n/a"}`),R.trace_id&&k.push(`trace_id: ${R.trace_id}`),k.push(""),k.push(G||"(empty)"),k.push(""),I&&R.role==="assistant"&&R.debug&&(k.push("### technical_debug_payload_json"),k.push("```json"),k.push(nd(R.debug)),k.push("```"),k.push(""))}return k.join(` -`)}function _t({value:s}){return c.jsx("pre",{className:"json-view",children:JSON.stringify(s??{},null,2)})}function cn({title:s,subtitle:_,actions:m,children:I}){return c.jsxs("section",{className:"panel-frame",children:[c.jsxs("header",{className:"panel-header",children:[c.jsxs("div",{children:[c.jsx("h2",{children:s}),_?c.jsx("p",{children:_}):null]}),m?c.jsx("div",{className:"panel-actions",children:m}):null]}),c.jsx("div",{className:"panel-body",children:I})]})}function od(s){return s==="assistant"?"Ассистент":"Вы"}function id(s){const _=new Date(s);return Number.isNaN(_.getTime())?s:_.toLocaleTimeString("ru-RU")}async function ud(s){if(navigator.clipboard&&window.isSecureContext)try{return await navigator.clipboard.writeText(s),!0}catch{}const _=document.createElement("textarea");_.value=s,_.setAttribute("readonly","true"),_.style.position="fixed",_.style.opacity="0",_.style.pointerEvents="none",document.body.appendChild(_),_.select();let m=!1;try{m=document.execCommand("copy")}catch{m=!1}finally{document.body.removeChild(_)}return m}function sd({sessionId:s,conversation:_,inputValue:m,onInputChange:I,periodHint:k,onPeriodHintChange:O,businessContext:R,onBusinessContextChange:G,useMock:V,onUseMockChange:ae,onSend:ue,onClear:M,busy:B,statusText:xe,errorMessage:ke}){const ee=q.useRef(null),Z=q.useRef(null),[Be,Le]=q.useState("idle"),[He,ve]=q.useState("чат");q.useEffect(()=>{ee.current&&(ee.current.scrollTop=ee.current.scrollHeight)},[_,xe]),q.useEffect(()=>()=>{Z.current!==null&&window.clearTimeout(Z.current)},[]);async function je(Y){if(_.length===0)return;const Ee=ld(s,_,Y),Ie=await ud(Ee);ve(Y==="technical"?"тех":"чат"),Le(Ie?"success":"error"),Z.current!==null&&window.clearTimeout(Z.current),Z.current=window.setTimeout(()=>{Le("idle")},2200)}return c.jsxs(cn,{title:"Режим ассистента",subtitle:"Диалоговый слой поверх normalizer, маршрутизации и factual retrieval.",actions:c.jsxs("div",{className:"assistant-panel-actions",children:[c.jsx("button",{type:"button",className:"assistant-copy-btn",onClick:()=>{je("default")},disabled:_.length===0,title:"Экспорт только user-facing чата",children:"Скопировать чат"}),c.jsx("button",{type:"button",className:"assistant-copy-btn",onClick:()=>{je("technical")},disabled:_.length===0,title:"Технический экспорт с debug payload",children:"Скопировать техчат"}),Be==="success"?c.jsxs("span",{className:"assistant-copy-feedback success",children:["Скопировано (",He,")"]}):null,Be==="error"?c.jsx("span",{className:"assistant-copy-feedback error",children:"Ошибка копирования"}):null,c.jsx("span",{className:"status-chip",children:s?`session: ${s}`:"новая сессия"})]}),children:[c.jsxs("div",{ref:ee,className:"assistant-chat-list",children:[_.length===0?c.jsx("div",{className:"assistant-empty muted",children:"Диалог пуст. Отправьте первый вопрос, чтобы запустить контур ассистента."}):null,_.map(Y=>c.jsxs("article",{className:`assistant-msg ${Y.role}`,children:[c.jsxs("header",{className:"assistant-msg-head",children:[c.jsx("strong",{children:od(Y.role)}),c.jsx("span",{children:id(Y.created_at)})]}),c.jsx("div",{className:"assistant-msg-body",children:Y.text}),Y.role==="assistant"&&Y.debug?c.jsxs("details",{className:"assistant-debug",children:[c.jsx("summary",{children:"Показать технический разбор"}),c.jsx(_t,{value:Y.debug})]}):null]},Y.message_id))]}),c.jsxs("div",{className:"assistant-compose",children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{children:["Подсказка по периоду",c.jsx("input",{value:k,onChange:Y=>O(Y.target.value)})]}),c.jsxs("label",{children:["Бизнес-контекст",c.jsx("input",{value:R,onChange:Y=>G(Y.target.value)})]})]}),c.jsxs("label",{className:"full-width",children:["Сообщение",c.jsx("textarea",{value:m,onChange:Y=>I(Y.target.value),rows:4,placeholder:"Введите вопрос к данным компании..."})]}),c.jsxs("div",{className:"button-row",children:[c.jsxs("label",{className:"checkbox-row",children:[c.jsx("input",{type:"checkbox",checked:V,onChange:Y=>ae(Y.target.checked)}),"Mock-режим"]}),c.jsx("button",{type:"button",onClick:()=>ue(),disabled:B||!m.trim(),children:B?"Выполняю...":"Отправить"}),c.jsx("button",{type:"button",onClick:()=>M(),disabled:B&&_.length===0,children:"Сбросить сессию"})]}),xe?c.jsx("p",{className:"diff-summary",children:xe}):null,ke?c.jsx("p",{className:"error-text",children:ke}):null]})]})}function $a({value:s,onChange:_,onTestConnection:m,onSaveLocalConfig:I,lastStatus:k,busy:O}){return c.jsxs(cn,{title:"Подключение OpenAI",subtitle:"Ключ живет только в памяти сессии (не пишется в localStorage).",actions:c.jsx("span",{className:"status-chip",children:k||"Статус: не проверено"}),children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{children:["OpenAI API Key",c.jsx("input",{type:"password",value:s.apiKey,onChange:R=>_({...s,apiKey:R.target.value}),placeholder:"sk-..."})]}),c.jsxs("label",{children:["Model ID",c.jsx("input",{value:s.model,onChange:R=>_({...s,model:R.target.value})})]}),c.jsxs("label",{children:["Base URL",c.jsx("input",{value:s.baseUrl,onChange:R=>_({...s,baseUrl:R.target.value})})]}),c.jsxs("label",{children:["Temperature",c.jsx("input",{type:"number",step:"0.1",value:s.temperature,onChange:R=>_({...s,temperature:Number(R.target.value)})})]}),c.jsxs("label",{children:["Max output tokens",c.jsx("input",{type:"number",value:s.maxOutputTokens,onChange:R=>_({...s,maxOutputTokens:Number(R.target.value)})})]})]}),c.jsxs("div",{className:"button-row",children:[c.jsx("button",{type:"button",onClick:()=>I(),children:"Сохранить локальную конфигурацию"}),c.jsx("button",{type:"button",onClick:()=>m(),disabled:O,children:O?"Проверяем...":"Проверить подключение"})]})]})}function ad({items:s,onRefresh:_,onOpenTrace:m}){return c.jsx(cn,{title:"История нормализаций",subtitle:"Короткий вопрос, confidence, route hint и статус валидации.",actions:c.jsx("button",{type:"button",onClick:()=>_(),children:"Обновить"}),children:c.jsxs("div",{className:"history-list",children:[s.length===0?c.jsx("p",{className:"muted",children:"История пока пустая."}):null,s.map(I=>c.jsxs("button",{type:"button",className:"history-item",onClick:()=>m(I.trace_id),children:[c.jsxs("div",{className:"history-row",children:[c.jsx("strong",{children:I.route_hint??"route: n/a"}),c.jsx("span",{children:I.validation_passed?"schema: ok":"schema: fail"})]}),c.jsx("p",{children:I.question_short}),c.jsxs("div",{className:"history-row",children:[c.jsx("span",{children:I.model}),c.jsx("span",{children:new Date(I.timestamp).toLocaleString("ru-RU")})]})]},I.trace_id))]})})}function At(s){return s==null||s===""?"—":String(s)}function cd({result:s}){return c.jsx(cn,{title:"Runtime метрики",subtitle:"trace_id, токены, latency и статус валидации.",children:c.jsxs("div",{className:"metrics-grid",children:[c.jsxs("div",{children:[c.jsx("span",{children:"trace_id"}),c.jsx("strong",{children:At(s?.trace_id)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"request_started_at"}),c.jsx("strong",{children:At(s?new Date(Date.now()-s.latency_ms).toISOString():null)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"request_finished_at"}),c.jsx("strong",{children:At(s?new Date().toISOString():null)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"latency_ms"}),c.jsx("strong",{children:At(s?.latency_ms)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"input_tokens"}),c.jsx("strong",{children:At(s?.usage?.input_tokens)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"output_tokens"}),c.jsx("strong",{children:At(s?.usage?.output_tokens)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"total_tokens"}),c.jsx("strong",{children:At(s?.usage?.total_tokens)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"validation_status"}),c.jsx("strong",{children:s?.validation?.passed?"passed":"failed"})]}),c.jsxs("div",{children:[c.jsx("span",{children:"prompt_version"}),c.jsx("strong",{children:At(s?.prompt_version)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"schema_version"}),c.jsx("strong",{children:At(s?.schema_version)})]})]})})}const fd={normalized:"Normalized JSON",fragments:"Fragment View",scope:"Scope View",flags:"Flags View",route:"Route Simulation",raw:"Raw model output",validation:"Validation",logs:"Logs"};function dd(s){return s&&typeof s=="object"?s:null}function pd({tab:s,onTabChange:_,result:m,appLogs:I}){const k=["normalized","fragments","scope","flags","route","raw","validation","logs"],O=dd(m?.normalized),R=String(O?.schema_version??""),G=R==="normalized_query_v2"||R==="normalized_query_v2_0_1"||R==="normalized_query_v2_0_2",V=G?{fragments:O?.fragments??[],discarded_fragments:O?.discarded_fragments??[]}:{note:"Fragment View доступен для normalized_query_v2."},ae=G?{message_in_scope:O?.message_in_scope??null,scope_confidence:O?.scope_confidence??null,contains_multiple_tasks:O?.contains_multiple_tasks??null,global_notes:O?.global_notes??null}:{note:"Scope View доступен для normalized_query_v2."},ue=G?Array.isArray(O?.fragments)?(O?.fragments).map(M=>({fragment_id:M.fragment_id??null,domain_relevance:M.domain_relevance??null,candidate_labels:M.candidate_labels??[],execution_readiness:M.execution_readiness??null,clarification_reason:M.clarification_reason??null,soft_assumption_used:M.soft_assumption_used??[],route_status:M.route_status??null,no_route_reason:M.no_route_reason??null,flags:M.flags??{}})):[]:{note:"Flags View доступен для normalized_query_v2."};return c.jsxs(cn,{title:"Выходные данные",subtitle:"Structured output и диагностические вкладки.",children:[c.jsx("div",{className:"tab-row",children:k.map(M=>c.jsx("button",{type:"button",className:s===M?"tab active":"tab",onClick:()=>_(M),children:fd[M]},M))}),s==="normalized"?c.jsx(_t,{value:m?.normalized??{note:"Нет данных."}}):null,s==="fragments"?c.jsx(_t,{value:V}):null,s==="scope"?c.jsx(_t,{value:ae}):null,s==="flags"?c.jsx(_t,{value:ue}):null,s==="route"?c.jsx(_t,{value:m?.route_hint_summary??{note:"Нет данных."}}):null,s==="raw"?c.jsx(_t,{value:m?.raw_model_output??{note:"Нет данных."}}):null,s==="validation"?c.jsx(_t,{value:m?.validation??{note:"Нет данных."}}):null,s==="logs"?c.jsx(_t,{value:I}):null]})}function Ba({value:s,onChange:_,presets:m,selectedPresetId:I,onSelectPreset:k,onLoadPreset:O,onSavePreset:R,onResetDefaults:G,onDiffPrevious:V,presetName:ae,onPresetNameChange:ue,diffSummary:M}){return c.jsxs(cn,{title:"Prompt Manager",subtitle:"Системный, developer и domain уровни управляются отдельно.",children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{children:["Системный prompt",c.jsx("textarea",{value:s.systemPrompt,onChange:B=>_({...s,systemPrompt:B.target.value}),rows:6})]}),c.jsxs("label",{children:["Developer / Instruction prompt",c.jsx("textarea",{value:s.developerPrompt,onChange:B=>_({...s,developerPrompt:B.target.value}),rows:6})]}),c.jsxs("label",{children:["Domain prompt",c.jsx("textarea",{value:s.domainPrompt,onChange:B=>_({...s,domainPrompt:B.target.value}),rows:6})]}),c.jsxs("label",{children:["Schema notes",c.jsx("textarea",{value:s.schemaNotes,onChange:B=>_({...s,schemaNotes:B.target.value}),rows:6})]}),c.jsxs("label",{className:"full-width",children:["Few-shot examples",c.jsx("textarea",{value:s.fewShotExamples,onChange:B=>_({...s,fewShotExamples:B.target.value}),rows:8})]})]}),c.jsxs("div",{className:"button-row",children:[c.jsxs("select",{value:I,onChange:B=>k(B.target.value),children:[c.jsx("option",{value:"",children:"Выберите preset..."}),m.map(B=>c.jsx("option",{value:B.id,children:B.name},B.id))]}),c.jsx("button",{type:"button",onClick:()=>O(),children:"Загрузить preset"}),c.jsx("input",{value:ae,onChange:B=>ue(B.target.value),placeholder:"Имя для сохранения"}),c.jsx("button",{type:"button",onClick:()=>R(),children:"Сохранить preset"}),c.jsx("button",{type:"button",onClick:()=>V(),children:"Diff с предыдущим"}),c.jsx("button",{type:"button",onClick:()=>G(),children:"Сбросить к default"})]}),M?c.jsx("p",{className:"diff-summary",children:M}):null]})}function md({value:s,onChange:_,onApplyBatchFormat:m,onNormalize:I,busy:k,useMock:O,onUseMockChange:R,errorMessage:G}){return c.jsxs(cn,{title:"Запрос пользователя",subtitle:"NDC semantic front-end: нормализуем, но не отвечаем за бухгалтерскую суть.",children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{className:"full-width",children:["Raw user question",c.jsx("textarea",{value:s.userQuestion,onChange:V=>_({...s,userQuestion:V.target.value}),rows:6,placeholder:"Например: По каким покупателям у нас на конец июня висят отгрузки без оплаты..."})]}),c.jsxs("label",{className:"full-width",children:["Batch queries (`;` separator)",c.jsx("textarea",{value:s.batchQuestionsRaw,onChange:V=>_({...s,batchQuestionsRaw:V.target.value}),onBlur:()=>m(),rows:8,placeholder:"Вопрос 1; Вопрос 2; Вопрос 3"})]}),c.jsxs("label",{children:["Optional period context",c.jsx("input",{value:s.periodHint,onChange:V=>_({...s,periodHint:V.target.value})})]}),c.jsxs("label",{children:["Optional business context",c.jsx("input",{value:s.businessContext,onChange:V=>_({...s,businessContext:V.target.value})})]}),c.jsxs("label",{children:["Optional expected route (eval)",c.jsx("input",{value:s.expectedRoute,onChange:V=>_({...s,expectedRoute:V.target.value})})]})]}),c.jsxs("div",{className:"button-row",children:[c.jsxs("label",{className:"checkbox-row",children:[c.jsx("input",{type:"checkbox",checked:O,onChange:V=>R(V.target.checked)}),"Mock-режим (без вызова OpenAI)"]}),c.jsx("button",{type:"button",onClick:()=>m(),disabled:k||!s.batchQuestionsRaw.trim(),children:"Применить `;` в переносы"}),c.jsx("button",{type:"button",onClick:()=>I(!1),disabled:k||!s.userQuestion.trim(),children:k?"Нормализуем...":"Normalize"}),c.jsx("button",{type:"button",onClick:()=>I(!0),disabled:k||!s.userQuestion.trim(),children:k?"Сохраняем...":"Normalize + Save as test case"})]}),G?c.jsx("p",{className:"error-text",children:G}):null]})}function hd({runs:s,selectedRunId:_,onSelectRun:m,onStartRun:I,onFinishRun:k,onRefreshRuns:O,onRunEval:R,onCopyEvalReport:G,evalBusy:V,traceItems:ae,evalReport:ue}){return c.jsxs(cn,{title:"NDC Run Monitor",subtitle:"Важно: кнопка Запустить run создает только run-сущность. Кнопка eval запускает batch-проверку normalizer v2.0.2.",children:[c.jsxs("div",{className:"button-row",children:[c.jsx("button",{type:"button",onClick:()=>I(),children:"Запустить run"}),c.jsx("button",{type:"button",onClick:()=>k(),disabled:!_,children:"Завершить выбранный run"}),c.jsx("button",{type:"button",onClick:()=>O(),children:"Обновить runs"}),c.jsx("button",{type:"button",onClick:()=>R(),disabled:V,children:V?"Идет eval v2.0.2...":"Запустить eval v2.0.2"})]}),c.jsxs("div",{className:"runtime-grid",children:[c.jsxs("div",{className:"runtime-runs",children:[s.map(M=>c.jsxs("button",{type:"button",className:_===M.runId?"history-item selected":"history-item",onClick:()=>m(M.runId),children:[c.jsxs("div",{className:"history-row",children:[c.jsx("strong",{children:M.status}),c.jsx("span",{children:M.runId})]}),c.jsxs("div",{className:"history-row",children:[c.jsx("span",{children:M.sessionId}),c.jsx("span",{children:new Date(M.updatedAt).toLocaleString("ru-RU")})]})]},M.runId)),s.length===0?c.jsx("p",{className:"muted",children:"Нет активных запусков."}):null]}),c.jsxs("div",{children:[c.jsx("h3",{children:"Trace выбранного run"}),c.jsx(_t,{value:ae}),c.jsxs("div",{className:"eval-report-wrap",children:[c.jsx("h3",{style:{marginTop:12},children:"Отчет eval"}),c.jsx(_t,{value:ue??{note:"Eval пока не запускался"}}),c.jsx("button",{type:"button",className:"copy-cube-button",title:"Скопировать отчет eval",onClick:()=>G(),children:"⧉"})]})]})]})]})}const vd={apiKey:"",model:"gpt-4o-mini",baseUrl:"https://api.openai.com/v1",temperature:0,maxOutputTokens:700},Ha={systemPrompt:"Ты semantic-normalizer для бухгалтерского ассистента NDC. Возвращай только JSON по схеме normalized_query_v2_0_2.",developerPrompt:"Сначала делай decomposition сообщения на task fragments, затем определяй domain scope и route-critical flags. Для каждого fragment заполняй execution_readiness + route_status + no_route_reason. Если fragment routable, не оставляй его в no_route.",domainPrompt:"Контур: данные текущего предприятия в 1С/NDC. In-scope: документы, проводки, взаиморасчеты, остатки, периодное закрытие, аномалии и контрольные проверки. Out-of-scope: общая теория, законы и оффтоп.",schemaNotes:"schema_version: normalized_query_v2_0_2. Строгий JSON без дополнительных полей.",fewShotExamples:"Q: Проверь по поставщикам хвосты и разложи цепочку документов/оплат. => fragment in_scope, flags: multi_entity + chain_explanation. Q: Как вообще по ФСБУ? => out_of_scope/generic_accounting."},yd={userQuestion:"",batchQuestionsRaw:"",periodHint:"",businessContext:"",expectedRoute:""},Qa="ndc_normalizer_session_config_v1",Gi=["Разбираю запрос","Ищу данные","Собираю ответ"],gd="assistant",Wa="normalizer_v2_0_2";function wd(s){return`[${new Date().toLocaleTimeString("ru-RU")}] ${s}`}function Sd(s,_){if(!_)return"Previous preset is not selected.";const I=["systemPrompt","developerPrompt","domainPrompt","schemaNotes","fewShotExamples"].filter(k=>s[k]!==_[k]).map(k=>`${k}: ${Math.abs(s[k].length-_[k].length)} chars delta`);return I.length===0?"No changes against previous preset.":`Changed fields: ${I.length}. ${I.join(" | ")}`}function xd(){const[s,_]=q.useState(vd),[m,I]=q.useState(Ha),[k,O]=q.useState(yd),[R,G]=q.useState(null),[V,ae]=q.useState([]),[ue,M]=q.useState([]),[B,xe]=q.useState("normalized"),[ke,ee]=q.useState(!1),[Z,Be]=q.useState(""),[Le,He]=q.useState([]),[ve,je]=q.useState(""),[Y,Ee]=q.useState("NDC custom preset"),[Ie,mt]=q.useState(null),[ot,jt]=q.useState(""),[Me,Fe]=q.useState(!1),[et,it]=q.useState([]),[ye,ce]=q.useState(""),[C,F]=q.useState([]),[P,d]=q.useState(!1),[y,W]=q.useState(null),[X,K]=q.useState(""),[b,le]=q.useState(gd),[te,ie]=q.useState(""),[Qe,Vt]=q.useState([]),[Zn,$t]=q.useState(""),[Pn,bn]=q.useState(!1),[Kr,Tt]=q.useState(""),[Yr,fn]=q.useState(""),zt=q.useRef(!1),Q=w=>{M(D=>[wd(w),...D].slice(0,300))};function er(){let w=0;Tt(Gi[0]);const D=window.setInterval(()=>{w=Math.min(w+1,Gi.length-1),Tt(Gi[w])},650);return()=>window.clearInterval(D)}q.useEffect(()=>{const w=localStorage.getItem(Qa);if(w)try{const D=JSON.parse(w);_($=>({...$,model:D.model??$.model,baseUrl:D.baseUrl??$.baseUrl,temperature:D.temperature??$.temperature,maxOutputTokens:D.maxOutputTokens??$.maxOutputTokens}))}catch{}dn(),tr(),pn()},[]);async function dn(){try{const w=await pt.loadHistory();ae(w.items??[])}catch(w){Q(`History load error: ${w instanceof Error?w.message:String(w)}`)}}async function tr(){try{const D=(await pt.loadPresets()).presets??[];if(He(D),zt.current)return;const $=D.find(Ce=>Ce.prompt_version===Wa)??D.find(Ce=>Ce.id==="default-normalizer-v2_0_2");if(!$){zt.current=!0,Q(`Preset autoload skipped: ${Wa} not found.`);return}je($.id),mt(m),I({systemPrompt:$.systemPrompt,developerPrompt:$.developerPrompt,domainPrompt:$.domainPrompt,schemaNotes:$.schemaNotes??"",fewShotExamples:$.fewShotExamples??""}),zt.current=!0,Q(`Preset autoloaded: ${$.name} (${$.prompt_version}).`)}catch(w){Q(`Presets load error: ${w instanceof Error?w.message:String(w)}`)}}async function pn(){try{const w=await pt.listRuns();it(w.items??[])}catch(w){Q(`Runs load error: ${w instanceof Error?w.message:String(w)}`)}}function nr(){localStorage.setItem(Qa,JSON.stringify({model:s.model,baseUrl:s.baseUrl,temperature:s.temperature,maxOutputTokens:s.maxOutputTokens})),Q("Local config saved (without API key).")}async function jn(){ee(!0),K("");try{const w=await pt.testConnection(s);Be(`OK - ${w.model}`),Q(`OpenAI connection ok: ${w.model}`)}catch(w){const D=w instanceof Error?w.message:String(w);Be("Connection error"),K(`Test connection: ${D}`),Q(`Test connection error: ${D}`)}finally{ee(!1)}}async function Tn(w){ee(!0),K("");try{const D=await pt.normalize({connection:s,prompts:m,promptVersion:"normalizer_v2_0_2",query:{userQuestion:k.userQuestion,periodHint:k.periodHint,businessContext:k.businessContext,expectedRoute:k.expectedRoute},saveAsTestCase:w,useMock:Me});G(D),xe("normalized"),Q(`Normalize done: trace=${D.trace_id}, validation=${D.validation.passed?"passed":"failed"}`),dn()}catch(D){const $=D instanceof Error?D.message:String(D);K(`Normalize: ${$}`),Q(`Normalize error: ${$}`)}finally{ee(!1)}}function rr(){const w=Le.find(D=>D.id===ve);if(!w){Q("Preset is not selected.");return}mt(m),I({systemPrompt:w.systemPrompt,developerPrompt:w.developerPrompt,domainPrompt:w.domainPrompt,schemaNotes:w.schemaNotes??"",fewShotExamples:w.fewShotExamples??""}),Q(`Preset loaded: ${w.name}`)}async function Bt(){try{await pt.savePreset({name:Y||"NDC preset",prompt_version:"normalizer_v2_0_2",systemPrompt:m.systemPrompt,developerPrompt:m.developerPrompt,domainPrompt:m.domainPrompt,schemaNotes:m.schemaNotes,fewShotExamples:m.fewShotExamples}),Q("Preset saved."),await tr()}catch(w){Q(`Preset save error: ${w instanceof Error?w.message:String(w)}`)}}function Ht(){I(Ha),Q("Prompt panel reset to defaults.")}function Xr(){const w=Sd(m,Ie);jt(w),Q(w)}function Gr(){const w=k.batchQuestionsRaw.split(";").map(D=>D.trim()).filter(Boolean).join(` - -`);w&&(O(D=>({...D,batchQuestionsRaw:w})),Q("Batch field formatted: `;` converted to blank-line separators."))}async function Jr(w){try{const $=(await pt.loadTrace(w)).trace,Ce=$.parsed_normalized_json??null;G({trace_id:String($.trace_id??w),ok:!!$.validation_result?.passed,normalized:Ce,route_hint_summary:$.route_hint_summary??(Ce?{route_hint:Ce.route_hint??null,confidence:Ce.confidence?.route_hint??null}:null),raw_model_output:$.raw_model_response??{},validation:$.validation_result??{passed:!1,errors:["validation not found"]},usage:$.usage??{input_tokens:0,output_tokens:0,total_tokens:0},latency_ms:Number($.latency_ms??0),prompt_version:String($.prompt_version??"unknown"),schema_version:String($.schema_version??"unknown")}),xe("raw"),K(""),Q(`Trace opened: ${w}`)}catch(D){const $=D instanceof Error?D.message:String(D);K(`Trace: ${$}`),Q(`Trace open error ${w}: ${$}`)}}async function oo(){try{const w=await pt.startRun();ce(w.run.runId),Q(`Run started: ${w.run.runId}`),Q("Tip: start run does not execute normalize by itself. Use 'Run eval v2.0.2' button."),await pn()}catch(w){Q(`Run start error: ${w instanceof Error?w.message:String(w)}`)}}async function lr(){if(ye)try{await pt.finishRun(ye),Q(`Run finished: ${ye}`),await pn()}catch(w){Q(`Run finish error: ${w instanceof Error?w.message:String(w)}`)}}async function or(){d(!0),K("");try{Q("Starting eval in v2 contour.");const w=k.batchQuestionsRaw.trim()||k.userQuestion.trim();if(!w)throw new Error("Fill batch field or Raw user question first.");const D=await pt.runEval({connection:s,prompts:m,promptVersion:"normalizer_v2_0_2",mode:"single-pass-strict",rawQuestions:w,useMock:Me});W(D.report),Q("Eval v2.0.2 run finished.");const $=D.report;if($.run_id&&Q(`Eval run id: ${$.run_id}`),$.metrics){const Ce=$.metrics;Q(`Eval metrics v2.0.2: schema=${Ce.schema_validation_pass_rate??"n/a"}%, route_accuracy=${Ce.route_resolution_accuracy??"n/a"}%, no_route_precision=${Ce.no_route_precision??"n/a"}%, state_consistency=${Ce.execution_state_consistency_rate??"n/a"}%`)}await dn()}catch(w){const D=w instanceof Error?w.message:String(w);D.includes("Legacy eval runner supports normalized_query_v1 only")?(W({status:"plan_only",prompt_version:"normalizer_v2",reason:"backend eval runner is still legacy-v1 only",plan_file:"reports/v2_pilot_eval_plan.md",next_steps:["run cheap mock sanity for schema/fragment/scope","run small real batch (10-15 messages, temperature=0)","run challenge-30 replay with v2 metrics"]}),Q("Backend is legacy-only for eval right now. Showing v2 pilot plan.")):(K(`Eval: ${D}`),Q(`Eval run error: ${D}`))}finally{d(!1)}}async function ir(){try{const w=JSON.stringify(y??{},null,2);await navigator.clipboard.writeText(w),Q("Eval report copied to clipboard.")}catch(w){Q(`Eval report copy error: ${w instanceof Error?w.message:String(w)}`)}}function ur(){ie(""),Vt([]),$t(""),Tt(""),fn(""),Q("Assistant session reset.")}async function sr(){const w=Zn.trim();if(!w)return;bn(!0),fn(""),$t(""),Vt($=>[...$,{message_id:`local-${Date.now()}`,session_id:te||"pending",role:"user",text:w,reply_type:null,created_at:new Date().toISOString(),trace_id:null,debug:null}]);const D=er();try{const $=await pt.sendAssistantMessage({connection:s,prompts:m,userMessage:w,sessionId:te||void 0,promptVersion:"normalizer_v2_0_2",context:{periodHint:k.periodHint,businessContext:k.businessContext},useMock:Me});ie($.session_id),Vt($.conversation),Tt("Ответ готов"),Q(`Assistant reply received: trace=${$.debug.trace_id}`)}catch($){const Ce=$ instanceof Error?$.message:String($);fn(Ce),Tt("Ошибка ассистента"),Q(`Assistant error: ${Ce}`)}finally{D(),bn(!1)}}return q.useEffect(()=>{if(!ye){F([]);return}pt.runTrace(ye).then(w=>F(w.items)).catch(w=>Q(`Run trace error: ${w instanceof Error?w.message:String(w)}`))},[ye]),c.jsxs("main",{className:"app-root",children:[c.jsxs("div",{className:"hero",children:[c.jsx("h1",{children:"NDC AI First Layer"}),c.jsx("p",{children:"Два режима в одном интерфейсе: диагностика декомпозиции и диалоговый ассистент на общем backend-контуре."})]}),c.jsxs("div",{className:"mode-switch-row",children:[c.jsx("button",{type:"button",className:b==="assistant"?"tab active":"tab",onClick:()=>le("assistant"),children:"Ассистент"}),c.jsx("button",{type:"button",className:b==="decomposition"?"tab active":"tab",onClick:()=>le("decomposition"),children:"Декомпозиция"})]}),b==="assistant"?c.jsxs("div",{className:"layout-grid",children:[c.jsx($a,{value:s,onChange:_,onSaveLocalConfig:nr,onTestConnection:jn,lastStatus:Z,busy:ke||Pn}),c.jsx(Ba,{value:m,onChange:I,presets:Le,selectedPresetId:ve,onSelectPreset:je,onLoadPreset:rr,onSavePreset:Bt,onResetDefaults:Ht,onDiffPrevious:Xr,presetName:Y,onPresetNameChange:Ee,diffSummary:ot}),c.jsx(sd,{sessionId:te,conversation:Qe,inputValue:Zn,onInputChange:$t,periodHint:k.periodHint,onPeriodHintChange:w=>O(D=>({...D,periodHint:w})),businessContext:k.businessContext,onBusinessContextChange:w=>O(D=>({...D,businessContext:w})),useMock:Me,onUseMockChange:Fe,onSend:sr,onClear:ur,busy:Pn,statusText:Kr,errorMessage:Yr})]}):c.jsxs("div",{className:"layout-grid",children:[c.jsx($a,{value:s,onChange:_,onSaveLocalConfig:nr,onTestConnection:jn,lastStatus:Z,busy:ke}),c.jsx(Ba,{value:m,onChange:I,presets:Le,selectedPresetId:ve,onSelectPreset:je,onLoadPreset:rr,onSavePreset:Bt,onResetDefaults:Ht,onDiffPrevious:Xr,presetName:Y,onPresetNameChange:Ee,diffSummary:ot}),c.jsx(md,{value:k,onChange:O,onApplyBatchFormat:Gr,onNormalize:Tn,busy:ke,useMock:Me,onUseMockChange:Fe,errorMessage:X}),c.jsx(pd,{tab:B,onTabChange:xe,result:R,appLogs:ue}),c.jsx(cd,{result:R}),c.jsx(ad,{items:V,onRefresh:dn,onOpenTrace:Jr}),c.jsx(hd,{runs:et,selectedRunId:ye,onSelectRun:ce,onStartRun:oo,onFinishRun:lr,onRefreshRuns:pn,onRunEval:or,onCopyEvalReport:ir,evalBusy:P,traceItems:C,evalReport:y})]})]})}Zf.createRoot(document.getElementById("root")).render(c.jsx(Wf.StrictMode,{children:c.jsx(xd,{})})); diff --git a/llm_normalizer/frontend/dist/assets/index-BFy6DcyX.js b/llm_normalizer/frontend/dist/assets/index-BFy6DcyX.js new file mode 100644 index 0000000..33ed717 --- /dev/null +++ b/llm_normalizer/frontend/dist/assets/index-BFy6DcyX.js @@ -0,0 +1,12 @@ +(function(){const x=document.createElement("link").relList;if(x&&x.supports&&x.supports("modulepreload"))return;for(const k of document.querySelectorAll('link[rel="modulepreload"]'))O(k);new MutationObserver(k=>{for(const I of k)if(I.type==="childList")for(const A of I.addedNodes)A.tagName==="LINK"&&A.rel==="modulepreload"&&O(A)}).observe(document,{childList:!0,subtree:!0});function m(k){const I={};return k.integrity&&(I.integrity=k.integrity),k.referrerPolicy&&(I.referrerPolicy=k.referrerPolicy),k.crossOrigin==="use-credentials"?I.credentials="include":k.crossOrigin==="anonymous"?I.credentials="omit":I.credentials="same-origin",I}function O(k){if(k.ep)return;k.ep=!0;const I=m(k);fetch(k.href,I)}})();function Ka(s){return s&&s.__esModule&&Object.prototype.hasOwnProperty.call(s,"default")?s.default:s}var Xi={exports:{}},Gr={},Gi={exports:{}},J={};var La;function Bf(){if(La)return J;La=1;var s=Symbol.for("react.element"),x=Symbol.for("react.portal"),m=Symbol.for("react.fragment"),O=Symbol.for("react.strict_mode"),k=Symbol.for("react.profiler"),I=Symbol.for("react.provider"),A=Symbol.for("react.context"),Y=Symbol.for("react.forward_ref"),F=Symbol.for("react.suspense"),oe=Symbol.for("react.memo"),se=Symbol.for("react.lazy"),N=Symbol.iterator;function U(d){return d===null||typeof d!="object"?null:(d=N&&d[N]||d["@@iterator"],typeof d=="function"?d:null)}var Se={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ee=Object.assign,ee={};function q(d,g,W){this.props=d,this.context=g,this.refs=ee,this.updater=W||Se}q.prototype.isReactComponent={},q.prototype.setState=function(d,g){if(typeof d!="object"&&typeof d!="function"&&d!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,d,g,"setState")},q.prototype.forceUpdate=function(d){this.updater.enqueueForceUpdate(this,d,"forceUpdate")};function He(){}He.prototype=q.prototype;function De(d,g,W){this.props=d,this.context=g,this.refs=ee,this.updater=W||Se}var Fe=De.prototype=new He;Fe.constructor=De,Ee(Fe,q.prototype),Fe.isPureReactComponent=!0;var ye=Array.isArray,_e=Object.prototype.hasOwnProperty,X={current:null},je={key:!0,ref:!0,__self:!0,__source:!0};function Te(d,g,W){var G,Z={},b=null,ie=null;if(g!=null)for(G in g.ref!==void 0&&(ie=g.ref),g.key!==void 0&&(b=""+g.key),g)_e.call(g,G)&&!je.hasOwnProperty(G)&&(Z[G]=g[G]);var le=arguments.length-2;if(le===1)Z.children=W;else if(1>>1,g=C[d];if(0>>1;dk(Z,P))bk(ie,Z)?(C[d]=ie,C[b]=P,d=b):(C[d]=Z,C[G]=P,d=G);else if(bk(ie,P))C[d]=ie,C[b]=P,d=b;else break e}}return B}function k(C,B){var P=C.sortIndex-B.sortIndex;return P!==0?P:C.id-B.id}if(typeof performance=="object"&&typeof performance.now=="function"){var I=performance;s.unstable_now=function(){return I.now()}}else{var A=Date,Y=A.now();s.unstable_now=function(){return A.now()-Y}}var F=[],oe=[],se=1,N=null,U=3,Se=!1,Ee=!1,ee=!1,q=typeof setTimeout=="function"?setTimeout:null,He=typeof clearTimeout=="function"?clearTimeout:null,De=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Fe(C){for(var B=m(oe);B!==null;){if(B.callback===null)O(oe);else if(B.startTime<=C)O(oe),B.sortIndex=B.expirationTime,x(F,B);else break;B=m(oe)}}function ye(C){if(ee=!1,Fe(C),!Ee)if(m(F)!==null)Ee=!0,xe(_e);else{var B=m(oe);B!==null&&ce(ye,B.startTime-C)}}function _e(C,B){Ee=!1,ee&&(ee=!1,He(Te),Te=-1),Se=!0;var P=U;try{for(Fe(B),N=m(F);N!==null&&(!(N.expirationTime>B)||C&&!Et());){var d=N.callback;if(typeof d=="function"){N.callback=null,U=N.priorityLevel;var g=d(N.expirationTime<=B);B=s.unstable_now(),typeof g=="function"?N.callback=g:N===m(F)&&O(F),Fe(B)}else O(F);N=m(F)}if(N!==null)var W=!0;else{var G=m(oe);G!==null&&ce(ye,G.startTime-B),W=!1}return W}finally{N=null,U=P,Se=!1}}var X=!1,je=null,Te=-1,it=5,et=-1;function Et(){return!(s.unstable_now()-etC||125d?(C.sortIndex=P,x(oe,C),m(F)===null&&C===m(oe)&&(ee?(He(Te),Te=-1):ee=!0,ce(ye,P-d))):(C.sortIndex=g,x(F,C),Ee||Se||(Ee=!0,xe(_e))),C},s.unstable_shouldYield=Et,s.unstable_wrapCallback=function(C){var B=U;return function(){var P=U;U=B;try{return C.apply(this,arguments)}finally{U=P}}}})(Zi)),Zi}var Fa;function Yf(){return Fa||(Fa=1,qi.exports=Kf()),qi.exports}var Ua;function Xf(){if(Ua)return be;Ua=1;var s=eu(),x=Yf();function m(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),F=Object.prototype.hasOwnProperty,oe=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,se={},N={};function U(e){return F.call(N,e)?!0:F.call(se,e)?!1:oe.test(e)?N[e]=!0:(se[e]=!0,!1)}function Se(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Ee(e,t,n,r){if(t===null||typeof t>"u"||Se(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ee(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var q={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){q[e]=new ee(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];q[t]=new ee(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){q[e]=new ee(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){q[e]=new ee(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){q[e]=new ee(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){q[e]=new ee(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){q[e]=new ee(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){q[e]=new ee(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){q[e]=new ee(e,5,!1,e.toLowerCase(),null,!1,!1)});var He=/[\-:]([a-z])/g;function De(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(He,De);q[t]=new ee(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(He,De);q[t]=new ee(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(He,De);q[t]=new ee(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){q[e]=new ee(e,1,!1,e.toLowerCase(),null,!1,!1)}),q.xlinkHref=new ee("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){q[e]=new ee(e,1,!1,e.toLowerCase(),null,!0,!0)});function Fe(e,t,n,r){var l=q.hasOwnProperty(t)?q[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var a=` +`+l[i].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=i&&0<=u);break}}}finally{W=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?g(e):""}function Z(e){switch(e.tag){case 5:return g(e.type);case 16:return g("Lazy");case 13:return g("Suspense");case 19:return g("SuspenseList");case 0:case 2:case 15:return e=G(e.type,!1),e;case 11:return e=G(e.type.render,!1),e;case 1:return e=G(e.type,!0),e;default:return""}}function b(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case je:return"Fragment";case X:return"Portal";case it:return"Profiler";case Te:return"StrictMode";case Ue:return"Suspense";case Ye:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Et:return(e.displayName||"Context")+".Consumer";case et:return(e._context.displayName||"Context")+".Provider";case ut:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case st:return t=e.displayName||null,t!==null?t:b(e.type)||"Memo";case xe:t=e._payload,e=e._init;try{return b(e(t))}catch{}}return null}function ie(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return b(t);case 8:return t===Te?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function le(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function te(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Oe(e){var t=te(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function fn(e){e._valueTracker||(e._valueTracker=Oe(e))}function zn(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=te(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function dn(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function or(e,t){var n=t.checked;return P({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Rn(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=le(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ir(e,t){t=t.checked,t!=null&&Fe(e,"checked",t,!1)}function pn(e,t){ir(e,t);var n=le(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Ln(e,t.type,n):t.hasOwnProperty("defaultValue")&&Ln(e,t.type,le(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ur(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Ln(e,t,n){(t!=="number"||dn(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var mn=Array.isArray;function at(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=hn.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function $t(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Vt={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},qr=["Webkit","ms","Moz","O"];Object.keys(Vt).forEach(function(e){qr.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Vt[t]=Vt[e]})});function Zr(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Vt.hasOwnProperty(e)&&Vt[e]?(""+t).trim():t+"px"}function ar(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Zr(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var br=P({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function In(e,t){if(t){if(br[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(m(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(m(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(m(61))}if(t.style!=null&&typeof t.style!="object")throw Error(m(62))}}function Dn(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var cr=null;function fr(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var dr=null,Bt=null,Ht=null;function el(e){if(e=Mr(e)){if(typeof dr!="function")throw Error(m(280));var t=e.stateNode;t&&(t=Cl(t),dr(e.stateNode,e.type,t))}}function tl(e){Bt?Ht?Ht.push(e):Ht=[e]:Bt=e}function nl(){if(Bt){var e=Bt,t=Ht;if(Ht=Bt=null,el(e),t)for(e=0;e>>=0,e===0?32:31-(rc(e)/lc|0)|0}var ul=64,sl=4194304;function vr(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function al(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=vr(u):(o&=i,o!==0&&(r=vr(o)))}else i=n&~l,i!==0?r=vr(i):o!==0&&(r=vr(o));if(r===0)return 0;if(t!==0&&t!==r&&(t&l)===0&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function yr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-vt(t),e[t]=n}function sc(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Cr),Cu=" ",Pu=!1;function Nu(e,t){switch(e){case"keyup":return Dc.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ju(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var An=!1;function Uc(e,t){switch(e){case"compositionend":return ju(t);case"keypress":return t.which!==32?null:(Pu=!0,Cu);case"textInput":return e=t.data,e===Cu&&Pu?null:e;default:return null}}function Ac(e,t){if(An)return e==="compositionend"||!jo&&Nu(e,t)?(e=wu(),ml=xo=Xt=null,An=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Iu(n)}}function Fu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Fu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Uu(){for(var e=window,t=dn();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=dn(e.document)}return t}function Ro(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Xc(e){var t=Uu(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Fu(n.ownerDocument.documentElement,n)){if(r!==null&&Ro(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Du(n,o);var i=Du(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,$n=null,Lo=null,Tr=null,Oo=!1;function Au(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Oo||$n==null||$n!==dn(r)||(r=$n,"selectionStart"in r&&Ro(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Tr&&jr(Tr,r)||(Tr=r,r=xl(Lo,"onSelect"),0Wn||(e.current=Wo[Wn],Wo[Wn]=null,Wn--)}function ae(e,t){Wn++,Wo[Wn]=e.current,e.current=t}var Zt={},Ae=qt(Zt),Xe=qt(!1),wn=Zt;function Kn(e,t){var n=e.type.contextTypes;if(!n)return Zt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function Ge(e){return e=e.childContextTypes,e!=null}function Pl(){de(Xe),de(Ae)}function es(e,t,n){if(Ae.current!==Zt)throw Error(m(168));ae(Ae,t),ae(Xe,n)}function ts(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(m(108,ie(e)||"Unknown",l));return P({},n,r)}function Nl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Zt,wn=Ae.current,ae(Ae,e),ae(Xe,Xe.current),!0}function ns(e,t,n){var r=e.stateNode;if(!r)throw Error(m(169));n?(e=ts(e,t,wn),r.__reactInternalMemoizedMergedChildContext=e,de(Xe),de(Ae),ae(Ae,e)):de(Xe),ae(Xe,n)}var Rt=null,jl=!1,Ko=!1;function rs(e){Rt===null?Rt=[e]:Rt.push(e)}function uf(e){jl=!0,rs(e)}function bt(){if(!Ko&&Rt!==null){Ko=!0;var e=0,t=ue;try{var n=Rt;for(ue=1;e>=i,l-=i,Lt=1<<32-vt(t)+l|n<H?(Le=V,V=null):Le=V.sibling;var re=w(p,V,h[H],E);if(re===null){V===null&&(V=Le);break}e&&V&&re.alternate===null&&t(p,V),f=o(re,f,H),$===null?L=re:$.sibling=re,$=re,V=Le}if(H===h.length)return n(p,V),pe&&_n(p,H),L;if(V===null){for(;HH?(Le=V,V=null):Le=V.sibling;var an=w(p,V,re.value,E);if(an===null){V===null&&(V=Le);break}e&&V&&an.alternate===null&&t(p,V),f=o(an,f,H),$===null?L=an:$.sibling=an,$=an,V=Le}if(re.done)return n(p,V),pe&&_n(p,H),L;if(V===null){for(;!re.done;H++,re=h.next())re=_(p,re.value,E),re!==null&&(f=o(re,f,H),$===null?L=re:$.sibling=re,$=re);return pe&&_n(p,H),L}for(V=r(p,V);!re.done;H++,re=h.next())re=j(V,p,H,re.value,E),re!==null&&(e&&re.alternate!==null&&V.delete(re.key===null?H:re.key),f=o(re,f,H),$===null?L=re:$.sibling=re,$=re);return e&&V.forEach(function(Vf){return t(p,Vf)}),pe&&_n(p,H),L}function we(p,f,h,E){if(typeof h=="object"&&h!==null&&h.type===je&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case _e:e:{for(var L=h.key,$=f;$!==null;){if($.key===L){if(L=h.type,L===je){if($.tag===7){n(p,$.sibling),f=l($,h.props.children),f.return=p,p=f;break e}}else if($.elementType===L||typeof L=="object"&&L!==null&&L.$$typeof===xe&&as(L)===$.type){n(p,$.sibling),f=l($,h.props),f.ref=Ir(p,$,h),f.return=p,p=f;break e}n(p,$);break}else t(p,$);$=$.sibling}h.type===je?(f=Tn(h.props.children,p.mode,E,h.key),f.return=p,p=f):(E=no(h.type,h.key,h.props,null,p.mode,E),E.ref=Ir(p,f,h),E.return=p,p=E)}return i(p);case X:e:{for($=h.key;f!==null;){if(f.key===$)if(f.tag===4&&f.stateNode.containerInfo===h.containerInfo&&f.stateNode.implementation===h.implementation){n(p,f.sibling),f=l(f,h.children||[]),f.return=p,p=f;break e}else{n(p,f);break}else t(p,f);f=f.sibling}f=Hi(h,p.mode,E),f.return=p,p=f}return i(p);case xe:return $=h._init,we(p,f,$(h._payload),E)}if(mn(h))return z(p,f,h,E);if(B(h))return R(p,f,h,E);Ll(p,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,f!==null&&f.tag===6?(n(p,f.sibling),f=l(f,h),f.return=p,p=f):(n(p,f),f=Bi(h,p.mode,E),f.return=p,p=f),i(p)):n(p,f)}return we}var Jn=cs(!0),fs=cs(!1),Ol=qt(null),Ml=null,qn=null,Zo=null;function bo(){Zo=qn=Ml=null}function ei(e){var t=Ol.current;de(Ol),e._currentValue=t}function ti(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Zn(e,t){Ml=e,Zo=qn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Je=!0),e.firstContext=null)}function dt(e){var t=e._currentValue;if(Zo!==e)if(e={context:e,memoizedValue:t,next:null},qn===null){if(Ml===null)throw Error(m(308));qn=e,Ml.dependencies={lanes:0,firstContext:e}}else qn=qn.next=e;return t}var xn=null;function ni(e){xn===null?xn=[e]:xn.push(e)}function ds(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,ni(t)):(n.next=l.next,l.next=n),t.interleaved=n,Mt(e,r)}function Mt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var en=!1;function ri(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ps(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function It(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function tn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(ne&2)!==0){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Mt(e,n)}return l=r.interleaved,l===null?(t.next=t,ni(r)):(t.next=l.next,l.next=t),r.interleaved=t,Mt(e,n)}function Il(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yo(e,n)}}function ms(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Dl(e,t,n,r){var l=e.updateQueue;en=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,v=a.next;a.next=null,i===null?o=v:i.next=v,i=a;var S=e.alternate;S!==null&&(S=S.updateQueue,u=S.lastBaseUpdate,u!==i&&(u===null?S.firstBaseUpdate=v:u.next=v,S.lastBaseUpdate=a))}if(o!==null){var _=l.baseState;i=0,S=v=a=null,u=o;do{var w=u.lane,j=u.eventTime;if((r&w)===w){S!==null&&(S=S.next={eventTime:j,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var z=e,R=u;switch(w=t,j=n,R.tag){case 1:if(z=R.payload,typeof z=="function"){_=z.call(j,_,w);break e}_=z;break e;case 3:z.flags=z.flags&-65537|128;case 0:if(z=R.payload,w=typeof z=="function"?z.call(j,_,w):z,w==null)break e;_=P({},_,w);break e;case 2:en=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,w=l.effects,w===null?l.effects=[u]:w.push(u))}else j={eventTime:j,lane:w,tag:u.tag,payload:u.payload,callback:u.callback,next:null},S===null?(v=S=j,a=_):S=S.next=j,i|=w;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;w=u,u=w.next,w.next=null,l.lastBaseUpdate=w,l.shared.pending=null}}while(!0);if(S===null&&(a=_),l.baseState=a,l.firstBaseUpdate=v,l.lastBaseUpdate=S,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Cn|=i,e.lanes=i,e.memoizedState=_}}function hs(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=si.transition;si.transition={};try{e(!1),t()}finally{ue=n,si.transition=r}}function Ms(){return pt().memoizedState}function ff(e,t,n){var r=on(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e))Ds(t,n);else if(n=ds(e,t,n,r),n!==null){var l=We();xt(n,e,r,l),Fs(n,t,r)}}function df(e,t,n){var r=on(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ds(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,yt(u,i)){var a=t.interleaved;a===null?(l.next=l,ni(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}n=ds(e,t,l,r),n!==null&&(l=We(),xt(n,e,r,l),Fs(n,t,r))}}function Is(e){var t=e.alternate;return e===he||t!==null&&t===he}function Ds(e,t){Ar=Al=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Fs(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yo(e,n)}}var Bl={readContext:dt,useCallback:$e,useContext:$e,useEffect:$e,useImperativeHandle:$e,useInsertionEffect:$e,useLayoutEffect:$e,useMemo:$e,useReducer:$e,useRef:$e,useState:$e,useDebugValue:$e,useDeferredValue:$e,useTransition:$e,useMutableSource:$e,useSyncExternalStore:$e,useId:$e,unstable_isNewReconciler:!1},pf={readContext:dt,useCallback:function(e,t){return jt().memoizedState=[e,t===void 0?null:t],e},useContext:dt,useEffect:Ps,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,$l(4194308,4,Ts.bind(null,t,e),n)},useLayoutEffect:function(e,t){return $l(4194308,4,e,t)},useInsertionEffect:function(e,t){return $l(4,2,e,t)},useMemo:function(e,t){var n=jt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=jt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ff.bind(null,he,e),[r.memoizedState,e]},useRef:function(e){var t=jt();return e={current:e},t.memoizedState=e},useState:Es,useDebugValue:hi,useDeferredValue:function(e){return jt().memoizedState=e},useTransition:function(){var e=Es(!1),t=e[0];return e=cf.bind(null,e[1]),jt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=he,l=jt();if(pe){if(n===void 0)throw Error(m(407));n=n()}else{if(n=t(),Re===null)throw Error(m(349));(En&30)!==0||ws(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Ps(_s.bind(null,r,o,e),[e]),r.flags|=2048,Br(9,Ss.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=jt(),t=Re.identifierPrefix;if(pe){var n=Ot,r=Lt;n=(r&~(1<<32-vt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=$r++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Pt]=t,e[Or]=r,ra(e,t,!1,!1),t.stateNode=e;e:{switch(i=Dn(n,r),n){case"dialog":fe("cancel",e),fe("close",e),l=r;break;case"iframe":case"object":case"embed":fe("load",e),l=r;break;case"video":case"audio":for(l=0;lrr&&(t.flags|=128,r=!0,Hr(o,!1),t.lanes=4194304)}else{if(!r)if(e=Fl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Hr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!pe)return Ve(t),null}else 2*ge()-o.renderingStartTime>rr&&n!==1073741824&&(t.flags|=128,r=!0,Hr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=ge(),t.sibling=null,n=me.current,ae(me,r?n&1|2:n&1),t):(Ve(t),null);case 22:case 23:return Ai(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(lt&1073741824)!==0&&(Ve(t),t.subtreeFlags&6&&(t.flags|=8192)):Ve(t),null;case 24:return null;case 25:return null}throw Error(m(156,t.tag))}function _f(e,t){switch(Xo(t),t.tag){case 1:return Ge(t.type)&&Pl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return bn(),de(Xe),de(Ae),ui(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return oi(t),null;case 13:if(de(me),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(m(340));Gn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return de(me),null;case 4:return bn(),null;case 10:return ei(t.type._context),null;case 22:case 23:return Ai(),null;case 24:return null;default:return null}}var Kl=!1,Be=!1,xf=typeof WeakSet=="function"?WeakSet:Set,T=null;function tr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){ve(e,t,r)}else n.current=null}function Ni(e,t,n){try{n()}catch(r){ve(e,t,r)}}var ia=!1;function kf(e,t){if(Ao=dl,e=Uu(),Ro(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,a=-1,v=0,S=0,_=e,w=null;t:for(;;){for(var j;_!==n||l!==0&&_.nodeType!==3||(u=i+l),_!==o||r!==0&&_.nodeType!==3||(a=i+r),_.nodeType===3&&(i+=_.nodeValue.length),(j=_.firstChild)!==null;)w=_,_=j;for(;;){if(_===e)break t;if(w===n&&++v===l&&(u=i),w===o&&++S===r&&(a=i),(j=_.nextSibling)!==null)break;_=w,w=_.parentNode}_=j}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for($o={focusedElem:e,selectionRange:n},dl=!1,T=t;T!==null;)if(t=T,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,T=e;else for(;T!==null;){t=T;try{var z=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(z!==null){var R=z.memoizedProps,we=z.memoizedState,p=t.stateNode,f=p.getSnapshotBeforeUpdate(t.elementType===t.type?R:wt(t.type,R),we);p.__reactInternalSnapshotBeforeUpdate=f}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163))}}catch(E){ve(t,t.return,E)}if(e=t.sibling,e!==null){e.return=t.return,T=e;break}T=t.return}return z=ia,ia=!1,z}function Qr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Ni(t,n,o)}l=l.next}while(l!==r)}}function Yl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function ji(e){var t=e.ref;if(t!==null){var n=e.stateNode;e.tag,e=n,typeof t=="function"?t(e):t.current=e}}function ua(e){var t=e.alternate;t!==null&&(e.alternate=null,ua(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Pt],delete t[Or],delete t[Qo],delete t[lf],delete t[of])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function sa(e){return e.tag===5||e.tag===3||e.tag===4}function aa(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||sa(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ti(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=El));else if(r!==4&&(e=e.child,e!==null))for(Ti(e,t,n),e=e.sibling;e!==null;)Ti(e,t,n),e=e.sibling}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}var Me=null,St=!1;function nn(e,t,n){for(n=n.child;n!==null;)ca(e,t,n),n=n.sibling}function ca(e,t,n){if(Ct&&typeof Ct.onCommitFiberUnmount=="function")try{Ct.onCommitFiberUnmount(il,n)}catch{}switch(n.tag){case 5:Be||tr(n,t);case 6:var r=Me,l=St;Me=null,nn(e,t,n),Me=r,St=l,Me!==null&&(St?(e=Me,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Me.removeChild(n.stateNode));break;case 18:Me!==null&&(St?(e=Me,n=n.stateNode,e.nodeType===8?Ho(e.parentNode,n):e.nodeType===1&&Ho(e,n),xr(e)):Ho(Me,n.stateNode));break;case 4:r=Me,l=St,Me=n.stateNode.containerInfo,St=!0,nn(e,t,n),Me=r,St=l;break;case 0:case 11:case 14:case 15:if(!Be&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&((o&2)!==0||(o&4)!==0)&&Ni(n,t,i),l=l.next}while(l!==r)}nn(e,t,n);break;case 1:if(!Be&&(tr(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){ve(n,t,u)}nn(e,t,n);break;case 21:nn(e,t,n);break;case 22:n.mode&1?(Be=(r=Be)||n.memoizedState!==null,nn(e,t,n),Be=r):nn(e,t,n);break;default:nn(e,t,n)}}function fa(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new xf),t.forEach(function(r){var l=Lf.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function _t(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=ge()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Cf(r/1960))-r,10e?16:e,ln===null)var r=!1;else{if(e=ln,ln=null,Zl=0,(ne&6)!==0)throw Error(m(331));var l=ne;for(ne|=4,T=e.current;T!==null;){var o=T,i=o.child;if((T.flags&16)!==0){var u=o.deletions;if(u!==null){for(var a=0;age()-Oi?Nn(e,0):Li|=n),Ze(e,t)}function Ea(e,t){t===0&&((e.mode&1)===0?t=1:(t=sl,sl<<=1,(sl&130023424)===0&&(sl=4194304)));var n=We();e=Mt(e,t),e!==null&&(yr(e,t,n),Ze(e,n))}function Rf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ea(e,n)}function Lf(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(m(314))}r!==null&&r.delete(t),Ea(e,n)}var Ca;Ca=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Xe.current)Je=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return Je=!1,wf(e,t,n);Je=(e.flags&131072)!==0}else Je=!1,pe&&(t.flags&1048576)!==0&&ls(t,zl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Wl(e,t),e=t.pendingProps;var l=Kn(t,Ae.current);Zn(t,n),l=ci(null,t,r,e,l,n);var o=fi();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ge(r)?(o=!0,Nl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ri(t),l.updater=Hl,t.stateNode=l,l._reactInternals=t,yi(t,r,e,n),t=_i(null,t,r,!0,o,n)):(t.tag=0,pe&&o&&Yo(t),Qe(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Wl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Mf(r),e=wt(r,e),l){case 0:t=Si(null,t,r,e,n);break e;case 1:t=qs(null,t,r,e,n);break e;case 11:t=Ks(null,t,r,e,n);break e;case 14:t=Ys(null,t,r,wt(r.type,e),n);break e}throw Error(m(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:wt(r,l),Si(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:wt(r,l),qs(e,t,r,l,n);case 3:e:{if(Zs(t),e===null)throw Error(m(387));r=t.pendingProps,o=t.memoizedState,l=o.element,ps(e,t),Dl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=er(Error(m(423)),t),t=bs(e,t,r,n,l);break e}else if(r!==l){l=er(Error(m(424)),t),t=bs(e,t,r,n,l);break e}else for(rt=Jt(t.stateNode.containerInfo.firstChild),nt=t,pe=!0,gt=null,n=fs(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Gn(),r===l){t=Dt(e,t,n);break e}Qe(e,t,r,n)}t=t.child}return t;case 5:return vs(t),e===null&&Jo(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,Vo(r,l)?i=null:o!==null&&Vo(r,o)&&(t.flags|=32),Js(e,t),Qe(e,t,i,n),t.child;case 6:return e===null&&Jo(t),null;case 13:return ea(e,t,n);case 4:return li(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Jn(t,null,r,n):Qe(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:wt(r,l),Ks(e,t,r,l,n);case 7:return Qe(e,t,t.pendingProps,n),t.child;case 8:return Qe(e,t,t.pendingProps.children,n),t.child;case 12:return Qe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,ae(Ol,r._currentValue),r._currentValue=i,o!==null)if(yt(o.value,i)){if(o.children===l.children&&!Xe.current){t=Dt(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(o.tag===1){a=It(-1,n&-n),a.tag=2;var v=o.updateQueue;if(v!==null){v=v.shared;var S=v.pending;S===null?a.next=a:(a.next=S.next,S.next=a),v.pending=a}}o.lanes|=n,a=o.alternate,a!==null&&(a.lanes|=n),ti(o.return,n,t),u.lanes|=n;break}a=a.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(m(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),ti(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}Qe(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Zn(t,n),l=dt(l),r=r(l),t.flags|=1,Qe(e,t,r,n),t.child;case 14:return r=t.type,l=wt(r,t.pendingProps),l=wt(r.type,l),Ys(e,t,r,l,n);case 15:return Xs(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:wt(r,l),Wl(e,t),t.tag=1,Ge(r)?(e=!0,Nl(t)):e=!1,Zn(t,n),As(t,r,l),yi(t,r,l,n),_i(null,t,r,!0,e,n);case 19:return na(e,t,n);case 22:return Gs(e,t,n)}throw Error(m(156,t.tag))};function Pa(e,t){return ou(e,t)}function Of(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ht(e,t,n,r){return new Of(e,t,n,r)}function Vi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Mf(e){if(typeof e=="function")return Vi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ut)return 11;if(e===st)return 14}return 2}function sn(e,t){var n=e.alternate;return n===null?(n=ht(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function no(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Vi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case je:return Tn(n.children,l,o,t);case Te:i=8,l|=8;break;case it:return e=ht(12,n,t,l|2),e.elementType=it,e.lanes=o,e;case Ue:return e=ht(13,n,t,l),e.elementType=Ue,e.lanes=o,e;case Ye:return e=ht(19,n,t,l),e.elementType=Ye,e.lanes=o,e;case ce:return ro(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case et:i=10;break e;case Et:i=9;break e;case ut:i=11;break e;case st:i=14;break e;case xe:i=16,r=null;break e}throw Error(m(130,e==null?e:typeof e,""))}return t=ht(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Tn(e,t,n,r){return e=ht(7,e,r,t),e.lanes=n,e}function ro(e,t,n,r){return e=ht(22,e,r,t),e.elementType=ce,e.lanes=n,e.stateNode={isHidden:!1},e}function Bi(e,t,n){return e=ht(6,e,null,t),e.lanes=n,e}function Hi(e,t,n){return t=ht(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function If(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=vo(0),this.expirationTimes=vo(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=vo(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Qi(e,t,n,r,l,o,i,u,a){return e=new If(e,t,n,u,a),t===1?(t=1,o===!0&&(t|=8)):t=0,o=ht(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ri(o),e}function Df(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(s)}catch(x){console.error(x)}}return s(),Ji.exports=Xf(),Ji.exports}var $a;function Jf(){if($a)return co;$a=1;var s=Gf();return co.createRoot=s.createRoot,co.hydrateRoot=s.hydrateRoot,co}var qf=Jf();const Zf=Ka(qf),bf="/api";async function Ke(s,x){const m=await fetch(`${bf}${s}`,{...x,headers:{"Content-Type":"application/json",...x?.headers??{}}}),O=await m.json();if(!m.ok){const k=O.error?.message??"Ошибка запроса";throw new Error(k)}return O}const ot={async listModels(s){return Ke("/llm/models",{method:"POST",body:JSON.stringify({llmProvider:s.llmProvider,apiKey:s.apiKey,model:s.model,baseUrl:s.baseUrl})})},async testConnection(s){return Ke("/llm/test-connection",{method:"POST",body:JSON.stringify({llmProvider:s.llmProvider,apiKey:s.apiKey,model:s.model,baseUrl:s.baseUrl})})},async normalize(s){return Ke("/normalize",{method:"POST",body:JSON.stringify({llmProvider:s.connection.llmProvider,apiKey:s.connection.apiKey,model:s.connection.model,baseUrl:s.connection.baseUrl,temperature:s.connection.temperature,maxOutputTokens:s.connection.maxOutputTokens,promptVersion:s.promptVersion,systemPrompt:s.prompts.systemPrompt,developerPrompt:s.prompts.developerPrompt,domainPrompt:s.prompts.domainPrompt,fewShotExamples:s.prompts.fewShotExamples,userQuestion:s.query.userQuestion,context:{period_hint:s.query.periodHint??"",business_context:s.query.businessContext??"",expected_route:s.query.expectedRoute??""},saveAsTestCase:!!s.saveAsTestCase,useMock:!!s.useMock})})},async loadHistory(){return Ke("/history")},async loadTrace(s){return Ke(`/history/${s}`)},async loadPresets(){return Ke("/presets")},async savePreset(s){return Ke("/presets/save",{method:"POST",body:JSON.stringify(s)})},async runEval(s){return Ke("/eval/run",{method:"POST",body:JSON.stringify({normalizeConfig:{llmProvider:s.connection.llmProvider,apiKey:s.connection.apiKey,model:s.connection.model,baseUrl:s.connection.baseUrl,temperature:s.connection.temperature,maxOutputTokens:s.connection.maxOutputTokens,promptVersion:s.promptVersion,systemPrompt:s.prompts.systemPrompt,developerPrompt:s.prompts.developerPrompt,domainPrompt:s.prompts.domainPrompt,fewShotExamples:s.prompts.fewShotExamples},caseIds:s.caseIds,useMock:!!s.useMock,mode:s.mode??"standard",caseSetFile:s.caseSetFile,rawQuestions:s.rawQuestions})})},async startRun(){return Ke("/accounting-agent/v1/runs/start",{method:"POST",body:JSON.stringify({initiator:"ndc_operator",source:"gui"})})},async finishRun(s){return Ke("/accounting-agent/v1/runs/finish",{method:"POST",body:JSON.stringify({runId:s,status:"DONE",source:"gui",reason:"Остановлено оператором из GUI"})})},async listRuns(){return Ke("/accounting-agent/v1/runs")},async listResults(){return Ke("/accounting-agent/v1/results")},async runTrace(s){return Ke(`/accounting-agent/v1/trace/run/${s}`)},async sendAssistantMessage(s){return Ke("/assistant/message",{method:"POST",body:JSON.stringify({session_id:s.sessionId??"",mode:"assistant",message:s.userMessage,user_message:s.userMessage,llmProvider:s.connection.llmProvider,apiKey:s.connection.apiKey,model:s.connection.model,baseUrl:s.connection.baseUrl,temperature:s.connection.temperature,maxOutputTokens:s.connection.maxOutputTokens,promptVersion:s.promptVersion??"address_query_runtime_v1",systemPrompt:s.prompts.systemPrompt,developerPrompt:s.prompts.developerPrompt,domainPrompt:s.prompts.domainPrompt,fewShotExamples:s.prompts.fewShotExamples,context:{period_hint:s.context?.periodHint??"",business_context:s.context?.businessContext??""},useMock:!!s.useMock})})},async loadAssistantSession(s){return Ke(`/assistant/session/${s}`)}},ed=/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json|debug_payload|technical_breakdown)\b/i,td=[/\b(?:debug_payload_json|technical_breakdown_json)\b/i,/\b(?:route_summary|semantic_profile|domain_scope|relation_patterns|account_scope)\b/i,/\b(?:coverage_report|retrieval_status|problem_unit_state|candidate_evidence)\b/i,/\b(?:graph_domain_scope|graph_runtime|selection_reason|why_included)\b/i];function nd(s){try{return JSON.stringify(s,null,2)}catch{return String(s)}}function rd(s){const x=String(s??""),m=x.match(ed);return(m?x.slice(0,m.index):x).replace(/###\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)[\s\S]*?(?:```[\s\S]*?```|$)/gi,"").replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)\b[\s\S]*$/gi,"").split(/\r?\n/g).map(A=>A.trimEnd()).filter(A=>A.trim().length>0).filter(A=>!td.some(Y=>Y.test(A))).join(` +`).trim()}function ld(s,x,m="default"){const O=m==="technical",k=[];k.push("# Assistant conversation export"),k.push(`session_id: ${s||"n/a"}`),k.push(`export_mode: ${m}`),k.push(`exported_at: ${new Date().toISOString()}`),k.push("");for(let I=0;I{ee.current&&(ee.current.scrollTop=ee.current.scrollHeight)},[x,Se]),K.useEffect(()=>()=>{q.current!==null&&window.clearTimeout(q.current)},[]);async function _e(X){if(x.length===0)return;const je=ld(s,x,X),Te=await ud(je);ye(X==="technical"?"тех":"чат"),De(Te?"success":"error"),q.current!==null&&window.clearTimeout(q.current),q.current=window.setTimeout(()=>{De("idle")},2200)}return c.jsxs(cn,{title:"Режим ассистента",subtitle:"Диалоговый слой поверх normalizer, маршрутизации и factual retrieval.",actions:c.jsxs("div",{className:"assistant-panel-actions",children:[c.jsx("button",{type:"button",className:"assistant-copy-btn",onClick:()=>{_e("default")},disabled:x.length===0,title:"Экспорт только user-facing чата",children:"Скопировать чат"}),c.jsx("button",{type:"button",className:"assistant-copy-btn",onClick:()=>{_e("technical")},disabled:x.length===0,title:"Технический экспорт с debug payload",children:"Скопировать техчат"}),He==="success"?c.jsxs("span",{className:"assistant-copy-feedback success",children:["Скопировано (",Fe,")"]}):null,He==="error"?c.jsx("span",{className:"assistant-copy-feedback error",children:"Ошибка копирования"}):null,c.jsx("span",{className:"status-chip",children:s?`session: ${s}`:"новая сессия"})]}),children:[c.jsxs("div",{ref:ee,className:"assistant-chat-list",children:[x.length===0?c.jsx("div",{className:"assistant-empty muted",children:"Диалог пуст. Отправьте первый вопрос, чтобы запустить контур ассистента."}):null,x.map(X=>c.jsxs("article",{className:`assistant-msg ${X.role}`,children:[c.jsxs("header",{className:"assistant-msg-head",children:[c.jsx("strong",{children:od(X.role)}),c.jsx("span",{children:id(X.created_at)})]}),c.jsx("div",{className:"assistant-msg-body",children:X.text}),X.role==="assistant"&&X.debug?c.jsxs("details",{className:"assistant-debug",children:[c.jsx("summary",{children:"Показать технический разбор"}),c.jsx(kt,{value:X.debug})]}):null]},X.message_id))]}),c.jsxs("div",{className:"assistant-compose",children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{children:["Подсказка по периоду",c.jsx("input",{value:k,onChange:X=>I(X.target.value)})]}),c.jsxs("label",{children:["Бизнес-контекст",c.jsx("input",{value:A,onChange:X=>Y(X.target.value)})]})]}),c.jsxs("label",{className:"full-width",children:["Сообщение",c.jsx("textarea",{value:m,onChange:X=>O(X.target.value),rows:4,placeholder:"Введите вопрос к данным компании..."})]}),c.jsxs("div",{className:"button-row",children:[c.jsxs("label",{className:"checkbox-row",children:[c.jsx("input",{type:"checkbox",checked:F,onChange:X=>oe(X.target.checked)}),"Mock-режим"]}),c.jsx("button",{type:"button",onClick:()=>se(),disabled:U||!m.trim(),children:U?"Выполняю...":"Отправить"}),c.jsx("button",{type:"button",onClick:()=>N(),disabled:U&&x.length===0,children:"Сбросить сессию"})]}),Se?c.jsx("p",{className:"diff-summary",children:Se}):null,Ee?c.jsx("p",{className:"error-text",children:Ee}):null]})]})}function Va({value:s,modelOptions:x,modelsBusy:m,onChange:O,onReloadModels:k,onTestConnection:I,onSaveLocalConfig:A,lastStatus:Y,busy:F}){const oe=s.llmProvider==="local",se=x.includes(s.model);return c.jsxs(cn,{title:"LLM Connection",subtitle:"Switch between OpenAI cloud and local OpenAI-compatible server.",actions:c.jsx("span",{className:"status-chip",children:Y||"Status: not checked"}),children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{children:["Provider",c.jsxs("select",{value:s.llmProvider,onChange:N=>{const U=N.target.value==="local"?"local":"openai";O({...s,llmProvider:U,baseUrl:U==="local"?"http://127.0.0.1:1234/v1":"https://api.openai.com/v1"})},children:[c.jsx("option",{value:"openai",children:"OpenAI (token)"}),c.jsx("option",{value:"local",children:"Local (LM Studio / OpenAI-compatible)"})]})]}),c.jsxs("label",{children:["Model",c.jsxs("select",{value:se?s.model:"__manual__",onChange:N=>{const U=N.target.value;U!=="__manual__"&&O({...s,model:U})},children:[c.jsx("option",{value:"__manual__",children:"Manual input"}),x.map(N=>c.jsx("option",{value:N,children:N},N))]})]}),c.jsxs("label",{children:["Model ID (manual)",c.jsx("input",{value:s.model,onChange:N=>O({...s,model:N.target.value}),placeholder:"qwen2.5-14b-instruct or lmstudio loaded model id"})]}),oe?null:c.jsxs("label",{className:"full-width",children:["OpenAI API Key",c.jsx("input",{type:"password",value:s.apiKey,onChange:N=>O({...s,apiKey:N.target.value}),placeholder:"sk-..."})]}),c.jsxs("label",{className:oe?"full-width":void 0,children:[oe?"Local server base URL":"Base URL",c.jsx("input",{value:s.baseUrl,onChange:N=>O({...s,baseUrl:N.target.value}),placeholder:oe?"http://127.0.0.1:1234/v1":"https://api.openai.com/v1"})]}),c.jsxs("label",{children:["Temperature",c.jsx("input",{type:"number",step:"0.1",value:s.temperature,onChange:N=>O({...s,temperature:Number(N.target.value)})})]}),c.jsxs("label",{children:["Max output tokens",c.jsx("input",{type:"number",value:s.maxOutputTokens,onChange:N=>O({...s,maxOutputTokens:Number(N.target.value)})})]})]}),c.jsxs("div",{className:"button-row",children:[c.jsx("button",{type:"button",onClick:()=>A(),children:"Save local config"}),c.jsx("button",{type:"button",onClick:()=>k(),disabled:F||m,children:m?"Loading models...":"Load model list"}),c.jsx("button",{type:"button",onClick:()=>I(),disabled:F,children:F?"Checking...":"Test connection"})]})]})}function ad({items:s,onRefresh:x,onOpenTrace:m}){return c.jsx(cn,{title:"История нормализаций",subtitle:"Короткий вопрос, confidence, route hint и статус валидации.",actions:c.jsx("button",{type:"button",onClick:()=>x(),children:"Обновить"}),children:c.jsxs("div",{className:"history-list",children:[s.length===0?c.jsx("p",{className:"muted",children:"История пока пустая."}):null,s.map(O=>c.jsxs("button",{type:"button",className:"history-item",onClick:()=>m(O.trace_id),children:[c.jsxs("div",{className:"history-row",children:[c.jsx("strong",{children:O.route_hint??"route: n/a"}),c.jsx("span",{children:O.validation_passed?"schema: ok":"schema: fail"})]}),c.jsx("p",{children:O.question_short}),c.jsxs("div",{className:"history-row",children:[c.jsx("span",{children:O.model}),c.jsx("span",{children:new Date(O.timestamp).toLocaleString("ru-RU")})]})]},O.trace_id))]})})}function Ut(s){return s==null||s===""?"—":String(s)}function cd({result:s}){return c.jsx(cn,{title:"Runtime метрики",subtitle:"trace_id, токены, latency и статус валидации.",children:c.jsxs("div",{className:"metrics-grid",children:[c.jsxs("div",{children:[c.jsx("span",{children:"trace_id"}),c.jsx("strong",{children:Ut(s?.trace_id)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"request_started_at"}),c.jsx("strong",{children:Ut(s?new Date(Date.now()-s.latency_ms).toISOString():null)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"request_finished_at"}),c.jsx("strong",{children:Ut(s?new Date().toISOString():null)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"latency_ms"}),c.jsx("strong",{children:Ut(s?.latency_ms)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"input_tokens"}),c.jsx("strong",{children:Ut(s?.usage?.input_tokens)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"output_tokens"}),c.jsx("strong",{children:Ut(s?.usage?.output_tokens)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"total_tokens"}),c.jsx("strong",{children:Ut(s?.usage?.total_tokens)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"validation_status"}),c.jsx("strong",{children:s?.validation?.passed?"passed":"failed"})]}),c.jsxs("div",{children:[c.jsx("span",{children:"prompt_version"}),c.jsx("strong",{children:Ut(s?.prompt_version)})]}),c.jsxs("div",{children:[c.jsx("span",{children:"schema_version"}),c.jsx("strong",{children:Ut(s?.schema_version)})]})]})})}const fd={normalized:"Normalized JSON",fragments:"Fragment View",scope:"Scope View",flags:"Flags View",route:"Route Simulation",raw:"Raw model output",validation:"Validation",logs:"Logs"};function dd(s){return s&&typeof s=="object"?s:null}function pd({tab:s,onTabChange:x,result:m,appLogs:O}){const k=["normalized","fragments","scope","flags","route","raw","validation","logs"],I=dd(m?.normalized),A=String(I?.schema_version??""),Y=A==="normalized_query_v2"||A==="normalized_query_v2_0_1"||A==="normalized_query_v2_0_2",F=Y?{fragments:I?.fragments??[],discarded_fragments:I?.discarded_fragments??[]}:{note:"Fragment View доступен для normalized_query_v2."},oe=Y?{message_in_scope:I?.message_in_scope??null,scope_confidence:I?.scope_confidence??null,contains_multiple_tasks:I?.contains_multiple_tasks??null,global_notes:I?.global_notes??null}:{note:"Scope View доступен для normalized_query_v2."},se=Y?Array.isArray(I?.fragments)?(I?.fragments).map(N=>({fragment_id:N.fragment_id??null,domain_relevance:N.domain_relevance??null,candidate_labels:N.candidate_labels??[],execution_readiness:N.execution_readiness??null,clarification_reason:N.clarification_reason??null,soft_assumption_used:N.soft_assumption_used??[],route_status:N.route_status??null,no_route_reason:N.no_route_reason??null,flags:N.flags??{}})):[]:{note:"Flags View доступен для normalized_query_v2."};return c.jsxs(cn,{title:"Выходные данные",subtitle:"Structured output и диагностические вкладки.",children:[c.jsx("div",{className:"tab-row",children:k.map(N=>c.jsx("button",{type:"button",className:s===N?"tab active":"tab",onClick:()=>x(N),children:fd[N]},N))}),s==="normalized"?c.jsx(kt,{value:m?.normalized??{note:"Нет данных."}}):null,s==="fragments"?c.jsx(kt,{value:F}):null,s==="scope"?c.jsx(kt,{value:oe}):null,s==="flags"?c.jsx(kt,{value:se}):null,s==="route"?c.jsx(kt,{value:m?.route_hint_summary??{note:"Нет данных."}}):null,s==="raw"?c.jsx(kt,{value:m?.raw_model_output??{note:"Нет данных."}}):null,s==="validation"?c.jsx(kt,{value:m?.validation??{note:"Нет данных."}}):null,s==="logs"?c.jsx(kt,{value:O}):null]})}function Ba({value:s,onChange:x,presets:m,selectedPresetId:O,onSelectPreset:k,onLoadPreset:I,onSavePreset:A,onResetDefaults:Y,onDiffPrevious:F,presetName:oe,onPresetNameChange:se,diffSummary:N}){return c.jsxs(cn,{title:"Prompt Manager",subtitle:"Системный, developer и domain уровни управляются отдельно.",children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{children:["Системный prompt",c.jsx("textarea",{value:s.systemPrompt,onChange:U=>x({...s,systemPrompt:U.target.value}),rows:6})]}),c.jsxs("label",{children:["Developer / Instruction prompt",c.jsx("textarea",{value:s.developerPrompt,onChange:U=>x({...s,developerPrompt:U.target.value}),rows:6})]}),c.jsxs("label",{children:["Domain prompt",c.jsx("textarea",{value:s.domainPrompt,onChange:U=>x({...s,domainPrompt:U.target.value}),rows:6})]}),c.jsxs("label",{children:["Schema notes",c.jsx("textarea",{value:s.schemaNotes,onChange:U=>x({...s,schemaNotes:U.target.value}),rows:6})]}),c.jsxs("label",{className:"full-width",children:["Few-shot examples",c.jsx("textarea",{value:s.fewShotExamples,onChange:U=>x({...s,fewShotExamples:U.target.value}),rows:8})]})]}),c.jsxs("div",{className:"button-row",children:[c.jsxs("select",{value:O,onChange:U=>k(U.target.value),children:[c.jsx("option",{value:"",children:"Выберите preset..."}),m.map(U=>c.jsx("option",{value:U.id,children:U.name},U.id))]}),c.jsx("button",{type:"button",onClick:()=>I(),children:"Загрузить preset"}),c.jsx("input",{value:oe,onChange:U=>se(U.target.value),placeholder:"Имя для сохранения"}),c.jsx("button",{type:"button",onClick:()=>A(),children:"Сохранить preset"}),c.jsx("button",{type:"button",onClick:()=>F(),children:"Diff с предыдущим"}),c.jsx("button",{type:"button",onClick:()=>Y(),children:"Сбросить к default"})]}),N?c.jsx("p",{className:"diff-summary",children:N}):null]})}function md({value:s,onChange:x,onApplyBatchFormat:m,onNormalize:O,busy:k,useMock:I,onUseMockChange:A,errorMessage:Y}){return c.jsxs(cn,{title:"Запрос пользователя",subtitle:"NDC semantic front-end: нормализуем, но не отвечаем за бухгалтерскую суть.",children:[c.jsxs("div",{className:"grid-two",children:[c.jsxs("label",{className:"full-width",children:["Raw user question",c.jsx("textarea",{value:s.userQuestion,onChange:F=>x({...s,userQuestion:F.target.value}),rows:6,placeholder:"Например: По каким покупателям у нас на конец июня висят отгрузки без оплаты..."})]}),c.jsxs("label",{className:"full-width",children:["Batch queries (`;` separator)",c.jsx("textarea",{value:s.batchQuestionsRaw,onChange:F=>x({...s,batchQuestionsRaw:F.target.value}),onBlur:()=>m(),rows:8,placeholder:"Вопрос 1; Вопрос 2; Вопрос 3"})]}),c.jsxs("label",{children:["Optional period context",c.jsx("input",{value:s.periodHint,onChange:F=>x({...s,periodHint:F.target.value})})]}),c.jsxs("label",{children:["Optional business context",c.jsx("input",{value:s.businessContext,onChange:F=>x({...s,businessContext:F.target.value})})]}),c.jsxs("label",{children:["Optional expected route (eval)",c.jsx("input",{value:s.expectedRoute,onChange:F=>x({...s,expectedRoute:F.target.value})})]})]}),c.jsxs("div",{className:"button-row",children:[c.jsxs("label",{className:"checkbox-row",children:[c.jsx("input",{type:"checkbox",checked:I,onChange:F=>A(F.target.checked)}),"Mock-режим (без вызова OpenAI)"]}),c.jsx("button",{type:"button",onClick:()=>m(),disabled:k||!s.batchQuestionsRaw.trim(),children:"Применить `;` в переносы"}),c.jsx("button",{type:"button",onClick:()=>O(!1),disabled:k||!s.userQuestion.trim(),children:k?"Нормализуем...":"Normalize"}),c.jsx("button",{type:"button",onClick:()=>O(!0),disabled:k||!s.userQuestion.trim(),children:k?"Сохраняем...":"Normalize + Save as test case"})]}),Y?c.jsx("p",{className:"error-text",children:Y}):null]})}function hd({runs:s,selectedRunId:x,onSelectRun:m,onStartRun:O,onFinishRun:k,onRefreshRuns:I,onRunEval:A,onCopyEvalReport:Y,evalBusy:F,traceItems:oe,evalReport:se}){return c.jsxs(cn,{title:"NDC Run Monitor",subtitle:"Важно: кнопка Запустить run создает только run-сущность. Кнопка eval запускает batch-проверку normalizer v2.0.2.",children:[c.jsxs("div",{className:"button-row",children:[c.jsx("button",{type:"button",onClick:()=>O(),children:"Запустить run"}),c.jsx("button",{type:"button",onClick:()=>k(),disabled:!x,children:"Завершить выбранный run"}),c.jsx("button",{type:"button",onClick:()=>I(),children:"Обновить runs"}),c.jsx("button",{type:"button",onClick:()=>A(),disabled:F,children:F?"Идет eval v2.0.2...":"Запустить eval v2.0.2"})]}),c.jsxs("div",{className:"runtime-grid",children:[c.jsxs("div",{className:"runtime-runs",children:[s.map(N=>c.jsxs("button",{type:"button",className:x===N.runId?"history-item selected":"history-item",onClick:()=>m(N.runId),children:[c.jsxs("div",{className:"history-row",children:[c.jsx("strong",{children:N.status}),c.jsx("span",{children:N.runId})]}),c.jsxs("div",{className:"history-row",children:[c.jsx("span",{children:N.sessionId}),c.jsx("span",{children:new Date(N.updatedAt).toLocaleString("ru-RU")})]})]},N.runId)),s.length===0?c.jsx("p",{className:"muted",children:"Нет активных запусков."}):null]}),c.jsxs("div",{children:[c.jsx("h3",{children:"Trace выбранного run"}),c.jsx(kt,{value:oe}),c.jsxs("div",{className:"eval-report-wrap",children:[c.jsx("h3",{style:{marginTop:12},children:"Отчет eval"}),c.jsx(kt,{value:se??{note:"Eval пока не запускался"}}),c.jsx("button",{type:"button",className:"copy-cube-button",title:"Скопировать отчет eval",onClick:()=>Y(),children:"⧉"})]})]})]})]})}const vd={llmProvider:"openai",apiKey:"",model:"gpt-4o-mini",baseUrl:"https://api.openai.com/v1",temperature:0,maxOutputTokens:700},Ha={systemPrompt:"Ты semantic-normalizer для бухгалтерского ассистента NDC. Возвращай только JSON по схеме normalized_query_v2_0_2.",developerPrompt:"Сначала делай decomposition сообщения на task fragments, затем определяй domain scope и route-critical flags. Для каждого fragment заполняй execution_readiness + route_status + no_route_reason. Если fragment routable, не оставляй его в no_route.",domainPrompt:"Контур: данные текущего предприятия в 1С/NDC. In-scope: документы, проводки, взаиморасчеты, остатки, периодное закрытие, аномалии и контрольные проверки. Out-of-scope: общая теория, законы и оффтоп.",schemaNotes:"schema_version: normalized_query_v2_0_2. Строгий JSON без дополнительных полей.",fewShotExamples:"Q: Проверь по поставщикам хвосты и разложи цепочку документов/оплат. => fragment in_scope, flags: multi_entity + chain_explanation. Q: Как вообще по ФСБУ? => out_of_scope/generic_accounting."},yd={userQuestion:"",batchQuestionsRaw:"",periodHint:"",businessContext:"",expectedRoute:""},Qa="ndc_normalizer_session_config_v1",bi=["Разбираю запрос","Ищу данные","Собираю ответ"],gd="assistant",Wa="normalizer_v2_0_2",wd="address_query_runtime_v1";function Sd(s){return`[${new Date().toLocaleTimeString("ru-RU")}] ${s}`}function _d(s,x){if(!x)return"Previous preset is not selected.";const O=["systemPrompt","developerPrompt","domainPrompt","schemaNotes","fewShotExamples"].filter(k=>s[k]!==x[k]).map(k=>`${k}: ${Math.abs(s[k].length-x[k].length)} chars delta`);return O.length===0?"No changes against previous preset.":`Changed fields: ${O.length}. ${O.join(" | ")}`}function xd(){const[s,x]=K.useState(vd),[m,O]=K.useState(Ha),[k,I]=K.useState(yd),[A,Y]=K.useState(null),[F,oe]=K.useState([]),[se,N]=K.useState([]),[U,Se]=K.useState("normalized"),[Ee,ee]=K.useState(!1),[q,He]=K.useState(!1),[De,Fe]=K.useState([]),[ye,_e]=K.useState(""),[X,je]=K.useState([]),[Te,it]=K.useState(""),[et,Et]=K.useState("NDC custom preset"),[ut,Ue]=K.useState(null),[Ye,st]=K.useState(""),[xe,ce]=K.useState(!1),[C,B]=K.useState([]),[P,d]=K.useState(""),[g,W]=K.useState([]),[G,Z]=K.useState(!1),[b,ie]=K.useState(null),[le,te]=K.useState(""),[Oe,fn]=K.useState(gd),[zn,dn]=K.useState(""),[or,Rn]=K.useState([]),[ir,pn]=K.useState(""),[ur,Ln]=K.useState(!1),[mn,at]=K.useState(""),[sr,On]=K.useState(""),Mn=K.useRef(!1),Q=y=>{N(M=>[Sd(y),...M].slice(0,300))};function Jr(){let y=0;at(bi[0]);const M=window.setInterval(()=>{y=Math.min(y+1,bi.length-1),at(bi[y])},650);return()=>window.clearInterval(M)}K.useEffect(()=>{const y=localStorage.getItem(Qa);if(y)try{const M=JSON.parse(y);x(D=>({...D,llmProvider:M.llmProvider==="local"?"local":"openai",model:M.model??D.model,baseUrl:M.baseUrl??D.baseUrl,temperature:M.temperature??D.temperature,maxOutputTokens:M.maxOutputTokens??D.maxOutputTokens}))}catch{}At(),hn(),vn()},[]);async function At(){try{const y=await ot.loadHistory();oe(y.items??[])}catch(y){Q(`History load error: ${y instanceof Error?y.message:String(y)}`)}}async function hn(){try{const M=(await ot.loadPresets()).presets??[];if(je(M),Mn.current)return;const D=M.find(Ce=>Ce.prompt_version===Wa)??M.find(Ce=>Ce.id==="default-normalizer-v2_0_2");if(!D){Mn.current=!0,Q(`Preset autoload skipped: ${Wa} not found.`);return}it(D.id),Ue(m),O({systemPrompt:D.systemPrompt,developerPrompt:D.developerPrompt,domainPrompt:D.domainPrompt,schemaNotes:D.schemaNotes??"",fewShotExamples:D.fewShotExamples??""}),Mn.current=!0,Q(`Preset autoloaded: ${D.name} (${D.prompt_version}).`)}catch(y){Q(`Presets load error: ${y instanceof Error?y.message:String(y)}`)}}async function vn(){try{const y=await ot.listRuns();B(y.items??[])}catch(y){Q(`Runs load error: ${y instanceof Error?y.message:String(y)}`)}}function $t(){localStorage.setItem(Qa,JSON.stringify({model:s.model,llmProvider:s.llmProvider,baseUrl:s.baseUrl,temperature:s.temperature,maxOutputTokens:s.maxOutputTokens})),Q("Local config saved (without API key).")}async function Vt(){ee(!0),te("");try{const y=await ot.testConnection(s);y.provider==="local"?y.model_found===!0?(_e(`LOCAL OK - ${y.model}`),Q(`Local model is available: ${y.model} (catalog size=${y.models_count??"n/a"}).`)):y.model_found===!1?(_e(`LOCAL OK, model not loaded - ${y.model}`),Q(`Local server is reachable, but model '${y.model}' is not in loaded catalog. Use 'Load model list' and select one of loaded models.`)):(_e(`LOCAL OK (model list unavailable) - ${y.model}`),Q("Local server is reachable, but model catalog could not be verified.")):(_e(`OPENAI OK - ${y.model}`),Q(`OpenAI connection ok: ${y.model}`))}catch(y){const M=y instanceof Error?y.message:String(y);_e("Connection error"),te(`Test connection: ${M}`),Q(`Test connection error: ${M}`)}finally{ee(!1)}}async function qr(){He(!0);try{const M=(await ot.listModels(s)).models??[];Fe(M),M.length>0&&x(D=>D.model&&M.includes(D.model)?D:{...D,model:M[0]}),Q(`Model catalog loaded (${s.llmProvider}): ${M.length} items.`)}catch(y){const M=y instanceof Error?y.message:String(y);Q(`Load model list error: ${M}`)}finally{He(!1)}}K.useEffect(()=>{Fe([])},[s.llmProvider,s.baseUrl]);async function Zr(y){ee(!0),te("");try{const M=await ot.normalize({connection:s,prompts:m,promptVersion:"normalizer_v2_0_2",query:{userQuestion:k.userQuestion,periodHint:k.periodHint,businessContext:k.businessContext,expectedRoute:k.expectedRoute},saveAsTestCase:y,useMock:xe});Y(M),Se("normalized"),Q(`Normalize done: trace=${M.trace_id}, validation=${M.validation.passed?"passed":"failed"}`),At()}catch(M){const D=M instanceof Error?M.message:String(M);te(`Normalize: ${D}`),Q(`Normalize error: ${D}`)}finally{ee(!1)}}function ar(){const y=X.find(M=>M.id===Te);if(!y){Q("Preset is not selected.");return}Ue(m),O({systemPrompt:y.systemPrompt,developerPrompt:y.developerPrompt,domainPrompt:y.domainPrompt,schemaNotes:y.schemaNotes??"",fewShotExamples:y.fewShotExamples??""}),Q(`Preset loaded: ${y.name}`)}async function br(){try{await ot.savePreset({name:et||"NDC preset",prompt_version:"normalizer_v2_0_2",systemPrompt:m.systemPrompt,developerPrompt:m.developerPrompt,domainPrompt:m.domainPrompt,schemaNotes:m.schemaNotes,fewShotExamples:m.fewShotExamples}),Q("Preset saved."),await hn()}catch(y){Q(`Preset save error: ${y instanceof Error?y.message:String(y)}`)}}function In(){O(Ha),Q("Prompt panel reset to defaults.")}function Dn(){const y=_d(m,ut);st(y),Q(y)}function cr(){const y=k.batchQuestionsRaw.split(";").map(M=>M.trim()).filter(Boolean).join(` + +`);y&&(I(M=>({...M,batchQuestionsRaw:y})),Q("Batch field formatted: `;` converted to blank-line separators."))}async function fr(y){try{const D=(await ot.loadTrace(y)).trace,Ce=D.parsed_normalized_json??null;Y({trace_id:String(D.trace_id??y),ok:!!D.validation_result?.passed,normalized:Ce,route_hint_summary:D.route_hint_summary??(Ce?{route_hint:Ce.route_hint??null,confidence:Ce.confidence?.route_hint??null}:null),raw_model_output:D.raw_model_response??{},validation:D.validation_result??{passed:!1,errors:["validation not found"]},usage:D.usage??{input_tokens:0,output_tokens:0,total_tokens:0},latency_ms:Number(D.latency_ms??0),prompt_version:String(D.prompt_version??"unknown"),schema_version:String(D.schema_version??"unknown")}),Se("raw"),te(""),Q(`Trace opened: ${y}`)}catch(M){const D=M instanceof Error?M.message:String(M);te(`Trace: ${D}`),Q(`Trace open error ${y}: ${D}`)}}async function dr(){try{const y=await ot.startRun();d(y.run.runId),Q(`Run started: ${y.run.runId}`),Q("Tip: start run does not execute normalize by itself. Use 'Run eval v2.0.2' button."),await vn()}catch(y){Q(`Run start error: ${y instanceof Error?y.message:String(y)}`)}}async function Bt(){if(P)try{await ot.finishRun(P),Q(`Run finished: ${P}`),await vn()}catch(y){Q(`Run finish error: ${y instanceof Error?y.message:String(y)}`)}}async function Ht(){Z(!0),te("");try{Q("Starting eval in v2 contour.");const y=k.batchQuestionsRaw.trim()||k.userQuestion.trim();if(!y)throw new Error("Fill batch field or Raw user question first.");const M=await ot.runEval({connection:s,prompts:m,promptVersion:"normalizer_v2_0_2",mode:"single-pass-strict",rawQuestions:y,useMock:xe});ie(M.report),Q("Eval v2.0.2 run finished.");const D=M.report;if(D.run_id&&Q(`Eval run id: ${D.run_id}`),D.metrics){const Ce=D.metrics;Q(`Eval metrics v2.0.2: schema=${Ce.schema_validation_pass_rate??"n/a"}%, route_accuracy=${Ce.route_resolution_accuracy??"n/a"}%, no_route_precision=${Ce.no_route_precision??"n/a"}%, state_consistency=${Ce.execution_state_consistency_rate??"n/a"}%`)}await At()}catch(y){const M=y instanceof Error?y.message:String(y);M.includes("Legacy eval runner supports normalized_query_v1 only")?(ie({status:"plan_only",prompt_version:"normalizer_v2",reason:"backend eval runner is still legacy-v1 only",plan_file:"reports/v2_pilot_eval_plan.md",next_steps:["run cheap mock sanity for schema/fragment/scope","run small real batch (10-15 messages, temperature=0)","run challenge-30 replay with v2 metrics"]}),Q("Backend is legacy-only for eval right now. Showing v2 pilot plan.")):(te(`Eval: ${M}`),Q(`Eval run error: ${M}`))}finally{Z(!1)}}async function el(){try{const y=JSON.stringify(b??{},null,2);await navigator.clipboard.writeText(y),Q("Eval report copied to clipboard.")}catch(y){Q(`Eval report copy error: ${y instanceof Error?y.message:String(y)}`)}}function tl(){dn(""),Rn([]),pn(""),at(""),On(""),Q("Assistant session reset.")}async function nl(){const y=ir.trim();if(!y)return;Ln(!0),On(""),pn(""),Rn(D=>[...D,{message_id:`local-${Date.now()}`,session_id:zn||"pending",role:"user",text:y,reply_type:null,created_at:new Date().toISOString(),trace_id:null,debug:null}]);const M=Jr();try{const D=await ot.sendAssistantMessage({connection:s,prompts:m,userMessage:y,sessionId:zn||void 0,promptVersion:wd,context:{periodHint:k.periodHint,businessContext:k.businessContext},useMock:xe});dn(D.session_id),Rn(D.conversation),at("Ответ готов"),Q(`Assistant reply received: trace=${D.debug.trace_id}`)}catch(D){const Ce=D instanceof Error?D.message:String(D);On(Ce),at("Ошибка ассистента"),Q(`Assistant error: ${Ce}`)}finally{M(),Ln(!1)}}return K.useEffect(()=>{if(!P){W([]);return}ot.runTrace(P).then(y=>W(y.items)).catch(y=>Q(`Run trace error: ${y instanceof Error?y.message:String(y)}`))},[P]),c.jsxs("main",{className:"app-root",children:[c.jsxs("div",{className:"hero",children:[c.jsx("h1",{children:"NDC AI First Layer"}),c.jsx("p",{children:"Два режима в одном интерфейсе: диагностика декомпозиции и диалоговый ассистент на общем backend-контуре."})]}),c.jsxs("div",{className:"mode-switch-row",children:[c.jsx("button",{type:"button",className:Oe==="assistant"?"tab active":"tab",onClick:()=>fn("assistant"),children:"Ассистент"}),c.jsx("button",{type:"button",className:Oe==="decomposition"?"tab active":"tab",onClick:()=>fn("decomposition"),children:"Декомпозиция"})]}),Oe==="assistant"?c.jsxs("div",{className:"layout-grid",children:[c.jsx(Va,{value:s,modelOptions:De,modelsBusy:q,onChange:x,onReloadModels:qr,onSaveLocalConfig:$t,onTestConnection:Vt,lastStatus:ye,busy:Ee||ur}),c.jsx(Ba,{value:m,onChange:O,presets:X,selectedPresetId:Te,onSelectPreset:it,onLoadPreset:ar,onSavePreset:br,onResetDefaults:In,onDiffPrevious:Dn,presetName:et,onPresetNameChange:Et,diffSummary:Ye}),c.jsx(sd,{sessionId:zn,conversation:or,inputValue:ir,onInputChange:pn,periodHint:k.periodHint,onPeriodHintChange:y=>I(M=>({...M,periodHint:y})),businessContext:k.businessContext,onBusinessContextChange:y=>I(M=>({...M,businessContext:y})),useMock:xe,onUseMockChange:ce,onSend:nl,onClear:tl,busy:ur,statusText:mn,errorMessage:sr})]}):c.jsxs("div",{className:"layout-grid",children:[c.jsx(Va,{value:s,modelOptions:De,modelsBusy:q,onChange:x,onReloadModels:qr,onSaveLocalConfig:$t,onTestConnection:Vt,lastStatus:ye,busy:Ee}),c.jsx(Ba,{value:m,onChange:O,presets:X,selectedPresetId:Te,onSelectPreset:it,onLoadPreset:ar,onSavePreset:br,onResetDefaults:In,onDiffPrevious:Dn,presetName:et,onPresetNameChange:Et,diffSummary:Ye}),c.jsx(md,{value:k,onChange:I,onApplyBatchFormat:cr,onNormalize:Zr,busy:Ee,useMock:xe,onUseMockChange:ce,errorMessage:le}),c.jsx(pd,{tab:U,onTabChange:Se,result:A,appLogs:se}),c.jsx(cd,{result:A}),c.jsx(ad,{items:F,onRefresh:At,onOpenTrace:fr}),c.jsx(hd,{runs:C,selectedRunId:P,onSelectRun:d,onStartRun:dr,onFinishRun:Bt,onRefreshRuns:vn,onRunEval:Ht,onCopyEvalReport:el,evalBusy:G,traceItems:g,evalReport:b})]})]})}Zf.createRoot(document.getElementById("root")).render(c.jsx(Wf.StrictMode,{children:c.jsx(xd,{})})); diff --git a/llm_normalizer/frontend/dist/index.html b/llm_normalizer/frontend/dist/index.html index bd0d7a9..f466e5c 100644 --- a/llm_normalizer/frontend/dist/index.html +++ b/llm_normalizer/frontend/dist/index.html @@ -4,7 +4,7 @@ NDC AI Normalizer Playground - + diff --git a/llm_normalizer/frontend/src/App.tsx b/llm_normalizer/frontend/src/App.tsx index 57597c9..e0e0167 100644 --- a/llm_normalizer/frontend/src/App.tsx +++ b/llm_normalizer/frontend/src/App.tsx @@ -25,6 +25,7 @@ const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1"; const ASSISTANT_STAGES = ["Разбираю запрос", "Ищу данные", "Собираю ответ"]; const DEFAULT_UI_MODE: UiMode = "assistant"; const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2"; +const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1"; function withTs(message: string): string { return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`; @@ -49,6 +50,8 @@ export default function App() { const [appLogs, setAppLogs] = useState([]); const [activeTab, setActiveTab] = useState("normalized"); const [busy, setBusy] = useState(false); + const [modelsBusy, setModelsBusy] = useState(false); + const [modelOptions, setModelOptions] = useState([]); const [connectionStatus, setConnectionStatus] = useState(""); const [presetList, setPresetList] = useState< Array<{ @@ -104,6 +107,7 @@ export default function App() { const parsed = JSON.parse(cached) as Partial; setConnection((prev) => ({ ...prev, + llmProvider: parsed.llmProvider === "local" ? "local" : "openai", model: parsed.model ?? prev.model, baseUrl: parsed.baseUrl ?? prev.baseUrl, temperature: parsed.temperature ?? prev.temperature, @@ -174,6 +178,7 @@ export default function App() { SESSION_CONFIG_KEY, JSON.stringify({ model: connection.model, + llmProvider: connection.llmProvider, baseUrl: connection.baseUrl, temperature: connection.temperature, maxOutputTokens: connection.maxOutputTokens @@ -187,8 +192,24 @@ export default function App() { setLastError(""); try { const payload = await apiClient.testConnection(connection); - setConnectionStatus(`OK - ${payload.model}`); - log(`OpenAI connection ok: ${payload.model}`); + if (payload.provider === "local") { + if (payload.model_found === true) { + setConnectionStatus(`LOCAL OK - ${payload.model}`); + log(`Local model is available: ${payload.model} (catalog size=${payload.models_count ?? "n/a"}).`); + } else if (payload.model_found === false) { + setConnectionStatus(`LOCAL OK, model not loaded - ${payload.model}`); + log( + `Local server is reachable, but model '${payload.model}' is not in loaded catalog. ` + + `Use 'Load model list' and select one of loaded models.` + ); + } else { + setConnectionStatus(`LOCAL OK (model list unavailable) - ${payload.model}`); + log("Local server is reachable, but model catalog could not be verified."); + } + } else { + setConnectionStatus(`OPENAI OK - ${payload.model}`); + log(`OpenAI connection ok: ${payload.model}`); + } } catch (error) { const message = error instanceof Error ? error.message : String(error); setConnectionStatus("Connection error"); @@ -199,6 +220,33 @@ export default function App() { } } + async function reloadModels() { + setModelsBusy(true); + try { + const payload = await apiClient.listModels(connection); + const models = payload.models ?? []; + setModelOptions(models); + if (models.length > 0) { + setConnection((prev) => { + if (prev.model && models.includes(prev.model)) { + return prev; + } + return { ...prev, model: models[0] }; + }); + } + log(`Model catalog loaded (${connection.llmProvider}): ${models.length} items.`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`Load model list error: ${message}`); + } finally { + setModelsBusy(false); + } + } + + useEffect(() => { + setModelOptions([]); + }, [connection.llmProvider, connection.baseUrl]); + async function normalize(saveAsCase: boolean) { setBusy(true); setLastError(""); @@ -451,7 +499,7 @@ export default function App() { prompts, userMessage, sessionId: assistantSessionId || undefined, - promptVersion: "normalizer_v2_0_2", + promptVersion: ASSISTANT_PROMPT_VERSION, context: { periodHint: query.periodHint, businessContext: query.businessContext @@ -504,7 +552,10 @@ export default function App() {
(path: string, init?: RequestInit): Promise { } export const apiClient = { - async testConnection(connection: ConnectionState): Promise<{ ok: boolean; model: string; timestamp: string }> { - return request("/openai/test-connection", { + async listModels(connection: ConnectionState): Promise<{ ok: boolean; models: string[]; count: number; timestamp: string }> { + return request("/llm/models", { method: "POST", body: JSON.stringify({ + llmProvider: connection.llmProvider, + apiKey: connection.apiKey, + model: connection.model, + baseUrl: connection.baseUrl + }) + }); + }, + + async testConnection(connection: ConnectionState): Promise<{ + ok: boolean; + provider: "openai" | "local"; + model: string; + model_found: boolean | null; + models_count: number | null; + timestamp: string; + }> { + return request("/llm/test-connection", { + method: "POST", + body: JSON.stringify({ + llmProvider: connection.llmProvider, apiKey: connection.apiKey, model: connection.model, baseUrl: connection.baseUrl @@ -54,6 +74,7 @@ export const apiClient = { return request("/normalize", { method: "POST", body: JSON.stringify({ + llmProvider: params.connection.llmProvider, apiKey: params.connection.apiKey, model: params.connection.model, baseUrl: params.connection.baseUrl, @@ -130,6 +151,7 @@ export const apiClient = { method: "POST", body: JSON.stringify({ normalizeConfig: { + llmProvider: input.connection.llmProvider, apiKey: input.connection.apiKey, model: input.connection.model, baseUrl: input.connection.baseUrl, @@ -203,12 +225,13 @@ export const apiClient = { mode: "assistant", message: input.userMessage, user_message: input.userMessage, + llmProvider: input.connection.llmProvider, apiKey: input.connection.apiKey, model: input.connection.model, baseUrl: input.connection.baseUrl, temperature: input.connection.temperature, maxOutputTokens: input.connection.maxOutputTokens, - promptVersion: input.promptVersion ?? "normalizer_v2_0_2", + promptVersion: input.promptVersion ?? "address_query_runtime_v1", systemPrompt: input.prompts.systemPrompt, developerPrompt: input.prompts.developerPrompt, domainPrompt: input.prompts.domainPrompt, diff --git a/llm_normalizer/frontend/src/components/ConnectionPanel.tsx b/llm_normalizer/frontend/src/components/ConnectionPanel.tsx index aee5d39..0505325 100644 --- a/llm_normalizer/frontend/src/components/ConnectionPanel.tsx +++ b/llm_normalizer/frontend/src/components/ConnectionPanel.tsx @@ -3,7 +3,10 @@ import type { ConnectionState } from "../state/types"; interface ConnectionPanelProps { value: ConnectionState; + modelOptions: string[]; + modelsBusy: boolean; onChange: (next: ConnectionState) => void; + onReloadModels: () => Promise | void; onTestConnection: () => Promise | void; onSaveLocalConfig: () => void; lastStatus: string; @@ -12,36 +15,94 @@ interface ConnectionPanelProps { export function ConnectionPanel({ value, + modelOptions, + modelsBusy, onChange, + onReloadModels, onTestConnection, onSaveLocalConfig, lastStatus, busy }: ConnectionPanelProps) { + const isLocal = value.llmProvider === "local"; + const modelInCatalog = modelOptions.includes(value.model); + return ( {lastStatus || "Статус: не проверено"}} + title="LLM Connection" + subtitle="Switch between OpenAI cloud and local OpenAI-compatible server." + actions={{lastStatus || "Status: not checked"}} >
+ + + + - -
+
+
diff --git a/llm_normalizer/frontend/src/state/defaults.ts b/llm_normalizer/frontend/src/state/defaults.ts index 47733ed..446cbf4 100644 --- a/llm_normalizer/frontend/src/state/defaults.ts +++ b/llm_normalizer/frontend/src/state/defaults.ts @@ -1,6 +1,7 @@ import type { ConnectionState, PromptState, QueryState } from "./types"; export const DEFAULT_CONNECTION: ConnectionState = { + llmProvider: "openai", apiKey: "", model: "gpt-4o-mini", baseUrl: "https://api.openai.com/v1", diff --git a/llm_normalizer/frontend/src/state/types.ts b/llm_normalizer/frontend/src/state/types.ts index 03e1d51..75708db 100644 --- a/llm_normalizer/frontend/src/state/types.ts +++ b/llm_normalizer/frontend/src/state/types.ts @@ -1,6 +1,7 @@ export type TabKey = "normalized" | "fragments" | "scope" | "flags" | "route" | "raw" | "validation" | "logs"; export interface ConnectionState { + llmProvider: "openai" | "local"; apiKey: string; model: string; baseUrl: string;