АРЧ АП11 - Commit title: Добавить контрактный слой переходов и capability-деклараций ассистента
This commit is contained in:
parent
8056bdfaf2
commit
93ad18daa3
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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:
|
||||||
|
|
||||||
|
- <https://github.com/langgenius/dify>
|
||||||
|
- <https://docs.dify.ai/versions/3-0-x/en/user-guide/workflow/key-concepts>
|
||||||
|
- <https://docs.dify.ai/en/use-dify/nodes/agent>
|
||||||
|
- <https://docs.dify.ai/en/use-dify/nodes/variable-assigner>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- <https://github.com/open-webui/open-webui>
|
||||||
|
- <https://docs.openwebui.com/features/extensibility/>
|
||||||
|
- <https://docs.openwebui.com/features/chat-conversations/rag/>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- <https://github.com/onyx-dot-app/onyx>
|
||||||
|
- <https://docs.onyx.app/admins/actions/overview>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- <https://github.com/danny-avila/LibreChat>
|
||||||
|
- <https://www.librechat.ai/docs/features/agents>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- <https://github.com/vanna-ai/vanna>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- <https://github.com/eosphoros-ai/DB-GPT>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- <https://docs.langchain.com/oss/javascript/langgraph/durable-execution>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -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](</x:/1C/NDC_1C/docs/ARCH/11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md:1>)
|
||||||
|
|
||||||
|
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."
|
||||||
|
|
@ -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](</x:/1C/NDC_1C/docs/ARCH/11 - architecture_turnaround/README.md:1>)
|
||||||
|
|
||||||
|
Документ нужен не для исторического обзора, а как единая опорная карта:
|
||||||
|
|
||||||
|
- что в проекте уже является устойчивым фундаментом;
|
||||||
|
- что является техническим и архитектурным долгом;
|
||||||
|
- где проект типовой по классу систем;
|
||||||
|
- где проект уже имеет осмысленную кастомную специализацию;
|
||||||
|
- как переходить от 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: <https://github.com/langgenius/dify>
|
||||||
|
- Dify docs, key concepts: <https://docs.dify.ai/versions/3-0-x/en/user-guide/workflow/key-concepts>
|
||||||
|
- Dify docs, agent node: <https://docs.dify.ai/en/use-dify/nodes/agent>
|
||||||
|
- Dify docs, variable assigner: <https://docs.dify.ai/en/use-dify/nodes/variable-assigner>
|
||||||
|
- Open WebUI: <https://github.com/open-webui/open-webui>
|
||||||
|
- Open WebUI docs, extensibility: <https://docs.openwebui.com/features/extensibility/>
|
||||||
|
- Open WebUI docs, RAG: <https://docs.openwebui.com/features/chat-conversations/rag/>
|
||||||
|
- Onyx: <https://github.com/onyx-dot-app/onyx>
|
||||||
|
- Onyx docs, actions overview: <https://docs.onyx.app/admins/actions/overview>
|
||||||
|
- LibreChat: <https://github.com/danny-avila/LibreChat>
|
||||||
|
- LibreChat docs, agents: <https://www.librechat.ai/docs/features/agents>
|
||||||
|
- Vanna: <https://github.com/vanna-ai/vanna>
|
||||||
|
- DB-GPT: <https://github.com/eosphoros-ai/DB-GPT>
|
||||||
|
- LangGraph docs, durable execution: <https://docs.langchain.com/oss/javascript/langgraph/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.
|
||||||
|
|
@ -1040,12 +1040,78 @@ function trimInventoryItemArrowSuffix(rawValue) {
|
||||||
return cleanupAnchorValue(cleanupAnchorValue(rawValue).replace(/\s*(?:->|=>|→).+$/u, ""));
|
return cleanupAnchorValue(cleanupAnchorValue(rawValue).replace(/\s*(?:->|=>|→).+$/u, ""));
|
||||||
}
|
}
|
||||||
function isTemporalWarehousePhrase(candidate) {
|
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)
|
const normalized = cleanupAnchorValue(candidate)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.trim();
|
.trim();
|
||||||
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized);
|
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) {
|
function normalizeSemanticAnchorCandidate(value) {
|
||||||
return cleanupAnchorValue(value)
|
return cleanupAnchorValue(value)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -1079,6 +1145,7 @@ function extractInventoryWarehouseAnchor(text) {
|
||||||
candidate.includes("->") ||
|
candidate.includes("->") ||
|
||||||
candidate.includes("=>") ||
|
candidate.includes("=>") ||
|
||||||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
|
isImplicitSelfScopeWarehouseAnchor(candidate) ||
|
||||||
|
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||||
normalizedCandidate.startsWith("по состоянию") ||
|
normalizedCandidate.startsWith("по состоянию") ||
|
||||||
isTemporalWarehousePhrase(candidate) ||
|
isTemporalWarehousePhrase(candidate) ||
|
||||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {
|
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {
|
||||||
|
|
|
||||||
|
|
@ -1598,6 +1598,14 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
|
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)) {
|
if (hasInventorySaleTraceSignalV2(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "inventory_sale_trace_for_item",
|
intent: "inventory_sale_trace_for_item",
|
||||||
|
|
|
||||||
|
|
@ -1588,7 +1588,13 @@ function hasExplicitPeriodWindow(filters) {
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
||||||
}
|
}
|
||||||
function canAutoBroadenPeriodWindow(intent, filters) {
|
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 false;
|
||||||
}
|
}
|
||||||
return (intent === "list_documents_by_counterparty" ||
|
return (intent === "list_documents_by_counterparty" ||
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ function hasAllTimeHint(text) {
|
||||||
function hasSameDateHint(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 ?? ""));
|
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) {
|
function hasExplicitPeriodLiteral(text) {
|
||||||
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(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 currentFrameKind = followupContext.current_frame_kind ?? null;
|
||||||
const previousIntent = followupContext.previous_intent;
|
const previousIntent = followupContext.previous_intent;
|
||||||
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
const normalized = String(userMessage ?? "");
|
|
||||||
if (hasSelectedObjectInventorySignal(normalized) ||
|
if (hasSelectedObjectInventorySignal(normalized) ||
|
||||||
hasInventorySupplierFollowupCue(normalized) ||
|
hasInventorySupplierFollowupCue(normalized) ||
|
||||||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
||||||
|
|
@ -401,6 +409,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||||
}
|
}
|
||||||
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
|
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
|
||||||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
||||||
|
hasSamePeriodHint(normalized) ||
|
||||||
hasExplicitPeriodLiteral(normalized) ||
|
hasExplicitPeriodLiteral(normalized) ||
|
||||||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
||||||
return hasTemporalPatch;
|
return hasTemporalPatch;
|
||||||
|
|
@ -408,6 +417,26 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||||
function hasSelectedObjectInventorySignal(text) {
|
function hasSelectedObjectInventorySignal(text) {
|
||||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(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) {
|
function hasInventorySupplierFollowupCue(text) {
|
||||||
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
|
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -506,6 +535,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
const sameDateRequested = hasSameDateHint(userMessage);
|
const sameDateRequested = hasSameDateHint(userMessage);
|
||||||
|
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
||||||
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
||||||
merged.organization = previousOrganization;
|
merged.organization = previousOrganization;
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
|
|
@ -618,7 +648,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date")) {
|
intent === "inventory_aging_by_purchase_date")) {
|
||||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||||
const explicitQuotedItem = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage));
|
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
||||||
const currentItem = toNonEmptyString(merged.item);
|
const currentItem = toNonEmptyString(merged.item);
|
||||||
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
|
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
|
||||||
(!currentItem ||
|
(!currentItem ||
|
||||||
|
|
@ -653,6 +683,22 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
reasons.push("as_of_date_from_followup_context");
|
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 &&
|
if (!sameDateRequested &&
|
||||||
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
|
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
|
||||||
!hasExplicitPeriodLiteral(userMessage) &&
|
!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 &&
|
if (!sameDateRequested &&
|
||||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
||||||
!hasExplicitPeriodLiteral(userMessage) &&
|
!hasExplicitPeriodLiteral(userMessage) &&
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,47 @@ function findLastGroundedInventoryAddressDebug(items) {
|
||||||
}
|
}
|
||||||
return null;
|
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) {
|
function buildInventoryHistoryCapabilityFollowupReply(input) {
|
||||||
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
|
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
|
||||||
? input.addressDebug.address_root_frame_context
|
? input.addressDebug.address_root_frame_context
|
||||||
|
|
@ -65,6 +106,42 @@ function buildInventoryHistoryCapabilityFollowupReply(input) {
|
||||||
"Если хочешь, сразу покажу нужный исторический период."
|
"Если хочешь, сразу покажу нужный исторический период."
|
||||||
].join("\n");
|
].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) {
|
async function runAssistantLivingChatRuntime(input) {
|
||||||
const userMessage = String(input.userMessage ?? "");
|
const userMessage = String(input.userMessage ?? "");
|
||||||
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
||||||
|
|
@ -83,9 +160,13 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
||||||
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
|
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
|
||||||
const contextualInventoryHistoryCapabilityFollowup = input.modeDecision?.reason === "inventory_history_capability_followup_detected";
|
const contextualInventoryHistoryCapabilityFollowup = input.modeDecision?.reason === "inventory_history_capability_followup_detected";
|
||||||
|
const contextualMemoryRecapFollowup = input.modeDecision?.reason === "memory_recap_followup_detected";
|
||||||
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
|
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
|
||||||
? findLastGroundedInventoryAddressDebug(input.sessionItems)
|
? findLastGroundedInventoryAddressDebug(input.sessionItems)
|
||||||
: null;
|
: null;
|
||||||
|
const lastMemoryAddressDebug = contextualMemoryRecapFollowup
|
||||||
|
? findLastAddressDebugWithItem(input.sessionItems) ?? findLastAddressDebug(input.sessionItems)
|
||||||
|
: null;
|
||||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||||
chatText = input.buildAssistantSafetyRefusalReply();
|
chatText = input.buildAssistantSafetyRefusalReply();
|
||||||
livingChatSource = "deterministic_safety_refusal";
|
livingChatSource = "deterministic_safety_refusal";
|
||||||
|
|
@ -139,6 +220,16 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||||
livingChatSource = "deterministic_inventory_history_capability_contract";
|
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) {
|
else if (capabilityMetaQuery) {
|
||||||
chatText = input.buildAssistantCapabilityContractReply();
|
chatText = input.buildAssistantCapabilityContractReply();
|
||||||
livingChatSource = "deterministic_capability_contract";
|
livingChatSource = "deterministic_capability_contract";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -2508,6 +2508,32 @@ function isInventoryDrilldownFrameIntent(intent) {
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date";
|
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) {
|
function extractAddressCarryoverAnchor(addressDebug) {
|
||||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -2877,6 +2903,18 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||||
: false;
|
: false;
|
||||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
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) ||
|
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||||
if (hasStandaloneAddressTopic &&
|
if (hasStandaloneAddressTopic &&
|
||||||
|
|
@ -2898,6 +2936,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
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 previousIntent = sourceIntent;
|
||||||
let followupSelectionMode = "carry_previous_intent";
|
let followupSelectionMode = "carry_previous_intent";
|
||||||
if (debtRoleSwapIntent) {
|
if (debtRoleSwapIntent) {
|
||||||
|
|
@ -4375,6 +4430,17 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
|
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
|
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
|
||||||
isGroundedInventoryContextDebug(lastGroundedAddressDebug));
|
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
|
const hardMetaMode = dataScopeMetaQuery
|
||||||
? "data_scope"
|
? "data_scope"
|
||||||
: capabilityMetaQuery && !dataRetrievalSignal
|
: capabilityMetaQuery && !dataRetrievalSignal
|
||||||
|
|
@ -4465,6 +4531,34 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (nonDomainQueryIndexed) {
|
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 {
|
return {
|
||||||
runAddressLane: false,
|
runAddressLane: false,
|
||||||
toolGateDecision: "skip_address_lane",
|
toolGateDecision: "skip_address_lane",
|
||||||
|
|
@ -4569,6 +4663,9 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
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}`)));
|
/(?:\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 &&
|
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||||
!llmRuntimeUnavailableDetected &&
|
!llmRuntimeUnavailableDetected &&
|
||||||
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
||||||
|
|
@ -4599,7 +4696,7 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
|
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
|
||||||
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
|
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
|
||||||
hasPriorAddressAnswerContext &&
|
hasPriorAddressAnswerContext &&
|
||||||
metaAnswerFollowupSignal &&
|
(metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) &&
|
||||||
!dataScopeMetaQuery &&
|
!dataScopeMetaQuery &&
|
||||||
!capabilityMetaQuery &&
|
!capabilityMetaQuery &&
|
||||||
!aggregateBusinessAnalyticsSignal &&
|
!aggregateBusinessAnalyticsSignal &&
|
||||||
|
|
@ -4844,7 +4941,28 @@ function hasMetaAnswerFollowupSignal(userMessage) {
|
||||||
sample.includes("по этому поводу") ||
|
sample.includes("по этому поводу") ||
|
||||||
sample.includes("об этом") ||
|
sample.includes("об этом") ||
|
||||||
(sample.includes("это") && hasReferentialPointer(sample)));
|
(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 false;
|
||||||
}
|
}
|
||||||
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
|
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
|
||||||
|
|
@ -4920,6 +5038,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization)
|
||||||
if (hasSelectionCue) {
|
if (hasSelectionCue) {
|
||||||
return true;
|
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 ?? ""));
|
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||||
}
|
}
|
||||||
function hasOperationalAdminActionRequestSignal(text) {
|
function hasOperationalAdminActionRequestSignal(text) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ function hasInventoryPurchaseStem(text) {
|
||||||
}
|
}
|
||||||
function hasInventorySupplierCue(text) {
|
function hasInventorySupplierCue(text) {
|
||||||
const value = toText(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 true;
|
||||||
}
|
}
|
||||||
return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value);
|
return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value);
|
||||||
|
|
@ -21,11 +24,11 @@ function hasInventorySaleCue(text) {
|
||||||
if (/(?:buyer|покупател)/iu.test(value)) {
|
if (/(?:buyer|покупател)/iu.test(value)) {
|
||||||
return true;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
||||||
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(value);
|
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(value);
|
||||||
if (hasDirectionCue && hasSaleVerb) {
|
if (hasDirectionCue && hasSaleVerb) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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 {
|
function normalizeSemanticAnchorCandidate(value: string): string {
|
||||||
return cleanupAnchorValue(value)
|
return cleanupAnchorValue(value)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -1236,6 +1305,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
|
||||||
candidate.includes("->") ||
|
candidate.includes("->") ||
|
||||||
candidate.includes("=>") ||
|
candidate.includes("=>") ||
|
||||||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
|
isImplicitSelfScopeWarehouseAnchor(candidate) ||
|
||||||
|
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||||
normalizedCandidate.startsWith("по состоянию") ||
|
normalizedCandidate.startsWith("по состоянию") ||
|
||||||
isTemporalWarehousePhrase(candidate) ||
|
isTemporalWarehousePhrase(candidate) ||
|
||||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)
|
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
if (hasInventorySaleTraceSignalV2(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "inventory_sale_trace_for_item",
|
intent: "inventory_sale_trace_for_item",
|
||||||
|
|
|
||||||
|
|
@ -1978,7 +1978,14 @@ function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAutoBroadenPeriodWindow(intent: AddressIntent, 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 false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -533,6 +533,30 @@ function hasSelectedObjectInventorySignal(text: string): boolean {
|
||||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
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 {
|
export function hasInventorySupplierFollowupCue(text: string): boolean {
|
||||||
return hasInventorySupplierCue(String(text ?? ""));
|
return hasInventorySupplierCue(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -800,7 +824,7 @@ function mergeFollowupFilters(
|
||||||
intent === "inventory_aging_by_purchase_date")
|
intent === "inventory_aging_by_purchase_date")
|
||||||
) {
|
) {
|
||||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||||
const explicitQuotedItem = toNonEmptyString(extractSelectedObjectQuotedValue(userMessage));
|
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
||||||
const currentItem = toNonEmptyString(merged.item);
|
const currentItem = toNonEmptyString(merged.item);
|
||||||
const shouldAdoptExplicitQuotedItem =
|
const shouldAdoptExplicitQuotedItem =
|
||||||
Boolean(explicitQuotedItem) &&
|
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 (
|
if (
|
||||||
!sameDateRequested &&
|
!sameDateRequested &&
|
||||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -4997,6 +4997,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization)
|
||||||
if (hasSelectionCue) {
|
if (hasSelectionCue) {
|
||||||
return true;
|
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 ?? ""));
|
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||||
}
|
}
|
||||||
function hasOperationalAdminActionRequestSignal(text) {
|
function hasOperationalAdminActionRequestSignal(text) {
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@ export function hasInventorySaleCue(text: string): boolean {
|
||||||
if (/(?:buyer|покупател)/iu.test(value)) {
|
if (/(?:buyer|покупател)/iu.test(value)) {
|
||||||
return true;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
||||||
const hasSaleVerb =
|
const hasSaleVerb =
|
||||||
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(
|
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
if (hasDirectionCue && hasSaleVerb) {
|
if (hasDirectionCue && hasSaleVerb) {
|
||||||
|
|
|
||||||
|
|
@ -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<string, unknown> | 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[];
|
||||||
|
}
|
||||||
|
|
@ -47,4 +47,21 @@ describe("inventory warehouse anchor extraction", () => {
|
||||||
expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current");
|
expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current");
|
||||||
expect(result.semantic_frame?.date_basis_hint).toBe("implicit_current_snapshot");
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5038,6 +5038,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
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", () => {
|
it("keeps inventory provenance wording out of inventory-on-hand routing", () => {
|
||||||
const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?");
|
const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?");
|
||||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
|
|
|
||||||
|
|
@ -1064,4 +1064,65 @@ describe("assistant living chat mode", () => {
|
||||||
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
|
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
|
||||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue