diff --git a/.gitignore b/.gitignore index 0e5a70e..c31acb2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ llm_normalizer/reports/normalizer_v*_micro_eval.md llm_normalizer/reports/minimal_package_* llm_normalizer/docs/runs/*/ !llm_normalizer/docs/runs/README.md + +# graphify artifacts +graphify-out/ +.graphify_* diff --git a/.graphifyignore b/.graphifyignore new file mode 100644 index 0000000..42f23d2 --- /dev/null +++ b/.graphifyignore @@ -0,0 +1,29 @@ +# Core VCS and caches +.git/ +.vs/ +.pytest_cache/ +__pycache__/ +.venv/ +graphify-out/ + +# Runtime outputs and local datasets +logs/ +tmp/ +data/ +llm_normalizer/data/traces/ +llm_normalizer/data/assistant_sessions/ +llm_normalizer/reports/ +docs/ARCH/9_audit_artifacts/snapshot_samples/ + +# Frontend/build artifacts +**/node_modules/ +**/dist/ +**/.next/ +**/.turbo/ + +# Large binary artifacts +*.zip +*.7z +*.db +*.sqlite +*.sqlite3 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..07df375 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +## graphify + +This project has a graphify knowledge graph at graphify-out/. + +Rules: +- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- After modifying code files in this session, run `python -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current diff --git a/docs/ADDRESS/1.txt b/docs/ADDRESS/1.txt deleted file mode 100644 index d7a4392..0000000 --- a/docs/ADDRESS/1.txt +++ /dev/null @@ -1,94 +0,0 @@ -# ADDRESS Test Questions (обновлено 2026-04-02) - -Важно: -1. Цепочки `FOLLOW-UP` прогоняй в одной сессии подряд. -2. Блоки `SINGLE` можно гонять по отдельности. -3. Для проверок смотри не только ответ, но и `technical_debug_payload_json`. - -## SINGLE: Документы по контрагенту -1. Покажи документы по СВК за 2020 год -2. свк доки за 20й -3. что по свк за 2020 выведи все доки плиз -4. Какие документы по контрагенту СВК за весь период -5. какие есть доки по свк с 2000 по 2025 год -6. Покажи документы по контрагенту СВК - -## SINGLE: Банковские операции -7. Покажи банковские операции по СВК за 2020 -8. Какие платежи по СВК в 2020 -9. Покажи банковские операции по контрагенту СВК за весь период - -## SINGLE: Договор -10. Покажи документы по договору 19/15 за 2020 -11. Покажи документы по договору 19/15 -12. Покажи банковские операции по договору 19/15 -13. Есть ли долг по договору 19/15 на 2020-07-31 -14. Покажи операции по договору 19/15 за весь период - -## SINGLE: Сальдо / остаток -15. Покажи сальдо по счету 60.01 на дату 2020-07-31 -16. Какой остаток по счету 60 на 2020.05 -17. Какой остаток по счету 60 на 2020 май -18. Какие документы формируют остаток по счету 60.01 на 2020-07-31 -19. Какой остаток по счету 62 на сегодня - -## FOLLOW-UP CHAIN A (одна сессия) -20. Покажи документы по СВК за 2020 -21. а теперь только за май 2020 -22. а за все время? -23. а по счету 60.01 на ту же дату - -## FOLLOW-UP CHAIN B (одна сессия) -24. Покажи банковские операции по договору 19/15 -25. а за 2020 год -26. а теперь только за май 2020 -27. а по счету 51 на ту же дату - -## FOLLOW-UP CHAIN C (одна сессия) -28. Какой остаток по счету 60 на 2020.05 -29. а теперь по 60.01 -30. а на ту же дату по 62 - -## NOISY / TYPO -31. бля епт покажи доки по свк за 20й -32. свк 20 год - покажи доки плс -33. покажи банк опер по дог 19/15 пж -34. остаток 60 на 2020 май покеж -35. по свк за весь период че есть - -## NEGATIVE / LIMIT CASE -36. Покажи технические константы конфигурации -37. кто президент США -## SINGLE: Дебиторка / Кредиторка -38. Кто нам должен на 2020-12-31 -39. Кому мы должны на 2020-12-31 -40. Покажи дебиторскую задолженность за 2020 -41. Покажи кредиторскую задолженность за 2020 - -## SINGLE: Незакрытые договоры / открытые позиции -42. Покажи незакрытые договоры на 2020-12-31 -43. Какие открытые договоры есть на 2020-12-31 -44. Покажи хвосты по контрагенту СВК на 2020-12-31 -45. Есть ли незакрытые позиции по договору 19/15 на 2020-12-31 -46. Покажи открытые позиции по договору 19/15 - -## FOLLOW-UP CHAIN D (одна сессия) -47. Покажи документы по контрагенту СВК за 2020 -48. а только за июль 2020 -49. а теперь банковские операции -50. а за весь период -51. а сальдо по счету 51 на ту же дату - -## FOLLOW-UP CHAIN E (одна сессия) -52. Покажи незакрытые позиции по контрагенту СВК -53. а на 2020-12-31 -54. а теперь по договору 19/15 -55. а только банковские операции -56. а за май 2020 - -## EN / Transliteration Stress -57. show documents by counterparty svk for 2020 -58. show bank operations by contract 19/15 -59. kakoi ostatok po schetu 60.01 na 2020-07-31 -60. kto nam dolzhen na 2020-12-31 - diff --git a/docs/ADDRESS/1915.txt b/docs/ADDRESS/1915.txt new file mode 100644 index 0000000..e43ee54 --- /dev/null +++ b/docs/ADDRESS/1915.txt @@ -0,0 +1,669 @@ +# Assistant conversation export +session_id: asst-qsZmniA1bx +export_mode: technical +exported_at: 2026-04-08T08:42:43.575Z + +## 1. user +message_id: msg-f9gF7eEEGy +created_at: 2026-04-08T08:40:56.493Z +reply_type: n/a + +Покажи документы по договору 19/15 за 2020 + +## 2. assistant +message_id: msg-Je6z6-84tL +created_at: 2026-04-08T08:41:03.503Z +reply_type: factual +trace_id: address-bMqqNTkC2y + +Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. +Собран список документов по договору (live address lane). +Строк отобрано: 8. +1. 2020-08-31T15:00:06Z | Поступление товаров и услуг 00000000024 от 31.08.2020 15:00:06 | 19.04 / 76.05 | 233.33 +2. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 43804.8 +3. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 9776 +4. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 73968 +5. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 94924.8 +6. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 54400 +7. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.02 / 62.01 | 276873.6 +8. 2020-06-30T12:00:00Z | Поступление товаров и услуг 00000000015 от 30.06.2020 12:00:00 | 19.04 / 76.05 | 233.33 + +### technical_debug_payload_json +```json +{ + "trace_id": "address-bMqqNTkC2y", + "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", + "document_list_signal_detected", + "documents_by_contract_signal_detected", + "query_limit_auto_expanded_for_anchor_recovery" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "high", + "query_shape": "DOCUMENT_LIST", + "query_shape_confidence": "medium", + "detected_intent": "list_documents_by_contract", + "detected_intent_confidence": "medium", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "contract": "19/15", + "period_from": "2020-01-01", + "period_to": "2020-12-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_documents_by_contract_v1", + "mcp_call_status_legacy": "matched_non_empty", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "anchor_type": "contract", + "anchor_value_raw": "19/15", + "anchor_value_resolved": "19/15", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "matched_non_empty", + "rows_fetched": 1000, + "raw_rows_received": 1000, + "rows_after_account_scope": 438, + "rows_after_recipe_filter": 11, + "rows_materialized": 438, + "rows_matched": 8, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "none", + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": null, + "response_type": "FACTUAL_LIST", + "execution_lane": "address_query", + "llm_decomposition_applied": true, + "llm_decomposition_attempted": true, + "llm_provider_used": "local", + "llm_decomposition_trace_id": "RsRWAyD29sjgY7", + "llm_decomposition_effective_message": "Показать документы по договору 19/15 за период 2020 года.", + "llm_decomposition_reason": "normalized_fragment_applied", + "llm_canonical_candidate_detected": true, + "llm_predecompose_contract": { + "schema_version": "address_llm_predecompose_contract_v1", + "source_message": "Покажи документы по договору 19/15 за 2020", + "canonical_message": "Показать документы по договору 19/15 за период 2020 года.", + "mode": "address_query", + "mode_confidence": "high", + "query_shape": "DOCUMENT_LIST", + "query_shape_confidence": "medium", + "intent": "list_documents_by_contract", + "intent_confidence": "medium", + "entities": { + "account": null, + "counterparty": null, + "contract": "19/15", + "document_type": null, + "document_ref": null, + "organization": null + }, + "period": { + "scope": "year", + "period_from": "2020-01-01", + "period_to": "2020-12-31", + "as_of_date": null, + "has_explicit_period": true + }, + "aggregation_profile": "list_lookup" + }, + "fallback_rule_hit": null, + "sanitized_user_message": "покажи документы по договору 19/15 за 2020", + "tool_gate_decision": "run_address_lane", + "tool_gate_reason": "address_mode_classifier_detected", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": { + "trace_id": "RsRWAyD29sjgY7", + "prompt_version": "normalizer_v2_0_2", + "applied": true, + "effective_message": "Показать документы по договору 19/15 за период 2020 года." + } +} +``` + +## 3. user +message_id: msg-1w0963YnGN +created_at: 2026-04-08T08:41:09.598Z +reply_type: n/a + +а теперь банковские операции + +## 4. assistant +message_id: msg-76jKqn5b1O +created_at: 2026-04-08T08:41:17.668Z +reply_type: factual +trace_id: address-mB-Zxj7T3o + +Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. +Собран список банковских операций по договору (live address lane). +Строк отобрано: 3. +1. 2020-10-15T13:34:19Z | Списание с расчетного счета 00000000245 от 15.10.2020 13:34:19 | 66.01 / 51 | 134206.53 +2. 2020-07-15T13:11:23Z | Списание с расчетного счета 00000000191 от 15.07.2020 13:11:23 | 66.02 / 51 | 54125.93 +3. 2020-07-15T13:11:22Z | Списание с расчетного счета 00000000190 от 15.07.2020 13:11:22 | 66.01 / 51 | 122035.07 + +### technical_debug_payload_json +```json +{ + "trace_id": "address-mB-Zxj7T3o", + "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_entity_detected", + "shape_not_detected", + "bank_ops_by_counterparty_signal_detected", + "intent_adjusted_to_contract_followup_context", + "contract_from_followup_context", + "period_from_followup_context", + "address_followup_context_applied", + "query_limit_auto_expanded_for_anchor_recovery" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "medium", + "query_shape": "UNKNOWN", + "query_shape_confidence": "low", + "detected_intent": "bank_operations_by_contract", + "detected_intent_confidence": "low", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "contract": "19/15", + "period_from": "2020-01-01", + "period_to": "2020-12-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_bank_operations_by_contract_v1", + "mcp_call_status_legacy": "matched_non_empty", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "anchor_type": "contract", + "anchor_value_raw": "19/15", + "anchor_value_resolved": "19/15", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "matched_non_empty", + "rows_fetched": 1000, + "raw_rows_received": 1000, + "rows_after_account_scope": 234, + "rows_after_recipe_filter": 3, + "rows_materialized": 234, + "rows_matched": 3, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "none", + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": null, + "response_type": "FACTUAL_LIST", + "execution_lane": "address_query", + "llm_decomposition_applied": false, + "llm_decomposition_attempted": true, + "llm_provider_used": "local", + "llm_decomposition_trace_id": "fQPoAyba2nzNW_", + "llm_decomposition_effective_message": "а теперь банковские операции", + "llm_decomposition_reason": "normalized_fragment_same", + "llm_canonical_candidate_detected": true, + "llm_predecompose_contract": { + "schema_version": "address_llm_predecompose_contract_v1", + "source_message": "а теперь банковские операции", + "canonical_message": "а теперь банковские операции", + "mode": "address_query", + "mode_confidence": "medium", + "query_shape": "UNKNOWN", + "query_shape_confidence": "low", + "intent": "bank_operations_by_counterparty", + "intent_confidence": "medium", + "entities": { + "account": null, + "counterparty": null, + "contract": null, + "document_type": null, + "document_ref": null, + "organization": null + }, + "period": { + "scope": "unspecified", + "period_from": null, + "period_to": null, + "as_of_date": null, + "has_explicit_period": false + }, + "aggregation_profile": "list_lookup" + }, + "fallback_rule_hit": null, + "sanitized_user_message": "а теперь банковские операции", + "tool_gate_decision": "run_address_lane", + "tool_gate_reason": "address_mode_classifier_detected", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": { + "trace_id": "fQPoAyba2nzNW_", + "prompt_version": "normalizer_v2_0_2", + "applied": false, + "effective_message": "а теперь банковские операции" + } +} +``` + +## 5. user +message_id: msg-FfUbRQG205 +created_at: 2026-04-08T08:41:40.344Z +reply_type: n/a + +есть ли открытые позиции по договору 19/15 на 2020-12-31 + +## 6. assistant +message_id: msg-kML8gXvOyB +created_at: 2026-04-08T08:41:48.227Z +reply_type: factual +trace_id: address-oAwR0ldclP + +Период сохранен. Глубина live-выборки автоматически расширена до 1000 строк. +Собраны открытые позиции по указанному фильтру (контрагент/договор). +Строк отобрано: 8. +1. 2020-08-31T15:00:06Z | Поступление товаров и услуг 00000000024 от 31.08.2020 15:00:06 | 19.04 / 76.05 | 233.33 +2. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 43804.8 +3. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 9776 +4. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 73968 +5. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 94924.8 +6. 2020-07-15T13:11:19Z | Реализация товаров и услуг 00000000008 от 15.07.2020 13:11:19 | 62.01 / 90.01.1 | 54400 + +### technical_debug_payload_json +```json +{ + "trace_id": "address-oAwR0ldclP", + "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_entity_detected", + "verify_signal_detected", + "open_items_signal_detected", + "query_limit_auto_expanded_for_anchor_recovery" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "medium", + "query_shape": "VERIFY_FACTUAL", + "query_shape_confidence": "medium", + "detected_intent": "open_items_by_counterparty_or_contract", + "detected_intent_confidence": "medium", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "contract": "19/15", + "as_of_date": "2020-12-31" + }, + "missing_required_filters": [], + "selected_recipe": "address_open_items_by_party_or_contract_v1", + "mcp_call_status_legacy": "matched_non_empty", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "anchor_type": "contract", + "anchor_value_raw": "19/15", + "anchor_value_resolved": "19/15", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "matched_non_empty", + "rows_fetched": 1000, + "raw_rows_received": 1000, + "rows_after_account_scope": 285, + "rows_after_recipe_filter": 8, + "rows_materialized": 285, + "rows_matched": 8, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "none", + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": null, + "response_type": "FACTUAL_LIST", + "execution_lane": "address_query", + "llm_decomposition_applied": true, + "llm_decomposition_attempted": true, + "llm_provider_used": "local", + "llm_decomposition_trace_id": "v905zWHzp1kuHh", + "llm_decomposition_effective_message": "Проверить наличие открытых позиций по договору 19/15 на конец периода 2020-12-31.", + "llm_decomposition_reason": "normalized_fragment_applied", + "llm_canonical_candidate_detected": true, + "llm_predecompose_contract": { + "schema_version": "address_llm_predecompose_contract_v1", + "source_message": "есть ли открытые позиции по договору 19/15 на 2020-12-31", + "canonical_message": "Проверить наличие открытых позиций по договору 19/15 на конец периода 2020-12-31.", + "mode": "address_query", + "mode_confidence": "medium", + "query_shape": "VERIFY_FACTUAL", + "query_shape_confidence": "medium", + "intent": "open_items_by_counterparty_or_contract", + "intent_confidence": "medium", + "entities": { + "account": null, + "counterparty": null, + "contract": "19/15", + "document_type": null, + "document_ref": null, + "organization": null + }, + "period": { + "scope": "as_of", + "period_from": null, + "period_to": null, + "as_of_date": "2020-12-31", + "has_explicit_period": true + }, + "aggregation_profile": "open_items" + }, + "fallback_rule_hit": null, + "sanitized_user_message": "есть ли открытые позиции по договору 19/15 на 2020-12-31", + "tool_gate_decision": "run_address_lane", + "tool_gate_reason": "address_mode_classifier_detected", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": { + "trace_id": "v905zWHzp1kuHh", + "prompt_version": "normalizer_v2_0_2", + "applied": true, + "effective_message": "Проверить наличие открытых позиций по договору 19/15 на конец периода 2020-12-31." + } +} +``` + +## 7. user +message_id: msg-VtC7fvCUIt +created_at: 2026-04-08T08:42:06.001Z +reply_type: n/a + +покажи документы по этому же договору + +## 8. assistant +message_id: msg-VswRgnV4fb +created_at: 2026-04-08T08:42:11.635Z +reply_type: factual +trace_id: address-zoj3HXDSnN + +Собран список документов по договору (live address lane). +Строк отобрано: 1. +1. 2021-12-31T12:00:00Z | Поступление товаров и услуг 00000000015 от 31.12.2021 12:00:00 | 19.04 / 76.05 | 233.33 + +### technical_debug_payload_json +```json +{ + "trace_id": "address-zoj3HXDSnN", + "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", + "document_list_signal_detected", + "documents_by_contract_signal_detected", + "contract_from_followup_context", + "address_followup_context_applied" + ], + "why_included_summary": [], + "selection_reason_summary": [] + }, + "dropped_intent_segments": [], + "detected_mode": "address_query", + "detected_mode_confidence": "high", + "query_shape": "DOCUMENT_LIST", + "query_shape_confidence": "medium", + "detected_intent": "list_documents_by_contract", + "detected_intent_confidence": "medium", + "extracted_filters": { + "sort": "period_desc", + "limit": 20, + "contract": "19/15" + }, + "missing_required_filters": [], + "selected_recipe": "address_documents_by_contract_v1", + "mcp_call_status_legacy": "matched_non_empty", + "account_scope_mode": "preferred", + "account_scope_fallback_applied": false, + "anchor_type": "contract", + "anchor_value_raw": "19/15", + "anchor_value_resolved": "19/15", + "resolver_confidence": "medium", + "ambiguity_count": 0, + "match_failure_stage": "none", + "match_failure_reason": null, + "mcp_call_status": "matched_non_empty", + "rows_fetched": 1000, + "raw_rows_received": 1000, + "rows_after_account_scope": 416, + "rows_after_recipe_filter": 2, + "rows_materialized": 416, + "rows_matched": 1, + "raw_row_keys_sample": [ + "Период", + "Регистратор", + "СчетДт", + "СчетКт", + "Сумма", + "Period", + "Registrator", + "AccountDt", + "AccountKt", + "Amount" + ], + "materialization_drop_reason": "none", + "account_token_raw": null, + "account_token_normalized": null, + "account_scope_fields_checked": [ + "account_dt", + "account_kt", + "registrator", + "analytics" + ], + "account_scope_match_strategy": "account_code_regex_plus_alias_map_v1", + "account_scope_drop_reason": "not_applicable", + "runtime_readiness": "LIVE_QUERYABLE_WITH_LIMITS", + "limited_reason_category": null, + "response_type": "FACTUAL_LIST", + "execution_lane": "address_query", + "llm_decomposition_applied": true, + "llm_decomposition_attempted": true, + "llm_provider_used": "local", + "llm_decomposition_trace_id": "2owWUuckOkI3Tu", + "llm_decomposition_effective_message": "Показать документы по указанному договору.", + "llm_decomposition_reason": "normalized_fragment_applied", + "llm_canonical_candidate_detected": true, + "llm_predecompose_contract": { + "schema_version": "address_llm_predecompose_contract_v1", + "source_message": "покажи документы по этому же договору", + "canonical_message": "Показать документы по указанному договору.", + "mode": "address_query", + "mode_confidence": "high", + "query_shape": "DOCUMENT_LIST", + "query_shape_confidence": "medium", + "intent": "list_documents_by_contract", + "intent_confidence": "medium", + "entities": { + "account": null, + "counterparty": null, + "contract": null, + "document_type": null, + "document_ref": null, + "organization": null + }, + "period": { + "scope": "unspecified", + "period_from": null, + "period_to": null, + "as_of_date": null, + "has_explicit_period": false + }, + "aggregation_profile": "list_lookup" + }, + "fallback_rule_hit": null, + "sanitized_user_message": "покажи документы по этому же договору", + "tool_gate_decision": "run_address_lane", + "tool_gate_reason": "address_mode_classifier_detected", + "answer_structure_v11": null, + "investigation_state_snapshot": null, + "normalized": null, + "normalizer_output": { + "trace_id": "2owWUuckOkI3Tu", + "prompt_version": "normalizer_v2_0_2", + "applied": true, + "effective_message": "Показать документы по указанному договору." + } +} +``` diff --git a/docs/ADDRESS/address_query/README.md b/docs/ADDRESS/address_query/README.md index 1b67c9f..53c59ff 100644 --- a/docs/ADDRESS/address_query/README.md +++ b/docs/ADDRESS/address_query/README.md @@ -1,6 +1,6 @@ # ADDRESS Query Docs -Дата: 2026-04-03 +Дата: 2026-04-08 Статус синхронизации: актуализировано по текущему коду в `llm_normalizer/backend/src/services/*`. ## Актуальный статус (2026-04-08) @@ -23,6 +23,15 @@ - `followup_25`: `25/25` - comparator: `PASS` - run: `docs/ADDRESS/runs/2026-04-08_Address_Nightly_Regression_2026-04-08_10-51-20/nightly_summary.json` +- Batch-2 lifecycle официально переведен в `Phase A active`: + - domain card: `domain_general_batch2_lifecycle_card_v1.md` + - acceptance set: `docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json` + - probe report: `wave1_batch2_phaseA_probe_report_2026-04-08.md` + - backlog: `step4_wave1_batch2_phaseA_backlog_v1.md` +- Batch-2 Phase-B progress: + - resolver hardening по `Q12/Q13/Q26/Q27/Q31/Q32` выполнен + - targeted test gate: `addressQueryRuntimeM23.test.ts = 210/210`, `build=PASS` + - live hotpass: `wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` (`route match 6/6`) - Финальный stress-pack: `102/102` `docs/ADDRESS/runs/2026-04-02_Address_Slang_Live_Stress_2026-04-02_12-57-27/run_summary.json` - Финальный follow-up pack: `25/25` @@ -37,14 +46,14 @@ Поддерживаемые intents в runtime: -- `period_coverage_profile` (Wave-1 B1, pre-gate) +- `period_coverage_profile` (Wave-1 B1, gate-closed) - `document_type_and_account_section_profile` (Wave-1 B1, pre-gate) - `counterparty_population_and_roles` (Wave-1 B1, pre-gate) -- `counterparty_activity_lifecycle` (Wave-1/B2, pre-gate) -- `contract_usage_overview` (Wave-1 B1, pre-gate) -- `customer_revenue_and_payments` (Wave-1 B3 value, pre-gate) -- `supplier_payouts_profile` (Wave-1 B3 value, pre-gate) -- `contract_usage_and_value` (Wave-1 B3 value, pre-gate) +- `counterparty_activity_lifecycle` (Wave-1/B2, Phase-A active) +- `contract_usage_overview` (Wave-1 B1, gate-closed) +- `customer_revenue_and_payments` (Wave-1 B3 value, gate-closed) +- `supplier_payouts_profile` (Wave-1 B3 value, gate-closed) +- `contract_usage_and_value` (Wave-1 B3 value, gate-closed) - `list_open_contracts` - `list_payables_counterparties` - `list_receivables_counterparties` @@ -84,6 +93,11 @@ - `wave1_batch1_readiness_report_2026-04-02.md` - авто-отчет готовности к старту Batch-1. - `stage3_value_universalization_closeout_2026-04-03.md` - закрытие этапа 3 по value-вопросам общего домена (профит, тестовый gate, остаточные риски). - `wave1_batch1_batch3_live_gate_report_2026-04-03.md` - фактический отчет по live-gate Batch-1/Batch-3 и статусу глобального regression/comparator (PASS в R6). +- `project_status_rails_graph_2026-04-08.md` - визуальный граф текущего статуса рельсов (закрытые гейты, operational-контур, точка входа в Batch-2). +- `domain_general_batch2_lifecycle_card_v1.md` - domain card для Batch-2 lifecycle (`Q8..Q13 + Q26 + Q27 + Q31 + Q32`). +- `wave1_batch2_phaseA_probe_report_2026-04-08.md` - baseline probe Batch-2 с фактическими gap-ами перед Phase B. +- `step4_wave1_batch2_phaseA_backlog_v1.md` - рабочий backlog Batch-2 (Phase A/B/C). +- `wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` - фиксация закрытия resolver drift на критичных Batch-2 формулировках (`6/6`). - `domain_general_batch1_foundation_card_v1.md` - domain card первой волны (Phase A). - `step4_wave1_batch1_phaseA_backlog_v1.md` - рабочий backlog по подготовке кода и gate-этапам Batch-1. - `domain_card_template_v1.md` - шаблон описания домена для repeatable delivery. diff --git a/docs/ADDRESS/address_query/domain_general_batch1_foundation_card_v1.md b/docs/ADDRESS/address_query/domain_general_batch1_foundation_card_v1.md index 408249e..a45fef5 100644 --- a/docs/ADDRESS/address_query/domain_general_batch1_foundation_card_v1.md +++ b/docs/ADDRESS/address_query/domain_general_batch1_foundation_card_v1.md @@ -2,7 +2,7 @@ Дата: `2026-04-02` Домен: `general_batch1_foundation` -Статус: `active` (Phase A prepared, Phase B runtime intents implemented, Phase C pending) +Статус: `closed` (Phase A/B/C закрыты, domain gate PASS) Владелец: `Address Runtime Team` ## 1. Scope @@ -152,3 +152,4 @@ Global gate: - `2026-04-02` — реализованы `counterparty_population_and_roles` и `contract_usage_overview`; targeted live-pack `Q6/Q7/Q28` прошел `strict factual 9/9` (`2026-04-02_Address_Batch1_NextPack_Q6_Q7_Q28`). - 2026-04-02 - hotfix slang count routing: скока/скок поставщиков|заказчиков стабильно маршрутизируются в counterparty_population_and_roles; targeted live-pack 2026-04-02_Address_SupplierCount_Targeted_AfterFix прошел strict factual 4/4. - 2026-04-02 - hotfix follow-up slang variant: скок клиентов now maps to counterparty_population_and_roles; targeted live-pack 2026-04-02_Address_SupplierClient_Followup_AfterFix passed strict factual 3/3. +- 2026-04-08 - статус карточки обновлен на `closed` после live-gate (`strict 28/28`) и global comparator PASS; следующий шаг вынесен в Batch-2 lifecycle. diff --git a/docs/ADDRESS/address_query/domain_general_batch2_lifecycle_card_v1.md b/docs/ADDRESS/address_query/domain_general_batch2_lifecycle_card_v1.md new file mode 100644 index 0000000..62daad6 --- /dev/null +++ b/docs/ADDRESS/address_query/domain_general_batch2_lifecycle_card_v1.md @@ -0,0 +1,138 @@ +# Domain Card — general_batch2_lifecycle V1 + +Дата: `2026-04-08` +Домен: `general_batch2_lifecycle` +Статус: `active` (Phase A стартован) +Владелец: `Address Runtime Team` + +## 1. Scope + +Кратко: домен закрывает lifecycle-вопросы общего домена (`Q8..Q13 + Q26 + Q27 + Q31 + Q32`) и готовит единый рельсовый контракт перед Phase B. + +In-scope intents: + +1. `counterparty_activity_lifecycle` +2. `contract_usage_overview` (для stale contract слоя) +3. `contract_usage_and_value` (для multi-contract структуры) + +Out-of-scope: + +1. value-ranking блок (`Q14..Q25 + Q29 + Q30`) — закрыт отдельным Batch-3. +2. debt/aging/risk блок (`Q33+`) — отдельные Batch-4/Batch-5. +3. `COMPOUND_FACTUAL_QUERY` multi-step execution вне явного lifecycle контракта. + +## 2. Intent Contract + +### 2.1 `counterparty_activity_lifecycle` + +- `query_shape`: `FACTUAL_LIST` +- `required_filters`: `[]` +- `optional_filters`: `[period_from, period_to, as_of_date, organization, limit, sort]` +- `resolver_signals`: `активны`, `новые`, `исчезли`, `только один раз`, `дольше всех`, `регулярные`, `эпизодические`, `давно не использовались` +- `ambiguity_rules`: если есть сигналы lifecycle + role split, приоритет у lifecycle (не у `counterparty_population_and_roles`) +- `fallback_policy`: при отсутствии явного периода используется all-time окно; factual без якорей разрешен + +### 2.2 `contract_usage_overview` + +- `query_shape`: `FACTUAL_SUMMARY` +- `required_filters`: `[]` +- `optional_filters`: `[period_from, period_to, as_of_date, organization, limit]` +- `resolver_signals`: `договоры не использовались`, `stale contracts`, `мертвые договоры` +- `ambiguity_rules`: если вопрос про общий статус использования договоров без топ-ранжирования, приоритет у `contract_usage_overview` +- `fallback_policy`: если stale-метрика не подтверждается, допустим `LIMITED_WITH_REASON` без ложного factual + +### 2.3 `contract_usage_and_value` + +- `query_shape`: `FACTUAL_LIST` +- `required_filters`: `[]` +- `optional_filters`: `[period_from, period_to, as_of_date, organization, limit, sort]` +- `resolver_signals`: `несколько договоров у контрагента`, `мультидоговорные`, `какие договоры активны` +- `ambiguity_rules`: multi-contract структура имеет приоритет над anchor-required lookup intent-ами +- `fallback_policy`: при невозможности построить структуру выдаем `LIMITED_WITH_REASON`, без перехода в broad document list + +## 3. Recipe Mapping + +Связка `intent -> recipe_id` должна совпадать с runtime catalog (`addressRecipeCatalog.ts`). + +| intent | recipe_id | mcp_method | expected_statuses | +| --- | --- | --- | --- | +| `counterparty_activity_lifecycle` | `address_counterparty_activity_lifecycle_v1` | `POST /api/execute_query` | `matched_non_empty`, `no_raw_rows`, `materialized_but_not_matched` | +| `contract_usage_overview` | `address_contract_usage_overview_v1` | `POST /api/execute_query` | `matched_non_empty`, `no_raw_rows`, `materialized_but_not_matched` | +| `contract_usage_and_value` | `address_contract_usage_and_value_v1` | `POST /api/execute_query` | `matched_non_empty`, `no_raw_rows`, `materialized_but_not_matched` | + +## 4. Anchor and Resolver Rules + +- `anchor_type`: `period`, `organization`, optional `counterparty` for follow-up context +- `anchor_resolution_order`: explicit period -> follow-up period -> all-time default +- `min_confidence`: `medium` +- `unresolved_behavior`: при unresolved lifecycle сегменте отдавать `LIMITED_WITH_REASON` (`unsupported`/`empty_match`) без псевдо-factual + +## 5. Limited Reasons (taxonomy) + +Разрешенные категории для домена: + +1. `empty_match` +2. `unsupported` +3. `execution_error` +4. `missing_anchor` (только для реального anchor-required маршрута) + +Запрещено: + +- подменять lifecycle intent на lookup intent с обязательным якорем без явного сигнала пользователя; +- выдавать broad fallback list в вопросах про lifecycle сегментацию. + +## 6. Test Coverage + +Unit: + +1. resolver positives/negatives для `Q8..Q13`, `Q26`, `Q27`, `Q31`, `Q32` +2. приоритет lifecycle над population-role split +3. follow-up period switch (`2020 -> 2021`) для lifecycle цепочек +4. contract lifecycle phrase routing без обязательного якоря + +Integration: + +1. recipe selection на lifecycle/contract-lifecycle фразах +2. execution status mapping (`matched_non_empty`, `unsupported`, `empty_match`) +3. debug payload completeness + +Live acceptance: + +1. canonical вопросы +2. noisy/slang вопросы +3. follow-up chains + +## 7. Gate Criteria + +Domain gate: + +- `strict_pass(route)=100%` +- `false_factual_rate=0` +- `execution_error_rate=0` + +Global gate: + +- baseline stress не ниже эталона `102/102` +- baseline follow-up не ниже эталона `25/25` + +## 8. Rollout Plan + +1. `phase_a` — contract freeze + acceptance set + probe baseline. +2. `phase_b` — resolver/compose hardening по lifecycle gap-ам. +3. `phase_c` — domain live gate + global comparator PASS. + +## 9. Artifacts + +Обязательные артефакты: + +1. `domain_general_batch2_lifecycle_card_v1.md` +2. `question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json` +3. `wave1_batch2_phaseA_probe_report_2026-04-08.md` +4. `step4_wave1_batch2_phaseA_backlog_v1.md` +5. `runs//run_summary.json` +6. `runs//full_live_results.json` + +## 10. Change Log + +- `2026-04-08` — создана карточка домена Batch-2 lifecycle и зафиксирован contract scope. +- `2026-04-08` — добавлен Phase-A acceptance набор `Q8..Q13 + Q26 + Q27 + Q31 + Q32`. diff --git a/docs/ADDRESS/address_query/global_execution_checklist_v1.md b/docs/ADDRESS/address_query/global_execution_checklist_v1.md index 836e8c3..cfd70bb 100644 --- a/docs/ADDRESS/address_query/global_execution_checklist_v1.md +++ b/docs/ADDRESS/address_query/global_execution_checklist_v1.md @@ -1,7 +1,7 @@ # Global Execution Checklist V1 Дата старта: 2026-04-02 -Последнее обновление: 2026-04-02 +Последнее обновление: 2026-04-08 Цель: последовательное закрытие Address Query V1 без архитектурного дрейфа и без company-specific хардкода. ## Правило выполнения @@ -89,12 +89,27 @@ - `domain_general_batch1_foundation_card_v1.md` - `domain_general_batch1_acceptance_2026-04-02_phaseA.json` - `step4_wave1_batch1_phaseA_backlog_v1.md` -- [ ] Phase A закрыт (intent naming freeze + negative cases). -- [ ] Phase B закрыт (resolver/types/recipes/compose для Batch-1). -- [ ] Phase C закрыт (domain gate + global non-regression gate). +- [x] Phase A закрыт (intent naming freeze + acceptance set + domain card). +- [x] Phase B закрыт (resolver/types/recipes/compose для Batch-1). +- [x] Phase C закрыт (domain gate + global non-regression gate). - [x] Phase B.1 начат: реализован первый Batch-1 intent `period_coverage_profile` (resolver + types + recipe + compose), unit/build green. - [x] Phase B.1 progress: реализован второй Batch-1 intent `document_type_and_account_section_profile` (resolver + classifier + recipe + compose), unit/build green (`107 passed`). +#### Step-4 Wave-1 (general domain, Batch-2 lifecycle: Q8..Q13 + Q26 + Q27 + Q31 + Q32) + +- [x] Phase A стартован: + - `domain_general_batch2_lifecycle_card_v1.md` + - `domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json` + - `wave1_batch2_phaseA_probe_report_2026-04-08.md` + - `step4_wave1_batch2_phaseA_backlog_v1.md` +- [ ] Phase A закрыт (negative-пары + final contract freeze). +- [ ] Phase B закрыт (resolver/decompose/compose hardening для lifecycle gap-ов). +- [x] Phase B progress: + - resolver hardening по `Q12/Q13/Q26/Q27/Q31/Q32` + - test gate: `addressQueryRuntimeM23.test.ts = 210/210`, `build=PASS` + - live hotpass: `wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` (`route match 6/6`) +- [ ] Phase C закрыт (Batch-2 domain gate + global comparator PASS). + ## Документация (code-sync) - [x] Базовые docs синхронизированы с текущим runtime-кодом (`README`, `address_scenario_matrix`, `query_recipes`, `runtime_readiness_matrix`, `address_runtime_contracts`, `runtime_integration_plan`). diff --git a/docs/ADDRESS/address_query/project_status_rails_graph_2026-04-08.md b/docs/ADDRESS/address_query/project_status_rails_graph_2026-04-08.md new file mode 100644 index 0000000..803176e --- /dev/null +++ b/docs/ADDRESS/address_query/project_status_rails_graph_2026-04-08.md @@ -0,0 +1,51 @@ +# ADDRESS Project Status Rails Graph (2026-04-08) + +Дата среза: 2026-04-08 +Контур: `question_mode=address_query` + +## Граф статуса + +```mermaid +flowchart LR + A["Step-0 Pre-Prod Rails
CLOSED"] + B["Wave-1 Runtime Prep (B1+B3)
intents + tests"] + C["Domain Live Gates
B1: 28/28 PASS
B3: 33/33 PASS"] + D["Global Nightly Comparator (R6)
102/102 + 25/25 PASS"] + E["Auto-nightly Incident
2026-04-08 09:52
MCP fetch failed"] + F["Targeted Fix
S019 predecompose anchor drift"] + G["Manual Control Nightly Recheck
2026-04-08 10:51
102/102 + 25/25 PASS"] + H["CURRENT STATE
BATCH2_PHASE_B_ACTIVE"] + I["NEXT STEP
Batch-2 Phase B closeout
Follow-up carryover + compose hardening"] + K["Batch-2 Gate
Phase C -> Global Regression"] + J["Operational Rail
Scheduler: Disabled
Manual nightly only"] + + A --> B --> C --> D --> H --> I --> K + D --> E --> F --> G --> H + E --> J + H --> J +``` + +## Контрольные артефакты + +- Global comparator PASS (reference): + `docs/ADDRESS/runs/2026-04-03_Address_Nightly_Regression_Post_AnchorHardening_R6/nightly_summary.json` +- Incident run (auto-nightly): + `docs/ADDRESS/runs/2026-04-08_Address_Nightly_Regression_2026-04-08_09-52-39/nightly_summary.json` +- Control recheck PASS (manual): + `docs/ADDRESS/runs/2026-04-08_Address_Nightly_Regression_2026-04-08_10-51-20/nightly_summary.json` +- Master checker (entry to Batch-2): + `docs/ADDRESS/address_query/step4_wave1_batch1_master_checker_v1.md` +- Batch-2 Phase-A artifacts: + - `docs/ADDRESS/address_query/domain_general_batch2_lifecycle_card_v1.md` + - `docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json` + - `docs/ADDRESS/address_query/wave1_batch2_phaseA_probe_report_2026-04-08.md` + - `docs/ADDRESS/address_query/step4_wave1_batch2_phaseA_backlog_v1.md` +- Batch-2 Phase-B hotpass (resolver drift fix): + - `docs/ADDRESS/address_query/wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` + +## Коротко по статусу + +- Функциональные гейты закрыты. +- Точечный flake `S019` закрыт. +- Batch-2 переведен в `Phase B active`; resolver drift по `Q12/Q13/Q26/Q27/Q31/Q32` закрыт (`hotpass 6/6`). +- Следующий практический шаг: Phase-B closeout (follow-up carryover + compose), затем Batch-2 domain gate. diff --git a/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md b/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md index 6f213cb..6180673 100644 --- a/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md +++ b/docs/ADDRESS/address_query/runtime_readiness_matrix_v1.md @@ -1,6 +1,6 @@ # Runtime Readiness Matrix V1 (Code Sync) -Дата: 2026-04-03 +Дата: 2026-04-08 Формат: `scenario -> structural_readiness -> runtime_readiness -> blocker` @@ -30,7 +30,7 @@ | AQ-B1-10 | period_coverage_profile | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | functional gates закрыты; остаточный риск в operational стабильности auto-nightly/MCP канала | держать comparator PASS (R6) и переходить к Batch-2 lifecycle | | AQ-B1-11 | document_type_and_account_section_profile | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | functional gates закрыты; остаточный риск в operational стабильности auto-nightly/MCP канала | держать comparator PASS (R6) и переходить к Batch-2 lifecycle | | AQ-B1-12 | counterparty_population_and_roles | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | functional gates закрыты; остаточный риск в operational стабильности auto-nightly/MCP канала | держать comparator PASS (R6) и переходить к Batch-2 lifecycle | -| AQ-B2-13 | counterparty_activity_lifecycle | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | lifecycle-сценарии требуют отдельного доменного live-pack (Batch-2) | собрать Batch-2 acceptance и закрыть route gate | +| AQ-B2-13 | counterparty_activity_lifecycle | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | resolver drift hotfix закрыт, но Batch-2 domain gate еще не прогнан | закрыть Phase-B (follow-up/compose) и выполнить Batch-2 domain gate | | AQ-B1-14 | contract_usage_overview | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | functional gates закрыты; остаточный риск в operational стабильности auto-nightly/MCP канала | держать comparator PASS (R6) и переходить к Batch-2 lifecycle | | AQ-B3-15 | customer_revenue_and_payments | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | functional gates закрыты; остаточный риск в operational стабильности auto-nightly/MCP канала | держать comparator PASS (R6) и переходить к Batch-2 lifecycle | | AQ-B3-16 | supplier_payouts_profile | STRUCTURALLY_VISIBLE | LIVE_QUERYABLE_WITH_LIMITS | functional gates закрыты; остаточный риск в operational стабильности auto-nightly/MCP канала | держать comparator PASS (R6) и переходить к Batch-2 lifecycle | @@ -67,6 +67,14 @@ - авто-запуск nightly из Task Scheduler дал `overall_ok=false` из-за инфраструктурного `MCP fetch failed: fetch failed` в части вызовов, не как функциональный откат доменных intent-ов. - ручной контрольный nightly (`2026-04-08 10:29`) показал точечный flake в `AQ-P0-07` семействе (`S019`: `materialized_but_not_anchor_matched`), при этом `followup_25` остался `25/25`. - повторный контрольный nightly (`2026-04-08 10:51`) закрыт в `PASS`: `stress_102=102/102`, `followup_25=25/25`, comparator `PASS` (`docs/ADDRESS/runs/2026-04-08_Address_Nightly_Regression_2026-04-08_10-51-20/nightly_summary.json`). +- Batch-2 lifecycle Phase-A probe (2026-04-08): + - report: `docs/ADDRESS/address_query/wave1_batch2_phaseA_probe_report_2026-04-08.md` + - baseline coverage: `semantic_match 4/10` + - основные gap-и: `Q12/Q13/Q27` -> `unsupported`, `Q26` -> drift в `counterparty_population_and_roles`, `Q31/Q32` -> drift в anchor-required lookup ветки. +- Batch-2 lifecycle Phase-B resolver hotpass (2026-04-08): + - report: `docs/ADDRESS/address_query/wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` + - фактический результат: `route match 6/6` на `Q12/Q13/Q26/Q27/Q31/Q32` + - кодовый gate: `addressQueryRuntimeM23.test.ts = 210/210`, `build=PASS`. - `COMPOUND_FACTUAL_QUERY` остается detection-only (без multi-intent execution). - Финальные gate-артефакты стабильности: - stress `102/102`: `docs/ADDRESS/runs/2026-04-02_Address_Slang_Live_Stress_2026-04-02_12-57-27/run_summary.json` diff --git a/docs/ADDRESS/address_query/step4_wave1_batch1_master_checker_v1.md b/docs/ADDRESS/address_query/step4_wave1_batch1_master_checker_v1.md index 9c242bd..81d631d 100644 --- a/docs/ADDRESS/address_query/step4_wave1_batch1_master_checker_v1.md +++ b/docs/ADDRESS/address_query/step4_wave1_batch1_master_checker_v1.md @@ -62,6 +62,15 @@ Batch-1 можно переводить в runtime только после за - [x] Контрольный nightly recheck (`2026-04-08 10:51`) закрыт в `PASS` run: `docs/ADDRESS/runs/2026-04-08_Address_Nightly_Regression_2026-04-08_10-51-20/nightly_summary.json` детали: `stress_102=102/102`, `followup_25=25/25`, comparator PASS. +- [x] Batch-2 lifecycle Phase A стартован: + - `domain_general_batch2_lifecycle_card_v1.md` + - `docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json` + - `wave1_batch2_phaseA_probe_report_2026-04-08.md` + - `step4_wave1_batch2_phaseA_backlog_v1.md` +- [x] Batch-2 Phase-B progress: + - resolver drift по `Q12/Q13/Q26/Q27/Q31/Q32` закрыт + - `addressQueryRuntimeM23.test.ts = 210/210`, `build=PASS` + - live hotpass `wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` (`route match 6/6`) - [x] Phase B закрыт. - [x] Phase C закрыт. @@ -69,4 +78,4 @@ Batch-1 можно переводить в runtime только после за 1. Начинать можно, но строго по фазам выше. 2. Прямое включение Batch-1 intents в production-path без Phase B/C — запрещено. -3. Точка входа в работу: контрольный nightly уже зафиксирован в `PASS`, следующий шаг — старт Batch-2 lifecycle (`Q8..Q13 + Q26 + Q27 + Q31 + Q32`) по тем же gate-правилам. +3. Точка входа в работу: Batch-2 уже в `Phase A active`; следующий шаг — выполнить Batch-2 Phase B и закрыть domain gate/глобальный comparator. diff --git a/docs/ADDRESS/address_query/step4_wave1_batch1_phaseA_backlog_v1.md b/docs/ADDRESS/address_query/step4_wave1_batch1_phaseA_backlog_v1.md index 2ac57ba..95bd763 100644 --- a/docs/ADDRESS/address_query/step4_wave1_batch1_phaseA_backlog_v1.md +++ b/docs/ADDRESS/address_query/step4_wave1_batch1_phaseA_backlog_v1.md @@ -1,9 +1,12 @@ # Step-4 Wave-1 Batch-1 — Phase A/B Backlog V1 Дата: 2026-04-02 -Статус: `active` +Статус: `closed` (historical backlog, фактическое закрытие Batch-1/Batch-3 зафиксировано в gate reports) Scope: `Q1..Q7 + Q28` +> Примечание на 2026-04-08: файл сохранен как исторический backlog старта Wave-1. +> Текущий рабочий backlog для новой волны: `step4_wave1_batch2_phaseA_backlog_v1.md`. + ## 1. Phase A (Design/Contract) — старт - [x] Зафиксирован master checker: `step4_wave1_batch1_master_checker_v1.md` diff --git a/docs/ADDRESS/address_query/step4_wave1_batch2_phaseA_backlog_v1.md b/docs/ADDRESS/address_query/step4_wave1_batch2_phaseA_backlog_v1.md new file mode 100644 index 0000000..c7a2714 --- /dev/null +++ b/docs/ADDRESS/address_query/step4_wave1_batch2_phaseA_backlog_v1.md @@ -0,0 +1,83 @@ +# Step-4 Wave-1 Batch-2 — Phase A/B Backlog V1 + +Дата: 2026-04-08 +Статус: `active` +Scope: `Q8..Q13 + Q26 + Q27 + Q31 + Q32` + +## 1. Phase A (Design/Contract) — статус + +- [x] Зафиксирован вход в Batch-2 из master checker: `step4_wave1_batch1_master_checker_v1.md` +- [x] Зафиксирована domain card: `domain_general_batch2_lifecycle_card_v1.md` +- [x] Сформирован acceptance набор: `domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json` +- [x] Выполнен Phase-A probe и зафиксированы gap-ы: `wave1_batch2_phaseA_probe_report_2026-04-08.md` +- [ ] Добавить explicit negative-пары (близкие формулировки для anti-drift на lookup intent-ы) + +## 2. Phase B (Runtime Prep) — задачи по коду + +### 2.1 Resolver / Decompose + +- [x] `llm_normalizer/backend/src/services/addressIntentResolver.ts` + Добавить сигналы lifecycle для `Q12/Q13/Q27`: + - `только один раз`, `single activity`, `one-time` + - `дольше всех`, `longest-running` + - `давно не использовались` (supplier lifecycle) +- [x] `llm_normalizer/backend/src/services/addressIntentResolver.ts` + Зафиксировать priority rule: + `counterparty_activity_lifecycle` > `counterparty_population_and_roles` + для сегментационных формулировок (`регулярные/эпизодические`, `исчезли`, `новые`, `long-term`). +- [x] `llm_normalizer/backend/src/services/addressIntentResolver.ts` + Добавить contract lifecycle routing: + - stale contract phrasing -> `contract_usage_overview` + - multi-contract/active contract phrasing -> `contract_usage_and_value` +- [ ] `llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts` + Усилить follow-up carryover для Batch-2 chain: + `а кто новые`, `а кто потом исчез`, `а теперь за 2021`, `и какие договоры активны`. + +### 2.2 Filter / Recipe / Compose + +- [ ] `llm_normalizer/backend/src/services/addressFilterExtractor.ts` + Нормализовать lifecycle period cues (`после <год>`, `за <год>`, `впервые в <год>`). +- [ ] `llm_normalizer/backend/src/services/address_runtime/composeStage.ts` + Добавить стабильные summary-профили для: + - new counterparties + - lost counterparties + - one-time counterparties + - long-running counterparties + - regular vs episodic suppliers + - stale suppliers/contracts +- [ ] `llm_normalizer/backend/src/services/address_runtime/composeStage.ts` + Для `contract_usage_and_value` добавить блок multi-contract структуры: + `контрагент -> количество договоров -> активные/неактивные`. + +### 2.3 Tests / Gates + +- [x] `llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts` + Добавить regression-кейсы под `Q12/Q13/Q26/Q27/Q31/Q32`. +- [ ] `llm_normalizer/backend/tests/assistantAddressFollowupContext.test.ts` + Добавить lifecycle follow-up chains для переключения года и сегментов. +- [x] Кодовый gate: + - `npm --prefix llm_normalizer/backend run test -- addressQueryRuntimeM23.test.ts --testTimeout 20000` + - `npm --prefix llm_normalizer/backend run build` +- [x] Быстрый live hotpass по проблемным маршрутам: + - report: `wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md` + - result: `route match 6/6` + +## 3. Phase C (Gate) — критерии закрытия + +- [ ] Domain run-pack по + `docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json`: + - `strict_pass(route)=100%` + - `false_factual_rate=0` + - `execution_error_rate=0` +- [ ] Global non-regression: + - `address_slang_stress_full_2026-04-02.json` + - `address_followup_context_chains_2026-04-02.json` + обе метрики не ниже baseline (`102/102`, `25/25`). +- [ ] Comparator PASS against latest green nightly: + `docs/ADDRESS/runs/2026-04-08_Address_Nightly_Regression_2026-04-08_10-51-20/nightly_summary.json` + +## 4. Текущий приоритет (следующий кодовый шаг) + +1. Закрыть follow-up carryover для lifecycle/contract lifecycle chain. +2. Дожать compose-представления для `stale`/`one-time`/`longest-running`/`regular-vs-episodic`. +3. Прогнать полный Batch-2 domain gate pack + global comparator. diff --git a/docs/ADDRESS/address_query/wave1_batch2_phaseA_probe_report_2026-04-08.md b/docs/ADDRESS/address_query/wave1_batch2_phaseA_probe_report_2026-04-08.md new file mode 100644 index 0000000..e497d46 --- /dev/null +++ b/docs/ADDRESS/address_query/wave1_batch2_phaseA_probe_report_2026-04-08.md @@ -0,0 +1,62 @@ +# Wave-1 Batch-2 Phase-A Probe Report + +Дата: 2026-04-08 +Контур: `question_mode=address_query` +Цель: зафиксировать текущий фактический coverage Batch-2 перед Phase-B implementation. + +## 1. Probe set + +Проверены 10 representative-вопросов: + +- `Q8..Q13` (customer lifecycle) +- `Q26..Q27` (supplier lifecycle) +- `Q31..Q32` (contract lifecycle/structure) + +## 2. Контур прогона + +Probe выполнен через временный `tsx` harness к `AddressQueryService` с 10 representative-кейсами. +Временный script удален после фиксации результата в отчете. + +## 3. Результат + +- `semantic_match = 4/10` +- `route coverage = partial` +- `execution_error = 0` (в probe-срезе) + +## 4. Детализация по кейсам + +| Q | expected_intent | detected_intent | status | note | +| --- | --- | --- | --- | --- | +| Q8 | `counterparty_activity_lifecycle` | `counterparty_activity_lifecycle` | PASS | factual list | +| Q9 | `counterparty_activity_lifecycle` | `counterparty_activity_lifecycle` | PASS | factual list | +| Q10 | `counterparty_activity_lifecycle` | `counterparty_activity_lifecycle` | PASS | factual list | +| Q11 | `counterparty_activity_lifecycle` | `counterparty_activity_lifecycle` | PASS | factual list | +| Q12 | `counterparty_activity_lifecycle` | `unknown` | FAIL | `unsupported` | +| Q13 | `counterparty_activity_lifecycle` | `unknown` | FAIL | `unsupported` | +| Q26 | `counterparty_activity_lifecycle` | `counterparty_population_and_roles` | FAIL | role-split intent hijack | +| Q27 | `counterparty_activity_lifecycle` | `unknown` | FAIL | `unsupported` | +| Q31 | `contract_usage_overview` | `list_documents_by_contract` | FAIL | `missing_anchor` drift to lookup | +| Q32 | `contract_usage_and_value` | `list_contracts_by_counterparty` | FAIL | `missing_anchor` drift to lookup | + +## 5. Выводы для Phase B + +1. Lifecycle клиентского слоя (`Q8..Q11`) базово работает. +2. Не закрыты важные lifecycle сегменты: + `one-time`, `longest-running`, `stale suppliers`. +3. Есть конфликт маршрутов между lifecycle intent-ом и population/lookup intent-ами: + - `Q26` уходит в `counterparty_population_and_roles`; + - `Q31/Q32` уходит в anchor-required lookup ветки. + +## 6. Приоритеты исправлений + +1. Resolver-сигналы для `Q12/Q13/Q27` -> `counterparty_activity_lifecycle`. +2. Resolver-priority rule: lifecycle segmentation > role population split. +3. Contract lifecycle routing: + - `stale contracts` -> `contract_usage_overview`; + - `multi-contract active map` -> `contract_usage_and_value`. +4. Follow-up carryover для lifecycle chain без обязательных anchor-фильтров. + +## 7. Решение + +`READY_FOR_PHASE_B_IMPLEMENTATION` +Фаза A закрыта как design/probe baseline; следующий шаг — runtime hardening и domain live-gate Batch-2. diff --git a/docs/ADDRESS/address_query/wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md b/docs/ADDRESS/address_query/wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md new file mode 100644 index 0000000..3e22c63 --- /dev/null +++ b/docs/ADDRESS/address_query/wave1_batch2_phaseB_resolver_hotpass_2026-04-08.md @@ -0,0 +1,38 @@ +# Wave-1 Batch-2 Phase-B Resolver Hotpass + +Дата: 2026-04-08 +Контур: `question_mode=address_query` +Цель: подтвердить устранение resolver drift по критичным Batch-2 lifecycle формулировкам. + +## 1. Проверочный набор + +Проверены бывшие проблемные формулировки: + +1. `Q12` one-time counterparties +2. `Q13` longest-running counterparties +3. `Q26` regular vs episodic suppliers +4. `Q27` stale suppliers +5. `Q31` stale contracts +6. `Q32` multi-contract counterparties + active contracts + +## 2. Факт результата + +- `route match = 6/6` +- `reply_type = factual` по всем 6 кейсам +- `mcp_call_status = matched_non_empty` по всем 6 кейсам + +## 3. Детализация + +| case | detected_intent | selected_recipe | status | +| --- | --- | --- | --- | +| Q12 one-time | `counterparty_activity_lifecycle` | `address_counterparty_activity_lifecycle_v1` | PASS | +| Q13 longest-running | `counterparty_activity_lifecycle` | `address_counterparty_activity_lifecycle_v1` | PASS | +| Q26 regular/episodic | `counterparty_activity_lifecycle` | `address_counterparty_activity_lifecycle_v1` | PASS | +| Q27 stale suppliers | `counterparty_activity_lifecycle` | `address_counterparty_activity_lifecycle_v1` | PASS | +| Q31 stale contracts | `contract_usage_overview` | `address_contract_usage_overview_v1` | PASS | +| Q32 multi-contract active map | `contract_usage_and_value` | `address_contract_usage_and_value_v1` | PASS | + +## 4. Примечание + +Это не финальный domain gate Batch-2. +Следующий шаг: закрыть оставшиеся Phase-B задачи (follow-up carryover + compose нюансы), затем прогнать полный Batch-2 acceptance pack и global comparator. diff --git a/docs/ADDRESS/question_lib.txt b/docs/ADDRESS/question_lib.txt new file mode 100644 index 0000000..69527ee --- /dev/null +++ b/docs/ADDRESS/question_lib.txt @@ -0,0 +1,398 @@ +# ADDRESS Test Questions (обновлено 2026-04-02) + +Важно: +1. Цепочки `FOLLOW-UP` прогоняй в одной сессии подряд. +2. Блоки `SINGLE` можно гонять по отдельности. +3. Для проверок смотри не только ответ, но и `technical_debug_payload_json`. + +## SINGLE: Документы по контрагенту +1. Покажи документы по СВК за 2020 год +2. свк доки за 20й +3. что по свк за 2020 выведи все доки плиз +4. Какие документы по контрагенту СВК за весь период +5. какие есть доки по свк с 2000 по 2025 год +6. Покажи документы по контрагенту СВК + +## SINGLE: Банковские операции +7. Покажи банковские операции по СВК за 2020 +8. Какие платежи по СВК в 2020 +9. Покажи банковские операции по контрагенту СВК за весь период + +## SINGLE: Договор +10. Покажи документы по договору 19/15 за 2020 +11. Покажи документы по договору 19/15 +12. Покажи банковские операции по договору 19/15 +13. Есть ли долг по договору 19/15 на 2020-07-31 +14. Покажи операции по договору 19/15 за весь период + +## SINGLE: Сальдо / остаток +15. Покажи сальдо по счету 60.01 на дату 2020-07-31 +16. Какой остаток по счету 60 на 2020.05 +17. Какой остаток по счету 60 на 2020 май +18. Какие документы формируют остаток по счету 60.01 на 2020-07-31 +19. Какой остаток по счету 62 на сегодня + +## FOLLOW-UP CHAIN A (одна сессия) +20. Покажи документы по СВК за 2020 +21. а теперь только за май 2020 +22. а за все время? +23. а по счету 60.01 на ту же дату + +## FOLLOW-UP CHAIN B (одна сессия) +24. Покажи банковские операции по договору 19/15 +25. а за 2020 год +26. а теперь только за май 2020 +27. а по счету 51 на ту же дату + +## FOLLOW-UP CHAIN C (одна сессия) +28. Какой остаток по счету 60 на 2020.05 +29. а теперь по 60.01 +30. а на ту же дату по 62 + +## NOISY / TYPO +31. бля епт покажи доки по свк за 20й +32. свк 20 год - покажи доки плс +33. покажи банк опер по дог 19/15 пж +34. остаток 60 на 2020 май покеж +35. по свк за весь период че есть + +## NEGATIVE / LIMIT CASE +36. Покажи технические константы конфигурации +37. кто президент США +## SINGLE: Дебиторка / Кредиторка +38. Кто нам должен на 2020-12-31 +39. Кому мы должны на 2020-12-31 +40. Покажи дебиторскую задолженность за 2020 +41. Покажи кредиторскую задолженность за 2020 + +## SINGLE: Незакрытые договоры / открытые позиции +42. Покажи незакрытые договоры на 2020-12-31 +43. Какие открытые договоры есть на 2020-12-31 +44. Покажи хвосты по контрагенту СВК на 2020-12-31 +45. Есть ли незакрытые позиции по договору 19/15 на 2020-12-31 +46. Покажи открытые позиции по договору 19/15 + +## FOLLOW-UP CHAIN D (одна сессия) +47. Покажи документы по контрагенту СВК за 2020 +48. а только за июль 2020 +49. а теперь банковские операции +50. а за весь период +51. а сальдо по счету 51 на ту же дату + +## FOLLOW-UP CHAIN E (одна сессия) +52. Покажи незакрытые позиции по контрагенту СВК +53. а на 2020-12-31 +54. а теперь по договору 19/15 +55. а только банковские операции +56. а за май 2020 + +## EN / Transliteration Stress +57. show documents by counterparty svk for 2020 +58. show bank operations by contract 19/15 +59. kakoi ostatok po schetu 60.01 na 2020-07-31 +60. kto nam dolzhen na 2020-12-31 + +## SUPPORTED FULL POOL (AUTO, 2026-04-08) + +Срез: 2026-04-08 +Принцип: включены только вопросы из покрытых доменов/наборов, которые прогонялись в рамках текущего runtime-контура. + +Источники: +- docs/ADDRESS/question_sets/address_slang_stress_full_2026-04-02.json +- docs/ADDRESS/question_sets/address_followup_context_chains_2026-04-02.json +- docs/ADDRESS/question_sets/domain_contracts_open_items_acceptance_2026-04-02.json +- docs/ADDRESS/question_sets/domain_general_batch1_acceptance_2026-04-02_phaseA.json +- docs/ADDRESS/question_sets/temp_batch3_value_top20_2026-04-02.json +- docs/ADDRESS/question_sets/temp_batch2_q8_q9_2026-04-02.json +- docs/ADDRESS/question_sets/temp_contracts_zhukovka_targeted_2026-04-02.json +- docs/ADDRESS/question_sets/temp_supplier_count_targeted_2026-04-02.json +- docs/ADDRESS/question_sets/temp_supplier_client_followup_targeted_2026-04-02.json +- docs/ADDRESS/question_sets/address_contract_19_15_postfix_2026-04-02.json + +- Всего уникальных поддерживаемых формулировок в этом пуле: 212 + +### Документы по контрагенту (`list_documents_by_counterparty`) — 22 +1. свк доки за 2020 покаж пж +2. покеж все доки по свк за 20 год +3. дай список документов по свк за 2020 +4. какие у свк есть доки за 2020? +5. че по свк по докам за 2020 год +6. ёпт покажи доки по свк за 2020 +7. бля, доки свк 2020 вывали +8. svk docs for 2020 please +9. docs by counterparty svk year 2020 +10. по контрагенту свк покаж документы за 2020 +11. плс доки по свк 2020 +12. свк док за 2020 гони +13. свк доки кинь за 2020 +14. по свк все документы за 20й год +15. пж вывали доки контрагент свк 2020 +16. svk poka docy za 2020 +17. какие доки по компании свк за 2020 +18. бля покаж свк документы 2020 +19. Покажи документы по СВК за 2020 +20. а теперь только за май 2020 +21. а теперь документы по нему +22. покажи документы все по жуковке 51 + +### Банковские операции по контрагенту (`bank_operations_by_counterparty`) — 18 +1. по свк банковские операции за 2020 покаж +2. свк выписка банка 2020 +3. платежи по свк за 2020 выведи +4. оплаты по свк за 2020 +5. свк банк опер за 2020 +6. че у свк по банку за 2020 +7. свк списания/поступления за 2020 +8. bank ops by svk for 2020 +9. transactions by counterparty svk 2020 +10. бля, по свк банковские платежки 2020 +11. свк банк операции за 2020 +12. по свк выписку банка за 2020 выведи +13. свк платежки за 2020 +14. bank ops svk 2020 +15. транзакции по свк за 2020 +16. ёпт по свк че по банку 2020 +17. Покажи банковские операции по СВК за 2020 +18. а теперь только ноябрь 2020 + +### Договоры по контрагенту (`list_contracts_by_counterparty`) — 2 +1. покажи договора все по жуковке 51 +2. покажи договора по свк + +### Документы по договору (`list_documents_by_contract`) — 16 +1. покажи документы по договору 19/15 за 2020 +2. доки по дог. 19/15 за 2020 +3. какие документы по контракту 19/15 +4. docs by contract 19/15 year 2020 +5. бля, покажи по договору 19/15 доки +6. договор 19/15 документы +7. плс документы договор №19/15 +8. покеж документы по 19/15 +9. доки по договору 19/15 за 2020 +10. покажи документы контракт 19/15 +11. docs by contract 19/15 2020 +12. по дог 19/15 какие документы +13. договор 19/15 выведи доки +14. нах покажи документы по 19/15 +15. покажи документы по этому же договору +16. а документы по этому же договору за тот же период + +### Банковские операции по договору (`bank_operations_by_contract`) — 14 +1. банковские операции по договору 19/15 +2. платежи по договору 19/15 +3. выписка по договору 19/15 +4. bank ops by contract 19/15 +5. ёпт, покажи банк операции по дог 19/15 +6. по контракту 19/15 платежки +7. банк операции по договору 19/15 +8. платежки по дог 19/15 +9. выписка по контракту 19/15 +10. покажи банк опер по дог 19/15 пж +11. епт транзакции договор 19/15 +12. а теперь банковские операции +13. а теперь только за 2020 +14. Покажи банковские операции по договору 19/15 + +### Остаток / сальдо по счету (`account_balance_snapshot`) — 31 +1. сальдо по счету 60.01 на дату 2020-07-31 +2. остаток по счету 60.01 на 2020.05 +3. покажи остаток по 60.01 за май 2020 +4. счет 60 остаток на май 2020 +5. баланс account 60.01 as of 2020-07-31 +6. 60.01 saldo на 31.07.2020 +7. сколько на счете 60 на конец 2020-05 +8. дай остаток 60.02 на 2020-12-31 +9. епт, сальдо по счету 62.01 на 2020-12-31 +10. плс остаток по счету 51 на 2020-06-30 +11. как там по счету 60.01 на 2020.07? +12. сальдо 60 на 2020 +13. покажи сальдо по счету 60 за 2020 +14. а теперь только май 2020 +15. ок, теперь 2020.05 +16. теперь на 2020-07-31 +17. остаток по 60.01 на 2020-07-31 +18. сальдо 60.01 на 31.07.2020 +19. что на счете 60 на 2020.05 +20. дай остаток 62.01 на 2020 май +21. saldo account 60 as of 2020-12-31 +22. плиз остаток по счету 51 на 2020-06-30 +23. скока по 60.02 на конец 2020-12 +24. бля сальдо 60.01 на 2020-05-31 +25. а по счету 60.01 на ту же дату +26. а на ту же дату по 62.01 +27. и остаток по 51 на ту же дату +28. Покажи сальдо по счету 60 на 2020-12-31 +29. а на ту же дату по 62 +30. вернись на 2020-12-31 по 60 +31. а по счету 51 на ту же дату + +### Документы, формирующие остаток (`documents_forming_balance`) — 16 +1. какие документы формируют остаток по счету 60.01 на 2020-07-31 +2. из чего состоит остаток по счету 60 на 2020.05 +3. раскрой остаток 60.01 по документам на конец июля 2020 +4. доки под остатком 60.01 на 2020-07-31 +5. documents forming balance for account 60.01 as of 2020-07-31 +6. бля, какие доки формируют остаток 60.01 на 2020-05-31 +7. по счету 62.01 раскрой остаток документами на 2020-12-31 +8. из чего остаток по счету 51 на 2020-06-30 +9. из чего остаток 60.01 на 2020-07-31 +10. какие документы формируют остаток по 60.01 на 2020-07-31 +11. docs forming balance 60.01 as of 2020-07-31 +12. раскидай остаток 62.01 по докам на 2020-12-31 +13. по счету 51 раскрой остаток документами на 2020-06-30 +14. бля из чего состоит остаток по 60.01 на 2020-05-31 +15. раскрой остаток по 62.01 документами на ту же дату +16. раскрой 62 документами на ту же дату + +### Открытые позиции / долг (`open_items_by_counterparty_or_contract`) — 15 +1. есть ли открытые позиции по договору 19/15 на 2020-12-31 +2. а теперь открытые позиции по нему +3. а теперь на 2020-12-31 +4. Есть ли долг по договору 19/15 на 2020-07-31 +5. Есть ли незакрытые позиции по договору 19/15 на 2020-12-31 +6. Покажи открытые позиции по договору 19/15 +7. по дог 19/15 есть хвосты на 2020-12-31 +8. долг по дог 19/15 на 2020-12-31 есть? +9. open items by contract 19/15 as of 2020-12-31 +10. есть ли долг по договору 19/15 на 31.12.2020 +11. покажи открытые позиции по дог 19/15 на 2020-12-31 пж +12. а теперь есть ли долг по договору 19/15 на 2020-12-31 +13. Есть ли незакрытые позиции по договору 19/15 на 2020-11-30 +14. Есть ли незакрытые позиции по договору 19/15 на 2022-12-31 +15. Есть ли незакрытые позиции по договору 19/15 на 2015-12-31 + +### Незакрытые договоры (`list_open_contracts`) — 9 +1. Покажи незакрытые договоры на 2020-12-31 +2. Show open contracts as of 2020-12-31 +3. Покажи незакрытые договоры на конец 2020-12-31 +4. list open contracts for 2020-12-31 +5. покажи незакрытые договоры на 2020-12-31 пж +6. незакрытые договоры на 2020-12-31 гони +7. open contracts 2020-12-31 pls +8. show open contracts for 2020.12.31 +9. незакрытые договоры 2020-12-31 + +### Покрытие периода и активность (`period_coverage_profile`) — 10 +1. За какие годы в базе есть данные? +2. Какой год самый активный по количеству документов? +3. Какой месяц самый активный по количеству операций? +4. Покажи профиль данных по базе: диапазон лет, топ год и топ месяц активности. +5. за какие года база ваще живая? +6. какой год тут самый движовый по докам +7. какой месяц прям пик по операциям? +8. покажи по базе years/top year/top month, только без воды +9. а какой год самый активный по документам? +10. а теперь месяц-пик по операциям + +### Типы документов и разделы учета (`document_type_and_account_section_profile`) — 5 +1. Какие типы документов используются чаще всего в базе? +2. Какие разделы учета наиболее заполнены и какие почти не используются? +3. Сформируй сводку по типам документов и их доле в общем объеме. +4. каких доков у нас больше всего крутится? +5. какие разделы учета жирные, а какие мертвые? + +### Контрагенты и роли (`counterparty_population_and_roles`) — 13 +1. Сколько всего уникальных контрагентов в базе? +2. Сколько у нас заказчиков, поставщиков и смешанных контрагентов? +3. Сколько контрагентов активны как клиенты и как поставщики? +4. скок уник контрагентов вообще есть +5. разбей контров на клиентов/поставщиков/смешанных +6. а теперь разбей их на заказчиков и поставщиков +7. скока всего контрагентов в базе +8. скока поставщиков +9. скока поставщиков в базе +10. скока заказчиков в базе +11. скок поставщиков в базе +12. скок клиентов +13. скок клиентов в базе + +### Активность заказчиков по периодам (`counterparty_activity_lifecycle`) — 5 +1. Какие заказчики работали с нами в 2020 году? +2. Какие заказчики работали с нами за все время? +3. какие клиенты у нас были активны в 2020? +4. выведи активных клиентов за все время +5. а за все время? + +### Использование договорной базы (`contract_usage_overview`) — 5 +1. Сколько всего договоров заведено и сколько из них реально использовались? +2. Дай обзор договорной базы: total договоров и used договоров. +3. договоров сколько всего и сколько реально юзались? +4. чо по договорной базе: total vs used +5. и сразу сколько всего договоров и сколько used + +### Заказчики и поступления (value, top-20) (`customer_revenue_and_payments`) — 17 +1. Покажи топ-20 заказчиков по сумме поступлений за все время. +2. Покажи топ-20 заказчиков по сумме поступлений за 2020 год. +3. Покажи топ-20 заказчиков по количеству входящих платежных операций за все время. +4. Покажи топ-20 заказчиков по максимальной сумме одной входящей операции за все время. +5. Покажи топ-20 заказчиков по среднему чеку среди активных клиентов (минимум 3 входящие операции). +6. Покажи топ-20 самых крупных разовых сделок по поступлениям (дата, контрагент, документ, сумма). +7. Покажи топ-20 самых маленьких разовых сделок по поступлениям среди активных заказчиков. +8. какие клиенты самые доходные, выдай топ-20 +9. топ-20 заказчиков по деньгам за все время +10. за 20й год кто нам больше всего занес, топ-20 +11. кто платит чаще всего, дай топ-20 +12. покажи топ-20 самых жирных сделок по поступлениям +13. покажи топ-20 самых маленьких сделок по бюджету +14. Теперь только за 2020 год, тоже топ-20. +15. И отдельно покажи топ-20 по частоте входящих платежей. +16. Покажи топ-20 самых крупных разовых сделок по поступлениям. +17. А теперь топ-20 самых маленьких сделок по бюджету среди активных заказчиков. + +### Поставщики и выплаты (value, top-20) (`supplier_payouts_profile`) — 10 +1. Покажи топ-20 поставщиков по сумме выплат за все время. +2. Покажи топ-20 поставщиков по сумме выплат за 2020 год. +3. Покажи топ-20 поставщиков по количеству исходящих платежных операций за все время. +4. Покажи топ-20 самых крупных разовых выплат поставщикам (дата, контрагент, документ, сумма). +5. кому мы больше всего сгрузили денег, топ-20 поставщиков +6. топ-20 поставщиков по выплатам за все время +7. за 2020 год кому ушло больше всего денег, топ-20 +8. поставщики с максимальным числом выплат, топ-20 +9. Теперь за 2020 год, тоже топ-20. +10. И дай топ-20 поставщиков по количеству выплат. + +### Договоры и суммы (value, top-20) (`contract_usage_and_value`) — 4 +1. Покажи топ-20 договоров по сумме оборота за все время. +2. Покажи топ-20 договоров с минимальным бюджетом среди активных договоров. +3. договоры по обороту ранкни и дай топ-20 +4. покажи топ-20 договоров с самым мелким бюджетом, но только активные + +## FOLLOW-UP CHAINS (из `address_followup_context_chains_2026-04-02.json`) +Важно: вопросы внутри каждой цепочки гонять в одной сессии подряд. + +### session: `chain_docs_svk_a` +1. Покажи документы по СВК за 2020 +2. а теперь только за май 2020 +3. а по счету 60.01 на ту же дату +4. а на ту же дату по 62.01 +5. раскрой остаток по 62.01 документами на ту же дату + +### session: `chain_bank_svk_b` +1. Покажи банковские операции по СВК за 2020 +2. а теперь только ноябрь 2020 +3. а теперь документы по нему +4. и остаток по 51 на ту же дату +5. а теперь только май 2020 + +### session: `chain_balance_c` +1. Покажи сальдо по счету 60 на 2020-12-31 +2. а теперь только май 2020 +3. а на ту же дату по 62 +4. раскрой 62 документами на ту же дату +5. вернись на 2020-12-31 по 60 + +### session: `chain_contract_d` +1. Покажи документы по договору 19/15 за 2020 +2. а теперь банковские операции +3. а теперь только за 2020 +4. есть ли открытые позиции по договору 19/15 на 2020-12-31 +5. покажи документы по этому же договору + +### session: `chain_contract_e` +1. Покажи банковские операции по договору 19/15 +2. а теперь открытые позиции по нему +3. а теперь на 2020-12-31 +4. а по счету 51 на ту же дату +5. а документы по этому же договору за тот же период + + diff --git a/docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json b/docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json new file mode 100644 index 0000000..0839dcb --- /dev/null +++ b/docs/ADDRESS/question_sets/domain_general_batch2_lifecycle_acceptance_2026-04-08_phaseA.json @@ -0,0 +1,266 @@ +[ + { + "id": "B2_C001", + "group": "canonical", + "text": "Какие заказчики работали с нами в 2020 году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C002", + "group": "canonical", + "text": "Какие заказчики работали с нами за все время?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C003", + "group": "canonical", + "text": "Какие заказчики появились у нас впервые в 2020 году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C004", + "group": "canonical", + "text": "Какие заказчики были активны в 2020 году, но дальше перестали встречаться?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C005", + "group": "canonical", + "text": "Какие контрагенты работали с нами только один раз за все время?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C006", + "group": "canonical", + "text": "Какие контрагенты работают с нами дольше всего по периоду активности?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C007", + "group": "canonical", + "text": "Раздели поставщиков на регулярных и эпизодических по частоте операций.", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C008", + "group": "canonical", + "text": "Какие поставщики давно не использовались в операционной активности?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C009", + "group": "canonical", + "text": "Какие договоры давно не использовались (stale contracts)?", + "expected_intent": "contract_usage_overview", + "expected_mode": "address_query" + }, + { + "id": "B2_C010", + "group": "canonical", + "text": "Покажи контрагентов с несколькими договорами и укажи, какие договоры у них активны.", + "expected_intent": "contract_usage_and_value", + "expected_mode": "address_query" + }, + { + "id": "B2_C011", + "group": "canonical", + "text": "Какие заказчики работали с нами в 2021 году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_C012", + "group": "canonical", + "text": "Какие заказчики впервые появились у нас в 2021 году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N001", + "group": "noisy_slang", + "text": "какие клиенты были активны в 20м", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N002", + "group": "noisy_slang", + "text": "выведи всех заков за все время", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N003", + "group": "noisy_slang", + "text": "кто к нам пришел впервые в 2020", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N004", + "group": "noisy_slang", + "text": "кто был активен в 2020 и потом отвалился", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N005", + "group": "noisy_slang", + "text": "кто с нами был ровно один раз и пропал", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N006", + "group": "noisy_slang", + "text": "самые старые по сотрудничеству кто", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N007", + "group": "noisy_slang", + "text": "разбей поставщиков на регуляр и разовые", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N008", + "group": "noisy_slang", + "text": "какие поставщики давно мертвые", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N009", + "group": "noisy_slang", + "text": "какие договоры уже протухли и не двигаются", + "expected_intent": "contract_usage_overview", + "expected_mode": "address_query" + }, + { + "id": "B2_N010", + "group": "noisy_slang", + "text": "у кого несколько договоров и какие из них рабочие", + "expected_intent": "contract_usage_and_value", + "expected_mode": "address_query" + }, + { + "id": "B2_N011", + "group": "noisy_slang", + "text": "новые клиенты за 21й год", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_N012", + "group": "noisy_slang", + "text": "клиенты которые исчезли после 21 года", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F001", + "group": "followup_chain", + "session": "b2_customer_lifecycle_chain", + "text": "Какие заказчики работали с нами в 2020 году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F002", + "group": "followup_chain", + "session": "b2_customer_lifecycle_chain", + "text": "А кто из них новые?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F003", + "group": "followup_chain", + "session": "b2_customer_lifecycle_chain", + "text": "А кто потом исчез?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F004", + "group": "followup_chain", + "session": "b2_customer_lifecycle_chain", + "text": "И кто был только один раз?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F005", + "group": "followup_chain", + "session": "b2_supplier_lifecycle_chain", + "text": "Раздели поставщиков на регулярных и эпизодических.", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F006", + "group": "followup_chain", + "session": "b2_supplier_lifecycle_chain", + "text": "Теперь покажи тех, кто давно не использовался.", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F007", + "group": "followup_chain", + "session": "b2_contract_lifecycle_chain", + "text": "Какие договоры давно не использовались?", + "expected_intent": "contract_usage_overview", + "expected_mode": "address_query" + }, + { + "id": "B2_F008", + "group": "followup_chain", + "session": "b2_contract_lifecycle_chain", + "text": "Теперь покажи контрагентов с несколькими договорами.", + "expected_intent": "contract_usage_and_value", + "expected_mode": "address_query" + }, + { + "id": "B2_F009", + "group": "followup_chain", + "session": "b2_contract_lifecycle_chain", + "text": "И у них какие договоры сейчас активны?", + "expected_intent": "contract_usage_and_value", + "expected_mode": "address_query" + }, + { + "id": "B2_F010", + "group": "followup_chain", + "session": "b2_year_switch_chain", + "text": "Какие заказчики работали с нами в 2020 году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F011", + "group": "followup_chain", + "session": "b2_year_switch_chain", + "text": "Теперь за 2021.", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + }, + { + "id": "B2_F012", + "group": "followup_chain", + "session": "b2_year_switch_chain", + "text": "А кто новые в этом году?", + "expected_intent": "counterparty_activity_lifecycle", + "expected_mode": "address_query" + } +] diff --git a/docs/GRAPHIFY_CODEX_SETUP.md b/docs/GRAPHIFY_CODEX_SETUP.md new file mode 100644 index 0000000..f5de739 --- /dev/null +++ b/docs/GRAPHIFY_CODEX_SETUP.md @@ -0,0 +1,51 @@ +# Graphify for Codex in `NDC_1C` + +This repository now has project-level setup for `graphify` (knowledge graph workflow for Codex). + +## What was added + +- `AGENTS.md` with graph-first Codex rules. +- `.graphifyignore` tuned for this repository. +- `.gitignore` rules for graphify temporary/build outputs. +- `scripts/setup_graphify_codex.ps1` bootstrap script. + +## One-command setup (Windows) + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\setup_graphify_codex.ps1 +``` + +Options: + +```powershell +# do not reinstall pip package +powershell -ExecutionPolicy Bypass -File .\scripts\setup_graphify_codex.ps1 -SkipPipInstall + +# also install graphify git hooks (post-commit/post-checkout) +powershell -ExecutionPolicy Bypass -File .\scripts\setup_graphify_codex.ps1 -InstallGitHooks +``` + +## Codex usage + +In Codex chat (skill trigger uses `$`): + +```text +$graphify . +$graphify . --update +$graphify query "what connects routing to runtime?" +``` + +Main outputs are created in `graphify-out/`: + +- `graphify-out/graph.json` +- `graphify-out/GRAPH_REPORT.md` +- `graphify-out/graph.html` + +## Important Codex config + +For parallel semantic extraction, make sure `~/.codex/config.toml` contains: + +```toml +[features] +multi_agent = true +``` diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index 8781f97..0581f89 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -236,12 +236,27 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [ "какие заказчики активны", "какие клиенты работали", "какие клиенты активны", + "какие контрагенты работали", + "какие поставщики работали", "список заказчиков", "список клиентов", "список заказчиков за все время", "список клиентов за все время", "список активных заказчиков", "список активных клиентов", + "новые заказчики", + "новые клиенты", + "новые контрагенты", + "впервые в", + "кто исчез", + "кто ушел", + "кто ушёл", + "только один раз", + "дольше всех", + "долгоживущие контрагенты", + "регулярные поставщики", + "эпизодические поставщики", + "давно не использовались поставщики", "active customers", "customer activity list", "counterparty lifecycle" @@ -255,6 +270,12 @@ const CONTRACT_USAGE_OVERVIEW_HINTS = [ "договоры total vs used", "обзор договорной базы", "договорная база total used", + "неиспользуемые договоры", + "давно не использовались договоры", + "мертвые договоры", + "мёртвые договоры", + "stale contracts", + "unused contracts", "contracts total used", "contract usage overview" ]; @@ -296,6 +317,11 @@ const CONTRACT_USAGE_AND_VALUE_HINTS = [ "договоры с минимальным бюджетом", "договоры с самым маленьким бюджетом", "активные договоры по бюджету", + "контрагенты с несколькими договорами", + "несколько договоров у контрагента", + "мультидоговорные контрагенты", + "какие договоры активны", + "рабочие договоры", "contracts by turnover", "contracts by budget" ]; @@ -506,6 +532,9 @@ function hasDocumentTypeAndAccountSectionProfileSignal(text) { return false; } function hasCounterpartyPopulationAndRolesSignal(text) { + if (hasLifecycleSegmentationSignal(text)) { + return false; + } if (hasAny(text, COUNTERPARTY_POPULATION_AND_ROLES_HINTS)) { return true; } @@ -520,6 +549,9 @@ function hasCounterpartyPopulationAndRolesSignal(text) { } return false; } +function hasLifecycleSegmentationSignal(text) { + return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|только\s+один\s+раз|однораз|дольше\s+всех|долгожив|регулярн|эпизодич|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text); +} function hasCounterpartyActivityLifecycleSignal(text) { if (hasDocumentSignal(text) || hasBankOperationSignal(text)) { return false; @@ -530,19 +562,22 @@ function hasCounterpartyActivityLifecycleSignal(text) { if (/(?:сколько|скока|скок)\s+/iu.test(text)) { return false; } - const hasCustomerLexeme = /(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?|customer(?:s)?|client(?:s)?)/iu.test(text); - const hasActivityLexeme = /(?:работал(?:и)?|активн(?:ые|ых|а|о)?|сотрудничал(?:и)?|были\s+в\s+работе|active)/iu.test(text); + const hasCounterpartyLexeme = /(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?|контрагент(?:ов|а|ы)?|поставщик(?:ов|а|и)?|customer(?:s)?|client(?:s)?|counterpart(?:y|ies)|supplier(?:s)?|vendor(?:s)?)/iu.test(text); + const hasActivityLexeme = /(?:работал(?:и)?|активн(?:ые|ых|а|о)?|сотрудничал(?:и)?|были\s+в\s+работе|active|использ(?:овал(?:и|ось)?|уются|ован(?:ы|о)?))/iu.test(text); const hasTimeWindowLexeme = /(?:за\s+вс[её]\s+время|all\s+time|\b(?:19|20)\d{2}\b|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|в\s+конкретн(?:ом|ый)\s+год|за\s+год|в\s+году)/iu.test(text); const hasListVerb = /(?:какие|кто|покажи|выведи|список|list|show)/iu.test(text); const hasRosterQualifier = /(?:у\s+нас|вообще|в\s+баз[еы]|какие\s+есть|кто\s+есть|who\s+are)/iu.test(text); - const hasListWithWindow = hasCustomerLexeme && hasListVerb && hasTimeWindowLexeme; + const hasListWithWindow = hasCounterpartyLexeme && hasListVerb && hasTimeWindowLexeme; if (hasListWithWindow) { return true; } - if (hasCustomerLexeme && hasListVerb && hasRosterQualifier) { + if (hasCounterpartyLexeme && hasListVerb && hasRosterQualifier) { return true; } - return hasCustomerLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb); + if (hasCounterpartyLexeme && hasLifecycleSegmentationSignal(text)) { + return true; + } + return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb); } function hasContractUsageOverviewSignal(text) { if (hasAny(text, CONTRACT_USAGE_OVERVIEW_HINTS)) { @@ -557,6 +592,9 @@ function hasContractUsageOverviewSignal(text) { if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:договор|contract)?/iu.test(text)) { return true; } + if (/(?:какие\s+договор(?:ы|а)?).*(?:давно\s+не\s+использ|неиспольз|протух|мертв|мёртв|stale|unused)/iu.test(text)) { + return true; + } return false; } function hasCustomerRevenueAndPaymentsSignal(text) { @@ -636,9 +674,10 @@ function hasContractUsageAndValueSignal(text) { if (hasContractUsageOverviewSignal(text)) { return false; } + const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+договор|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+договор|какие\s+договор(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+договор)/iu.test(text); const asksValue = /(?:оборот|бюджет|сумм|стоим|value|turnover|amount|revenue|крупн|мелк|миним|максим)/iu.test(text); const asksRank = /(?:топ|top|ранк|rank|сам(?:ый|ая|ое|ые))/iu.test(text); - return asksValue || asksRank; + return asksStructure || asksValue || asksRank; } function hasContractListByCounterpartySignal(text) { const hasContractLexeme = /(?:договор(?:а|у|ом|е|ы)?|contracts?|contract)/iu.test(text); diff --git a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js index e6556f7..d3fb269 100644 --- a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js +++ b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js @@ -22,7 +22,8 @@ const BANK_DOCS_QUERY_TEMPLATE = ` "" КАК СчетДт, "" КАК СчетКт, БанкСписание.СуммаДокумента КАК Сумма, - ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент + ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, + ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор ИЗ Документ.СписаниеСРасчетногоСчета КАК БанкСписание __WHERE_OUT__ @@ -33,7 +34,8 @@ __WHERE_OUT__ "" КАК СчетДт, "" КАК СчетКт, БанкПоступление.СуммаДокумента КАК Сумма, - ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент + ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор ИЗ Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление __WHERE_IN__ @@ -460,7 +462,8 @@ const BASE_RECIPES = [ optional_filters: ["as_of_date", "counterparty", "contract", "limit"], default_limit: 96, account_scope: ["60", "62", "76"], - account_scope_mode: "preferred" + account_scope_mode: "preferred", + query_template: "bank_docs" }, { recipe_id: "address_documents_by_counterparty_v1", diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 3e73be6..6e98995 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -2771,6 +2771,41 @@ function isLowQualityPredecomposeCounterpartyAnchor(value) { }); return meaningful.length === 0; } +function normalizePredecomposeCounterpartyAnchorTokensForMatch(value) { + return normalizePredecomposeAnchorTokens(value).filter((token) => { + if (token.length < 2) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(token)) { + return false; + } + return !ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS.has(token); + }); +} +function hasCounterpartyAnchorSubstitution(sourceValue, candidateValue) { + const sourceNormalized = String(sourceValue ?? "").trim().toLowerCase().replace(/ё/g, "е"); + const candidateNormalized = String(candidateValue ?? "").trim().toLowerCase().replace(/ё/g, "е"); + if (!sourceNormalized || !candidateNormalized) { + return false; + } + if (sourceNormalized === candidateNormalized) { + return false; + } + if (sourceNormalized.includes(candidateNormalized) || candidateNormalized.includes(sourceNormalized)) { + return false; + } + const sourceTokens = new Set(normalizePredecomposeCounterpartyAnchorTokensForMatch(sourceNormalized)); + const candidateTokens = normalizePredecomposeCounterpartyAnchorTokensForMatch(candidateNormalized); + if (sourceTokens.size === 0 || candidateTokens.length === 0) { + return false; + } + for (const token of candidateTokens) { + if (sourceTokens.has(token)) { + return false; + } + } + return true; +} function isLowQualityPredecomposeContractAnchor(value) { const normalized = String(value ?? "").trim().toLowerCase().replace(/ё/g, "е"); if (!normalized) { @@ -3003,6 +3038,27 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage sanitizedUserMessage }, userMessage); } + const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety && + sourceAnchorQuality.anchorType === "counterparty" && + candidateAnchorQuality.anchorType === "counterparty" && + sourceAnchorQuality.quality >= 2 && + candidateAnchorQuality.quality >= 2 && + Boolean(sourceAnchorQuality.anchorValue) && + Boolean(candidateAnchorQuality.anchorValue) && + hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? ""); + if (counterpartyAnchorSubstitutedByCandidate) { + return attachAddressPredecomposeContract({ + ...baseMeta, + attempted: true, + applied: false, + traceId: normalized?.trace_id ?? null, + llmCanonicalCandidateDetected: true, + effectiveMessage: userMessage, + reason: "normalized_fragment_rejected_anchor_substitution", + fallbackRuleHit: null, + sanitizedUserMessage + }, userMessage); + } if (fallbackCandidate) { const fallbackAnchorQuality = evaluateAddressAnchorQuality(String(fallbackCandidate.candidate ?? "")); const fallbackPreferredForAnchorSafety = sameIntentForAnchorSafety && diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 836cec1..3077601 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -247,12 +247,27 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [ "какие заказчики активны", "какие клиенты работали", "какие клиенты активны", + "какие контрагенты работали", + "какие поставщики работали", "список заказчиков", "список клиентов", "список заказчиков за все время", "список клиентов за все время", "список активных заказчиков", "список активных клиентов", + "новые заказчики", + "новые клиенты", + "новые контрагенты", + "впервые в", + "кто исчез", + "кто ушел", + "кто ушёл", + "только один раз", + "дольше всех", + "долгоживущие контрагенты", + "регулярные поставщики", + "эпизодические поставщики", + "давно не использовались поставщики", "active customers", "customer activity list", "counterparty lifecycle" @@ -267,6 +282,12 @@ const CONTRACT_USAGE_OVERVIEW_HINTS = [ "договоры total vs used", "обзор договорной базы", "договорная база total used", + "неиспользуемые договоры", + "давно не использовались договоры", + "мертвые договоры", + "мёртвые договоры", + "stale contracts", + "unused contracts", "contracts total used", "contract usage overview" ]; @@ -311,6 +332,11 @@ const CONTRACT_USAGE_AND_VALUE_HINTS = [ "договоры с минимальным бюджетом", "договоры с самым маленьким бюджетом", "активные договоры по бюджету", + "контрагенты с несколькими договорами", + "несколько договоров у контрагента", + "мультидоговорные контрагенты", + "какие договоры активны", + "рабочие договоры", "contracts by turnover", "contracts by budget" ]; @@ -552,6 +578,9 @@ function hasDocumentTypeAndAccountSectionProfileSignal(text: string): boolean { } function hasCounterpartyPopulationAndRolesSignal(text: string): boolean { + if (hasLifecycleSegmentationSignal(text)) { + return false; + } if (hasAny(text, COUNTERPARTY_POPULATION_AND_ROLES_HINTS)) { return true; } @@ -575,6 +604,12 @@ function hasCounterpartyPopulationAndRolesSignal(text: string): boolean { return false; } +function hasLifecycleSegmentationSignal(text: string): boolean { + return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|только\s+один\s+раз|однораз|дольше\s+всех|долгожив|регулярн|эпизодич|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test( + text + ); +} + function hasCounterpartyActivityLifecycleSignal(text: string): boolean { if (hasDocumentSignal(text) || hasBankOperationSignal(text)) { return false; @@ -585,25 +620,31 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean { if (/(?:сколько|скока|скок)\s+/iu.test(text)) { return false; } - const hasCustomerLexeme = /(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?|customer(?:s)?|client(?:s)?)/iu.test( + const hasCounterpartyLexeme = /(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?|контрагент(?:ов|а|ы)?|поставщик(?:ов|а|и)?|customer(?:s)?|client(?:s)?|counterpart(?:y|ies)|supplier(?:s)?|vendor(?:s)?)/iu.test( text ); - const hasActivityLexeme = /(?:работал(?:и)?|активн(?:ые|ых|а|о)?|сотрудничал(?:и)?|были\s+в\s+работе|active)/iu.test(text); + const hasActivityLexeme = + /(?:работал(?:и)?|активн(?:ые|ых|а|о)?|сотрудничал(?:и)?|были\s+в\s+работе|active|использ(?:овал(?:и|ось)?|уются|ован(?:ы|о)?))/iu.test( + text + ); const hasTimeWindowLexeme = /(?:за\s+вс[её]\s+время|all\s+time|\b(?:19|20)\d{2}\b|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|в\s+конкретн(?:ом|ый)\s+год|за\s+год|в\s+году)/iu.test( text ); const hasListVerb = /(?:какие|кто|покажи|выведи|список|list|show)/iu.test(text); const hasRosterQualifier = /(?:у\s+нас|вообще|в\s+баз[еы]|какие\s+есть|кто\s+есть|who\s+are)/iu.test(text); - const hasListWithWindow = hasCustomerLexeme && hasListVerb && hasTimeWindowLexeme; + const hasListWithWindow = hasCounterpartyLexeme && hasListVerb && hasTimeWindowLexeme; if (hasListWithWindow) { return true; } - if (hasCustomerLexeme && hasListVerb && hasRosterQualifier) { + if (hasCounterpartyLexeme && hasListVerb && hasRosterQualifier) { + return true; + } + if (hasCounterpartyLexeme && hasLifecycleSegmentationSignal(text)) { return true; } - return hasCustomerLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb); + return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb); } function hasContractUsageOverviewSignal(text: string): boolean { @@ -625,6 +666,9 @@ function hasContractUsageOverviewSignal(text: string): boolean { if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:договор|contract)?/iu.test(text)) { return true; } + if (/(?:какие\s+договор(?:ы|а)?).*(?:давно\s+не\s+использ|неиспольз|протух|мертв|мёртв|stale|unused)/iu.test(text)) { + return true; + } return false; } @@ -724,10 +768,13 @@ function hasContractUsageAndValueSignal(text: string): boolean { if (hasContractUsageOverviewSignal(text)) { return false; } + const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+договор|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+договор|какие\s+договор(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+договор)/iu.test( + text + ); const asksValue = /(?:оборот|бюджет|сумм|стоим|value|turnover|amount|revenue|крупн|мелк|миним|максим)/iu.test(text); const asksRank = /(?:топ|top|ранк|rank|сам(?:ый|ая|ое|ые))/iu.test(text); - return asksValue || asksRank; + return asksStructure || asksValue || asksRank; } function hasContractListByCounterpartySignal(text: string): boolean { diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index 9e7bbb0..f22eed7 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -26,7 +26,8 @@ const BANK_DOCS_QUERY_TEMPLATE = ` "" КАК СчетДт, "" КАК СчетКт, БанкСписание.СуммаДокумента КАК Сумма, - ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент + ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, + ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор ИЗ Документ.СписаниеСРасчетногоСчета КАК БанкСписание __WHERE_OUT__ @@ -37,7 +38,8 @@ __WHERE_OUT__ "" КАК СчетДт, "" КАК СчетКт, БанкПоступление.СуммаДокумента КАК Сумма, - ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент + ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, + ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор ИЗ Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление __WHERE_IN__ @@ -474,7 +476,8 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [ optional_filters: ["as_of_date", "counterparty", "contract", "limit"], default_limit: 96, account_scope: ["60", "62", "76"], - account_scope_mode: "preferred" + account_scope_mode: "preferred", + query_template: "bank_docs" }, { recipe_id: "address_documents_by_counterparty_v1", diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index d564bd1..060d9e5 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -2732,6 +2732,41 @@ function isLowQualityPredecomposeCounterpartyAnchor(value) { }); return meaningful.length === 0; } +function normalizePredecomposeCounterpartyAnchorTokensForMatch(value) { + return normalizePredecomposeAnchorTokens(value).filter((token) => { + if (token.length < 2) { + return false; + } + if (/^(?:19|20)\d{2}$/.test(token)) { + return false; + } + return !ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS.has(token); + }); +} +function hasCounterpartyAnchorSubstitution(sourceValue, candidateValue) { + const sourceNormalized = String(sourceValue ?? "").trim().toLowerCase().replace(/ё/g, "е"); + const candidateNormalized = String(candidateValue ?? "").trim().toLowerCase().replace(/ё/g, "е"); + if (!sourceNormalized || !candidateNormalized) { + return false; + } + if (sourceNormalized === candidateNormalized) { + return false; + } + if (sourceNormalized.includes(candidateNormalized) || candidateNormalized.includes(sourceNormalized)) { + return false; + } + const sourceTokens = new Set(normalizePredecomposeCounterpartyAnchorTokensForMatch(sourceNormalized)); + const candidateTokens = normalizePredecomposeCounterpartyAnchorTokensForMatch(candidateNormalized); + if (sourceTokens.size === 0 || candidateTokens.length === 0) { + return false; + } + for (const token of candidateTokens) { + if (sourceTokens.has(token)) { + return false; + } + } + return true; +} function isLowQualityPredecomposeContractAnchor(value) { const normalized = String(value ?? "").trim().toLowerCase().replace(/ё/g, "е"); if (!normalized) { @@ -2964,6 +2999,27 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage sanitizedUserMessage }, userMessage); } + const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety && + sourceAnchorQuality.anchorType === "counterparty" && + candidateAnchorQuality.anchorType === "counterparty" && + sourceAnchorQuality.quality >= 2 && + candidateAnchorQuality.quality >= 2 && + Boolean(sourceAnchorQuality.anchorValue) && + Boolean(candidateAnchorQuality.anchorValue) && + hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? ""); + if (counterpartyAnchorSubstitutedByCandidate) { + return attachAddressPredecomposeContract({ + ...baseMeta, + attempted: true, + applied: false, + traceId: normalized?.trace_id ?? null, + llmCanonicalCandidateDetected: true, + effectiveMessage: userMessage, + reason: "normalized_fragment_rejected_anchor_substitution", + fallbackRuleHit: null, + sanitizedUserMessage + }, userMessage); + } if (fallbackCandidate) { const fallbackAnchorQuality = evaluateAddressAnchorQuality(String(fallbackCandidate.candidate ?? "")); const fallbackPreferredForAnchorSafety = sameIntentForAnchorSafety && diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index c9ec808..9e5d830 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -1383,11 +1383,36 @@ describe("address intent resolver expansion (M2.3a)", () => { expect(result.intent).toBe("counterparty_activity_lifecycle"); }); + it("resolves counterparty lifecycle intent for one-time counterparties wording", () => { + const result = resolveAddressIntent("Какие контрагенты работали с нами только один раз за все время?"); + expect(result.intent).toBe("counterparty_activity_lifecycle"); + }); + + it("resolves counterparty lifecycle intent for longest-running counterparties wording", () => { + const result = resolveAddressIntent("Какие контрагенты работают с нами дольше всех?"); + expect(result.intent).toBe("counterparty_activity_lifecycle"); + }); + + it("resolves supplier lifecycle segmentation wording into lifecycle intent", () => { + const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по активности."); + expect(result.intent).toBe("counterparty_activity_lifecycle"); + }); + + it("resolves stale suppliers wording into lifecycle intent", () => { + const result = resolveAddressIntent("Какие поставщики давно не использовались?"); + expect(result.intent).toBe("counterparty_activity_lifecycle"); + }); + it("resolves contract usage overview intent", () => { const result = resolveAddressIntent("Сколько всего договоров заведено и сколько из них реально использовались?"); expect(result.intent).toBe("contract_usage_overview"); }); + it("resolves stale contracts wording into contract usage overview intent", () => { + const result = resolveAddressIntent("Какие договоры давно не использовались?"); + expect(result.intent).toBe("contract_usage_overview"); + }); + it("resolves customer revenue/payout ranking intent", () => { const result = resolveAddressIntent("какие клиенты самые доходные, выдай топ-20"); expect(result.intent).toBe("customer_revenue_and_payments"); @@ -1418,6 +1443,11 @@ describe("address intent resolver expansion (M2.3a)", () => { expect(result.intent).toBe("contract_usage_and_value"); }); + it("resolves multi-contract counterparties wording into contract usage and value intent", () => { + const result = resolveAddressIntent("Покажи контрагентов с несколькими договорами и какие из договоров активны."); + expect(result.intent).toBe("contract_usage_and_value"); + }); + it("resolves contracts-by-counterparty intent from list wording", () => { const result = resolveAddressIntent("покажи договора все по жуковке 51"); expect(result.intent).toBe("list_contracts_by_counterparty"); @@ -2017,6 +2047,36 @@ describe("address query limited taxonomy and stage diagnostics", () => { expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); + it("routes one-time counterparties wording into lifecycle aggregate recipe", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("Какие контрагенты работали с нами только один раз за все время?"); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle"); + expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1"); + expect(result?.debug.mcp_call_status).not.toBe("skipped"); + expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); + }); + + it("routes stale contracts wording into contract usage overview recipe", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("Какие договоры давно не использовались?"); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_intent).toBe("contract_usage_overview"); + expect(result?.debug.selected_recipe).toBe("address_contract_usage_overview_v1"); + expect(result?.debug.mcp_call_status).not.toBe("skipped"); + expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); + }); + + it("routes multi-contract counterparties wording into contract usage and value recipe", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("Покажи контрагентов с несколькими договорами и какие из договоров активны."); + expect(result?.handled).toBe(true); + expect(result?.debug.detected_intent).toBe("contract_usage_and_value"); + expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1"); + expect(result?.debug.mcp_call_status).not.toBe("skipped"); + expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); + }); + it("returns missing_anchor for open items without concrete counterparty/contract anchor", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("show open items by contract"); @@ -2510,6 +2570,22 @@ describe("address recipe catalog counterparty filtering", () => { expect(plan.limit).toBe(1000); }); + it("uses bank-doc profile with contract projection for open-items anchor matching", () => { + const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", { + counterparty: "СВК", + as_of_date: "2020-12-31" + }); + expect(selected.selected_recipe).toBeTruthy(); + const plan = buildAddressRecipePlan(selected.selected_recipe!, { + counterparty: "СВК", + as_of_date: "2020-12-31" + }); + + expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета"); + expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет"); + expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор"); + }); + it("allows extended limit for open-contracts intent", () => { const selected = selectAddressRecipe("list_open_contracts", { as_of_date: "2020-12-31", diff --git a/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts b/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts index 4f08dd1..2c842aa 100644 --- a/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressLlmPredecompose.test.ts @@ -348,6 +348,100 @@ describe("assistant address llm pre-decompose candidate preference", () => { expect(response.debug?.llm_decomposition_reason).not.toBe("normalized_fragment_applied"); }); + it("rejects llm fragment when counterparty anchor is substituted by unrelated noun", async () => { + const calls: Array<{ message: string }> = []; + const addressQueryService = { + tryHandle: vi.fn(async (message: string) => { + calls.push({ message }); + return buildAddressLaneResult(message); + }) + } as any; + + const normalizerService = { + normalize: vi.fn(async () => ({ + trace_id: "norm-predecompose-anchor-substitution", + ok: true, + normalized: { + schema_version: "normalized_query_v2_0_2", + user_message_raw: "свк доки за 20й", + message_in_scope: true, + scope_confidence: "medium", + contains_multiple_tasks: false, + fragments: [ + { + fragment_id: "F1", + raw_fragment_text: "свк доки за 20й", + normalized_fragment_text: "сканирование документов за 20-й период", + domain_relevance: "in_scope", + business_scope: "company_specific_accounting", + entity_hints: [], + account_hints: [], + document_hints: ["документы"], + register_hints: [], + time_scope: { + type: "explicit", + value: "2020", + confidence: "high" + }, + flags: { + has_multi_entity_scope: false, + asks_for_chain_explanation: false, + asks_for_ranking_or_top: false, + asks_for_period_summary: false, + asks_for_rule_check: false, + asks_for_anomaly_scan: false, + asks_for_exact_object_trace: false, + asks_for_evidence: false, + mentions_period_close_context: false + }, + candidate_labels: ["simple_factual"], + confidence: "medium", + execution_readiness: "executable", + clarification_reason: null, + soft_assumption_used: [], + route_status: "routed", + no_route_reason: null + } + ], + discarded_fragments: [], + global_notes: { + needs_clarification: false, + clarification_reason: null + } + }, + raw_model_output: null, + validation: { passed: true, errors: [] }, + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + latency_ms: 10, + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + request_count_for_case: 1 + })) + } as any; + + const sessions = new AssistantSessionStore(); + const service = new AssistantService( + normalizerService, + sessions as any, + {} as any, + { persistSession: vi.fn() } as any, + addressQueryService + ); + + const response = await service.handleMessage({ + session_id: `asst-predecompose-anchor-substitution-${Date.now()}`, + user_message: "свк доки за 20й", + llmProvider: "local", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(calls).toHaveLength(1); + expect(calls[0].message.toLowerCase()).toContain("свк"); + expect(calls[0].message.toLowerCase()).not.toContain("сканирование"); + expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_anchor_substitution"); + }); + it("rejects follow-up intent injection when llm adds documents to same-date account prompt", async () => { const calls: Array<{ message: string }> = []; const addressQueryService = { diff --git a/scripts/setup_graphify_codex.ps1 b/scripts/setup_graphify_codex.ps1 new file mode 100644 index 0000000..7f73749 --- /dev/null +++ b/scripts/setup_graphify_codex.ps1 @@ -0,0 +1,26 @@ +param( + [switch]$SkipPipInstall, + [switch]$InstallGitHooks +) + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +Set-Location $repoRoot + +if (-not $SkipPipInstall) { + py -m pip install --user graphifyy +} + +py -m graphify install --platform codex +py -m graphify codex install + +if ($InstallGitHooks) { + py -m graphify hook install +} + +Write-Host "" +Write-Host "Graphify configured for Codex in $repoRoot" +Write-Host "Use these commands inside Codex:" +Write-Host " `$graphify ." +Write-Host " `$graphify . --update"