From 4d596725766082d66b55e0614d92a2e96aa99915 Mon Sep 17 00:00:00 2001 From: dctouch Date: Wed, 1 Apr 2026 17:55:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=94=D0=A0=D0=95=D0=A1=D0=9D=D0=AB?= =?UTF-8?q?=D0=99=20=D0=A0=D0=95=D0=96=D0=98=D0=9C=20-=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=20=D0=BD=D0=B0=20=D0=B4=D0=B5=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=B7=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT.md | 26 + docs/ADDRESS/1.txt | 585 + docs/ADDRESS/address_query/README.md | 2 + .../address_architecture_contract_v1.md | 154 + .../address_runtime_contracts.md | 7 + .../curated_positive_live_suite_v1.zip | Bin 11245 -> 0 bytes .../ADDRESS/address_query/question_bank_v1.md | 21 + .../address_query/runtime_integration_plan.md | 12 + .../README.md | 44 + .../before_after_metrics.json | 24 + .../changed_files.txt | 15 + .../query_variation_expansion_matrix.md | 17 + .../run_summary.json | 32 + .../smoke_checks.md | 13 + .../ADDRESS/tz/_tmp_docx_extract/document.txt | 2 + .../tz/_tmp_docx_extract/document_py.txt | 157 + docs/ADDRESS/tz/_tmp_docx_extract/report.zip | Bin 0 -> 43183 bytes .../unzipped/[Content_Types].xml | 2 + .../tz/_tmp_docx_extract/unzipped/_rels/.rels | 2 + .../unzipped/customXml/_rels/item1.xml.rels | 2 + .../unzipped/customXml/item1.xml | 2 + .../unzipped/customXml/itemProps1.xml | 6 + .../unzipped/docProps/app.xml | 36 + .../unzipped/docProps/core.xml | 2 + .../unzipped/docProps/thumbnail.jpeg | Bin 0 -> 8324 bytes .../unzipped/word/_rels/document.xml.rels | 2 + .../unzipped/word/document.xml | 2 + .../unzipped/word/fontTable.xml | 61 + .../unzipped/word/numbering.xml | 2 + .../unzipped/word/settings.xml | 2 + .../unzipped/word/styles.xml | 2 + .../unzipped/word/stylesWithEffects.xml | 11800 ++++++++++++++++ .../unzipped/word/theme/theme1.xml | 318 + .../unzipped/word/webSettings.xml | 5 + .../tz/address_query_analysis_report.docx | Bin 0 -> 43183 bytes ...ddress_query_analysis_report.extracted.txt | 157 + .../tz/address_query_prod_pack_2026-03-30.zip | Bin 0 -> 19321 bytes .../README.md | 42 + .../address_domain_matrix_v2.md | 28 + .../domain_gap_register_v2.md | 21 + .../execution_backlog_v2.md | 60 + .../manifest.txt | 10 + .../prod_domain_expansion_report.md | 110 + .../production_acceptance_pack_v2.md | 92 + .../query_paraphrase_bank_v2.md | 359 + .../question_bank_massive_v2.json | 487 + .../resolver_normalization_spec_v2.md | 118 + llm_normalizer/backend/dist/config.js | 3 +- .../backend/dist/routes/testConnection.js | 61 +- .../dist/services/addressFilterExtractor.js | 355 +- .../dist/services/addressIntentResolver.js | 194 +- .../backend/dist/services/addressMcpClient.js | 84 +- .../dist/services/addressQueryClassifier.js | 76 + .../dist/services/addressQueryService.js | 375 +- .../dist/services/addressRecipeCatalog.js | 21 +- .../services/address_runtime/composeStage.js | 125 + .../address_runtime/decomposeStage.js | 181 + .../services/address_runtime/resolveStage.js | 179 + .../backend/dist/services/assistantService.js | 372 +- .../dist/services/normalizerService.js | 3 + .../dist/services/openaiResponsesClient.js | 274 +- llm_normalizer/backend/package-lock.json | 37 +- llm_normalizer/backend/package.json | 1 + llm_normalizer/backend/src/config.ts | 4 + .../backend/src/routes/testConnection.ts | 61 +- .../src/services/addressFilterExtractor.ts | 380 +- .../src/services/addressIntentResolver.ts | 214 +- .../backend/src/services/addressMcpClient.ts | 94 +- .../src/services/addressQueryClassifier.ts | 79 + .../src/services/addressQueryService.ts | 413 +- .../src/services/addressRecipeCatalog.ts | 23 +- .../services/address_runtime/composeStage.ts | 150 + .../address_runtime/decomposeStage.ts | 244 + .../services/address_runtime/resolveStage.ts | 211 + .../backend/src/services/assistantService.ts | 372 +- .../backend/src/services/normalizerService.ts | 3 + .../src/services/openaiResponsesClient.ts | 316 +- llm_normalizer/backend/src/types/assistant.ts | 8 + .../backend/src/types/normalizer.ts | 2 + .../tests/addressMcpClientEncoding.test.ts | 47 + .../tests/addressQueryRuntimeM23.test.ts | 226 +- .../assistantAddressFollowupContext.test.ts | 134 + .../frontend/dist/assets/index-B5_Zqbf2.js | 12 - .../frontend/dist/assets/index-BFy6DcyX.js | 12 + llm_normalizer/frontend/dist/index.html | 2 +- llm_normalizer/frontend/src/App.tsx | 60 +- llm_normalizer/frontend/src/api/client.ts | 29 +- .../src/components/ConnectionPanel.tsx | 100 +- llm_normalizer/frontend/src/state/defaults.ts | 1 + llm_normalizer/frontend/src/state/types.ts | 1 + 90 files changed, 19595 insertions(+), 785 deletions(-) create mode 100644 AGENT.md create mode 100644 docs/ADDRESS/1.txt create mode 100644 docs/ADDRESS/address_query/address_architecture_contract_v1.md delete mode 100644 docs/ADDRESS/address_query/curated_positive_live_suite_v1.zip create mode 100644 docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/README.md create mode 100644 docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/before_after_metrics.json create mode 100644 docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/changed_files.txt create mode 100644 docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/query_variation_expansion_matrix.md create mode 100644 docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/run_summary.json create mode 100644 docs/ADDRESS/runs/2026-04-01_Address_Query_Runtime_V1_M2_3D_Query_Variants_Expansion/smoke_checks.md create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/document.txt create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/document_py.txt create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/report.zip create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/[Content_Types].xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/_rels/.rels create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/_rels/item1.xml.rels create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/item1.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/customXml/itemProps1.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/app.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/core.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/docProps/thumbnail.jpeg create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/_rels/document.xml.rels create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/document.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/fontTable.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/numbering.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/settings.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/styles.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/stylesWithEffects.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/theme/theme1.xml create mode 100644 docs/ADDRESS/tz/_tmp_docx_extract/unzipped/word/webSettings.xml create mode 100644 docs/ADDRESS/tz/address_query_analysis_report.docx create mode 100644 docs/ADDRESS/tz/address_query_analysis_report.extracted.txt create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30.zip create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/README.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/address_domain_matrix_v2.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/domain_gap_register_v2.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/execution_backlog_v2.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/manifest.txt create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/prod_domain_expansion_report.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/production_acceptance_pack_v2.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/query_paraphrase_bank_v2.md create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/question_bank_massive_v2.json create mode 100644 docs/ADDRESS/tz/address_query_prod_pack_2026-03-30/address_query_prod_pack_2026-03-30/resolver_normalization_spec_v2.md create mode 100644 llm_normalizer/backend/dist/services/address_runtime/composeStage.js create mode 100644 llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js create mode 100644 llm_normalizer/backend/dist/services/address_runtime/resolveStage.js create mode 100644 llm_normalizer/backend/src/services/address_runtime/composeStage.ts create mode 100644 llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts create mode 100644 llm_normalizer/backend/src/services/address_runtime/resolveStage.ts create mode 100644 llm_normalizer/backend/tests/addressMcpClientEncoding.test.ts create mode 100644 llm_normalizer/backend/tests/assistantAddressFollowupContext.test.ts delete mode 100644 llm_normalizer/frontend/dist/assets/index-B5_Zqbf2.js create mode 100644 llm_normalizer/frontend/dist/assets/index-BFy6DcyX.js 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 fcd8933d883f8de05a9eb89d0be7878cb2ac2cdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11245 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 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 0000000000000000000000000000000000000000..9d326d0122790de1ef5d0e6a8be38f65a78ae2c7 GIT binary patch literal 43183 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%0DZ ze8fPiu=(5S!;XNQ&cUlFA{o6Wlow^{$DS7vOo2~_3>CxU+ku1q4L#$Z3NPz7RWz}f zlTiaP`=HuxES1>aU_NC@nEOtL3gzEqB}!9nZ-bGngJf%n@j}5Hzr6d-;9&*K-YKAr7-shJ`9K+O!fY1ElE;sbr&w0< z(#8QhY4PwNsmn~= z+MtEG{2f3JMB!{rs44bpSX`Gc;aMz+g}7SM*}rj7!I|rKa1q)la+!lVD#+~ zTL3#FWP~7sLxmNCHIQhNIAhinq&`#HSFXNVAyTMaJ*?USyhYNTa~PJ*O3IQljly(= z_@y8RrRHhQgM0-|(yusg>o@cXGo-rb%u6rDfZ?JzB4D`4>;c7!c#y9 zSfH8AnLfn6und-nfodxSIg>m)v>I^wMN%OyCJ`zrYwyPy&hzg6xLJ}0u?T=I4%2jKUzg&Wz%7P z1gVnvk6B2TC>t(|CbV%o!Cea$#_%|;`4l>H_OoQ?f_TEkg$~Zu{n&jF@Y4MbdH=6b}G9 zXf`A5*GDxV3(AR!GizYx{t%p`L>)2T!FJfc`{IZ@Of_5*s~D|}zds+9YuDeAvn(i= zr@;%i0)lY+7l8YA-F`d?H19tEUk)I+(6LACSu;FA#N=hjih@mtW9HIkMTm-siloGa z)P>}}>VJh@ERyY143!iZR99ueT!}HYdEz3`qakf^F!^ zXc$R$J7)S9ROv0EUwS|~H8wccf{irN0Af)!EG4((5aq`w1Mf&1i104V$wNMRx< zfJbsEEr`X1HzH+vS=2!j&`(?)k~9boFb<_gfRw)8j4<9vNoCZqqu~yl-$=~G1~`p> zweU;-@NV-cCoTw;o^L6JrBCgOjt;N=l6tW|NdZT4?(p88G(Ix}#FCi9^G3*k57^gQ z{gertr&Z!j$-37z)11}0$fW-DO+!9|(pRt>18$7X(oX(Zu9X`*{|~1rw3v_5hN$8i zXvwtbWWL;S0pO>4x^ZDK12`UnIix72t24MD;+}a8OlKfmS({NSjH5oU6y{}tf2&F8 zoSh*0YlftbVWpdxjUNR#ynL)P-Z%zrC8?VPpdt1UEGA{zTAd*#o~MWMtA!Acb~akB zK2c1LZ^?D#S6WI%*%WPU1VD$R&y78^ri;LM0v%U9fAA}bUW~JOY%Ogpi7bk`(T;HS zaA&o|aXeaM-SBkOy5GA2&1p{$3H-6BrN;hp7B{`TuD@90tXiwaJ}+D!_Mh)H{$hY> z5D*NFq$$cV5~>?)e=)$;9DW8L9zTJv&l8?Lbrcw@)}?`nfMG5{p*cqK6)+86rwqp! zSND)2);I%DGD3pvi=8r6cN~DgD7lV-Qa94x&7lp?m*54%&}c@Yt!1E9;vNY+r)9qC zt0qhP|Ju3=sHnCsJal)bbR(TADcv9;-QA%yNJtLS-O@>0SsC0tJZGpGn2>vEB-?(;*b1(isks~1q(&<99Qq%8;;$}NJ2}sEL zaS!-&!PR?FX`>D}0AL#h0KoZs!Ig!(yN#pOuXR=+t!39mLHzdBV&ld&OO$$OiD%|r zy6P1r27BlR1b|vUfWw(UJbIqWDlIs2DA_Ui>Ru=x#$+=?tA11t<$>EWl%*G@~cCFny;gKke)Bmt{3Xs zRfO9G+vthO^u)nLy=f}3ks{7Ng5S^06A`zt*0_XjnM`C}k77Jwj3e~U4Q04?&P3pt zZUM%o7y zWm+%nN)MlhCR~+wF$~N_vDR|m9ZAXWX$F7c&mJqHs3BjZ?c!DL-k^(zA98m=Md1j3 z>Fa|UQ5nDc`2?RE7{C9`98o}E?W9L8Bgd+(5hI>XB-A`=!#3TcZCAKkqv3`UBr24S z;b~Vx)jE-guI2bdIa4|>$Eozl6Q+^l3-QoickoviG-74q5oi5p*mWn7q@WNFH%r!h zfwom;$(+P>+s1(Jnlvh6_oWsUy6#mGw!2?7y9E!}N~^IFeVR(6mh@1ToMlwm%f_E9 zpOrd|i-2T1EYTmy_AE;i^wvC?chkvS&<<>#G)ppuWf&7$rP`y$>svrtA~PMUcBhZ4f^YTbT>5s&|+Bi&@cXPZGUZHs_`#{2TpX^_Hd#DCVbkvH{vzEnn!a<#B&o< zv?PkxV>y%4bAh0Xp~kK+cwX`ox12Fh?^2hp1dxtB{vwy>P|MB1ji|MryFX+8=rC;A zte*+_2{Mo{eIGRu0xmEtL%7D22nY7kn3i(a%57uo_X!P=MCX!I%~YcQOQI(1w3wG5{!}W<{AD7LpJMmATEQ$J~ll0u6`+>eFs`44OXpn zV8>}3*&-nA~?eOv1giP_9u=%T614qEe zp-xwUGU!>2op>HD7#K)})P#0-+hB(igg<-iMkv?K3>E;mK?MMCfA^TBlcW0!6H|MO zpH;1vle-)jIm!DE{UVuCGUe?PK=?Q;{cr0=Fv)xXc;wZD;5dj_)6~tUC-(0+A=Pp% zXAZI65Y|yTm>^^lcS%_|YN7k_E7SY4TMFHV*KD;6&skU+4?9Vn*}CDl3Vb)7Ji2Zh zd27ZDyuy|V!dp$l*<7mU(4DqqYSc<@$|`3-^Y!`QvE*(*5RKAP#$ehFBt{*o=1+0m z;ARtLIJThDiRPo22>+HA1!$~wBl8N6LY5?r)uxdpUK4IxO`_wD2`c9Tejax44hO#& zG+~nxOXSwGRNETh+_WRxfe>?%%H};Uv9sj4id1KHdjh{3E>+(bw3UHFgo{P94(=ax zG}6)f^eLTR$g=3pzaNoCzVJmf7g40xdllymaPEd#%icm4v4Yf@H$RItwIy$Q1zo1K z8n#(k@UeV`v0O?y+fHOmbb1NQ{D-bjEE5fDuMi-krB2#GGsQIqZ$gE3gY_Kyi_kT4 zxbr_WnA^7Dp`YHi#$Fk|5%z?6dMatTp?L&b{N?-NKC*{*8%9-SslLF|#-6>RNNsNv zo*XkT4=%D)DJ>V>lOaKNue?)d*pk5^J*`%c%}#xbOvmiL-Q6|tbPv|v@oDh@hta(n z5&!vgyp5*-3sEx%-c(MXIaW{XN}P#@SvQ8*1Yphrh;k2){j7njMKGViMpJyR5Ewo+ zn8j#a&1RkDC^vy=J=lS|XnNvdu}d zXuaW=7sk#TUCE~+$r=M6s@I(mU^&KW4mbTGK6(5|X=m6g_ExHtF7rDk`}37d6sKwI zM+5*`SO5Tm->;;*wS|KP$FC#TuUc!mN={&Y;`R$|?BZB$Jx!R-e)$J<>RuRBt3{aS0}zl;@Lk_5=@-~*Ou88v2#9&zZm!Cp z5w!}L$O~DvalERteiIc(pE;?KJ&d~JIo!rb6OIKX{z)8A0W%mWewMb-wrPpY3;Txf zC@*Yrc6kyRjEs(7=mCKRxZP34J!=1A%SH}D0fTz75@cD^v9tw!lCCr^M~hn(i08u! z9ENTwyg9|fx?$m6sTmiwgnoRK=aP}5jl+M=majPRy+<_bu{gH8&gb2EPvTR}>+j!3 zGHh(h(s>b%46EiKno})|YZLW_Qmj|_-~G#{r(0bNB^nAP zw=bP|BWhP~{7mN9>2{({AQ&Af-@%Vu?4@bUY`fppS8xinPJj%D;j`<_&tC_sC~x)Q zJ5X+2aPPKmIPUzfnJ@RQUOQ#`f&haFovQ)Y`wd1N`hnPTy?a$Tx5w)Xdrl(vr(ujz zjTnewa!UZ8@ZcvqfPGaJ7Na_P(pvgFIHGkr_nt>?JCX>GgT9B9cO58FzmW>WoT6~f z88X!iZ{+eSVFvBhM#v5%zir&~CN+&xpUV3%4`@gvmIEvOIEHPSr$d>4HXn7DWFs&4LBc{D%ZqWSLaO)}J!WniCn=EAwuP$PLVjqYU3 zC&XkE4nLNZ|F|>jH<4H1+@n-iC28I)q27o>lTPH=84+k$qcuS*H|W5$Bq8;xcyyM0 zAQZibiw6IUyw{8R@^OYO%W^!(McmvWuC!P(wbLgw792Z8XP$82VX%YH=WVj86eP0l z!>vIzUzD#SQnp>~og5Q-w;ta7E>5RbV!6j`jy&4dvK!TnK&|bq;t+Cv@2fIuOYe+t zujfUzI!?`76u|aLx61W#bE_h^ox;m+>r6~)i*LXZV$J0q%x*_vlQaBly{ppPZ^nHV zy;PnY+E|qovwx5;KQ#BAi#!N8ZCRf57}s+LlbJ}o>PyT8>pKPNr}yuUEWDqze!3CG zk^hFZ!*($-S|@^>hZqx9x{pz(vE?y(fg%Abs3+B~w)Z_%uKr36yB~{D$yxKxEq-z` zCK2D+g^chce7%{7+0VmUp?UeIej4k^iLFg6Gc&V{h0>4{n^Lt1VinnU@?TTEH!dMY zyEwptZpEzYjWm^HS!Sh=q&AEnad9_FIF%5to@S1_Mr*rfQsOjEh!4C>srD~?RsX(( zvovMmqAtIpm1(g%$f;yGa#n!QeLXn19<)-z&t}|{>j2!Kkn#Tx+w@4g0w=y^wcfsK zYSMNeVc;Up@y5d~zZkBd7QP(P{u@i9hG8UMFo`f`w_&1c;i z-J}c6sGT9~HEUjpdQNFCQx=d1uNg~=I}JA2BMXj`4zC`r(Bqq$uyE=K+)IzUPV+aV zi*BU@579SVaW`YmS z90bZK24(md&5-pBm5JfY1nby`Lli7p(Ddyu(psleUnFV)YEzlv01~%HqLMeI{X!{> z>*Mwc8=8vyU$~5Q_)XThL+blRSMD9>{{b}M;q?!>Sn2`MxK9s*cb0fO%@kyJyCWCmG#ja)dFwD z22C;qse=y6EjT9qT#*egRGthf*7xngRaiSQqffAnGy^{{5b1rKqQt^11nR!5m|3xn zx;Uhdyz$CJ=?E1Fp(^$!f?A~ApE%~Qdr97Wykl%T~bd9#cwCPf#PBDvl>7KS(%3_JRO2=W$h?qtjW6nf}PMIZT%JH_| zs7WQIZW1;pS$k`m-s~vEmjyHB$Y#Iq=ts_wVd2+2 z9|jOFsSL-K4SmN#fEER@D$e!eN<={uNCkWLGJR-OBWi}ZYslw?2XcuFZ+>or%EJhi zi}DY-{$Ho|Z|Qy&sRfSLK<#sE#4l|s)OFTn7FJAoI_J2s3lh%M<+0=S;e~k33bHHh z`5JH}+$$9K7j6y#Smll44DTjOipr7#cbv~$_`g}r2RJ5C)w|TON^^dLDY!m(|E$Zl z$=v!l;<-2tNfT$WQ^gQFHk)QDRq{xPkj`VXY>(OlQ>%%L=DVq@4(b+04RUGPQxp3F zVoH>i8kcnF4s^|SfrA_@buGANf??lNYj`8Own*k>$Fc9Ulr$AfTeL)P=Wb#Dv}8an zib`uxflZ-;6aOLjPmxt!ot)i%{i|8AHIBiY_)@RiyHD1KxN_mG@xx^S%yysU4JbSV z>FsI3&w*ukK1#xTW&^J)-48xLAs5{YaWwug!NNGnK!^OI{7K&1%+?M*UfMz7N?{Qj z1fiy2`T-h2_gAYf0~OPVk1^>^C^yr_j(3(?tw!rHNm}bu%nQw#p|$c@G?h54aDBz zfzsGEi+@m`)zI?jE<-=QBJdAXCnoM)~z)Opr2#c_T#$+O( z*4^Jehogsssf8;vD)ZC28`D{KTo)v6TP+rglhy}CB=ExX*5!+7833heL8kx);_7cX zgIt_T2MQ3gG1d=Rq9u_$iny;vfOr9`LU+E(KM*c|90kX(U`F7(@gD^16|YLNZ7Gb- ze!aU$*iH2I(Okfv^T?O8dc41ImM_|yQ&s#fcA(fFLlMXT9qEhCpU zy61W-5ta8mTp@ClnupSf*_4}goG`p3NJv3IQ0Uuiq7s4ty&UAN3V(}A;OeKdv_Ky| zaug2cfV5zyA=WA@_)iEi){&?#awtx7Y^*|lm*-;zLIkgumBFG2s|+&yOEHj$HZA=! zydRj_Ym>XPQ?L4-IvM9+|<{Qg!QNCfMUH(H5ysTxC_$-V-q}Wqg{ZQ<5dg^zKYk@e_C#n}uo2 zml##bz+#?;e!})yf>u}Qv05}U9YGh`wOXIZ8?fzN6?6rzHT~)CM#dnj`VCt@w@ige z>0StHpZJcT@T!QVX%YRzb19u=2P(b2J0~ykwCrQ>!pVYX{x(M+B14T8urK>YSLvNFlF=ZS-5i3jT`0^s9vR%$|?yMWdu5DuNpzTP} z@Hw#UNMaSK%Rt+pe4X?c`0-N_0EPAK;HznP=+`eRNx^b4<)5Czf zd(65=5vJjS38h+mz?y5qW8YjfW$vBlHrJN0Jf%4`1TP|?)^4cFYlg2&TaeKVmHY+Y zMWD-}%7hu=v(+)-6Oe(x4RSjogHhX7g$v@A8t>kYSi0}CRg#utkfzlSztNj{0wYgt-&G1f;q9`RSNH8`O48_LWNVm6Fsg?77oG;k?x2%3QDFl%o`kGB zNAIupO;y#oW>&_PB!n}aPkHt$5zh|>&T+QpIuAyx#WC18ds<27JJH?jjPpnkZH*;) zMukEb81M5|?$Q1nvGNCJt=bR(fDkGGfCxHbK}(4L8m6G07gRqTO>FER+d5lVLA30g z@ig&8T6_eU9mcSk*zu9NTev$5*dSb(`C1D zA)jr>{DFLI15klv%2;XM^L4+V7dH00lzz7r=h8P4PXarjXs?JamG}) z*}h3`6^$)AhA!11?{}fu;w5)9o)l0mfKzF^^1DXg&k2PfOzO$n&%B;idnJDm@H)0+5@T8 zq|FKU4bZ7qy)oYql_Ir`id8`{4)f}Z*VPrh8T1sdX@CQ4XL>$sY+#WjoC2{& zW_RxAy;+VY@Im|`84^QlA)@Eq-m*r`ySue4$m~1Cr5LrWho^4 z+|r+?wzHF~p3`he*GJ-5IVieK&@ak=7TgHJ{s8RTknz^*ss{$9j8CWTBLWxYY4sx1P?*hMUe0sGWNDaHqN25_aS{-IoaX6 zH?ON^$f}aP5%)j&)i2Vdi(+ z#Th~E626O-iyjALFR18BUo*a{QKy2%k*S9Knv}~o~{K+U!!(9Pb{S|EB-7*Q# za3$zi1$H0v`HfgT=&}ufMl_}INIj5qp>uk{bKFcC>j;+t3Cnr-Wgy6(ip$5s9#T8+ zM6iI#Uum4Z^JP6#vtSa>xbU9R(QbmOfh;)U0MQIJirF<9M~AkDByES1II8KSjOxzB zLMP@(*l##?h8KUI7?rKZg>B6<+t_9lZU@P9BMdmgK80blG4;+UDXzh?XA4?|1hPz< z8TLtOv!46bkAiQeaXaCKrNidY-W z!@s2uq4m}!>fzUT6L<90)uShpaMz&x?YnWkgm{yMYA>IQSd^!rtj?8$ zRTMN>T0q^4_Zk*(q4eK`F?Y!w1!=89uADdJG2N@Nih>YHt#jY}=i#0w{JdLKRRIPT zAMR&(92tP*@2_8>UKp1IwNQtlF}I&?$L~)d^cn!5CkYJ(TR6HKz3_6ja5MO6ojoM* zVo(sfgx<`|Mk@ER?5v>WB|bZ>chrUp&jws z$<>?#O5x!EmHofqj)7#j+|b5HpiPng(fB3xU-17+{%?~1?1er;&2j=x03b>W0f6-f zlR52QOm6O8_7=a=l@I&i;f9jG@qsW!8h?NvY%lqc=V6}oA0DEJ-#q_HqCA8@OuYO9 zM*#i>e~^ZG$nY=`@ehMQ%wG(Dq$M8WA4ZM;!4Jp&g?|`Ben|1q|Ne)fC*?oA@rMKt z9o>HjGE)Cd@Lz84L#~Iu<3C)9Y5(T>)02G2^3c`#howCIuU`4X@p_1VXb=8_m(Ko| biv4LFswyBrHRz{S;Q&ma>LFP0^Vk0XLAVNp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d0c4f1f553fedc9e469c7c5253fbed0b10e7476e GIT binary patch literal 8324 zcmeHJdu&rx82|3Q=eA>;4dt=HFmQQKvXO2>*hIj!>$Z+DO1JTf3F+F~wXAE)ZM%(S zCNdwO{2>G1|2PIJ8WV$x4%EmL0gVO_2Mh>th&p_QhvF+xzjNE(wqy|_f0^9V^xR*+ z*ZF=MS$>wZ7?BhEohW0GKqC z`<+2YGvYP?-s=cRApp$6_4;)W<*DcjW9OlU*7>Ktyn z6LAOPyoMxsV-k*XKnsjhA^Jqg;}B>qtdig5@rtq{I^9B@Wcg39*&D{38Erz=tc5^M_X(&G&2pOq-2I3#XP4%USX92LITvB;`)SkV!-Cef=aTdy* zo{*(3CU5sO)GS4ezHCpEe=&_iZ4NtvCafh)L43|FTIe1l9}b*inHg~&;)(8XWh_2d z6>O=cayd@B+f;)X<8abidzBS&4&qIc-$vI$&Rmb^t)OdReC}W%WQ}p)hJ4-{x)$b3 zbYf7>5o2;gA$J{~Q|e3RhNQZfFInyKlv`r<9RaU=7Lgm-DTQs68^$L`?UK0yF~%WR zL|;9v8)AN((_ThvI~(yrSkBl%1V1$3>;NBJ2LWtQ0Y-cVKthcRJm5u%h*}XP9_9{c zLfPMU?l^Hc7 zE04SrLB%dVC5Jw=41pczLA3KD*ErLZ9d-G(*GG)4sx$^?WsPxx~aM{-6Y*~UA1B*=(2SMx=P(l)J#zJ&707tO%=uQH%~JY zbtO2P7533LN3lrBKuog2swi>tl2{G5B4`Oi*w+lZzKPm9qdFJwReM^sc6Z z-mlar`eeJ=xCOJhn~P&~#Su9JCi?H;wgwGyTLGwZWIyQBSYS4Dj2>7$!|9pG0holD z@X^>ZUz`}0BfLhNHX=O(4fc!#mf<*-a6C`25X=Lp2cqXkO)eQd)5X+;g;66_3aNm{K%slH$C?F)@@Hd_4G5(KKFdjj-4;Q^zyD(UVY=uxAwgK zPVe4*AAWS;A~|~U-;(R?}jdZfBDMQUw*yz+wXt;N%LZW zQ%V-i?54c12nj^)(Kr^aznby8;^pwI4X=99C+jsYkm|A4JG`2xH zmYy|j&fxURG&MQ1f0tP2f67crERok~$mH-)>N!0uf*Ut-p__d$9sdHT548m9DNtVo z^)67aL$xEQ-B9g@YByB7q1p}AZm4!cwHvD4Q0<0lH&nZ!+6~oi_`m9gkpcJ{PX}Y} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9d326d0122790de1ef5d0e6a8be38f65a78ae2c7 GIT binary patch literal 43183 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%0DZ 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 biv4LFswyBrHRz{S;Q&ma>LFP0^Vk0XLAVNp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..76a5206644cdb56e4379c5a869eb53af88c7424d GIT binary patch literal 19321 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 BBwqjk literal 0 HcmV?d00001 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;