АРЧ АП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, ""));
|
||||
}
|
||||
function isTemporalWarehousePhrase(candidate) {
|
||||
const temporalZaPattern = /^(?:за)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu;
|
||||
if (temporalZaPattern.test(cleanupAnchorValue(candidate).toLowerCase().replace(/ё/g, "е").trim())) {
|
||||
return true;
|
||||
}
|
||||
const normalized = cleanupAnchorValue(candidate)
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.trim();
|
||||
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized);
|
||||
}
|
||||
function isLowQualityWarehouseAnchorValue(rawValue) {
|
||||
const value = cleanupAnchorValue(rawValue)
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.trim();
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (isTemporalWarehousePhrase(value) || isImplicitSelfScopeWarehouseAnchor(value)) {
|
||||
return true;
|
||||
}
|
||||
const hasQuestionOrRepairCue = /(?:^|[\s,.;:!?()\-])(?:что|какой|какая|какие|как|где|когда|почему|зачем|имел(?:ось|ся)\s+в\s+виду|имеется\s+в\s+виду|в\s+смысле|то\s+есть|which|what|where|when|why)(?=$|[\s,.;:!?()\-])/iu.test(value) || /[?]/u.test(rawValue);
|
||||
const hasProfanityCue = /(?:^|[\s,.;:!?()\-])(?:аху|оху|хуе|хуё|хуй|ебан|ебуч|бля|блять|пизд|нахуй|shit|fuck|damn)(?=$|[\s,.;:!?()\-])/iu.test(value);
|
||||
const lowQualityTokens = new Set([
|
||||
"что",
|
||||
"какой",
|
||||
"какая",
|
||||
"какие",
|
||||
"как",
|
||||
"где",
|
||||
"когда",
|
||||
"почему",
|
||||
"зачем",
|
||||
"имелось",
|
||||
"имелся",
|
||||
"имеется",
|
||||
"в",
|
||||
"виду",
|
||||
"то",
|
||||
"есть",
|
||||
"лежит",
|
||||
"лежат",
|
||||
"лежало",
|
||||
"лежали",
|
||||
"на",
|
||||
"по",
|
||||
"складе",
|
||||
"складу",
|
||||
"складом",
|
||||
"ебаном",
|
||||
"ахуеть",
|
||||
"охуеть",
|
||||
"пиздец",
|
||||
"блять",
|
||||
"бля"
|
||||
]);
|
||||
const tokens = value
|
||||
.split(/[^a-zа-я0-9]+/iu)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean);
|
||||
if (tokens.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1);
|
||||
if (meaningfulTokens.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if ((hasQuestionOrRepairCue || hasProfanityCue) && meaningfulTokens.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function normalizeSemanticAnchorCandidate(value) {
|
||||
return cleanupAnchorValue(value)
|
||||
.toLowerCase()
|
||||
|
|
@ -1079,6 +1145,7 @@ function extractInventoryWarehouseAnchor(text) {
|
|||
candidate.includes("->") ||
|
||||
candidate.includes("=>") ||
|
||||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
|
||||
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||
normalizedCandidate.startsWith("по состоянию") ||
|
||||
isTemporalWarehousePhrase(candidate) ||
|
||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {
|
||||
|
|
|
|||
|
|
@ -1598,6 +1598,14 @@ function resolveAddressIntent(userMessage) {
|
|||
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
|
||||
};
|
||||
}
|
||||
if (/(?:кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|кому\s+в\s+итоге\s+мы\s+впарили)/iu.test(text) &&
|
||||
/(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text)) {
|
||||
return {
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
confidence: "medium",
|
||||
reasons: ["inventory_sale_trace_signal_detected"]
|
||||
};
|
||||
}
|
||||
if (hasInventorySaleTraceSignalV2(text)) {
|
||||
return {
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
|
|
|
|||
|
|
@ -1588,7 +1588,13 @@ function hasExplicitPeriodWindow(filters) {
|
|||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
||||
}
|
||||
function canAutoBroadenPeriodWindow(intent, filters) {
|
||||
if (!hasExplicitPeriodWindow(filters)) {
|
||||
const hasRecoverableAsOfOnlyWindow = !hasExplicitPeriodWindow(filters) &&
|
||||
typeof filters.as_of_date === "string" &&
|
||||
filters.as_of_date.trim().length > 0 &&
|
||||
typeof filters.item === "string" &&
|
||||
filters.item.trim().length > 0 &&
|
||||
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item");
|
||||
if (!hasExplicitPeriodWindow(filters) && !hasRecoverableAsOfOnlyWindow) {
|
||||
return false;
|
||||
}
|
||||
return (intent === "list_documents_by_counterparty" ||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ function hasAllTimeHint(text) {
|
|||
function hasSameDateHint(text) {
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasSamePeriodHint(text) {
|
||||
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitPeriodLiteral(text) {
|
||||
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
|
||||
}
|
||||
|
|
@ -383,10 +386,15 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
|||
const currentFrameKind = followupContext.current_frame_kind ?? null;
|
||||
const previousIntent = followupContext.previous_intent;
|
||||
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
||||
if (!comingFromInventoryDrilldown) {
|
||||
const normalized = String(userMessage ?? "");
|
||||
const hasInventoryRootRestatementCue = /(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) &&
|
||||
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
|
||||
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
|
||||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
||||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
||||
if (!canReenterInventoryRoot) {
|
||||
return false;
|
||||
}
|
||||
const normalized = String(userMessage ?? "");
|
||||
if (hasSelectedObjectInventorySignal(normalized) ||
|
||||
hasInventorySupplierFollowupCue(normalized) ||
|
||||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
||||
|
|
@ -401,6 +409,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
|||
}
|
||||
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
|
||||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
||||
hasSamePeriodHint(normalized) ||
|
||||
hasExplicitPeriodLiteral(normalized) ||
|
||||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
||||
return hasTemporalPatch;
|
||||
|
|
@ -408,6 +417,26 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
|||
function hasSelectedObjectInventorySignal(text) {
|
||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasSelectedObjectInlineSnapshotMetadata(text) {
|
||||
return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function extractSelectedObjectItemFromFollowupText(text) {
|
||||
const rawSelectedObject = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(text));
|
||||
if (!rawSelectedObject) {
|
||||
return null;
|
||||
}
|
||||
const firstLine = rawSelectedObject
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean);
|
||||
const primarySegment = String(firstLine ?? rawSelectedObject)
|
||||
.replace(/^\d+\.\s*/, "")
|
||||
.split("|")[0]
|
||||
?.trim();
|
||||
const normalized = toNonEmptyString(primarySegment);
|
||||
return normalized;
|
||||
}
|
||||
function hasInventorySupplierFollowupCue(text) {
|
||||
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
|
||||
}
|
||||
|
|
@ -506,6 +535,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||
const sameDateRequested = hasSameDateHint(userMessage);
|
||||
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
||||
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
||||
merged.organization = previousOrganization;
|
||||
reasons.push("organization_from_followup_context");
|
||||
|
|
@ -618,7 +648,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date")) {
|
||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||
const explicitQuotedItem = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage));
|
||||
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
||||
const currentItem = toNonEmptyString(merged.item);
|
||||
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
|
||||
(!currentItem ||
|
||||
|
|
@ -653,6 +683,22 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (samePeriodRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
reasons.push("period_from_from_followup_context");
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
reasons.push("period_to_from_followup_context");
|
||||
}
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!sameDateRequested &&
|
||||
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
|
||||
!hasExplicitPeriodLiteral(userMessage) &&
|
||||
|
|
@ -668,6 +714,21 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if ((Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) &&
|
||||
hasSelectedObjectInventorySignal(userMessage) &&
|
||||
hasSelectedObjectInlineSnapshotMetadata(userMessage) &&
|
||||
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item") &&
|
||||
!hasExplicitPeriodLiteral(userMessage) &&
|
||||
!hasExplicitCurrentDateHint(userMessage)) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
reasons.push("period_from_from_followup_context");
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
reasons.push("period_to_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!sameDateRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
||||
!hasExplicitPeriodLiteral(userMessage) &&
|
||||
|
|
|
|||
|
|
@ -38,6 +38,47 @@ function findLastGroundedInventoryAddressDebug(items) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function findLastAddressDebugWithItem(items) {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const debug = item.debug;
|
||||
if (String(debug.execution_lane ?? "") !== "address_query") {
|
||||
continue;
|
||||
}
|
||||
const extractedFilters = debug.extracted_filters && typeof debug.extracted_filters === "object"
|
||||
? debug.extracted_filters
|
||||
: null;
|
||||
const itemLabel = String(extractedFilters?.item ?? "").trim() ||
|
||||
(String(debug.anchor_type ?? "") === "item"
|
||||
? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim()
|
||||
: "");
|
||||
if (itemLabel) {
|
||||
return debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function findLastAddressDebug(items) {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (String(item.debug.execution_lane ?? "") === "address_query") {
|
||||
return item.debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function buildInventoryHistoryCapabilityFollowupReply(input) {
|
||||
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
|
||||
? input.addressDebug.address_root_frame_context
|
||||
|
|
@ -65,6 +106,42 @@ function buildInventoryHistoryCapabilityFollowupReply(input) {
|
|||
"Если хочешь, сразу покажу нужный исторический период."
|
||||
].join("\n");
|
||||
}
|
||||
function buildAddressMemoryRecapReply(input) {
|
||||
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
|
||||
? input.addressDebug.extracted_filters
|
||||
: null;
|
||||
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
|
||||
? input.addressDebug.address_root_frame_context
|
||||
: null;
|
||||
const item = input.toNonEmptyString(extractedFilters?.item) ??
|
||||
(String(input.addressDebug?.anchor_type ?? "") === "item"
|
||||
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
|
||||
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
|
||||
: null);
|
||||
const organization = input.organization ??
|
||||
input.toNonEmptyString(extractedFilters?.organization) ??
|
||||
input.toNonEmptyString(rootFrameContext?.organization);
|
||||
const scopedDate = formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
||||
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
||||
formatIsoDateForReply(extractedFilters?.period_to);
|
||||
if (item) {
|
||||
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
return [
|
||||
`Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`,
|
||||
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
|
||||
].join(" ");
|
||||
}
|
||||
if (organization || scopedDate) {
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
return [
|
||||
`Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`,
|
||||
"Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию."
|
||||
].join(" ");
|
||||
}
|
||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||
}
|
||||
async function runAssistantLivingChatRuntime(input) {
|
||||
const userMessage = String(input.userMessage ?? "");
|
||||
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
||||
|
|
@ -83,9 +160,13 @@ async function runAssistantLivingChatRuntime(input) {
|
|||
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
||||
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
|
||||
const contextualInventoryHistoryCapabilityFollowup = input.modeDecision?.reason === "inventory_history_capability_followup_detected";
|
||||
const contextualMemoryRecapFollowup = input.modeDecision?.reason === "memory_recap_followup_detected";
|
||||
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
|
||||
? findLastGroundedInventoryAddressDebug(input.sessionItems)
|
||||
: null;
|
||||
const lastMemoryAddressDebug = contextualMemoryRecapFollowup
|
||||
? findLastAddressDebugWithItem(input.sessionItems) ?? findLastAddressDebug(input.sessionItems)
|
||||
: null;
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = input.buildAssistantSafetyRefusalReply();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
|
|
@ -139,6 +220,16 @@ async function runAssistantLivingChatRuntime(input) {
|
|||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_inventory_history_capability_contract";
|
||||
}
|
||||
else if (contextualMemoryRecapFollowup) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = buildAddressMemoryRecapReply({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: lastMemoryAddressDebug,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_memory_recap_contract";
|
||||
}
|
||||
else if (capabilityMetaQuery) {
|
||||
chatText = input.buildAssistantCapabilityContractReply();
|
||||
livingChatSource = "deterministic_capability_contract";
|
||||
|
|
|
|||
|
|
@ -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_aging_by_purchase_date";
|
||||
}
|
||||
function resolveAddressIntentFamily(intent) {
|
||||
const normalizedIntent = toNonEmptyString(intent);
|
||||
if (!normalizedIntent) {
|
||||
return null;
|
||||
}
|
||||
if (normalizedIntent.startsWith("inventory_")) {
|
||||
return "inventory";
|
||||
}
|
||||
if (normalizedIntent.startsWith("vat_")) {
|
||||
return "vat";
|
||||
}
|
||||
if (normalizedIntent === "account_balance_snapshot" || normalizedIntent === "documents_forming_balance") {
|
||||
return "balance";
|
||||
}
|
||||
if (normalizedIntent === "open_items_by_counterparty_or_contract" ||
|
||||
normalizedIntent === "list_documents_by_counterparty" ||
|
||||
normalizedIntent === "bank_operations_by_counterparty" ||
|
||||
normalizedIntent === "list_contracts_by_counterparty" ||
|
||||
normalizedIntent === "list_documents_by_contract" ||
|
||||
normalizedIntent === "bank_operations_by_contract" ||
|
||||
normalizedIntent === "receivables_confirmed_for_period" ||
|
||||
normalizedIntent === "payables_confirmed_for_period") {
|
||||
return "settlements";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractAddressCarryoverAnchor(addressDebug) {
|
||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||
return {
|
||||
|
|
@ -2877,6 +2903,18 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
|||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||
: false;
|
||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||
const hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
hasFollowupMarker(userMessage) ||
|
||||
hasReferentialPointer(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic &&
|
||||
|
|
@ -2898,6 +2936,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
|||
return null;
|
||||
}
|
||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||||
const llmExplicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const resolvedPrimaryIntent = (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(userMessage ?? ""))).intent;
|
||||
const resolvedAlternateIntent = toNonEmptyString(alternateMessage)
|
||||
? (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(alternateMessage ?? ""))).intent
|
||||
: null;
|
||||
const explicitIntent = llmExplicitIntent && llmExplicitIntent !== "unknown"
|
||||
? llmExplicitIntent
|
||||
: resolvedPrimaryIntent && resolvedPrimaryIntent !== "unknown"
|
||||
? resolvedPrimaryIntent
|
||||
: resolvedAlternateIntent && resolvedAlternateIntent !== "unknown"
|
||||
? resolvedAlternateIntent
|
||||
: null;
|
||||
const sourceIntentFamily = resolveAddressIntentFamily(sourceIntent);
|
||||
const explicitIntentFamily = resolveAddressIntentFamily(explicitIntent);
|
||||
if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) {
|
||||
return null;
|
||||
}
|
||||
let previousIntent = sourceIntent;
|
||||
let followupSelectionMode = "carry_previous_intent";
|
||||
if (debtRoleSwapIntent) {
|
||||
|
|
@ -4375,6 +4430,17 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
|
||||
isGroundedInventoryContextDebug(lastGroundedAddressDebug));
|
||||
const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dataRetrievalSignal &&
|
||||
!strongDataSignal &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
(hasConversationMemoryRecallFollowupSignal(rawUserMessage) ||
|
||||
hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) ||
|
||||
hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) &&
|
||||
(lastGroundedAddressDebug ||
|
||||
findLastAddressAssistantItem(sessionItems)?.debug));
|
||||
const hardMetaMode = dataScopeMetaQuery
|
||||
? "data_scope"
|
||||
: capabilityMetaQuery && !dataRetrievalSignal
|
||||
|
|
@ -4465,6 +4531,34 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
};
|
||||
}
|
||||
if (nonDomainQueryIndexed) {
|
||||
if (contextualMemoryRecapFollowupDetected) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "memory_recap_followup_detected",
|
||||
livingMode: "chat",
|
||||
livingReason: "memory_recap_followup_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: "non_domain",
|
||||
address_mode: resolvedModeDetection.mode,
|
||||
address_mode_confidence: resolvedModeDetection.confidence,
|
||||
address_intent: resolvedIntentResolution.intent,
|
||||
address_intent_confidence: resolvedIntentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug),
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "memory_recap_followup_detected",
|
||||
living_mode: "chat",
|
||||
living_reason: "memory_recap_followup_detected"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
|
|
@ -4569,6 +4663,9 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||||
const vatEvaluativeFollowupSignal = Boolean(followupContext &&
|
||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||
/(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||||
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
||||
|
|
@ -4599,7 +4696,7 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
|
||||
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
|
||||
hasPriorAddressAnswerContext &&
|
||||
metaAnswerFollowupSignal &&
|
||||
(metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) &&
|
||||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
|
|
@ -4844,7 +4941,28 @@ function hasMetaAnswerFollowupSignal(userMessage) {
|
|||
sample.includes("по этому поводу") ||
|
||||
sample.includes("об этом") ||
|
||||
(sample.includes("это") && hasReferentialPointer(sample)));
|
||||
if (!(hasReflectionCue && hasTopicPointerCue)) {
|
||||
const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample));
|
||||
if (!((hasReflectionCue || hasEvaluationCue) &&
|
||||
(hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) {
|
||||
return false;
|
||||
}
|
||||
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(sample) ||
|
||||
hasDataRetrievalRequestSignal(sample) ||
|
||||
hasStrongDataIntentSignal(sample));
|
||||
}
|
||||
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||
const samples = [rawText, repairedText]
|
||||
.filter((item) => item.length > 0)
|
||||
.map((item) => item.replace(/ё/g, "е"));
|
||||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample));
|
||||
const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample));
|
||||
if (!hasMemoryCue || !hasDiscussionCue) {
|
||||
return false;
|
||||
}
|
||||
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
|
||||
|
|
@ -4920,6 +5038,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization)
|
|||
if (hasSelectionCue) {
|
||||
return true;
|
||||
}
|
||||
const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:ну|мда|ох|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|пизда|нихуя|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) ||
|
||||
normalized.includes("\u0430\u0445\u0443") ||
|
||||
normalized.includes("\u043e\u0445\u0443") ||
|
||||
normalized.includes("\u043f\u0438\u0437\u0434") ||
|
||||
normalized.includes("\u0431\u043b\u044f");
|
||||
if (hasAffectiveReactionCue) {
|
||||
return false;
|
||||
}
|
||||
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||
}
|
||||
function hasOperationalAdminActionRequestSignal(text) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ function hasInventoryPurchaseStem(text) {
|
|||
}
|
||||
function hasInventorySupplierCue(text) {
|
||||
const value = toText(text);
|
||||
if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) {
|
||||
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value);
|
||||
|
|
@ -21,11 +24,11 @@ function hasInventorySaleCue(text) {
|
|||
if (/(?:buyer|покупател)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) {
|
||||
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
||||
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(value);
|
||||
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(value);
|
||||
if (hasDirectionCue && hasSaleVerb) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
return cleanupAnchorValue(value)
|
||||
.toLowerCase()
|
||||
|
|
@ -1236,6 +1305,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
|
|||
candidate.includes("->") ||
|
||||
candidate.includes("=>") ||
|
||||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
|
||||
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||
normalizedCandidate.startsWith("по состоянию") ||
|
||||
isTemporalWarehousePhrase(candidate) ||
|
||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)
|
||||
|
|
|
|||
|
|
@ -1949,6 +1949,17 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
};
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|кому\s+в\s+итоге\s+мы\s+впарили)/iu.test(text) &&
|
||||
/(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text)
|
||||
) {
|
||||
return {
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
confidence: "medium",
|
||||
reasons: ["inventory_sale_trace_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
if (hasInventorySaleTraceSignalV2(text)) {
|
||||
return {
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
|
|
|
|||
|
|
@ -1978,7 +1978,14 @@ function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
|
|||
}
|
||||
|
||||
function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean {
|
||||
if (!hasExplicitPeriodWindow(filters)) {
|
||||
const hasRecoverableAsOfOnlyWindow =
|
||||
!hasExplicitPeriodWindow(filters) &&
|
||||
typeof filters.as_of_date === "string" &&
|
||||
filters.as_of_date.trim().length > 0 &&
|
||||
typeof filters.item === "string" &&
|
||||
filters.item.trim().length > 0 &&
|
||||
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item");
|
||||
if (!hasExplicitPeriodWindow(filters) && !hasRecoverableAsOfOnlyWindow) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -533,6 +533,30 @@ function hasSelectedObjectInventorySignal(text: string): boolean {
|
|||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||
}
|
||||
|
||||
function hasSelectedObjectInlineSnapshotMetadata(text: string): boolean {
|
||||
return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function extractSelectedObjectItemFromFollowupText(text: string): string | null {
|
||||
const rawSelectedObject = toNonEmptyString(extractSelectedObjectQuotedValue(text));
|
||||
if (!rawSelectedObject) {
|
||||
return null;
|
||||
}
|
||||
const firstLine = rawSelectedObject
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean);
|
||||
const primarySegment = String(firstLine ?? rawSelectedObject)
|
||||
.replace(/^\d+\.\s*/, "")
|
||||
.split("|")[0]
|
||||
?.trim();
|
||||
const normalized = toNonEmptyString(primarySegment);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function hasInventorySupplierFollowupCue(text: string): boolean {
|
||||
return hasInventorySupplierCue(String(text ?? ""));
|
||||
}
|
||||
|
|
@ -800,7 +824,7 @@ function mergeFollowupFilters(
|
|||
intent === "inventory_aging_by_purchase_date")
|
||||
) {
|
||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||
const explicitQuotedItem = toNonEmptyString(extractSelectedObjectQuotedValue(userMessage));
|
||||
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
||||
const currentItem = toNonEmptyString(merged.item);
|
||||
const shouldAdoptExplicitQuotedItem =
|
||||
Boolean(explicitQuotedItem) &&
|
||||
|
|
@ -873,6 +897,23 @@ function mergeFollowupFilters(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
(Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) &&
|
||||
hasSelectedObjectInventorySignal(userMessage) &&
|
||||
hasSelectedObjectInlineSnapshotMetadata(userMessage) &&
|
||||
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item") &&
|
||||
!hasExplicitPeriodLiteral(userMessage) &&
|
||||
!hasExplicitCurrentDateHint(userMessage)
|
||||
) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
reasons.push("period_from_from_followup_context");
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
reasons.push("period_to_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (
|
||||
!sameDateRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
return true;
|
||||
}
|
||||
const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:ну|мда|ох|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|пизда|нихуя|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) ||
|
||||
normalized.includes("\u0430\u0445\u0443") ||
|
||||
normalized.includes("\u043e\u0445\u0443") ||
|
||||
normalized.includes("\u043f\u0438\u0437\u0434") ||
|
||||
normalized.includes("\u0431\u043b\u044f");
|
||||
if (hasAffectiveReactionCue) {
|
||||
return false;
|
||||
}
|
||||
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||
}
|
||||
function hasOperationalAdminActionRequestSignal(text) {
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ export function hasInventorySaleCue(text: string): boolean {
|
|||
if (/(?:buyer|покупател)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) {
|
||||
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
||||
const hasSaleVerb =
|
||||
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(
|
||||
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasDirectionCue && hasSaleVerb) {
|
||||
|
|
|
|||
|
|
@ -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_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");
|
||||
});
|
||||
|
||||
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
|
||||
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
});
|
||||
|
||||
it("keeps inventory provenance wording out of inventory-on-hand routing", () => {
|
||||
const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?");
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
|
|
|
|||
|
|
@ -1064,4 +1064,65 @@ describe("assistant living chat mode", () => {
|
|||
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it("does not treat short emotional reaction as organization-selection confirmation", async () => {
|
||||
const normalizer = {
|
||||
normalize: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
trace_id: "norm-chat-affective-reaction",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
normalized: null,
|
||||
validation: { passed: false, errors: ["mock"] },
|
||||
route_hint_summary: null,
|
||||
raw_model_output: {},
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
||||
latency_ms: 1,
|
||||
request_count_for_case: 1
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const sessionId = "asst-living-chat-affective-reaction";
|
||||
sessions.ensureSession(sessionId);
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-seed-selected-org",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "Отлично, фиксирую рабочую организацию: ООО Альтернатива Плюс.",
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: "chat-seed-selected-org",
|
||||
debug: {
|
||||
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
|
||||
assistant_selected_organization: "ООО Альтернатива Плюс",
|
||||
assistant_active_organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
} as any);
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn().mockResolvedValue({ handled: false })
|
||||
} as any;
|
||||
const chatClient = {
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
raw: { id: "chat-affective-reaction" },
|
||||
outputText: "Понимаю, это выглядит неприятно. Давай разберем следующий шаг.",
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
||||
})
|
||||
} as any;
|
||||
|
||||
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
|
||||
const response = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: "ну ахуеть",
|
||||
llmProvider: "local",
|
||||
model: "qwen2.5",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual_with_explanation");
|
||||
expect(response.debug?.living_chat_response_source).not.toBe("deterministic_data_scope_selection_contract");
|
||||
expect(String(response.assistant_reply)).not.toContain("фиксирую рабочую организацию");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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