From 93ad18daa33b0e39bbdc34a5479d9687a5773c78 Mon Sep 17 00:00:00 2001 From: dctouch Date: Wed, 15 Apr 2026 22:53:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20Commi?= =?UTF-8?q?t=20title:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D0=BE=D0=B2=20=D0=B8=20capability-=D0=B4?= =?UTF-8?q?=D0=B5=D0=BA=D0=BB=D0=B0=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../01 - project_architecture_baseline_map.md | 256 +++++ .../02 - state_and_transition_contracts.md | 393 +++++++ .../03 - capability_contract_spec.md | 253 +++++ .../04 - coverage_evidence_truth_gate.md | 196 ++++ .../05 - assistantService_extraction_map.md | 213 ++++ .../06 - phase_acceptance_matrix.md | 238 +++++ .../07 - external_reference_appendix.md | 135 +++ .../11 - architecture_turnaround/README.md | 70 ++ ...re_and_reference_update_plan_2026-04-15.md | 992 ++++++++++++++++++ .../dist/services/addressFilterExtractor.js | 67 ++ .../dist/services/addressIntentResolver.js | 8 + .../dist/services/addressQueryService.js | 8 +- .../address_runtime/decomposeStage.js | 67 +- .../assistantLivingChatRuntimeAdapter.js | 91 ++ .../assistantRuntimeContractRegistry.js | 286 +++++ .../backend/dist/services/assistantService.js | 130 ++- .../services/inventoryLifecycleCueHelpers.js | 9 +- .../dist/types/assistantRuntimeContracts.js | 4 + .../src/services/addressFilterExtractor.ts | 70 ++ .../src/services/addressIntentResolver.ts | 11 + .../src/services/addressQueryService.ts | 9 +- .../address_runtime/decomposeStage.ts | 43 +- .../assistantRuntimeContractRegistry.ts | 306 ++++++ .../backend/src/services/assistantService.ts | 8 + .../services/inventoryLifecycleCueHelpers.ts | 4 +- .../src/types/assistantRuntimeContracts.ts | 154 +++ .../addressInventoryWarehouseAnchor.test.ts | 17 + .../tests/addressQueryRuntimeM23.test.ts | 5 + .../tests/assistantLivingChatMode.test.ts | 61 ++ .../assistantRuntimeContractRegistry.test.ts | 84 ++ 30 files changed, 4175 insertions(+), 13 deletions(-) create mode 100644 docs/ARCH/11 - architecture_turnaround/01 - project_architecture_baseline_map.md create mode 100644 docs/ARCH/11 - architecture_turnaround/02 - state_and_transition_contracts.md create mode 100644 docs/ARCH/11 - architecture_turnaround/03 - capability_contract_spec.md create mode 100644 docs/ARCH/11 - architecture_turnaround/04 - coverage_evidence_truth_gate.md create mode 100644 docs/ARCH/11 - architecture_turnaround/05 - assistantService_extraction_map.md create mode 100644 docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md create mode 100644 docs/ARCH/11 - architecture_turnaround/07 - external_reference_appendix.md create mode 100644 docs/ARCH/11 - architecture_turnaround/README.md create mode 100644 docs/ARCH/11 - architecture_turnaround/unified_project_architecture_and_reference_update_plan_2026-04-15.md create mode 100644 llm_normalizer/backend/dist/services/assistantRuntimeContractRegistry.js create mode 100644 llm_normalizer/backend/dist/types/assistantRuntimeContracts.js create mode 100644 llm_normalizer/backend/src/services/assistantRuntimeContractRegistry.ts create mode 100644 llm_normalizer/backend/src/types/assistantRuntimeContracts.ts create mode 100644 llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts diff --git a/docs/ARCH/11 - architecture_turnaround/01 - project_architecture_baseline_map.md b/docs/ARCH/11 - architecture_turnaround/01 - project_architecture_baseline_map.md new file mode 100644 index 0000000..2dd492f --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/01 - project_architecture_baseline_map.md @@ -0,0 +1,256 @@ +# 01 - Project Architecture Baseline Map + +## Purpose + +This note is the compact execution-oriented map of the current project. + +It is not a market review and not a historical report. It answers: + +- what the main subsystems are; +- where the assistant runtime really starts and ends; +- what already acts as architecture, not as incidental code; +- where the main structural debt sits. + +## System Map + +### 1. 1C Acquisition And Probe Layer + +Main areas: + +- `odata_probe/` +- `scripts/*` probe and verification scripts + +Role: + +- verify read-only access viability; +- inspect published 1C entity sets and links; +- produce source-level readiness for higher layers. + +This is a prerequisite layer, not the assistant runtime itself. + +### 2. Canonical And Analytical Data Layer + +Main areas: + +- `canonical_layer/` +- `data/` +- `logs/` + +Current runtime entry: + +- [canonical_layer/app.py](/x:/1C/NDC_1C/canonical_layer/app.py:1) + +Role: + +- expose normalized read-only data services; +- refresh and maintain the canonical store; +- run feature engine; +- run risk engine. + +This layer is the project's data foundation. + +### 3. LLM Backend And Assistant Runtime + +Main area: + +- `llm_normalizer/backend/` + +Current server entry: + +- [server.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/server.ts:1) + +Role: + +- provider gateway; +- normalizer runtime; +- assistant runtime; +- eval runtime; +- autorun runtime; +- session persistence. + +This is the architectural center of the interactive product. + +### 4. Domain Orchestration And Acceptance Loop + +Main areas: + +- `.codex/` +- `docs/orchestration/active_domain_contract.json` +- `artifacts/domain_runs/` + +Role: + +- define the current active domain pack; +- execute scenario-based hardening; +- store machine-readable before/after artifacts; +- turn user runs into acceptance and repair targets. + +This is a first-class quality contour, not auxiliary documentation. + +### 5. Experimental Routing / Benchmark Contour + +Main area: + +- `router/` + +Role: + +- route-selection experiments; +- store-sufficiency heuristics; +- benchmark and validation support via Python scripts. + +Important: + +- this contour is not the current source of truth for the production assistant runtime; +- current production routing lives in the TypeScript assistant stack. + +## Assistant Runtime Map + +The assistant runtime currently has these working layers: + +1. `provider/model gateway` +2. `living router` +3. `address orchestration runtime` +4. `exact execution lane` +5. `coverage / evidence / grounding` +6. `answer policy / packaging / debug` +7. `session memory / navigation state` + +### Provider / Model Gateway + +Current main entry: + +- [openaiResponsesClient.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/openaiResponsesClient.ts:1) + +Current shape: + +- pragmatic two-mode provider layer: `openai` and `local` + +This layer is functional, but future-fragile if hybrid execution semantics grow. + +### Living Router + +Current main entry: + +- [assistantService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4248) + +Current top-level modes: + +- `address_data` +- `assistant_data_scope` +- `chat` + +This is where the system decides which runtime contour gets control. + +### Address Orchestration Runtime + +Current main entry: + +- [assistantAddressOrchestrationRuntimeAdapter.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts:1) + +Current responsibilities: + +- predecompose; +- effective message normalization; +- carryover resolution; +- rewrite protection; +- continuation contract assembly; +- address runtime metadata. + +### Exact Execution Lane + +Current main areas: + +- [addressQueryService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressQueryService.ts:1) +- [addressIntentResolver.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressIntentResolver.ts:1) +- [addressFilterExtractor.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressFilterExtractor.ts:1) +- [addressRecipeCatalog.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressRecipeCatalog.ts:1) +- [decomposeStage.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts:1325) +- [composeStage.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/address_runtime/composeStage.ts:1) + +This lane already acts as a real exact-data runtime, not as generic chat assistance. + +### Coverage / Evidence / Grounding + +Current main areas: + +- [assistantCoverageGrounding.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantCoverageGrounding.ts:1) +- [assistantClaimBoundEvidence.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts:1) +- [assistantDataLayer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantDataLayer.ts:1) + +This layer already exists in code, even if it has not yet been named strongly enough in architecture docs. + +### Answer Policy / Packaging / Debug + +Current main areas: + +- [answerComposer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/answerComposer.ts:1) +- [assistantAnswerPackageBuilder.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantAnswerPackageBuilder.ts:1) +- [assistantDebugPayloadAssembler.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts:1) + +This layer should own answer shape, not truth determination. + +### Session Memory / Navigation State + +Current main areas: + +- [assistantSessionStore.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantSessionStore.ts:1) +- [addressNavigationState.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressNavigationState.ts:329) +- [investigationState.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/investigationState.ts:1) + +This layer is one of the strongest parts of the architecture today. + +## First-Class Runtime Artifacts Already Present + +The following should be treated as first-class architectural artifacts, not helper byproducts: + +- `dialogContinuationContractV2` +- `result_set` +- `focus_object` +- `date_scope` +- `organization_scope` +- `coverage report` +- `grounding check` +- `evidence bundle` +- `scenario manifest` +- `scenario state` +- `baseline_turn / rerun_turn artifacts` + +## Structural Debt + +The main structural debt is concentrated in oversized modules with mixed responsibilities: + +- `assistantService.ts` +- `composeStage.ts` +- `answerComposer.ts` +- `addressQueryService.ts` +- `assistantDataLayer.ts` + +This indicates the following problem: + +- the system already has the right layers; +- but too many of those layers are still implemented as tightly coupled code inside a few large files. + +## Baseline Constraints + +Any architectural turnaround must preserve: + +- `AddressQueryService` as exact lane; +- structured navigation state; +- continuation contract semantics; +- selected-object continuity; +- limited-mode truthfulness; +- scenario-based acceptance. + +## Baseline Diagnosis + +The project is not missing architecture. + +The project already has architecture, but it is still expressed too much through: + +- god services; +- implicit workflow; +- heuristic branching; +- mixed policy/state/answer concerns. + +That is the baseline condition that the rest of this package is designed to address. diff --git a/docs/ARCH/11 - architecture_turnaround/02 - state_and_transition_contracts.md b/docs/ARCH/11 - architecture_turnaround/02 - state_and_transition_contracts.md new file mode 100644 index 0000000..a6301a6 --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/02 - state_and_transition_contracts.md @@ -0,0 +1,393 @@ +# 02 - State And Transition Contracts + +## Purpose + +This document defines the objects that must become explicit in project design: + +1. `state objects` +2. `transition classes` + +The goal is to stop treating the assistant as a prompt-driven flow with ad hoc carryover and start treating it as a stateful runtime with explicit transitions. + +## State Model + +### 1. Assistant Session Aggregate + +Top-level first-class runtime state: + +`assistant_session_state` + +It must aggregate: + +- `living_mode_state` +- `address_navigation_state` +- `investigation_state` +- `clarification_state` +- `answer_context_state` +- `coverage_gate_state` + +This aggregate is the architectural owner of cross-turn continuity. + +### 2. Living Mode State + +Purpose: + +- remember which major runtime contour is active. + +Fields: + +- `living_mode` +- `mode_reason` +- `mode_source` +- `mode_entry_turn_id` + +Allowed values: + +- `address_data` +- `assistant_data_scope` +- `chat` +- `meta_followup` +- `clarification` + +### 3. Root Frame State + +Purpose: + +- represent the currently active root business slice. + +Fields: + +- `domain_id` +- `root_route_id` +- `organization_scope` +- `date_scope` +- `root_result_set_id` +- `root_answer_object_ref` +- `frame_status` + +This state survives more transitions than object state. + +### 4. Selected Object Frame State + +Purpose: + +- represent the currently active drilldown object. + +Fields: + +- `focus_object_ref` +- `focus_object_kind` +- `source_result_set_id` +- `compatible_route_family` +- `provenance_bundle_ref` +- `temporal_ceiling` +- `frame_status` + +This state may only survive compatible object-level transitions. + +### 5. Meta Frame State + +Purpose: + +- support questions about the already returned answer without replaying exact execution blindly. + +Fields: + +- `source_answer_object_ref` +- `meta_question_kind` +- `source_gate_status` +- `meta_truth_mode` + +Allowed meta classes: + +- `evaluation` +- `comparison` +- `memory_recap` +- `boundary_explanation` +- `answer_interpretation` + +### 6. Clarification State + +Purpose: + +- represent unfinished business queries that are waiting for missing anchors or disambiguation. + +Fields: + +- `clarification_kind` +- `missing_anchors` +- `candidate_scopes` +- `resume_target_route` +- `resume_target_frame` + +### 7. Coverage Gate State + +Purpose: + +- capture whether the exact execution result is admissible for full, partial, or blocked downstream behavior. + +Fields: + +- `coverage_status` +- `evidence_grade` +- `grounding_status` +- `truth_mode` +- `carryover_eligibility` +- `reason_codes` + +Allowed values: + +- `coverage_status`: `full`, `partial`, `blocked` +- `truth_mode`: `confirmed`, `limited`, `clarification_required`, `unsupported` +- `carryover_eligibility`: `full`, `root_only`, `object_only`, `meta_only`, `none` + +## Transition Model + +The architecture must distinguish transition classes explicitly. + +### T1. Root Query Entry + +Trigger: + +- new root business question + +Inputs: + +- raw user query +- living mode state + +Outputs: + +- new `root_frame_state` +- new `coverage_gate_state` + +Must not: + +- inherit stale `focus_object` + +### T2. Root Follow-Up With Date Or Scope Change + +Trigger: + +- `а на март 2020` +- `на тот же период` +- `еще раз по этой дате` + +Inputs: + +- existing `root_frame_state` +- compatible temporal or organization shift + +Outputs: + +- updated `root_frame_state` +- new exact route execution + +Must preserve: + +- root domain +- organization scope if not explicitly changed + +Must not: + +- downgrade into `unknown` if supported root route exists + +### T3. Explicit Selected Object Drilldown + +Trigger: + +- item selected from current result set +- UI selected-object wording +- full explicit object mention + +Inputs: + +- active `root_frame_state` +- selected object reference + +Outputs: + +- `selected_object_frame_state` + +Must preserve: + +- source result set +- compatible temporal ceiling + +### T4. Short Action Follow-Up On Selected Object + +Trigger: + +- `кто поставщик` +- `где купили` +- `кому продали` +- `какие документы` + +Inputs: + +- active `selected_object_frame_state` +- compatible action request + +Outputs: + +- exact item-level capability route + +Must not: + +- fall into generic chat +- fall into data-scope selection +- lose selected object because the wording is short + +### T5. Pronoun Or Compressed Object Follow-Up + +Trigger: + +- `по ней` +- `по этой позиции` +- `а эта кому ушла` + +Inputs: + +- active `selected_object_frame_state` +- prior compatible object route + +Outputs: + +- continued object drilldown + +Must not: + +- degrade full object anchor into vague semantic noise + +### T6. Domain Pivot With Root-Only Carryover + +Trigger: + +- user leaves the current object drilldown but remains in compatible higher business scope + +Inputs: + +- `root_frame_state` +- incompatible `selected_object_frame_state` + +Outputs: + +- preserved root context +- dropped object context + +Must preserve: + +- organization/date root scope + +Must not: + +- replay object route into another domain + +### T7. Clarification Continuation + +Trigger: + +- user resolves missing anchor or ambiguity + +Inputs: + +- active `clarification_state` + +Outputs: + +- resumed target route +- cleared or updated clarification state + +Must not: + +- forget the suspended target route + +### T8. Meta Follow-Up Over Answer Object + +Trigger: + +- `это много или мало` +- `это мы должны или нам` +- `что из этого важнее` + +Inputs: + +- `answer_context_state` +- `coverage_gate_state` + +Outputs: + +- meta answer + +Must not: + +- blindly replay exact route + +### T9. Memory Recap + +Trigger: + +- `мы это обсуждали?` +- `помнишь, о чем говорили` + +Inputs: + +- prior grounded answer context + +Outputs: + +- truthful recap + +Must not: + +- invent conversation memory + +### T10. Unsupported Or Blocked Boundary + +Trigger: + +- unsupported route +- blocked evidence gate +- execution failure + +Inputs: + +- exact runtime outcome +- `coverage_gate_state` + +Outputs: + +- bounded truthful answer or clarification + +Must not: + +- masquerade blocked execution as confirmed factual answer + +## Transition Invariants + +Every transition must declare: + +- `entry condition` +- `required prior state` +- `allowed carryover depth` +- `state mutations` +- `forbidden carryover` +- `expected answer mode` + +## Required Artifacts + +Any future implementation of these contracts should produce: + +- transition table or registry; +- state schema definitions; +- transition tests; +- scenario acceptance coverage by transition class. + +## Done Criteria + +This document is only considered implemented in architecture when: + +- every major follow-up case maps to a named transition class; +- every transition class has a declared state owner; +- every transition class has a scenario-based regression family; +- no critical follow-up behavior depends only on unnamed heuristic carryover. diff --git a/docs/ARCH/11 - architecture_turnaround/03 - capability_contract_spec.md b/docs/ARCH/11 - architecture_turnaround/03 - capability_contract_spec.md new file mode 100644 index 0000000..9a8ea0b --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/03 - capability_contract_spec.md @@ -0,0 +1,253 @@ +# 03 - Capability Contract Specification + +## Purpose + +This document defines what every runtime capability must declare in order to participate safely in the assistant architecture. + +The system should stop treating capabilities as partially implicit products of: + +- intent detection; +- filter extraction; +- recipe selection; +- wording heuristics. + +Instead, each capability must be a first-class contract object. + +## Capability Contract Object + +Each capability must declare the following fields. + +### 1. Identity + +- `capability_id` +- `domain_id` +- `runtime_lane` +- `intent_ids` + +Purpose: + +- stable identification; +- relation between capability, domain and route family. + +### 2. Entry Semantics + +- `entry_modes` +- `supported_transition_classes` +- `frame_compatibility` + +Minimum allowed values: + +- `root_entry` +- `root_followup` +- `selected_object_drilldown` +- `meta_reuse` +- `clarification_resume` + +Purpose: + +- define where the capability may legally be entered from. + +### 3. Anchor Contract + +- `required_anchors` +- `optional_anchors` +- `anchor_source_priority` +- `anchor_admissibility_rules` + +Purpose: + +- make anchor requirements explicit; +- prevent garbage anchor extraction from being treated as business input. + +### 4. Scope Contract + +- `organization_scope_behavior` +- `date_scope_behavior` +- `temporal_ceiling_policy` +- `root_context_compatibility` + +Purpose: + +- define whether the capability reuses, narrows, widens, or rejects prior scope. + +### 5. Selected-Object Contract + +- `requires_focus_object` +- `accepted_focus_object_kinds` +- `focus_object_override_policy` +- `bundle_reuse_policy` + +Purpose: + +- define whether the capability depends on an existing selected object; +- define how object continuity is preserved. + +### 6. Execution Contract + +- `resolver_owner` +- `recipe_owner` +- `execution_adapter` +- `result_shape` +- `answer_object_shape` + +Purpose: + +- clearly identify the exact runtime owner of execution and output form. + +### 7. Truth Gate Contract + +- `minimum_evidence_policy` +- `coverage_gate_behavior` +- `truth_mode_fallbacks` +- `blocked_reason_codes` + +Purpose: + +- declare what counts as sufficient evidence for this capability; +- prevent answer policy from inventing truth semantics later. + +### 8. Clarification Contract + +- `clarification_triggers` +- `clarification_questions` +- `resume_policy` + +Purpose: + +- define when the capability must ask, not guess. + +### 9. Failure Contract + +- `empty_match_behavior` +- `route_expectation_failure_behavior` +- `execution_error_behavior` + +Purpose: + +- keep failures truthful and stable. + +### 10. Acceptance Contract + +- `required_unit_tests` +- `required_transition_tests` +- `required_scenario_families` + +Purpose: + +- make capability completion measurable. + +## Minimal Contract Template + +Each capability should be designable in the following shape: + +```yaml +capability_id: inventory_purchase_provenance_for_item +domain_id: inventory_stock +runtime_lane: address_exact +intent_ids: + - inventory_purchase_provenance_for_item +entry_modes: + - selected_object_drilldown + - clarification_resume +supported_transition_classes: + - T3 + - T4 + - T5 +frame_compatibility: + root_frame: required + selected_object_frame: required +required_anchors: + - item +optional_anchors: + - organization + - date_scope +anchor_admissibility_rules: + - no_low_quality_item_rewrite + - no_conversational_noise_as_entity +organization_scope_behavior: reuse_or_clarify +date_scope_behavior: respect_root_temporal_ceiling +temporal_ceiling_policy: must_not_expand_beyond_root_without_reason_code +requires_focus_object: true +accepted_focus_object_kinds: + - inventory_item +bundle_reuse_policy: provenance_bundle_preferred +minimum_evidence_policy: route_specific_threshold +coverage_gate_behavior: partial_or_blocked_if_evidence_insufficient +empty_match_behavior: truthful_empty_match +required_scenario_families: + - canonical + - colloquial + - ui_selected_object + - short_action_followup + - pronoun_followup +``` + +## Contract Rules + +### Rule 1. Capability Must Declare Entry Legality + +No capability may be entered only because a heuristic guessed it. + +It must declare: + +- which transition classes are legal; +- which frames are required; +- which frames are incompatible. + +### Rule 2. Capability Must Declare Admissible Anchors + +No capability may silently accept low-quality business anchors. + +It must declare: + +- what entity values are required; +- what counts as an admissible extracted value; +- when clarification is mandatory. + +### Rule 3. Capability Must Declare Truth Gate Behavior + +No capability may leave truth semantics to answer wording. + +It must declare: + +- what evidence threshold it needs; +- how it behaves under partial evidence; +- when it becomes blocked. + +### Rule 4. Capability Must Declare Selected-Object Compatibility + +All selected-object actions must explicitly declare: + +- whether they require focus object; +- whether they reuse a bundle from prior steps; +- whether they tolerate short follow-up wording. + +### Rule 5. Capability Must Declare Scenario Coverage + +A capability is not accepted just because canonical wording works. + +At minimum it must declare which of the following wording families are required: + +- `canonical` +- `colloquial` +- `ui_selected_object` +- `ui_selected_object_colloquial` +- `short_action_followup` +- `pronoun_followup` +- `followup_date_carryover` + +## What This Specification Replaces + +This specification is designed to reduce architectural pressure on: + +- `assistantService` +- hidden carryover logic +- implicit capability semantics in answer shaping + +## Done Criteria + +This specification is considered operational only when: + +- critical capabilities are represented as explicit contract objects; +- contract fields are sufficient to predict allowed entry, required anchors, truth behavior, and scenario acceptance; +- new capability enablement can be reviewed primarily through contract review and tests, not only by reading large service files. diff --git a/docs/ARCH/11 - architecture_turnaround/04 - coverage_evidence_truth_gate.md b/docs/ARCH/11 - architecture_turnaround/04 - coverage_evidence_truth_gate.md new file mode 100644 index 0000000..f5e92da --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/04 - coverage_evidence_truth_gate.md @@ -0,0 +1,196 @@ +# 04 - Coverage Evidence Truth Gate + +## Purpose + +This document defines the missing architectural layer between: + +- `exact execution lane` + +and + +- `answer policy / packaging` + +This layer must exist as a first-class object because the system already needs to answer questions such as: + +- do we have enough evidence; +- is the result full, partial, or blocked; +- may this result power follow-up carryover; +- is limited mode required; +- can the assistant speak as confirmed truth or only as bounded evidence. + +## Architectural Position + +The target runtime stack should be read as: + +1. route and transition policy +2. exact execution lane +3. `coverage / evidence / truth gate` +4. answer policy +5. packaging and debug + +Answer policy must not own the truth gate. + +## Current Code Owners + +The gate already exists in code in fragmented form: + +- [assistantCoverageGrounding.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantCoverageGrounding.ts:1) +- [assistantClaimBoundEvidence.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts:1) +- [assistantDataLayer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantDataLayer.ts:622) +- [addressQueryService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressQueryService.ts:2806) +- [answerComposer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/answerComposer.ts:22) + +The architectural problem is not absence, but insufficient naming and isolation. + +## Gate Inputs + +The gate should consume: + +- exact runtime result +- normalized retrieval result +- evidence items +- candidate evidence items +- coverage report +- route expectation outcome +- current frame state +- requested truth mode + +## Gate Outputs + +The gate should produce: + +- `coverage_status` +- `grounding_status` +- `truth_mode` +- `carryover_eligibility` +- `reason_codes` +- `evidence_grade` +- `blocked_or_limited_explanation` + +These outputs must be machine-readable. + +## Status Classes + +### 1. `full_confirmed` + +Meaning: + +- coverage is sufficient; +- evidence is admissible; +- route expectation passed; +- answer may speak in confirmed mode. + +### 2. `partial_supported` + +Meaning: + +- some evidence exists; +- some requirements remain uncovered or weakly covered; +- answer must remain bounded and explicit about limitations. + +### 3. `blocked_missing_anchor` + +Meaning: + +- exact execution cannot safely complete because required anchors are unresolved. + +### 4. `blocked_route_expectation_failure` + +Meaning: + +- route was attempted, but the route contract itself did not pass baseline expectations. + +### 5. `blocked_execution_error` + +Meaning: + +- system failure or execution failure prevents reliable business answer. + +### 6. `limited_temporal_or_contextual` + +Meaning: + +- the system can say something useful, but only within a narrow and explicit evidence window. + +## Carryover Eligibility Contract + +The gate must also determine follow-up eligibility. + +Allowed values: + +- `full` +- `root_only` +- `object_only` +- `meta_only` +- `none` + +Examples: + +- `full_confirmed` result from root stock snapshot may often allow `full` or `root_only` +- selected-object provenance with weak buyer evidence may allow `object_only` or `meta_only` +- blocked route expectation should usually allow `none` + +## Truth Rules + +### Rule 1. Answer Policy May Downgrade, But Not Upgrade + +Answer policy may choose a clearer wording. + +Answer policy may not: + +- turn blocked into confirmed; +- turn partial into full; +- suppress critical truth reason codes. + +### Rule 2. Limited Mode Must Remain Truthful + +If evidence is insufficient, limited mode must be explicit about: + +- what is confirmed; +- what is not confirmed; +- why the system is limited. + +### Rule 3. Carryover Must Respect The Gate + +Follow-up policy may not assume that every previous answer is equally reusable. + +Carryover must obey: + +- gate-produced eligibility; +- evidence window; +- blocked or limited reason codes. + +### Rule 4. Route Expectation Failure Is A Truth Event + +If route expectation fails, this is not just a technical footnote. + +It is a first-class truth gate outcome. + +## Required Gate Artifacts + +Every gated answer should be traceable through these artifacts: + +- `evidence_bundle` +- `coverage_contract` +- `grounding_check` +- `truth_mode` +- `carryover_eligibility` +- `reason_codes` + +## Non-Goals + +This gate is not responsible for: + +- model wording style; +- cosmetic answer formatting; +- provider selection; +- UI packaging details. + +## Done Criteria + +This layer is considered architecturally established only when: + +- it is documented as separate from answer policy; +- its outputs are explicit inputs to answer shaping; +- carryover policy depends on gate output rather than hidden heuristics; +- scenario acceptance can fail specifically on gate behavior, not only on final wording. diff --git a/docs/ARCH/11 - architecture_turnaround/05 - assistantService_extraction_map.md b/docs/ARCH/11 - architecture_turnaround/05 - assistantService_extraction_map.md new file mode 100644 index 0000000..f098c4d --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/05 - assistantService_extraction_map.md @@ -0,0 +1,213 @@ +# 05 - AssistantService Extraction Map + +## Purpose + +This document maps the current architectural overload of `assistantService.ts` and identifies what should stop living there over time. + +The goal is not to empty the file arbitrarily. + +The goal is to turn it from a god-service into a thinner coordinator. + +## Current Situation + +`assistantService.ts` is currently the largest and most overloaded module in the assistant runtime. + +Approximate size: + +- `6243` lines + +It currently mixes concerns from: + +- living route selection; +- follow-up carryover; +- continuation contracts; +- mode boundaries; +- meta-followup logic; +- memory recap detection; +- provider-aware predecompose orchestration; +- chat/data-scope/address boundary policy. + +## Extraction Principle + +`assistantService` should remain the top-level coordinator. + +It should stop being the main owner of: + +- policy logic; +- transition semantics; +- boundary semantics; +- meta semantics. + +## Extraction Targets + +### 1. Living Route Policy + +Current owner: + +- `resolveAssistantOrchestrationDecision()` + +Current location: + +- [assistantService.ts:4248](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4248) + +Target owner: + +- `assistantRoutePolicyRuntimeAdapter` + +Expected artifact: + +- explicit living-route decision contract + +Done when: + +- route policy can be reviewed without reading the full coordinator; +- top-level mode decisions are data-driven or contract-driven enough to be testable as a separate unit. + +### 2. Carryover And Transition Policy + +Current owner: + +- `resolveAddressFollowupCarryoverContext()` +- `buildAddressDialogContinuationContractV2()` + +Current location: + +- [assistantService.ts:2828](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:2828) +- [assistantService.ts:3111](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3111) + +Target owner: + +- `assistantTransitionRuntimeAdapter` +- `assistantContinuationContractBuilder` + +Expected artifact: + +- explicit transition classes with state inputs and carryover depth + +Done when: + +- follow-up continuation is understood in terms of transitions, not just heuristic carryover. + +### 3. Meta Follow-Up Policy + +Current owner: + +- meta and evaluative follow-up detection inside route decision logic + +Target owner: + +- `assistantMetaFollowupPolicy` + +Expected artifact: + +- meta question class registry +- legal source answer object types + +Done when: + +- meta questions no longer depend on implicit branches buried inside living route logic. + +### 4. Data-Scope And Boundary Policy + +Current owner: + +- `assistant_data_scope_query_detected` branches + +Current references: + +- [assistantService.ts:3974](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3974) +- [assistantService.ts:4412](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4412) +- [assistantService.ts:6052](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:6052) + +Target owner: + +- `assistantBoundaryPolicy` + +Expected artifact: + +- explicit boundary mode contract: + - `data_scope` + - `operational_boundary` + - `capability_contract` + - `non_domain_chat` + +Done when: + +- emotional or colloquial user messages cannot accidentally trigger data-scope selection through hidden shared logic. + +### 5. Memory Recap Policy + +Current owner: + +- route decision + living chat cooperation + +Related consumer: + +- [assistantLivingChatRuntimeAdapter.ts:255](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts:255) + +Target owner: + +- `assistantMemoryRecapPolicy` + +Expected artifact: + +- truthful recap contract over grounded prior answer objects + +Done when: + +- memory recap logic becomes a named subsystem with its own truth rules. + +### 6. Provider-Aware Orchestration Glue + +Current owner: + +- coordinator-level logic that adapts to `openai` vs `local` + +Target owner: + +- `assistantProviderExecutionPolicy` + +Expected artifact: + +- provider/runtime compatibility layer for orchestration expectations + +Done when: + +- provider quirks no longer leak into business routing logic. + +## What Should Stay In AssistantService + +After extraction, `assistantService` should still own: + +- high-level request coordination; +- dependency wiring; +- stage invocation order; +- final turn assembly and persistence orchestration. + +It should not be removed as an architectural object. + +It should become thinner. + +## Proposed Extraction Order + +1. extract route decision contract +2. extract transition/carryover policy +3. extract boundary/data-scope policy +4. extract meta and memory recap policy +5. isolate provider-aware orchestration glue + +This order is chosen because route and transition pressure are currently the main source of runtime fragility. + +## Non-Goals + +- do not split the file mechanically just to reduce line count; +- do not create many tiny helpers with no architectural ownership; +- do not move exact execution logic out of its proper lane. + +## Done Criteria + +This extraction plan is complete only when: + +- the remaining `assistantService` can be described as a coordinator; +- major policy categories have explicit owners outside `assistantService`; +- scenario regressions can point to policy subsystems instead of a single god-service. diff --git a/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md b/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md new file mode 100644 index 0000000..c0ea46f --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md @@ -0,0 +1,238 @@ +# 06 - Phase Acceptance Matrix + +## Purpose + +This document turns the high-level turnaround direction into a phase-by-phase execution matrix. + +Each phase must specify: + +- `goal` +- `output artifacts` +- `done criteria` +- `non-goals` +- `acceptance signals` + +## Phase 0. Shared Baseline + +Goal: + +- align the team on one architecture vocabulary and one baseline map. + +Output artifacts: + +- baseline note in `docs/ARCH/11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md` +- this package under `docs/ARCH/11 - architecture_turnaround/` + +Done when: + +- project discussions use the same names for layers, state, transitions, truth gate, and capabilities; +- no major planning discussion treats the system as "just a chat with LLM". + +Non-goals: + +- code changes; +- capability rewrites. + +Acceptance signals: + +- planning references point to the package rather than ad hoc prose; +- new refactor proposals can be mapped to a package artifact. + +## Phase 1. Formal Layer Separation + +Goal: + +- explicitly separate project subsystems and their sources of truth. + +Output artifacts: + +- updated architecture notes if needed; +- clear internal naming for: + - data foundation + - assistant runtime + - domain loop + - experimental router contour + +Done when: + +- production assistant routing is no longer confused with Python router experiments; +- `canonical_layer` and `llm_normalizer/backend` are treated as different subsystems in planning. + +Non-goals: + +- merging runtimes; +- changing domain behavior. + +Acceptance signals: + +- no architecture review uses the wrong contour as the current runtime source of truth. + +## Phase 2. State And Transition Contracts + +Goal: + +- make state and transition classes explicit. + +Output artifacts: + +- state schema note +- transition class registry note +- transition-oriented tests or planning matrix + +Done when: + +- every critical follow-up path is described as a named transition class; +- root frame, selected object frame, meta frame, clarification state, and coverage gate state are explicit objects. + +Non-goals: + +- answer wording cleanup; +- adding new business capabilities. + +Acceptance signals: + +- critical scenario failures can be described as transition failures, not only as "assistant got confused"; +- at least one major follow-up-heavy domain can be read through transition contracts alone. + +## Phase 3. Capability Contracts + +Goal: + +- make capabilities explicit contract objects instead of half-implicit route behavior. + +Output artifacts: + +- capability contract schema +- pilot contract set for critical inventory capabilities + +Done when: + +- critical capabilities declare entry modes, anchors, scope policy, truth behavior, and scenario families; +- new capability review can happen via contract inspection. + +Non-goals: + +- low-code workflow migration; +- removing recipe catalog. + +Acceptance signals: + +- at least one selected-object capability and one root capability are represented as full contract specs. + +## Phase 4. Coverage / Evidence / Truth Gate Isolation + +Goal: + +- separate truth determination from answer policy. + +Output artifacts: + +- gate contract +- reason-code taxonomy +- carryover-eligibility contract + +Done when: + +- answer layer no longer decides whether a result is full, partial, or blocked; +- truth mode and carryover eligibility are explicit gate outputs. + +Non-goals: + +- UI redesign; +- full answer package rewrite. + +Acceptance signals: + +- scenario regressions can fail specifically on gate semantics; +- limited mode honesty is measurable independently from wording quality. + +## Phase 5. AssistantService Extraction + +Goal: + +- reduce `assistantService` from god-service to coordinator. + +Output artifacts: + +- extraction map +- named policy owners +- reduced coordinator responsibility map + +Done when: + +- route policy, transition policy, boundary policy, and meta policy have explicit owners outside the coordinator; +- `assistantService` retains orchestration ownership but not most raw policy logic. + +Non-goals: + +- file splitting for its own sake; +- replacing the coordinator with a monolithic new router. + +Acceptance signals: + +- scenario regressions can point to extracted policy owners; +- code review no longer requires reading most of `assistantService.ts` to understand one policy area. + +## Phase 6. Provider / Runtime Axis Hardening + +Goal: + +- make provider/runtime behavior an explicit architectural concern. + +Output artifacts: + +- provider execution contract +- structured-output compatibility matrix +- local/openai execution semantics note + +Done when: + +- provider quirks no longer bleed into business routing policy; +- structured output, tool calling expectations, and fallback behavior are documented per provider mode. + +Non-goals: + +- adding many providers immediately; +- model benchmarking as primary objective. + +Acceptance signals: + +- changing provider mode does not silently change core business semantics without explicit compatibility review. + +## Phase 7. Scenario Acceptance As Primary Gate + +Goal: + +- enforce scenario-tree acceptance as the refactoring completion standard. + +Output artifacts: + +- phase-specific acceptance matrix +- updated scenario packs and required wording families + +Done when: + +- no phase is considered complete based only on unit tests or prettier answers; +- critical paths, critical edges, and selected-object continuity remain mandatory acceptance criteria. + +Non-goals: + +- shrinking evaluation to smoke tests; +- accepting root-only success as domain completion. + +Acceptance signals: + +- `pack_state.final_status` +- scenario acceptance matrix +- no unresolved `P0` +- direct answer, temporal honesty, selected-object continuity, and truth gate invariants all pass. + +## Cross-Phase Rule + +A phase is not done when the code "looks cleaner". + +A phase is done only when: + +- the declared artifact exists; +- the responsible layer is explicit; +- acceptance signals are green. diff --git a/docs/ARCH/11 - architecture_turnaround/07 - external_reference_appendix.md b/docs/ARCH/11 - architecture_turnaround/07 - external_reference_appendix.md new file mode 100644 index 0000000..70390a4 --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/07 - external_reference_appendix.md @@ -0,0 +1,135 @@ +# 07 - External Reference Appendix + +## Purpose + +This appendix keeps external references available without overloading the core planning documents. + +It records: + +- which pattern is borrowed; +- why it is relevant; +- what should not be copied literally. + +## Dify + +Sources: + +- +- +- +- + +Borrow: + +- explicit distinction between workflow and chatflow; +- named nodes for classifier, agent, answer, variable management; +- persistent conversation variables. + +Do not borrow literally: + +- low-code canvas as a direct replacement for exact 1C runtime logic. + +## Open WebUI + +Sources: + +- +- +- + +Borrow: + +- layered extensibility model; +- clear distinction between in-process tools, external APIs, and separate heavy pipelines; +- input/output filter mindset. + +Do not borrow literally: + +- generic plugin-first shell as the main architecture for exact-data routing. + +## Onyx + +Sources: + +- +- + +Borrow: + +- platform view of chat, agents, actions, connectors; +- separation between indexed knowledge, actions, and chat experience. + +Do not borrow literally: + +- enterprise search architecture as a substitute for 1C exact route discipline. + +## LibreChat + +Sources: + +- +- + +Borrow: + +- explicit agent capability surface; +- tool exposure discipline; +- deferred tools concept. + +Do not borrow literally: + +- treat MCP/tool abundance as a substitute for domain-specific capability contracts. + +## Vanna + +Source: + +- + +Borrow: + +- exact-data assistant framing; +- tool registry plus structured outputs; +- user-aware execution semantics. + +Do not borrow literally: + +- SQL assistant assumptions as the whole model for our multi-layer accounting runtime. + +## DB-GPT + +Source: + +- + +Borrow: + +- AI data assistant as platform-plus-domain-skill composition; +- workflows and skills as explicit architectural objects. + +Do not borrow literally: + +- broad platform scope at the expense of current bounded hardening goals. + +## LangGraph + +Source: + +- + +Borrow: + +- durable execution mindset; +- checkpointing and deterministic replay; +- explicit state graph thinking. + +Do not borrow literally: + +- framework migration as an end in itself. + +## Summary + +The external references collectively support one conclusion: + +- the project should move toward more explicit state, transition, and capability contracts; +- it should not dissolve its exact-data rails into a generic agent shell. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md new file mode 100644 index 0000000..1468660 --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -0,0 +1,70 @@ +# 11 - Architecture Turnaround Package + +## Purpose + +This folder is the execution-oriented continuation of the baseline note: + +- [11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md]() + +That baseline note answers: + +- what the project is today; +- where the main architectural fragility sits; +- what direction is safe. + +This package answers the next question: + +- how the team should design the architectural turnaround without breaking the current exact-data baseline. + +## Package Contents + +1. [01 - project_architecture_baseline_map.md](./01%20-%20project_architecture_baseline_map.md) +2. [02 - state_and_transition_contracts.md](./02%20-%20state_and_transition_contracts.md) +3. [03 - capability_contract_spec.md](./03%20-%20capability_contract_spec.md) +4. [04 - coverage_evidence_truth_gate.md](./04%20-%20coverage_evidence_truth_gate.md) +5. [05 - assistantService_extraction_map.md](./05%20-%20assistantService_extraction_map.md) +6. [06 - phase_acceptance_matrix.md](./06%20-%20phase_acceptance_matrix.md) +7. [07 - external_reference_appendix.md](./07%20-%20external_reference_appendix.md) + +## Architectural Objects Of Planning + +This package makes five objects explicit: + +1. `state model` +2. `transition model` +3. `capability contract model` +4. `coverage / evidence / truth gate` +5. `assistantService extraction plan` + +These are the objects that should now drive refactoring discussions. + +## How To Use The Package + +Read in this order: + +1. baseline note in `docs/ARCH/11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md` +2. `01 - project_architecture_baseline_map.md` +3. `02 - state_and_transition_contracts.md` +4. `03 - capability_contract_spec.md` +5. `04 - coverage_evidence_truth_gate.md` +6. `05 - assistantService_extraction_map.md` +7. `06 - phase_acceptance_matrix.md` +8. `07 - external_reference_appendix.md` + +## Planning Rules + +- Do not treat this package as a rewrite plan. +- Do not dissolve `AddressQueryService` into generic chat logic. +- Do not move state back into transcript-only memory. +- Do not let answer wording substitute for policy/runtime fixes. +- Use scenario-based acceptance as the primary gate for all phases. + +## Expected Outcome + +When this package is fully operational, the project should stop being described as: + +- "a big custom assistant service with many heuristics" + +and start being described as: + +- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating." diff --git a/docs/ARCH/11 - architecture_turnaround/unified_project_architecture_and_reference_update_plan_2026-04-15.md b/docs/ARCH/11 - architecture_turnaround/unified_project_architecture_and_reference_update_plan_2026-04-15.md new file mode 100644 index 0000000..0c198c1 --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/unified_project_architecture_and_reference_update_plan_2026-04-15.md @@ -0,0 +1,992 @@ +# 11 - Unified Project Architecture And Reference Update Plan 2026-04-15 + +## 1. Назначение документа + +Этот документ фиксирует единый архитектурный срез проекта `NDC_1C` на `2026-04-15` и связывает: + +- текущее фактическое устройство репозитория; +- текущее устройство assistant runtime; +- уже существующие архитектурные ограничения и сильные стороны; +- внешний reference landscape из open-source LLM-проектов; +- безопасный план обновления подхода без разрушения рабочего baseline. + +Связанный execution-oriented пакет planning artifacts: + +- [11 - architecture_turnaround]() + +Документ нужен не для исторического обзора, а как единая опорная карта: + +- что в проекте уже является устойчивым фундаментом; +- что является техническим и архитектурным долгом; +- где проект типовой по классу систем; +- где проект уже имеет осмысленную кастомную специализацию; +- как переходить от heuristic-heavy hardening к более устойчивой архитектурной форме. + +Этот документ дополняет, а не заменяет: + +- `docs/ARCH/10 - current_assistant_architecture_2026-04-15.md` +- `docs/ARCH/10A - current_assistant_hardening_plan_2026-04-15.md` +- `graphify-out/GRAPH_REPORT.md` + + +## 2. Executive Summary + +На текущем этапе `NDC_1C` уже не является "просто чат-приложением с LLM". + +Фактически проект состоит из нескольких самостоятельных архитектурных контуров: + +1. `1C/OData acquisition + canonical layer` +2. `feature/risk engines over canonical store` +3. `LLM normalizer + assistant runtime` +4. `domain orchestration / scenario loop / eval artifacts` +5. `research and benchmark contour` вокруг Python `router/` + +Главный вывод по результатам локального анализа и сравнения с open-source references: + +- по классу продукта проект действительно типовой: это `LLM + tools/data + state + UI/API`; +- по реализации текущий runtime у нас уже существенно более специализирован, чем обычный generic chat shell; +- основная проблема проекта не в отсутствии "правильной идеи", а в том, что слишком много системной политики собрано вручную внутри нескольких очень больших модулей; +- мы уже имеем сильные архитектурные элементы, которые нельзя терять; +- правильный следующий шаг это не rewrite в сторону "универсального суперагента", а переход к более явной workflow/state-oriented форме поверх уже существующих exact-capability rails. + +Короткая формула: + +`проект типовой по классу` + +но + +`архитектурно перегружен ручной orchestration-логикой` + +и потому + +`нуждается не в изобретении новых возможностей, а в упорядочивании существующих слоев`. + + +## 3. Источники для этого среза + +### 3.1 Локальные источники + +- `graphify-out/GRAPH_REPORT.md` +- `README.md` +- `canonical_layer/app.py` +- `llm_normalizer/backend/src/server.ts` +- `llm_normalizer/backend/src/services/*` +- `docs/orchestration/active_domain_contract.json` +- `.codex/` и `artifacts/domain_runs/*` + +### 3.2 Внешние reference sources + +- Dify: +- Dify docs, key concepts: +- Dify docs, agent node: +- Dify docs, variable assigner: +- Open WebUI: +- Open WebUI docs, extensibility: +- Open WebUI docs, RAG: +- Onyx: +- Onyx docs, actions overview: +- LibreChat: +- LibreChat docs, agents: +- Vanna: +- DB-GPT: +- LangGraph docs, durable execution: + + +## 4. Текущая карта проекта целиком + +### 4.1 Верхнеуровневые контуры репозитория + +Текущий репозиторий содержит не один сервис, а несколько связанных слоев: + +- `odata_probe/` + - слой исследования и проверки read-only доступа к 1C OData; +- `canonical_layer/` + - Python/FastAPI слой над канонической моделью и хранилищем; +- `llm_normalizer/backend/` + - TypeScript/Express runtime для LLM normalizer, assistant, eval и autoruns; +- `router/` + - Python policy/benchmark контур для route selection и sufficiency checks; +- `orchestration/` + - служебный orchestration runtime для batch/use-case loops; +- `docs/orchestration/active_domain_contract.json` + - single mutable source для текущего активного domain/scenario pack; +- `.codex/` + - project-scoped loop/orchestration навыки и роли; +- `artifacts/domain_runs/` + - машинно-читаемые артефакты сценарных прогонов и hardening loop; +- `graphify-out/` + - knowledge graph и архитектурный срез кодовой базы. + +### 4.2 Ключевой смысл этой композиции + +Проект уже организован как многоступенчатая AI/data система: + +1. сначала добываются и нормализуются read-only данные 1C; +2. затем они кладутся в канонический слой и feature/risk engines; +3. затем поверх них строится exact-data assistant runtime; +4. затем этот runtime проверяется domain scenarios, evaluation и artifacts; +5. отдельно существует research/policy contour, который не является главным runtime. + +Это важно: обсуждая архитектуру, нельзя сводить проект только к `llm_normalizer/backend`. + + +## 5. Слой 1: 1C acquisition и canonical layer + +### 5.1 Что это такое + +Базовый контур проекта по-прежнему rooted в read-only интеграции с 1C. + +Это видно уже из top-level README: + +- `odata_probe/` собирает и проверяет доступность entity sets и link semantics; +- `canonical_layer/` строит каноническую модель поверх 1C данных; +- проект принципиально read-only по policy. + +### 5.2 Текущая реализация + +`canonical_layer/app.py` поднимает FastAPI-приложение: + +- `CanonicalService` +- `RefreshService` +- `FeatureService` +- `RiskService` + +Доступные API-поверхности включают: + +- `GET /documents` +- `GET /postings` +- `GET /graph/document/{document_id}` +- `POST /refresh/run` +- `POST /features/run` +- `POST /risk/run` + +То есть canonical layer уже не ограничивается "просто прокси к 1C", а включает: + +- data access API; +- refresh pipeline; +- feature engine; +- risk engine. + +### 5.3 Архитектурная роль + +Этот слой это не accessory для ассистента, а нижний data foundation проекта. + +Именно он отвечает за: + +- устойчивый read-only доступ; +- канонизацию сущностей; +- промежуточное локальное store-представление; +- data services, которые могут использоваться не только LLM-слоем. + +По классу архитектуры это типичный `data platform substrate` для AI assistant products. + + +## 6. Слой 2: LLM normalizer backend + +### 6.1 Входная точка + +`llm_normalizer/backend/src/server.ts` поднимает Express-приложение и регистрирует: + +- `/api/health` +- `testConnection` +- `sharedLlmConfig` +- `normalize` +- `eval` +- `assistant` +- `autoRuns` +- `history` +- `presets` +- `accountingAgent` + +Текущая сборка сервисов: + +- `OpenAIResponsesClient` +- `NormalizerService` +- `EvalService` +- `AssistantSessionStore` +- `AssistantService` +- `InMemoryRuntimeAdapter` + +### 6.2 Что это значит + +Backend уже совмещает несколько ролей: + +- LLM gateway; +- normalizer runtime; +- assistant runtime; +- eval runtime; +- batch/autorun runtime; +- session persistence for assistant mode. + +Именно здесь архитектура становится наиболее плотной и наиболее хрупкой. + + +## 7. Слой 3: Текущая архитектура assistant runtime + +### 7.1 Зафиксированная форма + +Согласно `ARCH 10` и текущему коду, assistant runtime уже устроен как пятислойная система: + +1. `living router` +2. `address orchestration runtime` +3. `address exact execution lane` +4. `session memory + navigation state` +5. `answer/debug contract layer` + +### 7.2 Living router + +Главная точка выбора режима сейчас это: + +- `resolveAssistantOrchestrationDecision()` в `assistantService.ts` + +Текущие верхние living modes: + +- `address_data` +- `assistant_data_scope` +- `chat` + +На этом слое решается: + +- идти ли в exact data contour; +- уйти ли в data-scope/meta contract; +- оставить ли запрос в обычном chat mode; +- перехватить ли follow-up/meta/memory case. + +### 7.3 Address orchestration runtime + +Вынесен в: + +- `assistantAddressOrchestrationRuntimeAdapter.ts` + +Он уже отвечает за: + +- LLM predecompose и fallback predecompose; +- нормализацию effective message; +- resolution carryover context; +- защиту от плохой canonical rewrite; +- сборку `dialogContinuationContract`; +- формирование `addressRuntimeMeta`. + +Это уже похоже не на "просто helper", а на отдельный orchestration stage. + +### 7.4 Exact execution lane + +Текущий exact lane строится вокруг: + +- `AddressQueryService` +- `addressRecipeCatalog` +- `addressCapabilityPolicy` +- `addressIntentResolver` +- `addressFilterExtractor` +- `address_runtime/decomposeStage.ts` +- `address_runtime/composeStage.ts` + +Именно этот слой исполняет конкретные capabilities, routes и recipes. + +### 7.5 Session/navigation state + +Критический текущий актив: + +- `addressNavigationState.ts` + +State уже хранит: + +- `active_result_set_id` +- `active_focus_object` +- `last_confirmed_route` +- `date_scope` +- `organization_scope` +- `result_sets` +- `navigation_history` + +То есть система уже не полагается только на transcript memory. + +### 7.6 Answer/debug contract layer + +Ответ формируется не напрямую из LLM output, а через отдельный packaging/debug contour: + +- `answerComposer.ts` +- `assistantAnswerPackageBuilder.ts` +- `assistantDebugPayloadAssembler.ts` +- `assistantStage4AnswerContractAudit.ts` + +Это означает, что проект уже имеет машинно-читаемые answer/debug rails, а не просто "text in/text out". + + +## 8. Что graphify говорит о текущей кодовой базе + +На `2026-04-15` по `graphify-out/GRAPH_REPORT.md`: + +- корпус: `473 files` +- граф: `4865 nodes` +- `10622 edges` +- `132 communities` + +Ключевые god nodes: + +- `resolveAddressIntent()` +- `composeFactualReply()` +- `resolveAssistantOrchestrationDecision()` + +Наиболее важные сообщества для assistant runtime: + +- orchestration around `AssistantService` +- exact execution around `AddressQueryService` +- navigation state / focus object / result sets +- address orchestration runtime adapters + +Это подтверждает центральный архитектурный факт: + +`главная форма системы уже не prompt-centric, а runtime-centric` + +но при этом + +`runtime слишком сильно сосредоточен вокруг нескольких god modules`. + + +## 9. Что в проекте уже сделано сильно + +### 9.1 Structured state вместо transcript-only memory + +Во многих LLM-продуктах контекст держится только историей сообщений. + +У нас уже есть явные сущности: + +- `result_set` +- `focus_object` +- `date_scope` +- `organization_scope` +- `last_confirmed_route` + +Это зрелее, чем у большинства generic agent shells. + +### 9.2 Exact-data lane как отдельный контур + +`AddressQueryService` у нас уже отделен от chat/mode выбора. + +Это означает: + +- есть шанс держать business truthfulness; +- есть шанс тестировать exact capabilities отдельно; +- можно hardening делать не только через wording, но и через route policy. + +### 9.3 Limited mode и truthfulness rails + +В кодовой базе уже присутствуют: + +- route expectation contracts; +- limited mode; +- missing anchor handling; +- honesty around empty/incomplete results. + +Это правильное направление для exact assistant над бухгалтерскими данными. + +### 9.4 Scenario-oriented hardening loop + +Через `artifacts/domain_runs/*` и `.codex/skills/domain-case-loop` проект уже имеет не только тесты, но и domain acceptance artifacts: + +- `baseline_turn.json` +- `rerun_turn.json` +- `scenario_manifest.json` +- `scenario_state.json` +- acceptance matrices + +Это очень сильная инженерная практика, которой нет у многих open-source chat products. + + +## 10. Где сейчас архитектурная хрупкость + +### 10.1 God services + +По размеру и плотности ответственности особенно выделяются: + +- `assistantService.ts` - 6243 lines +- `address_runtime/composeStage.ts` - 4817 lines +- `answerComposer.ts` - 4681 lines +- `addressQueryService.ts` - 4415 lines +- `assistantDataLayer.ts` - 4272 lines + +Это главный симптом того, что: + +- routing; +- state evolution; +- policy; +- answer shaping; +- partial chat/meta behavior; +- exact capability glue + +слишком часто живут в одних и тех же модулях. + +### 10.2 Implicit workflow вместо explicit workflow + +Система уже ведет себя как state machine, но выражена в основном через: + +- `if/else` +- guard trees +- carryover heuristics +- rewrite protection +- mode decisions + +а не через явный graph/workflow model. + +Из-за этого: + +- локальный фикс легко меняет глобальное поведение; +- сложно визуально понимать допустимые переходы; +- acceptance часто приходится собирать эмпирически по прогонам. + +### 10.3 Policy смешана с orchestration и answer layer + +В текущей форме слишком много policy decisions живет внутри orchestration ядра: + +- domain pivot +- meta-followup +- memory recap +- data-scope boundary +- short follow-up behavior +- raw vs canonical message preference + +Это делает систему чувствительной к incremental patching. + +### 10.4 Provider/runtime abstraction узкая + +`OpenAIResponsesClient` сейчас фактически является главным LLM gateway с режимом: + +- `openai` +- `local` + +То есть провайдерная архитектура пока прагматичная, но не полноформатная. + +Для текущего этапа этого достаточно, но это означает: + +- модельный слой пока не является сильной самостоятельной осью архитектуры; +- orchestration и provider logic не так чисто разведены, как в крупных product shells. + +### 10.5 Python router не является главным runtime + +`router/` полезен как bench/policy contour: + +- `query_classifier.py` +- `route_selector.py` +- `store_sufficiency.py` + +Но он не является source of truth для текущего assistant runtime. + +Это значит, что: + +- нельзя проектировать основную архитектуру, думая, что Python router и есть текущий production routing; +- любые будущие сближения этих контуров надо делать явно и документированно. + + +## 11. Что показывают внешние references + +### 11.1 Dify + +Полезный architectural pattern: + +- явное разделение `Workflow` и `Chatflow`; +- node-based orchestration; +- встроенные classifier, agent, tool, variable, answer nodes; +- отдельные conversation variables, переживающие многоходовый чат. + +Что полезно для нас: + +- идея сделать часть нашей текущей implicit orchestration более явной; +- отделить workflow state от answer wording; +- использовать более явную модель conversation variables/state transitions. + +Что не нужно делать буквально: + +- переписывать текущий exact runtime в low-code canvas; +- пытаться заменить существующие domain rails на generic visual flow. + +### 11.2 Open WebUI + +Полезный architectural pattern: + +- product shell + extensibility layers; +- разделение на in-process tools, external OpenAPI/MCP, separate pipelines; +- input/output filters; +- четкое понимание, где живут heavy operations. + +Что полезно для нас: + +- явнее развести `tool/action layer`, `message filters`, `heavy execution lanes`; +- отделить extensibility concerns от core orchestration; +- думать о boundary/filter layer как о самостоятельном слое. + +### 11.3 Onyx + +Полезный architectural pattern: + +- AI platform как слой поверх connectors/search/chat/agents/actions; +- MCP/OpenAPI actions; +- enterprise-friendly separation между agents, actions и indexed knowledge. + +Что полезно для нас: + +- видеть assistant не как monolith, а как application layer; +- усиливать distinction между exact domain actions и general conversational shell. + +### 11.4 LibreChat + +Полезный architectural pattern: + +- agent/tool shell; +- MCP integration; +- deferred tools, чтобы не перегружать контекст; +- тонкая настройка agent capabilities. + +Что полезно для нас: + +- capability exposure должен быть более декларативным; +- tool universe не должен быть размазан по orchestration heuristics; +- selection of available actions может быть более explicit. + +### 11.5 Vanna + +Полезный architectural pattern: + +- exact-data assistant через agent + tool registry; +- user-aware execution; +- structured UI outputs; +- observability и lifecycle hooks. + +Что полезно для нас: + +- точные data routes надо трактовать как first-class actions; +- identity/scope/date constraints должны протекать через tool execution и answer shape явно; +- structured outputs должны быть устойчивым контрактом. + +### 11.6 DB-GPT + +Полезный architectural pattern: + +- AI data assistant как отдельный класс продукта; +- skills, workflows, SQL/code execution, sandboxing; +- separation between platform and domain skills. + +Что полезно для нас: + +- наш проект не обязательно "уникален"; +- data assistant products уже давно строятся как composable platform + domain logic; +- это аргумент в пользу архитектурной декомпозиции, а не против нее. + +### 11.7 LangGraph + +Полезный conceptual pattern: + +- durable execution; +- persistence/checkpointing; +- deterministic replay; +- explicit state graph; +- idempotent tasks. + +Что полезно для нас: + +- follow-up heavy assistant с carryover почти неизбежно выигрывает от explicit state graph mindset; +- наш current navigation state уже является хорошей базой для более graph-like architecture; +- особенно важно для multi-turn domain scenarios и bounded recovery после ошибок. + + +## 12. Где мы типовые, а где уже нет + +### 12.1 Типовые части проекта + +Мы полностью типовые в следующем: + +- есть LLM gateway; +- есть tool/data execution; +- есть retrieval/data grounding; +- есть session state; +- есть answer packaging; +- есть eval/autorun контур; +- есть multi-layer backend over business data. + +В этом смысле проект действительно не изобретает новый класс систем. + +### 12.2 Нетиповые, но сильные части + +Мы уже сильнее generic OSS chat shells в следующем: + +- exact capability discipline; +- structured navigation state; +- selected-object continuity; +- scenario-tree acceptance; +- limited-mode truthfulness; +- explicit answer/debug contracts; +- domain hardening through machine-readable artifacts. + +Это нельзя терять в погоне за "более красивой" типовой архитектурой. + +### 12.3 Нетиповые и хрупкие части + +Мы хрупки там, где типовые проекты обычно используют более явный framework layer: + +- implicit orchestration inside god services; +- policy encoded in large custom functions; +- state transitions expressed through heuristic branches instead of explicit graph/node semantics; +- boundary/meta handling слишком тесно связано с main assistant coordinator. + + +## 13. Что нельзя ломать при архитектурном обновлении + +При любом дальнейшем update plan нельзя разрушать следующие baseline элементы: + +- `AddressQueryService` как отдельный exact lane; +- `addressNavigationState` с `result_set/focus_object/date_scope/organization_scope`; +- `dialogContinuationContractV2`; +- selected-object continuity; +- route expectation audit; +- limited mode truthfulness; +- domain artifacts и scenario acceptance loop; +- `docs/orchestration/active_domain_contract.json` как mutable active domain source. + +Принцип: + +`generic best practices не должны размывать exact-data baseline`. + + +## 14. Unified diagnosis + +На `2026-04-15` архитектурная проблема проекта формулируется так: + +### 14.1 Не проблема + +Проблема не в том, что: + +- проект слишком специфичен; +- проект делает что-то принципиально уникальное; +- для проекта не существует внешних референсов; +- нужен новый "умный супер-роутер", который заменит все существующие слои. + +### 14.2 Реальная проблема + +Реальная проблема в том, что: + +- правильные слои уже есть; +- правильные state entities уже есть; +- правильный exact runtime уже есть; + +но + +- orchestration и policy слишком централизованы; +- implicit workflow слишком велик; +- код слишком легко деградирует от incremental heuristics; +- system behavior недостаточно выражен как явные runtime contracts и state transitions. + +Коротко: + +`архитектура не отсутствует` + +а + +`архитектура уже есть, но собрана слишком вручную и слишком плотно`. + + +## 15. Целевой update direction + +Правильное направление обновления: + +`не rewrite` + +и + +`не новый универсальный агент` + +а + +`explicit workflow/state shell поверх существующих exact rails`. + +Это значит: + +- меньше hidden orchestration; +- больше explicit route/state contracts; +- тоньше coordinator modules; +- более явные boundaries между: + - route policy; + - state transition policy; + - capability registry/execution; + - answer policy; + - meta/chat boundary. + + +## 16. План обновления по найденным references + +### Phase 0. Зафиксировать единый baseline + +Цель: + +- прекратить обсуждать проект как "просто LLM чат"; +- закрепить единый словарь слоев и boundaries. + +Что сделать: + +- использовать этот документ как общий architecture map; +- сохранить `ARCH 10` и `ARCH 10A` как assistant-runtime specific docs; +- считать текущий baseline immutable without explicit note. + +Ожидаемый результат: + +- любые дальнейшие изменения обсуждаются относительно явной карты проекта, а не по ощущениям. + +### Phase 1. Развести проектные слои формально + +Borrowed pattern: + +- product shell layering из Open WebUI / Onyx + +Цель: + +- явно отделить: + - data foundation; + - assistant runtime; + - orchestration/eval loop; + - experimental router contour. + +Что обновить концептуально: + +- `canonical_layer` и `llm_normalizer/backend` должны описываться как разные subsystems; +- Python `router/` должен быть явно помечен как benchmark/research contour; +- `.codex` и `artifacts/domain_runs` должны считаться отдельным quality/hardening contour. + +Ожидаемый результат: + +- меньше путаницы, где именно находится source of truth для runtime behavior. + +### Phase 2. Вынести orchestration grammar в явные contracts + +Borrowed pattern: + +- Workflow/Chatflow + conversation variables из Dify +- state graph mindset из LangGraph + +Цель: + +- превратить главный orchestration policy из "большого дерева if-ов" в более явный contract system. + +Что обновить концептуально: + +- формализовать переходы между: + - `address_data` + - `assistant_data_scope` + - `chat` + - `meta-followup` + - `memory-recap` + - `organization-clarification` +- для каждого класса перехода иметь: + - входной trigger class; + - допустимый carryover depth; + - allowed state reuse; + - forbidden cross-domain leakage; + - expected answer mode. + +Ожидаемый результат: + +- routing перестает быть набором локальных heuristic patches и становится описуемой state policy. + +### Phase 3. Разделить root policy и object policy + +Borrowed pattern: + +- explicit conversation variables из Dify +- durable state semantics из LangGraph + +Цель: + +- окончательно закрепить различие между: + - `root frame` + - `selected object frame` + - `meta frame` + +Что обновить концептуально: + +- root state живет дольше и используется шире; +- object state переносится только в совместимых сценариях; +- meta state работает поверх answer object/result object, а не replay-ит exact route вслепую. + +Ожидаемый результат: + +- меньше ложных carryover; +- меньше cross-domain contamination; +- меньше случайных уходов в generic chat. + +### Phase 4. Сделать capability exposure более декларативным + +Borrowed pattern: + +- tool/action registry from LibreChat / Open WebUI / Vanna / Onyx + +Цель: + +- capabilities должны быть first-class registry, а не только следствием разбросанной policy. + +Что обновить концептуально: + +- описывать capability не только через intent/resolver; +- дополнительно иметь явные поля: + - supported wording families; + - allowed anchor types; + - selected-object compatibility; + - root-context compatibility; + - meta-followup compatibility; + - answer mode; + - clarification policy. + +Ожидаемый результат: + +- меньше логики в giant service functions; +- больше declarative routing discipline. + +### Phase 5. Выделить answer policy как самостоятельный слой + +Borrowed pattern: + +- answer nodes / structured outputs из Dify и Vanna +- filters/boundary layers из Open WebUI + +Цель: + +- отделить: + - exact answer shape; + - limited answer shape; + - clarification shape; + - meta/explanatory shape; + - operational boundary shape. + +Что обновить концептуально: + +- не позволять routing fix-ам решаться только wording patch-ами; +- answer layer должен знать свой response class и формировать shape по контракту; +- `direct_answer_first` должен быть частью policy, а не удачной случайностью конкретного compose path. + +Ожидаемый результат: + +- ответы становятся устойчивее и предсказуемее даже при edge-cases. + +### Phase 6. Зафиксировать runtime checkpoints и replay-safe state + +Borrowed pattern: + +- durable execution / replay / idempotent tasks из LangGraph + +Цель: + +- сблизить session/navigation state с моделью checkpointed execution. + +Что обновить концептуально: + +- каждый важный state transition должен иметь machine-readable checkpoint semantics; +- follow-up interpretation должна опираться на сохраненные transition objects, а не только на текст прошлых сообщений; +- сценарные прогоны должны проверять не только answer text, но и корректность state transitions. + +Ожидаемый результат: + +- меньше регрессий, где exact route есть, но follow-up interpretation ломает доступ к нему. + +### Phase 7. Упорядочить provider and execution interfaces + +Borrowed pattern: + +- product shell abstraction из LibreChat / Open WebUI / Dify + +Цель: + +- постепенно превратить LLM gateway в более явный provider runtime layer. + +Что обновить концептуально: + +- развести: + - orchestration logic; + - provider execution; + - model-specific JSON / response normalization; + - local/openai compatibility concerns. + +Ожидаемый результат: + +- меньше provider-driven логики внутри business orchestration. + +### Phase 8. Сценарная приемка как главный architectural gate + +Borrowed pattern: + +- LLMOps/observability from Dify +- enterprise auditability from Onyx/Vanna + +Цель: + +- закрепить, что acceptance идет по scenario-tree, а не по отдельным красивым ответам. + +Что обновить концептуально: + +- любой architectural update считается успешным только если: + - сохраняется root path; + - сохраняются critical edges; + - сохраняется selected-object continuity; + - не деградирует limited truthfulness; + - не ломаются neighboring domains. + +Ожидаемый результат: + +- меньше "качелей", где один кейс улучшается ценой трех соседних. + + +## 17. Что не нужно делать + +В рамках update plan не нужно: + +1. Переписывать runtime под Dify/Open WebUI/LibreChat как платформенную миграцию. +2. Вводить "универсальный суперклассификатор", который обходит текущие rails. +3. Возвращать state обратно в transcript-only memory. +4. Маскировать state/policy проблемы только answer wording правками. +5. Смешивать Python `router/` и текущий TS assistant runtime без явного redesign note. +6. Жертвовать exact-data discipline ради более "естественного" чата. + + +## 18. Целевая формула обновленного проекта + +Если выражать желаемое состояние кратко, то проект должен двигаться к следующей форме: + +### 18.1 Слой проекта + +`1C/OData -> canonical store -> feature/risk data services -> assistant orchestration shell -> exact capabilities -> answer/debug contracts -> scenario acceptance loop` + +### 18.2 Архитектурная форма assistant runtime + +`thin router + explicit state transitions + declarative capability contracts + isolated answer policy + exact execution lane` + +### 18.3 Главный принцип + +`не делать систему "умнее любой ценой"` + +а + +`делать систему более явной, более контрактной и менее хрупкой`. + + +## 19. Итог + +На `2026-04-15` проект `NDC_1C` уже нельзя честно описывать как "обычный чат с LLM и другой базой". + +Правильнее описывать его так: + +- это read-only AI/data system поверх 1C; +- у нее есть canonical/data foundation; +- есть feature/risk layer; +- есть specialized exact-data assistant runtime; +- есть scenario-based hardening loop; +- и есть существенный orchestration debt, возникший из-за ручной эволюции. + +Поэтому правильный next move: + +`не ломать то, что уже собрано` + +и + +`не пытаться заменить систему generic framework-ом` + +а + +`собрать более явную architecture grammar поверх уже существующих рабочих слоев`. + +Это и есть safest update path по итогам локального анализа и внешних references. diff --git a/llm_normalizer/backend/dist/services/addressFilterExtractor.js b/llm_normalizer/backend/dist/services/addressFilterExtractor.js index 7854b2c..ad18bfa 100644 --- a/llm_normalizer/backend/dist/services/addressFilterExtractor.js +++ b/llm_normalizer/backend/dist/services/addressFilterExtractor.js @@ -1040,12 +1040,78 @@ function trimInventoryItemArrowSuffix(rawValue) { return cleanupAnchorValue(cleanupAnchorValue(rawValue).replace(/\s*(?:->|=>|→).+$/u, "")); } function isTemporalWarehousePhrase(candidate) { + const temporalZaPattern = /^(?:за)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu; + if (temporalZaPattern.test(cleanupAnchorValue(candidate).toLowerCase().replace(/ё/g, "е").trim())) { + return true; + } const normalized = cleanupAnchorValue(candidate) .toLowerCase() .replace(/ё/g, "е") .trim(); return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized); } +function isLowQualityWarehouseAnchorValue(rawValue) { + const value = cleanupAnchorValue(rawValue) + .toLowerCase() + .replace(/ё/g, "е") + .trim(); + if (!value) { + return true; + } + if (isTemporalWarehousePhrase(value) || isImplicitSelfScopeWarehouseAnchor(value)) { + return true; + } + const hasQuestionOrRepairCue = /(?:^|[\s,.;:!?()\-])(?:что|какой|какая|какие|как|где|когда|почему|зачем|имел(?:ось|ся)\s+в\s+виду|имеется\s+в\s+виду|в\s+смысле|то\s+есть|which|what|where|when|why)(?=$|[\s,.;:!?()\-])/iu.test(value) || /[?]/u.test(rawValue); + const hasProfanityCue = /(?:^|[\s,.;:!?()\-])(?:аху|оху|хуе|хуё|хуй|ебан|ебуч|бля|блять|пизд|нахуй|shit|fuck|damn)(?=$|[\s,.;:!?()\-])/iu.test(value); + const lowQualityTokens = new Set([ + "что", + "какой", + "какая", + "какие", + "как", + "где", + "когда", + "почему", + "зачем", + "имелось", + "имелся", + "имеется", + "в", + "виду", + "то", + "есть", + "лежит", + "лежат", + "лежало", + "лежали", + "на", + "по", + "складе", + "складу", + "складом", + "ебаном", + "ахуеть", + "охуеть", + "пиздец", + "блять", + "бля" + ]); + const tokens = value + .split(/[^a-zа-я0-9]+/iu) + .map((token) => token.trim()) + .filter(Boolean); + if (tokens.length === 0) { + return true; + } + const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1); + if (meaningfulTokens.length === 0) { + return true; + } + if ((hasQuestionOrRepairCue || hasProfanityCue) && meaningfulTokens.length <= 1) { + return true; + } + return false; +} function normalizeSemanticAnchorCandidate(value) { return cleanupAnchorValue(value) .toLowerCase() @@ -1079,6 +1145,7 @@ function extractInventoryWarehouseAnchor(text) { candidate.includes("->") || candidate.includes("=>") || isImplicitSelfScopeWarehouseAnchor(candidate) || + isLowQualityWarehouseAnchorValue(candidate) || normalizedCandidate.startsWith("по состоянию") || isTemporalWarehousePhrase(candidate) || /^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) { diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index 5805400..9461936 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1598,6 +1598,14 @@ function resolveAddressIntent(userMessage) { reasons: ["inventory_selected_object_sale_trace_signal_detected"] }; } + if (/(?:кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|кому\s+в\s+итоге\s+мы\s+впарили)/iu.test(text) && + /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text)) { + return { + intent: "inventory_sale_trace_for_item", + confidence: "medium", + reasons: ["inventory_sale_trace_signal_detected"] + }; + } if (hasInventorySaleTraceSignalV2(text)) { return { intent: "inventory_sale_trace_for_item", diff --git a/llm_normalizer/backend/dist/services/addressQueryService.js b/llm_normalizer/backend/dist/services/addressQueryService.js index 98acdbc..2f86037 100644 --- a/llm_normalizer/backend/dist/services/addressQueryService.js +++ b/llm_normalizer/backend/dist/services/addressQueryService.js @@ -1588,7 +1588,13 @@ function hasExplicitPeriodWindow(filters) { (typeof filters.period_to === "string" && filters.period_to.trim().length > 0)); } function canAutoBroadenPeriodWindow(intent, filters) { - if (!hasExplicitPeriodWindow(filters)) { + const hasRecoverableAsOfOnlyWindow = !hasExplicitPeriodWindow(filters) && + typeof filters.as_of_date === "string" && + filters.as_of_date.trim().length > 0 && + typeof filters.item === "string" && + filters.item.trim().length > 0 && + (intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item"); + if (!hasExplicitPeriodWindow(filters) && !hasRecoverableAsOfOnlyWindow) { return false; } return (intent === "list_documents_by_counterparty" || diff --git a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js index 405d977..e84cfa4 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js @@ -32,6 +32,9 @@ function hasAllTimeHint(text) { function hasSameDateHint(text) { return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? "")); } +function hasSamePeriodHint(text) { + return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? "")); +} function hasExplicitPeriodLiteral(text) { return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? "")); } @@ -383,10 +386,15 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, const currentFrameKind = followupContext.current_frame_kind ?? null; const previousIntent = followupContext.previous_intent; const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent); - if (!comingFromInventoryDrilldown) { + const normalized = String(userMessage ?? ""); + const hasInventoryRootRestatementCue = /(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) && + /(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized); + const canReenterInventoryRoot = comingFromInventoryDrilldown || + (currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) || + (currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized)); + if (!canReenterInventoryRoot) { return false; } - const normalized = String(userMessage ?? ""); if (hasSelectedObjectInventorySignal(normalized) || hasInventorySupplierFollowupCue(normalized) || hasInventoryPurchaseDocumentsFollowupCue(normalized) || @@ -401,6 +409,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, } const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) || Boolean(toNonEmptyString(extractedFilters.as_of_date)) || + hasSamePeriodHint(normalized) || hasExplicitPeriodLiteral(normalized) || Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext)); return hasTemporalPatch; @@ -408,6 +417,26 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, function hasSelectedObjectInventorySignal(text) { return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? "")); } +function hasSelectedObjectInlineSnapshotMetadata(text) { + return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(String(text ?? "")); +} +function extractSelectedObjectItemFromFollowupText(text) { + const rawSelectedObject = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(text)); + if (!rawSelectedObject) { + return null; + } + const firstLine = rawSelectedObject + .replace(/\r\n?/g, "\n") + .split("\n") + .map((line) => line.trim()) + .find(Boolean); + const primarySegment = String(firstLine ?? rawSelectedObject) + .replace(/^\d+\.\s*/, "") + .split("|")[0] + ?.trim(); + const normalized = toNonEmptyString(primarySegment); + return normalized; +} function hasInventorySupplierFollowupCue(text) { return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? "")); } @@ -506,6 +535,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) { const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext); const allTimeRequested = hasAllTimeHint(userMessage); const sameDateRequested = hasSameDateHint(userMessage); + const samePeriodRequested = hasSamePeriodHint(userMessage); if (!toNonEmptyString(merged.organization) && previousOrganization) { merged.organization = previousOrganization; reasons.push("organization_from_followup_context"); @@ -618,7 +648,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) { intent === "inventory_purchase_to_sale_chain" || intent === "inventory_aging_by_purchase_date")) { const inheritedItem = previousItem ?? previousAnchorItem; - const explicitQuotedItem = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)); + const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage); const currentItem = toNonEmptyString(merged.item); const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) && (!currentItem || @@ -653,6 +683,22 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) { reasons.push("as_of_date_from_followup_context"); } } + if (samePeriodRequested && + (intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")) { + if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) { + merged.period_from = previousPeriodFrom; + reasons.push("period_from_from_followup_context"); + } + if (previousPeriodTo && merged.period_to !== previousPeriodTo) { + merged.period_to = previousPeriodTo; + reasons.push("period_to_from_followup_context"); + } + const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom; + if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) { + merged.as_of_date = inheritedAsOfDate; + reasons.push("as_of_date_from_followup_context"); + } + } if (!sameDateRequested && (intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) && !hasExplicitPeriodLiteral(userMessage) && @@ -668,6 +714,21 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) { } } } + if ((Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) && + hasSelectedObjectInventorySignal(userMessage) && + hasSelectedObjectInlineSnapshotMetadata(userMessage) && + (intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item") && + !hasExplicitPeriodLiteral(userMessage) && + !hasExplicitCurrentDateHint(userMessage)) { + if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) { + merged.period_from = previousPeriodFrom; + reasons.push("period_from_from_followup_context"); + } + if (previousPeriodTo && merged.period_to !== previousPeriodTo) { + merged.period_to = previousPeriodTo; + reasons.push("period_to_from_followup_context"); + } + } if (!sameDateRequested && (intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") && !hasExplicitPeriodLiteral(userMessage) && diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js index 148df1f..689941b 100644 --- a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js @@ -38,6 +38,47 @@ function findLastGroundedInventoryAddressDebug(items) { } return null; } +function findLastAddressDebugWithItem(items) { + if (!Array.isArray(items)) { + return null; + } + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { + continue; + } + const debug = item.debug; + if (String(debug.execution_lane ?? "") !== "address_query") { + continue; + } + const extractedFilters = debug.extracted_filters && typeof debug.extracted_filters === "object" + ? debug.extracted_filters + : null; + const itemLabel = String(extractedFilters?.item ?? "").trim() || + (String(debug.anchor_type ?? "") === "item" + ? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim() + : ""); + if (itemLabel) { + return debug; + } + } + return null; +} +function findLastAddressDebug(items) { + if (!Array.isArray(items)) { + return null; + } + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { + continue; + } + if (String(item.debug.execution_lane ?? "") === "address_query") { + return item.debug; + } + } + return null; +} function buildInventoryHistoryCapabilityFollowupReply(input) { const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" ? input.addressDebug.address_root_frame_context @@ -65,6 +106,42 @@ function buildInventoryHistoryCapabilityFollowupReply(input) { "Если хочешь, сразу покажу нужный исторический период." ].join("\n"); } +function buildAddressMemoryRecapReply(input) { + const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" + ? input.addressDebug.extracted_filters + : null; + const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" + ? input.addressDebug.address_root_frame_context + : null; + const item = input.toNonEmptyString(extractedFilters?.item) ?? + (String(input.addressDebug?.anchor_type ?? "") === "item" + ? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ?? + input.toNonEmptyString(input.addressDebug?.anchor_value_raw) + : null); + const organization = input.organization ?? + input.toNonEmptyString(extractedFilters?.organization) ?? + input.toNonEmptyString(rootFrameContext?.organization); + const scopedDate = formatIsoDateForReply(extractedFilters?.as_of_date) ?? + formatIsoDateForReply(rootFrameContext?.as_of_date) ?? + formatIsoDateForReply(extractedFilters?.period_to); + if (item) { + const datePart = scopedDate ? ` в срезе на ${scopedDate}` : ""; + const organizationPart = organization ? ` по компании «${organization}»` : ""; + return [ + `Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`, + "Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали." + ].join(" "); + } + if (organization || scopedDate) { + const organizationPart = organization ? ` по компании «${organization}»` : ""; + const datePart = scopedDate ? ` на ${scopedDate}` : ""; + return [ + `Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`, + "Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию." + ].join(" "); + } + return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг."; +} async function runAssistantLivingChatRuntime(input) { const userMessage = String(input.userMessage ?? ""); const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); @@ -83,9 +160,13 @@ async function runAssistantLivingChatRuntime(input) { let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization); const contextualInventoryHistoryCapabilityFollowup = input.modeDecision?.reason === "inventory_history_capability_followup_detected"; + const contextualMemoryRecapFollowup = input.modeDecision?.reason === "memory_recap_followup_detected"; const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup ? findLastGroundedInventoryAddressDebug(input.sessionItems) : null; + const lastMemoryAddressDebug = contextualMemoryRecapFollowup + ? findLastAddressDebugWithItem(input.sessionItems) ?? findLastAddressDebug(input.sessionItems) + : null; if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) { chatText = input.buildAssistantSafetyRefusalReply(); livingChatSource = "deterministic_safety_refusal"; @@ -139,6 +220,16 @@ async function runAssistantLivingChatRuntime(input) { activeOrganization = scopedOrganization ?? activeOrganization; livingChatSource = "deterministic_inventory_history_capability_contract"; } + else if (contextualMemoryRecapFollowup) { + const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; + chatText = buildAddressMemoryRecapReply({ + organization: scopedOrganization, + addressDebug: lastMemoryAddressDebug, + toNonEmptyString: input.toNonEmptyString + }); + activeOrganization = scopedOrganization ?? activeOrganization; + livingChatSource = "deterministic_memory_recap_contract"; + } else if (capabilityMetaQuery) { chatText = input.buildAssistantCapabilityContractReply(); livingChatSource = "deterministic_capability_contract"; diff --git a/llm_normalizer/backend/dist/services/assistantRuntimeContractRegistry.js b/llm_normalizer/backend/dist/services/assistantRuntimeContractRegistry.js new file mode 100644 index 0000000..98f6df7 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantRuntimeContractRegistry.js @@ -0,0 +1,286 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.INVENTORY_CAPABILITY_CONTRACTS = exports.ASSISTANT_TRANSITION_CONTRACTS = void 0; +exports.listAssistantTransitionContracts = listAssistantTransitionContracts; +exports.getAssistantTransitionContract = getAssistantTransitionContract; +exports.listInventoryCapabilityContracts = listInventoryCapabilityContracts; +exports.getAssistantCapabilityContract = getAssistantCapabilityContract; +exports.getAssistantCapabilityContractByIntent = getAssistantCapabilityContractByIntent; +const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts"); +exports.ASSISTANT_TRANSITION_CONTRACTS = [ + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T1", + title: "Root Query Entry", + trigger_class: "new_root_business_question", + required_prior_state: ["living_mode_state"], + allowed_carryover_depth: "none", + state_mutations: ["create_root_frame_state", "clear_selected_object_frame_state", "create_coverage_gate_state"], + forbidden_carryover: ["stale_focus_object", "stale_object_intent"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T2", + title: "Root Follow-Up With Date Or Scope Change", + trigger_class: "root_followup_temporal_or_organization_shift", + required_prior_state: ["root_frame_state"], + allowed_carryover_depth: "root_only", + state_mutations: ["update_root_frame_state", "run_exact_route", "refresh_coverage_gate_state"], + forbidden_carryover: ["incompatible_selected_object_route"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T3", + title: "Explicit Selected Object Drilldown", + trigger_class: "explicit_selected_object_or_ui_object_selection", + required_prior_state: ["root_frame_state"], + allowed_carryover_depth: "object_only", + state_mutations: ["create_selected_object_frame_state", "bind_source_result_set", "preserve_temporal_ceiling"], + forbidden_carryover: ["unrelated_prior_focus_object"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T4", + title: "Short Action Follow-Up On Selected Object", + trigger_class: "short_action_followup_on_active_focus_object", + required_prior_state: ["selected_object_frame_state"], + allowed_carryover_depth: "object_only", + state_mutations: ["reuse_selected_object_frame_state", "route_to_compatible_item_action"], + forbidden_carryover: ["generic_chat_fallback", "data_scope_selection_fallback", "object_focus_reset"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T5", + title: "Pronoun Or Compressed Object Follow-Up", + trigger_class: "pronoun_or_compressed_reference_to_active_focus_object", + required_prior_state: ["selected_object_frame_state"], + allowed_carryover_depth: "object_only", + state_mutations: ["reuse_selected_object_frame_state", "resolve_pronoun_to_focus_object"], + forbidden_carryover: ["low_quality_object_rewrite", "semantic_noise_as_anchor"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T6", + title: "Domain Pivot With Root-Only Carryover", + trigger_class: "supported_domain_pivot_from_active_drilldown", + required_prior_state: ["root_frame_state", "selected_object_frame_state"], + allowed_carryover_depth: "root_only", + state_mutations: ["preserve_root_frame_state", "drop_selected_object_frame_state"], + forbidden_carryover: ["object_route_replay_into_new_domain"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T7", + title: "Clarification Continuation", + trigger_class: "user_resolves_missing_anchor_or_scope", + required_prior_state: ["clarification_state"], + allowed_carryover_depth: "full", + state_mutations: ["resume_target_route", "update_or_clear_clarification_state"], + forbidden_carryover: ["forget_suspended_route"], + expected_answer_mode: "confirmed" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T8", + title: "Meta Follow-Up Over Answer Object", + trigger_class: "evaluation_comparison_or_interpretation_of_previous_answer", + required_prior_state: ["answer_context_state", "coverage_gate_state"], + allowed_carryover_depth: "meta_only", + state_mutations: ["create_meta_frame_state", "reuse_answer_object_without_blind_replay"], + forbidden_carryover: ["blind_exact_route_replay"], + expected_answer_mode: "meta" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T9", + title: "Memory Recap", + trigger_class: "conversation_memory_recap_request", + required_prior_state: ["answer_context_state"], + allowed_carryover_depth: "meta_only", + state_mutations: ["reuse_grounded_prior_answer_context"], + forbidden_carryover: ["invented_conversation_memory"], + expected_answer_mode: "recap" + }, + { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T10", + title: "Unsupported Or Blocked Boundary", + trigger_class: "unsupported_route_or_blocked_evidence_gate", + required_prior_state: ["coverage_gate_state"], + allowed_carryover_depth: "none", + state_mutations: ["emit_bounded_boundary_or_clarification"], + forbidden_carryover: ["blocked_as_confirmed_factual_answer"], + expected_answer_mode: "boundary" + } +]; +const SHARED_INVENTORY_ACCEPTANCE_FAMILIES = [ + "canonical", + "colloquial", + "ui_selected_object", + "ui_selected_object_colloquial", + "short_action_followup", + "pronoun_followup", + "followup_date_carryover" +]; +const INVENTORY_ITEM_ANCHOR_RULES = [ + "no_low_quality_item_rewrite", + "no_numeric_tail_account_poisoning", + "no_conversational_noise_as_entity", + "confirmed_focus_object_beats_semantic_hint" +]; +const INVENTORY_SELECTED_OBJECT_TESTS = [ + "selected_object_memory_survives_short_followup", + "new_explicit_selected_object_overrides_old_focus", + "full_anchor_not_degraded_by_canonical_rewrite" +]; +function inventoryExactCapability(input) { + return { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + capability_id: input.capability_id, + domain_id: "inventory_stock", + runtime_lane: "address_exact", + intent_ids: input.intent_ids, + entry_modes: input.entry_modes, + supported_transition_classes: input.transitions, + frame_compatibility: { + root_frame: input.entry_modes.includes("root_entry") ? "optional" : "required", + selected_object_frame: input.requiresFocusObject ? "required" : "optional", + meta_frame: "forbidden" + }, + required_anchors: input.requiredAnchors, + optional_anchors: ["organization", "warehouse", "date_scope"], + anchor_source_priority: ["explicit_user_anchor", "ui_selected_object", "selected_object_frame", "root_frame", "semantic_hint"], + anchor_admissibility_rules: [...INVENTORY_ITEM_ANCHOR_RULES], + organization_scope_behavior: "reuse_or_clarify", + date_scope_behavior: "reuse", + temporal_ceiling_policy: input.requiresFocusObject ? "respect_root_temporal_ceiling" : "must_not_expand_without_reason_code", + root_context_compatibility: "required", + requires_focus_object: input.requiresFocusObject, + accepted_focus_object_kinds: input.requiresFocusObject ? ["inventory_item", "item"] : [], + focus_object_override_policy: input.requiresFocusObject ? "explicit_new_object_wins" : "not_applicable", + bundle_reuse_policy: input.bundleReusePolicy, + resolver_owner: "addressIntentResolver", + recipe_owner: "addressRecipeCatalog", + execution_adapter: "AddressQueryService", + result_shape: input.resultShape, + answer_object_shape: input.answerObjectShape, + minimum_evidence_policy: "route_specific_threshold", + coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient", + truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"], + blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"], + clarification_triggers: ["missing_required_item_anchor", "ambiguous_organization_scope", "ambiguous_date_scope"], + clarification_questions: ["Уточните товар, организацию или дату, чтобы не подставлять неподтвержденный anchor."], + resume_policy: "resume_original_route_with_resolved_anchors", + empty_match_behavior: "truthful_empty_match", + route_expectation_failure_behavior: "blocked_route_expectation_failure", + execution_error_behavior: "blocked_execution_error", + required_unit_tests: input.requiresFocusObject + ? [...INVENTORY_SELECTED_OBJECT_TESTS, "limited_mode_remains_truthful"] + : ["root_context_survives_domain_pivot_without_object_leak", "limited_mode_remains_truthful"], + required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`), + required_scenario_families: input.scenarioFamilies ?? [...SHARED_INVENTORY_ACCEPTANCE_FAMILIES] + }; +} +exports.INVENTORY_CAPABILITY_CONTRACTS = [ + inventoryExactCapability({ + capability_id: "confirmed_inventory_on_hand_as_of_date", + intent_ids: ["inventory_on_hand_as_of_date"], + entry_modes: ["root_entry", "root_followup", "clarification_resume"], + transitions: ["T1", "T2", "T7"], + requiresFocusObject: false, + requiredAnchors: [], + resultShape: "item_list_with_quantity_cost_warehouse_organization", + answerObjectShape: "inventory_stock_snapshot", + bundleReusePolicy: "none", + scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"] + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_purchase_provenance_for_item", + intent_ids: ["inventory_purchase_provenance_for_item"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "supplier_purchase_provenance_trace", + answerObjectShape: "inventory_provenance_bundle", + bundleReusePolicy: "provenance_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_purchase_documents_for_item", + intent_ids: ["inventory_purchase_documents_for_item"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "purchase_document_list_for_selected_item", + answerObjectShape: "inventory_purchase_documents_bundle", + bundleReusePolicy: "provenance_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date", + intent_ids: ["inventory_supplier_stock_overlap_as_of_date"], + entry_modes: ["root_entry", "root_followup", "clarification_resume"], + transitions: ["T1", "T2", "T7"], + requiresFocusObject: false, + requiredAnchors: ["supplier"], + resultShape: "supplier_to_stock_item_overlap", + answerObjectShape: "inventory_supplier_overlap", + bundleReusePolicy: "none", + scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"] + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_sale_trace_for_item", + intent_ids: ["inventory_sale_trace_for_item"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "buyer_sale_trace_for_selected_item", + answerObjectShape: "inventory_sale_trace_bundle", + bundleReusePolicy: "sale_trace_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_purchase_to_sale_chain", + intent_ids: ["inventory_purchase_to_sale_chain"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "purchase_stock_sale_document_chain", + answerObjectShape: "inventory_purchase_to_sale_chain", + bundleReusePolicy: "sale_trace_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_aging_by_purchase_date", + intent_ids: ["inventory_aging_by_purchase_date"], + entry_modes: ["root_entry", "root_followup", "clarification_resume"], + transitions: ["T1", "T2", "T6", "T7"], + requiresFocusObject: false, + requiredAnchors: [], + resultShape: "oldest_first_inventory_aging_list", + answerObjectShape: "inventory_aging_snapshot", + bundleReusePolicy: "none", + scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"] + }) +]; +function listAssistantTransitionContracts() { + return exports.ASSISTANT_TRANSITION_CONTRACTS; +} +function getAssistantTransitionContract(transitionId) { + return exports.ASSISTANT_TRANSITION_CONTRACTS.find((contract) => contract.transition_id === transitionId) ?? null; +} +function listInventoryCapabilityContracts() { + return exports.INVENTORY_CAPABILITY_CONTRACTS; +} +function getAssistantCapabilityContract(capabilityId) { + return exports.INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null; +} +function getAssistantCapabilityContractByIntent(intent) { + return exports.INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null; +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 6435ae5..f756850 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -2508,6 +2508,32 @@ function isInventoryDrilldownFrameIntent(intent) { intent === "inventory_purchase_to_sale_chain" || intent === "inventory_aging_by_purchase_date"; } +function resolveAddressIntentFamily(intent) { + const normalizedIntent = toNonEmptyString(intent); + if (!normalizedIntent) { + return null; + } + if (normalizedIntent.startsWith("inventory_")) { + return "inventory"; + } + if (normalizedIntent.startsWith("vat_")) { + return "vat"; + } + if (normalizedIntent === "account_balance_snapshot" || normalizedIntent === "documents_forming_balance") { + return "balance"; + } + if (normalizedIntent === "open_items_by_counterparty_or_contract" || + normalizedIntent === "list_documents_by_counterparty" || + normalizedIntent === "bank_operations_by_counterparty" || + normalizedIntent === "list_contracts_by_counterparty" || + normalizedIntent === "list_documents_by_contract" || + normalizedIntent === "bank_operations_by_contract" || + normalizedIntent === "receivables_confirmed_for_period" || + normalizedIntent === "payables_confirmed_for_period") { + return "settlements"; + } + return null; +} function extractAddressCarryoverAnchor(addressDebug) { if (!isAddressLaneDebugPayload(addressDebug)) { return { @@ -2877,6 +2903,18 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes ? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null : false; const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal; + const hasStrongFollowupReference = hasPrimaryIndexReferenceSignal || + hasAlternateIndexReferenceSignal || + hasOrganizationClarificationContinuation || + hasImplicitContinuationSignal || + inventoryShortFollowupPrimary || + inventoryShortFollowupAlternate || + Boolean(debtRoleSwapIntent) || + hasFollowupMarker(userMessage) || + hasReferentialPointer(userMessage) || + (toNonEmptyString(alternateMessage) + ? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? "")) + : false); const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) || (toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false); if (hasStandaloneAddressTopic && @@ -2898,6 +2936,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes return null; } const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent); + const llmExplicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); + const resolvedPrimaryIntent = (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(userMessage ?? ""))).intent; + const resolvedAlternateIntent = toNonEmptyString(alternateMessage) + ? (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(alternateMessage ?? ""))).intent + : null; + const explicitIntent = llmExplicitIntent && llmExplicitIntent !== "unknown" + ? llmExplicitIntent + : resolvedPrimaryIntent && resolvedPrimaryIntent !== "unknown" + ? resolvedPrimaryIntent + : resolvedAlternateIntent && resolvedAlternateIntent !== "unknown" + ? resolvedAlternateIntent + : null; + const sourceIntentFamily = resolveAddressIntentFamily(sourceIntent); + const explicitIntentFamily = resolveAddressIntentFamily(explicitIntent); + if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) { + return null; + } let previousIntent = sourceIntent; let followupSelectionMode = "carry_previous_intent"; if (debtRoleSwapIntent) { @@ -4375,6 +4430,17 @@ function resolveAssistantOrchestrationDecision(input) { hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) || hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) && isGroundedInventoryContextDebug(lastGroundedAddressDebug)); + const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal && + !strongDataSignal && + !aggregateBusinessAnalyticsSignal && + (hasConversationMemoryRecallFollowupSignal(rawUserMessage) || + hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) || + hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) || + hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) && + (lastGroundedAddressDebug || + findLastAddressAssistantItem(sessionItems)?.debug)); const hardMetaMode = dataScopeMetaQuery ? "data_scope" : capabilityMetaQuery && !dataRetrievalSignal @@ -4465,6 +4531,34 @@ function resolveAssistantOrchestrationDecision(input) { }; } if (nonDomainQueryIndexed) { + if (contextualMemoryRecapFollowupDetected) { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "memory_recap_followup_detected", + livingMode: "chat", + livingReason: "memory_recap_followup_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "non_domain", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "memory_recap_followup_detected", + living_mode: "chat", + living_reason: "memory_recap_followup_detected" + } + } + }; + } return { runAddressLane: false, toolGateDecision: "skip_address_lane", @@ -4569,6 +4663,9 @@ function resolveAssistantOrchestrationDecision(input) { const vatExplainFollowupSignal = Boolean(followupContext && toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && /(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); + const vatEvaluativeFollowupSignal = Boolean(followupContext && + toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && + /(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane && !llmRuntimeUnavailableDetected && (deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) && @@ -4599,7 +4696,7 @@ function resolveAssistantOrchestrationDecision(input) { const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); const metaFollowupOverGroundedAnswer = Boolean(followupContext && hasPriorAddressAnswerContext && - metaAnswerFollowupSignal && + (metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) && !dataScopeMetaQuery && !capabilityMetaQuery && !aggregateBusinessAnalyticsSignal && @@ -4844,7 +4941,28 @@ function hasMetaAnswerFollowupSignal(userMessage) { sample.includes("по этому поводу") || sample.includes("об этом") || (sample.includes("это") && hasReferentialPointer(sample))); - if (!(hasReflectionCue && hasTopicPointerCue)) { + const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample)); + if (!((hasReflectionCue || hasEvaluationCue) && + (hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) { + return false; + } + return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || + shouldHandleAsAssistantCapabilityMetaQuery(sample) || + hasDataRetrievalRequestSignal(sample) || + hasStrongDataIntentSignal(sample)); +} +function hasConversationMemoryRecallFollowupSignal(userMessage) { + const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + const samples = [rawText, repairedText] + .filter((item) => item.length > 0) + .map((item) => item.replace(/ё/g, "е")); + if (samples.length === 0) { + return false; + } + const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample)); + const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample)); + if (!hasMemoryCue || !hasDiscussionCue) { return false; } return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || @@ -4920,6 +5038,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) if (hasSelectionCue) { return true; } + const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС…|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|РїРёР·РґР°|РЅРёС…СѓСЏ|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) || + normalized.includes("\u0430\u0445\u0443") || + normalized.includes("\u043e\u0445\u0443") || + normalized.includes("\u043f\u0438\u0437\u0434") || + normalized.includes("\u0431\u043b\u044f"); + if (hasAffectiveReactionCue) { + return false; + } return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? "")); } function hasOperationalAdminActionRequestSignal(text) { diff --git a/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js b/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js index 9734a7c..6e532da 100644 --- a/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js +++ b/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js @@ -11,7 +11,10 @@ function hasInventoryPurchaseStem(text) { } function hasInventorySupplierCue(text) { const value = toText(text); - if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) { + if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) { + return true; + } + if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) { return true; } return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value); @@ -21,11 +24,11 @@ function hasInventorySaleCue(text) { if (/(?:buyer|покупател)/iu.test(value)) { return true; } - if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) { + if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?)/iu.test(value)) { return true; } const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value); - const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(value); + const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(value); if (hasDirectionCue && hasSaleVerb) { return true; } diff --git a/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js b/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js new file mode 100644 index 0000000..f0416fb --- /dev/null +++ b/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = void 0; +exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1"; diff --git a/llm_normalizer/backend/src/services/addressFilterExtractor.ts b/llm_normalizer/backend/src/services/addressFilterExtractor.ts index fe6171a..1d67dab 100644 --- a/llm_normalizer/backend/src/services/addressFilterExtractor.ts +++ b/llm_normalizer/backend/src/services/addressFilterExtractor.ts @@ -1191,6 +1191,75 @@ function isTemporalWarehousePhrase(candidate: string): boolean { ); } +function isLowQualityWarehouseAnchorValue(rawValue: string): boolean { + const value = cleanupAnchorValue(rawValue) + .toLowerCase() + .replace(/ё/g, "е") + .trim(); + if (!value) { + return true; + } + if (isTemporalWarehousePhrase(value) || isImplicitSelfScopeWarehouseAnchor(value)) { + return true; + } + const hasQuestionOrRepairCue = + /(?:^|[\s,.;:!?()\-])(?:что|какой|какая|какие|как|где|когда|почему|зачем|имел(?:ось|ся)\s+в\s+виду|имеется\s+в\s+виду|в\s+смысле|то\s+есть|which|what|where|when|why)(?=$|[\s,.;:!?()\-])/iu.test( + value + ) || /[?]/u.test(rawValue); + const hasProfanityCue = + /(?:^|[\s,.;:!?()\-])(?:аху|оху|хуе|хуё|хуй|ебан|ебуч|бля|блять|пизд|нахуй|shit|fuck|damn)(?=$|[\s,.;:!?()\-])/iu.test( + value + ); + const lowQualityTokens = new Set([ + "что", + "какой", + "какая", + "какие", + "как", + "где", + "когда", + "почему", + "зачем", + "имелось", + "имелся", + "имеется", + "в", + "виду", + "то", + "есть", + "лежит", + "лежат", + "лежало", + "лежали", + "на", + "по", + "складе", + "складу", + "складом", + "ебаном", + "ахуеть", + "охуеть", + "пиздец", + "блять", + "бля" + ]); + const tokens = value + .split(/[^a-zа-я0-9]+/iu) + .map((token) => token.trim()) + .filter(Boolean); + if (tokens.length === 0) { + return true; + } + const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1); + if (meaningfulTokens.length === 0) { + return true; + } + if ((hasQuestionOrRepairCue || hasProfanityCue) && meaningfulTokens.length <= 1) { + return true; + } + return false; +} + function normalizeSemanticAnchorCandidate(value: string): string { return cleanupAnchorValue(value) .toLowerCase() @@ -1236,6 +1305,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined { candidate.includes("->") || candidate.includes("=>") || isImplicitSelfScopeWarehouseAnchor(candidate) || + isLowQualityWarehouseAnchorValue(candidate) || normalizedCandidate.startsWith("по состоянию") || isTemporalWarehousePhrase(candidate) || /^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate) diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 8d61abf..1ac376a 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -1949,6 +1949,17 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti }; } + if ( + /(?:кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|кому\s+в\s+итоге\s+мы\s+впарили)/iu.test(text) && + /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text) + ) { + return { + intent: "inventory_sale_trace_for_item", + confidence: "medium", + reasons: ["inventory_sale_trace_signal_detected"] + }; + } + if (hasInventorySaleTraceSignalV2(text)) { return { intent: "inventory_sale_trace_for_item", diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index d7883c6..ead432e 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -1978,7 +1978,14 @@ function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean { } function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean { - if (!hasExplicitPeriodWindow(filters)) { + const hasRecoverableAsOfOnlyWindow = + !hasExplicitPeriodWindow(filters) && + typeof filters.as_of_date === "string" && + filters.as_of_date.trim().length > 0 && + typeof filters.item === "string" && + filters.item.trim().length > 0 && + (intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item"); + if (!hasExplicitPeriodWindow(filters) && !hasRecoverableAsOfOnlyWindow) { return false; } return ( diff --git a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts index 15fc396..2f07ade 100644 --- a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts @@ -533,6 +533,30 @@ function hasSelectedObjectInventorySignal(text: string): boolean { return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? "")); } +function hasSelectedObjectInlineSnapshotMetadata(text: string): boolean { + return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test( + String(text ?? "") + ); +} + +function extractSelectedObjectItemFromFollowupText(text: string): string | null { + const rawSelectedObject = toNonEmptyString(extractSelectedObjectQuotedValue(text)); + if (!rawSelectedObject) { + return null; + } + const firstLine = rawSelectedObject + .replace(/\r\n?/g, "\n") + .split("\n") + .map((line) => line.trim()) + .find(Boolean); + const primarySegment = String(firstLine ?? rawSelectedObject) + .replace(/^\d+\.\s*/, "") + .split("|")[0] + ?.trim(); + const normalized = toNonEmptyString(primarySegment); + return normalized; +} + export function hasInventorySupplierFollowupCue(text: string): boolean { return hasInventorySupplierCue(String(text ?? "")); } @@ -800,7 +824,7 @@ function mergeFollowupFilters( intent === "inventory_aging_by_purchase_date") ) { const inheritedItem = previousItem ?? previousAnchorItem; - const explicitQuotedItem = toNonEmptyString(extractSelectedObjectQuotedValue(userMessage)); + const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage); const currentItem = toNonEmptyString(merged.item); const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) && @@ -873,6 +897,23 @@ function mergeFollowupFilters( } } } + if ( + (Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) && + hasSelectedObjectInventorySignal(userMessage) && + hasSelectedObjectInlineSnapshotMetadata(userMessage) && + (intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item") && + !hasExplicitPeriodLiteral(userMessage) && + !hasExplicitCurrentDateHint(userMessage) + ) { + if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) { + merged.period_from = previousPeriodFrom; + reasons.push("period_from_from_followup_context"); + } + if (previousPeriodTo && merged.period_to !== previousPeriodTo) { + merged.period_to = previousPeriodTo; + reasons.push("period_to_from_followup_context"); + } + } if ( !sameDateRequested && (intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") && diff --git a/llm_normalizer/backend/src/services/assistantRuntimeContractRegistry.ts b/llm_normalizer/backend/src/services/assistantRuntimeContractRegistry.ts new file mode 100644 index 0000000..4a63b35 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantRuntimeContractRegistry.ts @@ -0,0 +1,306 @@ +import { + ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + type AssistantCapabilityContract, + type AssistantTransitionClassId, + type AssistantTransitionContract +} from "../types/assistantRuntimeContracts"; +import type { AddressIntent } from "../types/addressQuery"; + +export const ASSISTANT_TRANSITION_CONTRACTS: readonly AssistantTransitionContract[] = [ + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T1", + title: "Root Query Entry", + trigger_class: "new_root_business_question", + required_prior_state: ["living_mode_state"], + allowed_carryover_depth: "none", + state_mutations: ["create_root_frame_state", "clear_selected_object_frame_state", "create_coverage_gate_state"], + forbidden_carryover: ["stale_focus_object", "stale_object_intent"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T2", + title: "Root Follow-Up With Date Or Scope Change", + trigger_class: "root_followup_temporal_or_organization_shift", + required_prior_state: ["root_frame_state"], + allowed_carryover_depth: "root_only", + state_mutations: ["update_root_frame_state", "run_exact_route", "refresh_coverage_gate_state"], + forbidden_carryover: ["incompatible_selected_object_route"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T3", + title: "Explicit Selected Object Drilldown", + trigger_class: "explicit_selected_object_or_ui_object_selection", + required_prior_state: ["root_frame_state"], + allowed_carryover_depth: "object_only", + state_mutations: ["create_selected_object_frame_state", "bind_source_result_set", "preserve_temporal_ceiling"], + forbidden_carryover: ["unrelated_prior_focus_object"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T4", + title: "Short Action Follow-Up On Selected Object", + trigger_class: "short_action_followup_on_active_focus_object", + required_prior_state: ["selected_object_frame_state"], + allowed_carryover_depth: "object_only", + state_mutations: ["reuse_selected_object_frame_state", "route_to_compatible_item_action"], + forbidden_carryover: ["generic_chat_fallback", "data_scope_selection_fallback", "object_focus_reset"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T5", + title: "Pronoun Or Compressed Object Follow-Up", + trigger_class: "pronoun_or_compressed_reference_to_active_focus_object", + required_prior_state: ["selected_object_frame_state"], + allowed_carryover_depth: "object_only", + state_mutations: ["reuse_selected_object_frame_state", "resolve_pronoun_to_focus_object"], + forbidden_carryover: ["low_quality_object_rewrite", "semantic_noise_as_anchor"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T6", + title: "Domain Pivot With Root-Only Carryover", + trigger_class: "supported_domain_pivot_from_active_drilldown", + required_prior_state: ["root_frame_state", "selected_object_frame_state"], + allowed_carryover_depth: "root_only", + state_mutations: ["preserve_root_frame_state", "drop_selected_object_frame_state"], + forbidden_carryover: ["object_route_replay_into_new_domain"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T7", + title: "Clarification Continuation", + trigger_class: "user_resolves_missing_anchor_or_scope", + required_prior_state: ["clarification_state"], + allowed_carryover_depth: "full", + state_mutations: ["resume_target_route", "update_or_clear_clarification_state"], + forbidden_carryover: ["forget_suspended_route"], + expected_answer_mode: "confirmed" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T8", + title: "Meta Follow-Up Over Answer Object", + trigger_class: "evaluation_comparison_or_interpretation_of_previous_answer", + required_prior_state: ["answer_context_state", "coverage_gate_state"], + allowed_carryover_depth: "meta_only", + state_mutations: ["create_meta_frame_state", "reuse_answer_object_without_blind_replay"], + forbidden_carryover: ["blind_exact_route_replay"], + expected_answer_mode: "meta" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T9", + title: "Memory Recap", + trigger_class: "conversation_memory_recap_request", + required_prior_state: ["answer_context_state"], + allowed_carryover_depth: "meta_only", + state_mutations: ["reuse_grounded_prior_answer_context"], + forbidden_carryover: ["invented_conversation_memory"], + expected_answer_mode: "recap" + }, + { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_id: "T10", + title: "Unsupported Or Blocked Boundary", + trigger_class: "unsupported_route_or_blocked_evidence_gate", + required_prior_state: ["coverage_gate_state"], + allowed_carryover_depth: "none", + state_mutations: ["emit_bounded_boundary_or_clarification"], + forbidden_carryover: ["blocked_as_confirmed_factual_answer"], + expected_answer_mode: "boundary" + } +] as const; + +const SHARED_INVENTORY_ACCEPTANCE_FAMILIES = [ + "canonical", + "colloquial", + "ui_selected_object", + "ui_selected_object_colloquial", + "short_action_followup", + "pronoun_followup", + "followup_date_carryover" +] as const; + +const INVENTORY_ITEM_ANCHOR_RULES = [ + "no_low_quality_item_rewrite", + "no_numeric_tail_account_poisoning", + "no_conversational_noise_as_entity", + "confirmed_focus_object_beats_semantic_hint" +] as const; + +const INVENTORY_SELECTED_OBJECT_TESTS = [ + "selected_object_memory_survives_short_followup", + "new_explicit_selected_object_overrides_old_focus", + "full_anchor_not_degraded_by_canonical_rewrite" +] as const; + +function inventoryExactCapability(input: { + capability_id: string; + intent_ids: AddressIntent[]; + entry_modes: AssistantCapabilityContract["entry_modes"]; + transitions: AssistantTransitionClassId[]; + requiresFocusObject: boolean; + requiredAnchors: string[]; + resultShape: string; + answerObjectShape: string; + bundleReusePolicy: AssistantCapabilityContract["bundle_reuse_policy"]; + scenarioFamilies?: string[]; +}): AssistantCapabilityContract { + return { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + capability_id: input.capability_id, + domain_id: "inventory_stock", + runtime_lane: "address_exact", + intent_ids: input.intent_ids, + entry_modes: input.entry_modes, + supported_transition_classes: input.transitions, + frame_compatibility: { + root_frame: input.entry_modes.includes("root_entry") ? "optional" : "required", + selected_object_frame: input.requiresFocusObject ? "required" : "optional", + meta_frame: "forbidden" + }, + required_anchors: input.requiredAnchors, + optional_anchors: ["organization", "warehouse", "date_scope"], + anchor_source_priority: ["explicit_user_anchor", "ui_selected_object", "selected_object_frame", "root_frame", "semantic_hint"], + anchor_admissibility_rules: [...INVENTORY_ITEM_ANCHOR_RULES], + organization_scope_behavior: "reuse_or_clarify", + date_scope_behavior: "reuse", + temporal_ceiling_policy: input.requiresFocusObject ? "respect_root_temporal_ceiling" : "must_not_expand_without_reason_code", + root_context_compatibility: "required", + requires_focus_object: input.requiresFocusObject, + accepted_focus_object_kinds: input.requiresFocusObject ? ["inventory_item", "item"] : [], + focus_object_override_policy: input.requiresFocusObject ? "explicit_new_object_wins" : "not_applicable", + bundle_reuse_policy: input.bundleReusePolicy, + resolver_owner: "addressIntentResolver", + recipe_owner: "addressRecipeCatalog", + execution_adapter: "AddressQueryService", + result_shape: input.resultShape, + answer_object_shape: input.answerObjectShape, + minimum_evidence_policy: "route_specific_threshold", + coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient", + truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"], + blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"], + clarification_triggers: ["missing_required_item_anchor", "ambiguous_organization_scope", "ambiguous_date_scope"], + clarification_questions: ["Уточните товар, организацию или дату, чтобы не подставлять неподтвержденный anchor."], + resume_policy: "resume_original_route_with_resolved_anchors", + empty_match_behavior: "truthful_empty_match", + route_expectation_failure_behavior: "blocked_route_expectation_failure", + execution_error_behavior: "blocked_execution_error", + required_unit_tests: input.requiresFocusObject + ? [...INVENTORY_SELECTED_OBJECT_TESTS, "limited_mode_remains_truthful"] + : ["root_context_survives_domain_pivot_without_object_leak", "limited_mode_remains_truthful"], + required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`), + required_scenario_families: input.scenarioFamilies ?? [...SHARED_INVENTORY_ACCEPTANCE_FAMILIES] + }; +} + +export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [ + inventoryExactCapability({ + capability_id: "confirmed_inventory_on_hand_as_of_date", + intent_ids: ["inventory_on_hand_as_of_date"], + entry_modes: ["root_entry", "root_followup", "clarification_resume"], + transitions: ["T1", "T2", "T7"], + requiresFocusObject: false, + requiredAnchors: [], + resultShape: "item_list_with_quantity_cost_warehouse_organization", + answerObjectShape: "inventory_stock_snapshot", + bundleReusePolicy: "none", + scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"] + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_purchase_provenance_for_item", + intent_ids: ["inventory_purchase_provenance_for_item"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "supplier_purchase_provenance_trace", + answerObjectShape: "inventory_provenance_bundle", + bundleReusePolicy: "provenance_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_purchase_documents_for_item", + intent_ids: ["inventory_purchase_documents_for_item"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "purchase_document_list_for_selected_item", + answerObjectShape: "inventory_purchase_documents_bundle", + bundleReusePolicy: "provenance_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date", + intent_ids: ["inventory_supplier_stock_overlap_as_of_date"], + entry_modes: ["root_entry", "root_followup", "clarification_resume"], + transitions: ["T1", "T2", "T7"], + requiresFocusObject: false, + requiredAnchors: ["supplier"], + resultShape: "supplier_to_stock_item_overlap", + answerObjectShape: "inventory_supplier_overlap", + bundleReusePolicy: "none", + scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"] + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_sale_trace_for_item", + intent_ids: ["inventory_sale_trace_for_item"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "buyer_sale_trace_for_selected_item", + answerObjectShape: "inventory_sale_trace_bundle", + bundleReusePolicy: "sale_trace_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_purchase_to_sale_chain", + intent_ids: ["inventory_purchase_to_sale_chain"], + entry_modes: ["selected_object_drilldown", "clarification_resume"], + transitions: ["T3", "T4", "T5", "T7"], + requiresFocusObject: true, + requiredAnchors: ["item"], + resultShape: "purchase_stock_sale_document_chain", + answerObjectShape: "inventory_purchase_to_sale_chain", + bundleReusePolicy: "sale_trace_bundle_preferred" + }), + inventoryExactCapability({ + capability_id: "inventory_inventory_aging_by_purchase_date", + intent_ids: ["inventory_aging_by_purchase_date"], + entry_modes: ["root_entry", "root_followup", "clarification_resume"], + transitions: ["T1", "T2", "T6", "T7"], + requiresFocusObject: false, + requiredAnchors: [], + resultShape: "oldest_first_inventory_aging_list", + answerObjectShape: "inventory_aging_snapshot", + bundleReusePolicy: "none", + scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"] + }) +] as const; + +export function listAssistantTransitionContracts(): readonly AssistantTransitionContract[] { + return ASSISTANT_TRANSITION_CONTRACTS; +} + +export function getAssistantTransitionContract(transitionId: AssistantTransitionClassId): AssistantTransitionContract | null { + return ASSISTANT_TRANSITION_CONTRACTS.find((contract) => contract.transition_id === transitionId) ?? null; +} + +export function listInventoryCapabilityContracts(): readonly AssistantCapabilityContract[] { + return INVENTORY_CAPABILITY_CONTRACTS; +} + +export function getAssistantCapabilityContract(capabilityId: string): AssistantCapabilityContract | null { + return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null; +} + +export function getAssistantCapabilityContractByIntent(intent: AddressIntent): AssistantCapabilityContract | null { + return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index a87bddb..fa3b6f5 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -4997,6 +4997,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) if (hasSelectionCue) { return true; } + const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС…|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|РїРёР·РґР°|РЅРёС…СѓСЏ|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) || + normalized.includes("\u0430\u0445\u0443") || + normalized.includes("\u043e\u0445\u0443") || + normalized.includes("\u043f\u0438\u0437\u0434") || + normalized.includes("\u0431\u043b\u044f"); + if (hasAffectiveReactionCue) { + return false; + } return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? "")); } function hasOperationalAdminActionRequestSignal(text) { diff --git a/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts b/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts index 285641c..6432a1b 100644 --- a/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts +++ b/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts @@ -26,12 +26,12 @@ export function hasInventorySaleCue(text: string): boolean { if (/(?:buyer|покупател)/iu.test(value)) { return true; } - if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) { + if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?)/iu.test(value)) { return true; } const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value); const hasSaleVerb = - /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test( + /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test( value ); if (hasDirectionCue && hasSaleVerb) { diff --git a/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts b/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts new file mode 100644 index 0000000..17cc204 --- /dev/null +++ b/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts @@ -0,0 +1,154 @@ +import type { AddressIntent } from "./addressQuery"; + +export const ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1" as const; + +export type AssistantLivingMode = "address_data" | "assistant_data_scope" | "chat" | "meta_followup" | "clarification"; +export type AssistantFrameStatus = "active" | "suspended" | "closed" | "blocked"; +export type AssistantTransitionClassId = "T1" | "T2" | "T3" | "T4" | "T5" | "T6" | "T7" | "T8" | "T9" | "T10"; +export type AssistantStateSlice = + | "living_mode_state" + | "root_frame_state" + | "selected_object_frame_state" + | "meta_frame_state" + | "clarification_state" + | "coverage_gate_state" + | "answer_context_state"; +export type AssistantCarryoverDepth = "full" | "root_only" | "object_only" | "meta_only" | "none"; +export type AssistantAnswerMode = "confirmed" | "limited" | "clarification" | "boundary" | "meta" | "recap"; + +export interface AssistantDateScopeState { + as_of_date: string | null; + period_from: string | null; + period_to: string | null; +} + +export interface AssistantRootFrameState { + domain_id: string | null; + root_route_id: string | null; + organization_scope: string | null; + date_scope: AssistantDateScopeState; + root_result_set_id: string | null; + root_answer_object_ref: string | null; + frame_status: AssistantFrameStatus; +} + +export interface AssistantSelectedObjectFrameState { + focus_object_ref: string | null; + focus_object_kind: string | null; + source_result_set_id: string | null; + compatible_route_family: string[]; + provenance_bundle_ref: string | null; + temporal_ceiling: AssistantDateScopeState; + frame_status: AssistantFrameStatus; +} + +export interface AssistantMetaFrameState { + source_answer_object_ref: string | null; + meta_question_kind: "evaluation" | "comparison" | "memory_recap" | "boundary_explanation" | "answer_interpretation" | null; + source_gate_status: AssistantCoverageGateState["coverage_status"] | null; + meta_truth_mode: AssistantCoverageGateState["truth_mode"] | null; +} + +export interface AssistantClarificationState { + clarification_kind: string | null; + missing_anchors: string[]; + candidate_scopes: string[]; + resume_target_route: string | null; + resume_target_frame: AssistantStateSlice | null; +} + +export interface AssistantCoverageGateState { + coverage_status: "full" | "partial" | "blocked"; + evidence_grade: "none" | "weak" | "medium" | "strong"; + grounding_status: "grounded" | "partial" | "route_mismatch_blocked" | "no_grounded_answer" | "unsupported"; + truth_mode: "confirmed" | "limited" | "clarification_required" | "unsupported"; + carryover_eligibility: AssistantCarryoverDepth; + reason_codes: string[]; +} + +export interface AssistantSessionAggregateState { + schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION; + living_mode_state: { + living_mode: AssistantLivingMode; + mode_reason: string | null; + mode_source: "router" | "transition" | "clarification" | "manual" | null; + mode_entry_turn_id: string | null; + }; + root_frame_state: AssistantRootFrameState | null; + selected_object_frame_state: AssistantSelectedObjectFrameState | null; + meta_frame_state: AssistantMetaFrameState | null; + clarification_state: AssistantClarificationState | null; + coverage_gate_state: AssistantCoverageGateState | null; + answer_context_state: Record | null; +} + +export interface AssistantTransitionContract { + schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION; + transition_id: AssistantTransitionClassId; + title: string; + trigger_class: string; + required_prior_state: AssistantStateSlice[]; + allowed_carryover_depth: AssistantCarryoverDepth; + state_mutations: string[]; + forbidden_carryover: string[]; + expected_answer_mode: AssistantAnswerMode; +} + +export type AssistantCapabilityEntryMode = + | "root_entry" + | "root_followup" + | "selected_object_drilldown" + | "meta_reuse" + | "clarification_resume"; +export type AssistantRuntimeLane = "address_exact" | "assistant_data_scope" | "chat" | "meta"; +export type AssistantFrameRequirement = "required" | "optional" | "forbidden"; +export type AssistantScopeBehavior = "create" | "reuse" | "reuse_or_clarify" | "narrow" | "reject" | "none"; +export type AssistantTemporalCeilingPolicy = "none" | "respect_root_temporal_ceiling" | "must_not_expand_without_reason_code"; +export type AssistantBundleReusePolicy = "none" | "provenance_bundle_preferred" | "sale_trace_bundle_preferred"; +export type AssistantCoverageGateBehavior = "full_required" | "partial_or_blocked_if_evidence_insufficient"; +export type AssistantTruthFallback = "limited" | "clarification_required" | "unsupported"; + +export interface AssistantCapabilityContract { + schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION; + capability_id: string; + domain_id: string; + runtime_lane: AssistantRuntimeLane; + intent_ids: AddressIntent[]; + entry_modes: AssistantCapabilityEntryMode[]; + supported_transition_classes: AssistantTransitionClassId[]; + frame_compatibility: { + root_frame: AssistantFrameRequirement; + selected_object_frame: AssistantFrameRequirement; + meta_frame: AssistantFrameRequirement; + }; + required_anchors: string[]; + optional_anchors: string[]; + anchor_source_priority: string[]; + anchor_admissibility_rules: string[]; + organization_scope_behavior: AssistantScopeBehavior; + date_scope_behavior: AssistantScopeBehavior; + temporal_ceiling_policy: AssistantTemporalCeilingPolicy; + root_context_compatibility: "required" | "optional" | "not_applicable"; + requires_focus_object: boolean; + accepted_focus_object_kinds: string[]; + focus_object_override_policy: "explicit_new_object_wins" | "preserve_existing" | "not_applicable"; + bundle_reuse_policy: AssistantBundleReusePolicy; + resolver_owner: string; + recipe_owner: string; + execution_adapter: string; + result_shape: string; + answer_object_shape: string; + minimum_evidence_policy: string; + coverage_gate_behavior: AssistantCoverageGateBehavior; + truth_mode_fallbacks: AssistantTruthFallback[]; + blocked_reason_codes: string[]; + clarification_triggers: string[]; + clarification_questions: string[]; + resume_policy: string; + empty_match_behavior: string; + route_expectation_failure_behavior: string; + execution_error_behavior: string; + required_unit_tests: string[]; + required_transition_tests: string[]; + required_scenario_families: string[]; +} diff --git a/llm_normalizer/backend/tests/addressInventoryWarehouseAnchor.test.ts b/llm_normalizer/backend/tests/addressInventoryWarehouseAnchor.test.ts index 8cce7b6..f14d49b 100644 --- a/llm_normalizer/backend/tests/addressInventoryWarehouseAnchor.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryWarehouseAnchor.test.ts @@ -47,4 +47,21 @@ describe("inventory warehouse anchor extraction", () => { expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current"); expect(result.semantic_frame?.date_basis_hint).toBe("implicit_current_snapshot"); }); + it("does not materialize profanity tail as warehouse anchor in slang stock query", () => { + const filters = extractAddressFilters( + "ассистент, рассказывай что нам на складе ебаном лежит", + "inventory_on_hand_as_of_date" + ).extracted_filters; + + expect(filters.warehouse).toBeUndefined(); + }); + + it("does not materialize repair phrasing as warehouse anchor in stock follow-up", () => { + const filters = extractAddressFilters( + "остатки на складе какие имелось в виду", + "inventory_on_hand_as_of_date" + ).extracted_filters; + + expect(filters.warehouse).toBeUndefined(); + }); }); diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index 4777e69..dde11a1 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -5038,6 +5038,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => { expect(result.intent).toBe("inventory_sale_trace_for_item"); }); + it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => { + const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?"); + expect(result.intent).toBe("inventory_sale_trace_for_item"); + }); + it("keeps inventory provenance wording out of inventory-on-hand routing", () => { const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); diff --git a/llm_normalizer/backend/tests/assistantLivingChatMode.test.ts b/llm_normalizer/backend/tests/assistantLivingChatMode.test.ts index 3fd72a1..6fd08a3 100644 --- a/llm_normalizer/backend/tests/assistantLivingChatMode.test.ts +++ b/llm_normalizer/backend/tests/assistantLivingChatMode.test.ts @@ -1064,4 +1064,65 @@ describe("assistant living chat mode", () => { expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1); expect(chatClient.chat).toHaveBeenCalledTimes(0); }); + it("does not treat short emotional reaction as organization-selection confirmation", async () => { + const normalizer = { + normalize: vi.fn().mockResolvedValue({ + ok: false, + trace_id: "norm-chat-affective-reaction", + prompt_version: "normalizer_v2_0_2", + schema_version: "v2_0_2", + normalized: null, + validation: { passed: false, errors: ["mock"] }, + route_hint_summary: null, + raw_model_output: {}, + usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, + latency_ms: 1, + request_count_for_case: 1 + }) + } as any; + + const sessions = new AssistantSessionStore(); + const sessionId = "asst-living-chat-affective-reaction"; + sessions.ensureSession(sessionId); + sessions.appendItem(sessionId, { + message_id: "msg-seed-selected-org", + session_id: sessionId, + role: "assistant", + text: "Отлично, фиксирую рабочую организацию: ООО Альтернатива Плюс.", + reply_type: "factual_with_explanation", + created_at: new Date().toISOString(), + trace_id: "chat-seed-selected-org", + debug: { + assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"], + assistant_selected_organization: "ООО Альтернатива Плюс", + assistant_active_organization: "ООО Альтернатива Плюс" + } + } as any); + + const addressQueryService = { + tryHandle: vi.fn().mockResolvedValue({ handled: false }) + } as any; + const chatClient = { + chat: vi.fn().mockResolvedValue({ + raw: { id: "chat-affective-reaction" }, + outputText: "Понимаю, это выглядит неприятно. Давай разберем следующий шаг.", + usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } + }) + } as any; + + const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient); + const response = await service.handleMessage({ + session_id: sessionId, + user_message: "ну ахуеть", + llmProvider: "local", + model: "qwen2.5", + useMock: false + } as any); + + expect(response.ok).toBe(true); + expect(response.reply_type).toBe("factual_with_explanation"); + expect(response.debug?.living_chat_response_source).not.toBe("deterministic_data_scope_selection_contract"); + expect(String(response.assistant_reply)).not.toContain("фиксирую рабочую организацию"); + expect(chatClient.chat).toHaveBeenCalledTimes(1); + }); }); diff --git a/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts b/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts new file mode 100644 index 0000000..39618ef --- /dev/null +++ b/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy"; +import { + getAssistantCapabilityContract, + getAssistantCapabilityContractByIntent, + getAssistantTransitionContract, + listAssistantTransitionContracts, + listInventoryCapabilityContracts +} from "../src/services/assistantRuntimeContractRegistry"; + +describe("assistant runtime contract registry", () => { + it("declares the architecture turnaround transition set T1-T10", () => { + const transitions = listAssistantTransitionContracts(); + expect(transitions.map((item) => item.transition_id)).toEqual(["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10"]); + + const ids = new Set(transitions.map((item) => item.transition_id)); + expect(ids.size).toBe(10); + expect(transitions.every((item) => item.schema_version === "assistant_runtime_contracts_v1")).toBe(true); + }); + + it("keeps selected-object action follow-ups object-scoped instead of generic-chat scoped", () => { + const transition = getAssistantTransitionContract("T4"); + expect(transition).not.toBeNull(); + expect(transition?.required_prior_state).toContain("selected_object_frame_state"); + expect(transition?.allowed_carryover_depth).toBe("object_only"); + expect(transition?.forbidden_carryover).toContain("generic_chat_fallback"); + expect(transition?.forbidden_carryover).toContain("object_focus_reset"); + }); + + it("declares meta follow-up as answer-object reuse, not blind exact-route replay", () => { + const transition = getAssistantTransitionContract("T8"); + expect(transition).not.toBeNull(); + expect(transition?.required_prior_state).toEqual(["answer_context_state", "coverage_gate_state"]); + expect(transition?.allowed_carryover_depth).toBe("meta_only"); + expect(transition?.state_mutations).toContain("reuse_answer_object_without_blind_replay"); + expect(transition?.forbidden_carryover).toContain("blind_exact_route_replay"); + expect(transition?.expected_answer_mode).toBe("meta"); + }); + + it("keeps pilot inventory capability ids aligned with the current address capability policy", () => { + for (const contract of listInventoryCapabilityContracts()) { + for (const intent of contract.intent_ids) { + const decision = resolveAddressCapabilityRouteDecision(intent); + expect(decision.capability_id).toBe(contract.capability_id); + expect(decision.capability_route_mode).toBe("exact"); + } + } + }); + + it("declares root inventory snapshot as root-capable and focus-object-free", () => { + const contract = getAssistantCapabilityContract("confirmed_inventory_on_hand_as_of_date"); + expect(contract).not.toBeNull(); + expect(contract?.entry_modes).toEqual(["root_entry", "root_followup", "clarification_resume"]); + expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T7"]); + expect(contract?.requires_focus_object).toBe(false); + expect(contract?.result_shape).toBe("item_list_with_quantity_cost_warehouse_organization"); + expect(contract?.required_scenario_families).toContain("colloquial"); + }); + + it("declares selected-item provenance as focus-object and bundle-aware", () => { + const contract = getAssistantCapabilityContractByIntent("inventory_purchase_provenance_for_item"); + expect(contract?.capability_id).toBe("inventory_inventory_purchase_provenance_for_item"); + expect(contract?.requires_focus_object).toBe(true); + expect(contract?.accepted_focus_object_kinds).toEqual(["inventory_item", "item"]); + expect(contract?.supported_transition_classes).toEqual(["T3", "T4", "T5", "T7"]); + expect(contract?.required_anchors).toEqual(["item"]); + expect(contract?.bundle_reuse_policy).toBe("provenance_bundle_preferred"); + expect(contract?.anchor_admissibility_rules).toContain("confirmed_focus_object_beats_semantic_hint"); + expect(contract?.required_scenario_families).toContain("ui_selected_object_colloquial"); + expect(contract?.required_scenario_families).toContain("pronoun_followup"); + }); + + it("keeps truth semantics outside answer wording for every pilot inventory capability", () => { + for (const contract of listInventoryCapabilityContracts()) { + expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient"); + expect(contract.truth_mode_fallbacks).toEqual(["limited", "clarification_required", "unsupported"]); + expect(contract.blocked_reason_codes).toEqual( + expect.arrayContaining(["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"]) + ); + expect(contract.route_expectation_failure_behavior).toBe("blocked_route_expectation_failure"); + expect(contract.execution_error_behavior).toBe("blocked_execution_error"); + } + }); +});