ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры
This commit is contained in:
parent
19dbaff741
commit
80d108e506
|
|
@ -14,7 +14,7 @@ export const designConfig = {
|
||||||
scrollbarThumbHoverRgb: "30, 50, 30"
|
scrollbarThumbHoverRgb: "30, 50, 30"
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
modeColumnWidthPx: 440,
|
modeColumnWidthPx: 406,
|
||||||
modeToggleWidthPx: 188
|
modeToggleWidthPx: 188
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,867 @@
|
||||||
|
# 1CLLMARCH Fact Check And Stabilization Plan
|
||||||
|
|
||||||
|
Updated at: 2026-04-10
|
||||||
|
Source baseline: `docs/TECH/1CLLMARCH.md`
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
This document fixes the current factual state of the codebase against `1CLLMARCH` and records a production-focused stabilization plan that preserves:
|
||||||
|
|
||||||
|
1. existing MCP routes;
|
||||||
|
2. manual routing and guard logic;
|
||||||
|
3. GUI manual markup + autorun operational loop.
|
||||||
|
|
||||||
|
## 2. Executive Reality Check
|
||||||
|
|
||||||
|
Current state is **not MVP** and also **not stable production**.
|
||||||
|
It is an advanced prototype with strong observability and eval tooling, but with architectural coupling and quality bottlenecks.
|
||||||
|
|
||||||
|
Approximate readiness against target architecture: **55/100**.
|
||||||
|
|
||||||
|
## 3. Verified Facts (Code + Runtime)
|
||||||
|
|
||||||
|
1. Strong engineering layers already exist:
|
||||||
|
- requirement extraction, coverage, grounding checks;
|
||||||
|
- rich debug payload and session trace;
|
||||||
|
- investigation state/followup continuity;
|
||||||
|
- manual annotation schema and post-analysis queues;
|
||||||
|
- async eval run + live history APIs.
|
||||||
|
2. Main production blockers:
|
||||||
|
- orchestration monolith and coupling in `assistantService.ts`;
|
||||||
|
- deterministic/template-heavy final answer construction;
|
||||||
|
- large lexical heuristic surface for intent routing;
|
||||||
|
- temporal policy inconsistency (date handling differs by lane/path);
|
||||||
|
- hidden fallback behavior around historical snapshot assumptions.
|
||||||
|
3. Quality metrics from latest Stage1 runs confirm weak user value:
|
||||||
|
- low retrieval differentiation;
|
||||||
|
- high generic explanation rate;
|
||||||
|
- low accountant actionability;
|
||||||
|
- zero mechanism specificity.
|
||||||
|
|
||||||
|
## 4. Gap Vs `1CLLMARCH`
|
||||||
|
|
||||||
|
### 4.1 Already aligned (partially or strongly)
|
||||||
|
|
||||||
|
1. Structured normalization contracts and validator loops.
|
||||||
|
2. Coverage and grounding artifacts (with different naming than target).
|
||||||
|
3. Conversation state persistence.
|
||||||
|
4. Operational diagnostics and report generation.
|
||||||
|
|
||||||
|
### 4.2 Not aligned enough for stable prod
|
||||||
|
|
||||||
|
1. Role separation (Interpreter/Planner/Critic/Answer) is incomplete and strongly coupled.
|
||||||
|
2. Final user answer quality is constrained by deterministic template synthesis.
|
||||||
|
3. Unified contract for analysis date/time scope is missing across all lanes.
|
||||||
|
4. Result classes from target (`FULLY_ANSWERED`, `BLOCKED_BY_*`, etc.) are not normalized as one contract.
|
||||||
|
5. Reason-code taxonomy exists but is fragmented across modules.
|
||||||
|
|
||||||
|
## 5. Stabilization Plan (No Route Breakage)
|
||||||
|
|
||||||
|
## Stage 1 (P0): Unified Analysis Context + Temporal Hardening
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
1. Introduce unified `analysis_context` contract.
|
||||||
|
2. Propagate it through eval -> assistant -> runtime lanes.
|
||||||
|
3. Remove hidden hardcoded period fallback in live MCP plan generation.
|
||||||
|
4. Keep backward compatibility (`period_hint` still supported).
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
1. If analysis date is set, runtime uses it explicitly in both deep/address paths.
|
||||||
|
2. If analysis date is absent, runtime no longer silently injects fixed historical date in live-plan fallback.
|
||||||
|
3. Existing APIs and manual workflows remain operational.
|
||||||
|
|
||||||
|
Implemented in current pass:
|
||||||
|
1. Added unified `analysis_context` contract to request context (`as_of_date`, `period_from`, `period_to`, `snapshot_mode`, `source`).
|
||||||
|
2. Added compatibility bridge: legacy `period_hint` is still accepted and normalized into `analysis_context`.
|
||||||
|
3. Propagated analysis context through eval flows into assistant runtime.
|
||||||
|
4. Applied analysis context in temporal guard with explicit precedence over implicit snapshot lock.
|
||||||
|
5. Removed hidden hardcoded live-plan fallback period by switching to:
|
||||||
|
- explicit analysis period/date when provided;
|
||||||
|
- query-derived period when present;
|
||||||
|
- generic live probe when period is absent.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted runtime tests passed:
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Status: **Completed**
|
||||||
|
|
||||||
|
## Stage 2 (P1): Orchestrator Decomposition (Behavior-Preserving Refactor)
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
1. Split monolith into explicit modules:
|
||||||
|
- QueryFrame builder
|
||||||
|
- Execution planner
|
||||||
|
- Evidence assembler
|
||||||
|
- Coverage critic
|
||||||
|
- Answer package builder
|
||||||
|
2. Preserve current behavior under compatibility adapter.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
1. No MCP route regressions.
|
||||||
|
2. Existing tests and autorun loop remain green.
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.1):
|
||||||
|
1. Added new orchestration contract module:
|
||||||
|
- `assistant_query_frame_v1`
|
||||||
|
- `assistant_execution_plan_v1`
|
||||||
|
- `assistant_evidence_bundle_v1`
|
||||||
|
- `assistant_coverage_contract_v1`
|
||||||
|
- outcome classifier (`FULLY_ANSWERED`, `PARTIALLY_ANSWERED`, `BLOCKED_*`, `MISROUTED`, `FAILED_TO_BIND_ENTITIES`)
|
||||||
|
2. Integrated contracts into deep-lane runtime without route/answer behavior changes:
|
||||||
|
- debug payload now includes `assistant_outcome_class_v1`;
|
||||||
|
- debug payload and event logs now include `assistant_orchestration_contracts_v1`.
|
||||||
|
3. Added unit regression tests:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted tests passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.2):
|
||||||
|
1. Added runtime orchestration adapter for the deep lane:
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.ts`
|
||||||
|
- unified pipeline call for `requirements -> coverage -> grounding` with stable interfaces.
|
||||||
|
2. Integrated adapter into `assistantService` main deep-lane flow (behavior-preserving):
|
||||||
|
- existing extraction/coverage/grounding logic preserved;
|
||||||
|
- execution now routed through one orchestration boundary.
|
||||||
|
3. Added adapter unit tests:
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted tests passed:
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.3):
|
||||||
|
1. Extracted answer package builder (`answer_structure_v11`) from `assistantService` into dedicated module:
|
||||||
|
- `assistantAnswerPackageBuilder.ts`
|
||||||
|
2. Rewired deep-lane answer structure assembly to use the new module without contract changes.
|
||||||
|
3. Added focused unit tests for answer package behavior:
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 test pack passed:
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.4):
|
||||||
|
1. Extracted coverage/grounding pipeline into dedicated module:
|
||||||
|
- `assistantCoverageGrounding.ts`
|
||||||
|
- moved requirement extraction, coverage resolution, and grounding checks under one reusable boundary.
|
||||||
|
2. Rewired `assistantService` to use extracted coverage/grounding module via compatibility wrappers (behavior-preserving).
|
||||||
|
3. Added focused regression tests:
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
4. Stabilized deterministic Stage 2 regression environment for followup continuity checks:
|
||||||
|
- fixed accidental false-positive token in grounding test fixture;
|
||||||
|
- fixed env isolation in `assistantWave10SettlementCorrectiveRegression.test.ts` by explicitly controlling `FEATURE_ASSISTANT_ADDRESS_QUERY_V1`.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.5):
|
||||||
|
1. Extracted query-planning helpers from `assistantService` into dedicated module:
|
||||||
|
- `assistantQueryPlanning.ts`
|
||||||
|
- moved `fragmentTextById`, execution-plan builder, and debug-routes builder under one reusable boundary.
|
||||||
|
2. Rewired `assistantService` to use extracted query-planning module (behavior-preserving wrapper integration).
|
||||||
|
3. Added focused unit tests for query-planning behavior:
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.6):
|
||||||
|
1. Extracted evidence-bundle assembly from `assistantService` into dedicated module:
|
||||||
|
- `assistantEvidenceBundleAssembler.ts`
|
||||||
|
- centralized:
|
||||||
|
- `assistant_evidence_bundle_v1` contract assembly;
|
||||||
|
- debug `retrieval_status` projection from normalized retrieval results.
|
||||||
|
2. Rewired `assistantService` to use assembler output for both:
|
||||||
|
- `assistant_orchestration_contracts_v1.evidence_bundle`;
|
||||||
|
- debug payload `retrieval_status`.
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.7):
|
||||||
|
1. Extracted deep-lane debug payload assembly from `assistantService` into dedicated module:
|
||||||
|
- `assistantDebugPayloadAssembler.ts`
|
||||||
|
2. Rewired `assistantService` to build `debug` via assembler (behavior-preserving):
|
||||||
|
- keeps all existing fields, guard audits, orchestration contracts and optional sections.
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.8):
|
||||||
|
1. Extracted deep-lane processed log payload assembly from `assistantService` into dedicated module:
|
||||||
|
- `assistantMessageLogAssembler.ts`
|
||||||
|
2. Rewired `assistantService` to build `assistant_message_processed.details` via assembler (behavior-preserving).
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.9):
|
||||||
|
1. Extracted orchestration-contract bundle assembly from `assistantService` into dedicated module:
|
||||||
|
- `assistantContractsBundleAssembler.ts`
|
||||||
|
2. Rewired `assistantService` to consume bundled contracts/outcome class from assembler (behavior-preserving):
|
||||||
|
- `query_frame`, `execution_plan`, `evidence_bundle`, `coverage`, `outcome_class`.
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.10):
|
||||||
|
1. Extracted deep response envelope assembly from `assistantService` into dedicated module:
|
||||||
|
- `assistantDeepResponseAssembler.ts`
|
||||||
|
- centralized:
|
||||||
|
- safe final assistant text cleanup (debug tail stripping);
|
||||||
|
- `answer_structure_v11` selection/building policy;
|
||||||
|
- assistant conversation item construction.
|
||||||
|
2. Rewired `assistantService` to consume deep response assembler (behavior-preserving).
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 + safety regressions passed:
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.11):
|
||||||
|
1. Added top-level deep turn packager to compose extracted Stage 2 modules behind one boundary:
|
||||||
|
- `assistantDeepTurnPackaging.ts`
|
||||||
|
- centralizes orchestration of:
|
||||||
|
- evidence bundle assembly;
|
||||||
|
- contracts/outcome class assembly;
|
||||||
|
- deep answer artifacts;
|
||||||
|
- debug payload;
|
||||||
|
- assistant conversation item;
|
||||||
|
- processed log details.
|
||||||
|
2. Rewired deep-lane `assistantService` flow to use `assembleAssistantDeepTurnPackaging(...)` (behavior-preserving):
|
||||||
|
- replaced duplicated per-artifact assembly block with single orchestrator call;
|
||||||
|
- preserved investigation-state update and existing response/event contract.
|
||||||
|
3. Added focused unit test:
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.12):
|
||||||
|
1. Extracted deep-turn input normalization/defaulting from `assistantService` into dedicated module:
|
||||||
|
- `assistantDeepTurnInputBuilder.ts`
|
||||||
|
- centralized normalization of:
|
||||||
|
- `followupStateUsage` default (`null` when absent);
|
||||||
|
- composition defaults (`problem_*` fields, `answer_structure_v11`);
|
||||||
|
- `problem_unit_ids_used` array normalization.
|
||||||
|
2. Rewired `assistantService` to build deep-turn input via:
|
||||||
|
- `buildAssistantDeepTurnPackagingInput(...)`
|
||||||
|
- followed by existing `assembleAssistantDeepTurnPackaging(...)` call (behavior-preserving).
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.13):
|
||||||
|
1. Extracted investigation-state update/persist runtime block from `assistantService` into dedicated adapter:
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `buildAssistantInvestigationStateSnapshot(...)`
|
||||||
|
- `persistAssistantInvestigationStateSnapshot(...)`
|
||||||
|
2. Rewired `assistantService` to consume new adapter (behavior-preserving):
|
||||||
|
- same `investigation_state` snapshot semantics;
|
||||||
|
- same `FEATURE_ASSISTANT_INVESTIGATION_STATE_V1` gating;
|
||||||
|
- same session persistence call path.
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.14):
|
||||||
|
1. Extracted deep-lane assistant post-turn commit/persist/log block from `assistantService` into dedicated adapter:
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `commitAssistantTurnAndLog(...)`
|
||||||
|
2. Rewired `assistantService` deep-lane to use new adapter (behavior-preserving):
|
||||||
|
- same append-to-session semantics;
|
||||||
|
- same session persistence behavior when session exists;
|
||||||
|
- same `assistant_message_processed` log envelope and event type.
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.15):
|
||||||
|
1. Extracted deep-lane pre-packaging context assembly from `assistantService` into dedicated module:
|
||||||
|
- `assistantDeepTurnPrePackagingContext.ts`
|
||||||
|
- centralized:
|
||||||
|
- dropped intent segments extraction;
|
||||||
|
- analysis context projection for contracts;
|
||||||
|
- debug route projection;
|
||||||
|
- resolved execution state projection;
|
||||||
|
- safe assistant reply base sanitization.
|
||||||
|
2. Rewired `assistantService` deep-lane to consume `buildAssistantDeepTurnPrePackagingContext(...)` (behavior-preserving).
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.16):
|
||||||
|
1. Extracted deep-lane success response envelope assembly from `assistantService` into dedicated builder:
|
||||||
|
- `assistantDeepTurnResponseBuilder.ts`
|
||||||
|
- introduced:
|
||||||
|
- `buildAssistantDeepTurnSuccessResponse(...)`
|
||||||
|
2. Rewired `assistantService` deep-lane to return response via new builder (behavior-preserving).
|
||||||
|
3. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.17):
|
||||||
|
1. Extracted deep-lane composition assembly from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `buildAssistantDeepTurnComposition(...)`
|
||||||
|
2. Centralized composition-time derivations (behavior-preserving):
|
||||||
|
- `focusDomainHint` from followup usage + investigation state;
|
||||||
|
- `questionTypeClass` via `resolveQuestionType(...)`;
|
||||||
|
- period presence signals from company anchors/normalization payload;
|
||||||
|
- downstream `composeAssistantAnswer(...)` call wiring.
|
||||||
|
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.18):
|
||||||
|
1. Extracted deep-lane guard runtime pipeline from `assistantService` into dedicated adapter:
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `applyAssistantDeepTurnRetrievalGuards(...)`
|
||||||
|
- `applyAssistantDeepTurnGroundingEligibility(...)`
|
||||||
|
2. Centralized and isolated runtime sequence (behavior-preserving):
|
||||||
|
- polarity guard on retrieval results;
|
||||||
|
- targeted evidence acquisition;
|
||||||
|
- evidence admissibility gate;
|
||||||
|
- grounded-answer eligibility evaluation + grounding status overlay.
|
||||||
|
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.19):
|
||||||
|
1. Extracted deep-lane retrieval execution loop from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `executeAssistantDeepTurnRetrievalPlan(...)`
|
||||||
|
2. Centralized retrieval runtime behavior (behavior-preserving):
|
||||||
|
- skipped/no-route call record generation;
|
||||||
|
- sequential route execution with temporal hint propagation;
|
||||||
|
- raw result capture;
|
||||||
|
- failed-route fallback normalization with stable error envelope.
|
||||||
|
3. Rewired `assistantService` deep-lane to consume retrieval adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.20):
|
||||||
|
1. Extracted deep-lane execution-plan assembly/enforcement from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantDeepTurnPlanRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `buildAssistantDeepTurnExecutionPlan(...)`
|
||||||
|
2. Centralized planning runtime sequence (behavior-preserving):
|
||||||
|
- requirement extraction by fragment;
|
||||||
|
- execution plan build from route summary;
|
||||||
|
- RBP route-plan enforcement;
|
||||||
|
- FA route-plan enforcement;
|
||||||
|
- temporal hint overlay;
|
||||||
|
- polarity hint overlay.
|
||||||
|
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.21):
|
||||||
|
1. Extracted deep-lane pre-guard runtime context assembly from `assistantService` into dedicated adapter:
|
||||||
|
- `assistantDeepTurnContextRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `buildAssistantDeepTurnRuntimeContext(...)`
|
||||||
|
2. Centralized context runtime sequence (behavior-preserving):
|
||||||
|
- company anchors resolution;
|
||||||
|
- initial business-scope alignment;
|
||||||
|
- P0 domain inference + domain whitelist gating for guard focus;
|
||||||
|
- temporal guard resolution with runtime analysis context;
|
||||||
|
- domain polarity guard resolution;
|
||||||
|
- claim-bound anchors resolution;
|
||||||
|
- live business-scope resolution with followup flag;
|
||||||
|
- normalized live temporal hint projection.
|
||||||
|
3. Rewired `assistantService` deep-lane to consume context adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.22):
|
||||||
|
1. Extracted deep-lane post-retrieval grounding runtime orchestration from `assistantService` into dedicated adapter:
|
||||||
|
- `assistantDeepTurnGroundingRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `runAssistantDeepTurnGroundingRuntime(...)`
|
||||||
|
2. Centralized grounding/runtime sequence (behavior-preserving):
|
||||||
|
- RBP live-route audit projection;
|
||||||
|
- FA live-route audit projection;
|
||||||
|
- coverage+grounding pipeline execution;
|
||||||
|
- grounded-answer eligibility overlay on base grounding check.
|
||||||
|
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnGroundingRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGroundingRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.23):
|
||||||
|
1. Extracted deep-lane packaging orchestration block from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantDeepTurnPackagingRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `runAssistantDeepTurnPackagingRuntime(...)`
|
||||||
|
2. Centralized packaging/runtime sequence (behavior-preserving):
|
||||||
|
- pre-packaging context assembly;
|
||||||
|
- investigation-state snapshot build/persist;
|
||||||
|
- deep-turn packaging input assembly;
|
||||||
|
- deep-turn packaging output projection (`safeAssistantReply`, debug payload, assistant item, processed log details).
|
||||||
|
3. Rewired `assistantService` deep-lane to consume packaging runtime adapter output (behavior-preserving).
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted Stage 2 assembler/adapter pack passed:
|
||||||
|
- `assistantOrchestrationContracts.test.ts`
|
||||||
|
- `assistantOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAnswerPackageBuilder.test.ts`
|
||||||
|
- `assistantCoverageGrounding.test.ts`
|
||||||
|
- `assistantQueryPlanning.test.ts`
|
||||||
|
- `assistantEvidenceBundleAssembler.test.ts`
|
||||||
|
- `assistantDebugPayloadAssembler.test.ts`
|
||||||
|
- `assistantMessageLogAssembler.test.ts`
|
||||||
|
- `assistantContractsBundleAssembler.test.ts`
|
||||||
|
- `assistantDeepResponseAssembler.test.ts`
|
||||||
|
- `assistantDeepTurnPackaging.test.ts`
|
||||||
|
- `assistantDeepTurnInputBuilder.test.ts`
|
||||||
|
- `assistantInvestigationStateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantTurnCommitRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPrePackagingContext.test.ts`
|
||||||
|
- `assistantDeepTurnResponseBuilder.test.ts`
|
||||||
|
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnGroundingRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
|
||||||
|
3. Additional safety regressions passed:
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
- `assistantMcpRuntimeBridge.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
|
||||||
|
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 completed)**
|
||||||
|
|
||||||
|
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
1. Use LLM for semantic extraction/decomposition in strict schema.
|
||||||
|
2. Keep deterministic guardrails as verifier, not primary “brain”.
|
||||||
|
3. Reduce dictionary overfitting and false route drifts.
|
||||||
|
|
||||||
|
Status: Planned
|
||||||
|
|
||||||
|
## Stage 4 (P2): Human-Centric Answer Layer
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
1. Move final user response to contract-driven answer package with:
|
||||||
|
- direct answer;
|
||||||
|
- what was checked;
|
||||||
|
- what was found;
|
||||||
|
- what remains unproven;
|
||||||
|
- best next step.
|
||||||
|
2. Keep claim-to-evidence binding strict.
|
||||||
|
|
||||||
|
Status: Planned
|
||||||
|
|
||||||
|
## Stage 5 (P3): Quality Loop Driven By GUI Markup
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
1. Drive backlog from `manual_case_decision` queues.
|
||||||
|
2. Build targeted regression packs from real failed comments.
|
||||||
|
3. Track trend by reason-code clusters.
|
||||||
|
|
||||||
|
Status: Planned
|
||||||
|
|
||||||
|
## 6. Non-Negotiable Constraints
|
||||||
|
|
||||||
|
1. Do not break MCP route interfaces.
|
||||||
|
2. Do not remove manual logic without compatible replacement.
|
||||||
|
3. Preserve UTF-8 (without BOM) for all source/doc files.
|
||||||
|
4. Keep manual markup and autorun API contract stable.
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -797,6 +797,7 @@ function buildRunSummary(run) {
|
||||||
llm_provider: llmProvider,
|
llm_provider: llmProvider,
|
||||||
model,
|
model,
|
||||||
use_mock: toBooleanSafe(run.report.use_mock),
|
use_mock: toBooleanSafe(run.report.use_mock),
|
||||||
|
analysis_date: toStringSafe(run.report.analysis_date),
|
||||||
prompt_version: toStringSafe(run.report.prompt_version),
|
prompt_version: toStringSafe(run.report.prompt_version),
|
||||||
schema_version: toStringSafe(run.report.schema_version),
|
schema_version: toStringSafe(run.report.schema_version),
|
||||||
suite_id: toStringSafe(run.report.suite_id),
|
suite_id: toStringSafe(run.report.suite_id),
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,32 @@ function normalizeCaseIds(value) {
|
||||||
.filter((item) => item.length > 0);
|
.filter((item) => item.length > 0);
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
function normalizeAnalysisDate(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
function buildEvalPayloadFromBody(body) {
|
function buildEvalPayloadFromBody(body) {
|
||||||
|
const analysisDate = normalizeAnalysisDate(body.analysis_date) ??
|
||||||
|
normalizeAnalysisDate(body.analysisDate);
|
||||||
return {
|
return {
|
||||||
normalizeConfig: (body.normalizeConfig ?? {}),
|
normalizeConfig: (body.normalizeConfig ?? {}),
|
||||||
caseIds: normalizeCaseIds(body.caseIds),
|
caseIds: normalizeCaseIds(body.caseIds),
|
||||||
|
|
@ -103,7 +128,8 @@ function buildEvalPayloadFromBody(body) {
|
||||||
? body.compare_with_report_file
|
? body.compare_with_report_file
|
||||||
: typeof body.comparisonBaselineReportFile === "string"
|
: typeof body.comparisonBaselineReportFile === "string"
|
||||||
? body.comparisonBaselineReportFile
|
? body.comparisonBaselineReportFile
|
||||||
: undefined
|
: undefined,
|
||||||
|
analysisDate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveReadablePath(inputPath) {
|
function resolveReadablePath(inputPath) {
|
||||||
|
|
@ -245,6 +271,7 @@ function snapshotJob(job) {
|
||||||
eval_target: job.eval_target,
|
eval_target: job.eval_target,
|
||||||
run_id: job.run_id,
|
run_id: job.run_id,
|
||||||
case_set_file: job.case_set_file,
|
case_set_file: job.case_set_file,
|
||||||
|
analysis_date: job.analysis_date,
|
||||||
total_cases: job.total_cases,
|
total_cases: job.total_cases,
|
||||||
completed_cases: job.completed_cases,
|
completed_cases: job.completed_cases,
|
||||||
error: job.error,
|
error: job.error,
|
||||||
|
|
@ -258,7 +285,8 @@ function snapshotJob(job) {
|
||||||
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
|
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
|
||||||
? Number(toRecord(job.report.metrics)?.score_index)
|
? Number(toRecord(job.report.metrics)?.score_index)
|
||||||
: null,
|
: null,
|
||||||
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null
|
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null,
|
||||||
|
analysis_date: toStringSafe(job.report.analysis_date) ?? job.analysis_date
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
};
|
};
|
||||||
|
|
@ -310,6 +338,7 @@ function buildEvalRouter(services) {
|
||||||
eval_target: payload.evalTarget,
|
eval_target: payload.evalTarget,
|
||||||
run_id: runId,
|
run_id: runId,
|
||||||
case_set_file: runtimeCaseSetFile,
|
case_set_file: runtimeCaseSetFile,
|
||||||
|
analysis_date: payload.analysisDate ?? null,
|
||||||
total_cases: caseSeeds.length,
|
total_cases: caseSeeds.length,
|
||||||
completed_cases: 0,
|
completed_cases: 0,
|
||||||
cases: caseSeeds.map((item) => ({
|
cases: caseSeeds.map((item) => ({
|
||||||
|
|
|
||||||
|
|
@ -427,8 +427,20 @@ function extractLooseByAnchorValue(text) {
|
||||||
}
|
}
|
||||||
const lowered = token.toLowerCase();
|
const lowered = token.toLowerCase();
|
||||||
const stopWords = new Set([
|
const stopWords = new Set([
|
||||||
|
"какой",
|
||||||
|
"какая",
|
||||||
|
"какие",
|
||||||
|
"каких",
|
||||||
|
"каким",
|
||||||
|
"какими",
|
||||||
|
"каком",
|
||||||
|
"кто",
|
||||||
|
"что",
|
||||||
|
"мы",
|
||||||
|
"видим",
|
||||||
"контрагенту",
|
"контрагенту",
|
||||||
"контрагента",
|
"контрагента",
|
||||||
|
"контрагентам",
|
||||||
"контре",
|
"контре",
|
||||||
"компании",
|
"компании",
|
||||||
"компанию",
|
"компанию",
|
||||||
|
|
@ -436,10 +448,14 @@ function extractLooseByAnchorValue(text) {
|
||||||
"организацию",
|
"организацию",
|
||||||
"поставщику",
|
"поставщику",
|
||||||
"поставщика",
|
"поставщика",
|
||||||
|
"поставщикам",
|
||||||
"клиенту",
|
"клиенту",
|
||||||
"клиента",
|
"клиента",
|
||||||
|
"клиентам",
|
||||||
"покупателю",
|
"покупателю",
|
||||||
"покупателя",
|
"покупателя",
|
||||||
|
"покупателям",
|
||||||
|
"заказчикам",
|
||||||
"партнеру",
|
"партнеру",
|
||||||
"партнера",
|
"партнера",
|
||||||
"договору",
|
"договору",
|
||||||
|
|
@ -552,6 +568,9 @@ function isLikelyCounterpartyToken(rawToken) {
|
||||||
"какая",
|
"какая",
|
||||||
"какое",
|
"какое",
|
||||||
"каких",
|
"каких",
|
||||||
|
"каким",
|
||||||
|
"какими",
|
||||||
|
"каком",
|
||||||
"какому",
|
"какому",
|
||||||
"какую",
|
"какую",
|
||||||
"кто",
|
"кто",
|
||||||
|
|
@ -566,6 +585,8 @@ function isLikelyCounterpartyToken(rawToken) {
|
||||||
"чья",
|
"чья",
|
||||||
"чей",
|
"чей",
|
||||||
"чью",
|
"чью",
|
||||||
|
"мы",
|
||||||
|
"видим",
|
||||||
"самый",
|
"самый",
|
||||||
"самая",
|
"самая",
|
||||||
"самое",
|
"самое",
|
||||||
|
|
@ -620,10 +641,23 @@ function isLikelyCounterpartyToken(rawToken) {
|
||||||
"контрагент",
|
"контрагент",
|
||||||
"контрагенту",
|
"контрагенту",
|
||||||
"контрагента",
|
"контрагента",
|
||||||
|
"контрагентам",
|
||||||
"компания",
|
"компания",
|
||||||
"компании",
|
"компании",
|
||||||
"организация",
|
"организация",
|
||||||
"организации",
|
"организации",
|
||||||
|
"поставщикам",
|
||||||
|
"клиентам",
|
||||||
|
"покупателям",
|
||||||
|
"заказчикам",
|
||||||
|
"аванс",
|
||||||
|
"авансы",
|
||||||
|
"проблемный",
|
||||||
|
"проблемные",
|
||||||
|
"проблемным",
|
||||||
|
"закрытия",
|
||||||
|
"закрыть",
|
||||||
|
"закрыты",
|
||||||
"год",
|
"год",
|
||||||
"года",
|
"года",
|
||||||
"г",
|
"г",
|
||||||
|
|
@ -727,7 +761,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||||
if (tokens.length === 0) {
|
if (tokens.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const questionCue = /(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
|
const questionCue = /(?:кто|что|какой|какая|какие|какого|каких|каким|какими|каком|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
|
||||||
/[?]/u.test(String(rawValue ?? ""));
|
/[?]/u.test(String(rawValue ?? ""));
|
||||||
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
|
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
|
||||||
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
|
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
|
||||||
|
|
|
||||||
|
|
@ -604,6 +604,10 @@ function hasLifecycleSegmentationSignal(text) {
|
||||||
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
|
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
|
||||||
}
|
}
|
||||||
function hasCounterpartyActivityLifecycleSignal(text) {
|
function hasCounterpartyActivityLifecycleSignal(text) {
|
||||||
|
const hasPaymentRiskLexeme = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|долг|задолж)/iu.test(text);
|
||||||
|
if (hasPaymentRiskLexeme) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
|
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -678,6 +682,7 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
|
||||||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
|
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
|
||||||
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
||||||
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
||||||
|
const asksMajorShare = /(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(text);
|
||||||
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(text);
|
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(text);
|
||||||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
|
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
|
||||||
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
|
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
|
||||||
|
|
@ -702,6 +707,9 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
|
||||||
if (asksCounterpartySource && asksValue) {
|
if (asksCounterpartySource && asksValue) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
|
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -801,6 +809,58 @@ function hasOpenContractsListSignal(text) {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
function hasSupplierTailRiskSignal(text) {
|
||||||
|
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
|
||||||
|
const hasTail = /(?:хвост|висят|незакрыт|задолж|долг|просроч)/iu.test(text);
|
||||||
|
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
|
||||||
|
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц)/iu.test(text);
|
||||||
|
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
|
||||||
|
}
|
||||||
|
function hasReceivablesLatencyRiskSignal(text) {
|
||||||
|
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
|
||||||
|
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
|
||||||
|
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
|
||||||
|
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
|
||||||
|
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
|
||||||
|
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
|
||||||
|
const hasPeriodOrRiskCue = /(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн)/iu.test(text);
|
||||||
|
const hasBetweenShipmentAndPayment = /между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
|
||||||
|
if (hasBuyer && hasPayment && ((hasShipment && hasDelay) || hasBetweenShipmentAndPayment)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
|
||||||
|
}
|
||||||
|
function hasSettlementGapSignal(text) {
|
||||||
|
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
|
||||||
|
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
|
||||||
|
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
|
||||||
|
const hasNoDocumentForClosing = /(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
|
||||||
|
/(?:закрыти|взаиморасч|акт)/iu.test(text);
|
||||||
|
const hasNoDocumentForClosingReversed = /(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
|
||||||
|
/(?:закрыти|взаиморасч|акт)/iu.test(text);
|
||||||
|
const hasNoPayments = /(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
|
||||||
|
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
|
||||||
|
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
|
||||||
|
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
|
||||||
|
const hasUnclosedAdvanceGap = hasAdvance &&
|
||||||
|
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт)/iu.test(text) ||
|
||||||
|
hasNoDocumentForClosing ||
|
||||||
|
hasNoDocumentForClosingReversed);
|
||||||
|
return hasPaymentsWithoutClosingDocs || hasDocsWithoutPayments || hasUnclosedAdvanceGap;
|
||||||
|
}
|
||||||
|
function hasReconciliationMismatchSignal(text) {
|
||||||
|
const hasCounterparty = /(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
|
||||||
|
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
|
||||||
|
const hasMismatchLexeme = /(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
|
||||||
|
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
|
||||||
|
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
|
||||||
|
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
|
||||||
|
return (hasCounterparty &&
|
||||||
|
hasReconciliationLexeme &&
|
||||||
|
hasMismatchLexeme &&
|
||||||
|
hasBalanceLexeme &&
|
||||||
|
(hasLookupVerb || hasInterrogativeLookup));
|
||||||
|
}
|
||||||
function isLikelyCounterpartyToken(rawToken) {
|
function isLikelyCounterpartyToken(rawToken) {
|
||||||
const token = String(rawToken ?? "").trim().toLowerCase();
|
const token = String(rawToken ?? "").trim().toLowerCase();
|
||||||
if (!token || token.length < 2) {
|
if (!token || token.length < 2) {
|
||||||
|
|
@ -1115,6 +1175,34 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons: ["payables_signal_detected"]
|
reasons: ["payables_signal_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (hasSettlementGapSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_open_contracts",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["settlement_gap_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasReconciliationMismatchSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_open_contracts",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["reconciliation_mismatch_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasReceivablesLatencyRiskSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_receivables_counterparties",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["receivables_payment_lag_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasSupplierTailRiskSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_payables_counterparties",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["supplier_tail_risk_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
|
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "documents_forming_balance",
|
intent: "documents_forming_balance",
|
||||||
|
|
@ -1137,7 +1225,7 @@ function resolveAddressIntent(userMessage) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasAny(text, OPEN_ITEMS_HINTS) &&
|
if (hasAny(text, OPEN_ITEMS_HINTS) &&
|
||||||
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))) {
|
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "open_items_by_counterparty_or_contract",
|
intent: "open_items_by_counterparty_or_contract",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,33 @@ function parseFiniteNumber(value) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function normalizeAnalysisDateHint(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
|
||||||
|
if (!isoPrefix) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(isoPrefix[1]);
|
||||||
|
const month = Number(isoPrefix[2]);
|
||||||
|
const day = Number(isoPrefix[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
|
||||||
|
}
|
||||||
function valueAsString(value) {
|
function valueAsString(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -665,6 +692,50 @@ function runtimeReadinessForLimitedCategory(category) {
|
||||||
}
|
}
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
function normalizeLimitedReason(reason) {
|
||||||
|
let normalized = String(reason ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return "не хватает подтвержденных данных для уверенного вывода";
|
||||||
|
}
|
||||||
|
const replacements = [
|
||||||
|
[/address_query\s*v?1/giu, "текущий адресный режим"],
|
||||||
|
[/address\s*v1/giu, "текущий адресный режим"],
|
||||||
|
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
|
||||||
|
[/live\s+recipe/giu, "текущий сценарий выборки"],
|
||||||
|
[/materialized\s+live-строках/giu, "доступном срезе данных"],
|
||||||
|
[/live-выборке/giu, "выборке данных"],
|
||||||
|
[/live-данных/giu, "данных"],
|
||||||
|
[/deep-analysis/giu, "режим расширенной проверки"],
|
||||||
|
[/\blookup\b/giu, "поиск"],
|
||||||
|
[/\bintent\b/giu, "сценария"],
|
||||||
|
[/\brecipe\b/giu, "шаблон выборки"],
|
||||||
|
[/\byakor\b/giu, "ориентир"],
|
||||||
|
[/\banchor\b/giu, "ориентир"],
|
||||||
|
[/\s+/gu, " "]
|
||||||
|
];
|
||||||
|
for (const [pattern, value] of replacements) {
|
||||||
|
normalized = normalized.replace(pattern, value);
|
||||||
|
}
|
||||||
|
return normalized.trim();
|
||||||
|
}
|
||||||
|
function normalizeLimitedNextStep(nextStep) {
|
||||||
|
let normalized = String(nextStep ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const replacements = [
|
||||||
|
[/address_query\s*v?1/giu, "текущий адресный режим"],
|
||||||
|
[/deep-analysis/giu, "режим расширенной проверки"],
|
||||||
|
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
|
||||||
|
[/\bintent\b/giu, "сценарий"],
|
||||||
|
[/\blookup\b/giu, "поиск"],
|
||||||
|
[/\s+/gu, " "]
|
||||||
|
];
|
||||||
|
for (const [pattern, value] of replacements) {
|
||||||
|
normalized = normalized.replace(pattern, value);
|
||||||
|
}
|
||||||
|
return normalized.trim();
|
||||||
|
}
|
||||||
function rowHasNonEmptyField(row, keys) {
|
function rowHasNonEmptyField(row, keys) {
|
||||||
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
|
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
|
||||||
}
|
}
|
||||||
|
|
@ -766,20 +837,27 @@ function toLegacyMcpStatus(status) {
|
||||||
}
|
}
|
||||||
function composeLimitedReply(category, reason, nextStep) {
|
function composeLimitedReply(category, reason, nextStep) {
|
||||||
const heading = category === "empty_match"
|
const heading = category === "empty_match"
|
||||||
? "В live-данных по текущему фильтру записи не найдены."
|
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
|
||||||
: category === "missing_anchor"
|
: category === "missing_anchor"
|
||||||
? "Для точного адресного поиска не хватает обязательного якоря."
|
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
|
||||||
: category === "recipe_visibility_gap"
|
: category === "recipe_visibility_gap"
|
||||||
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
|
? "Запрос понятен, но текущий режим не дает нужной детализации."
|
||||||
: category === "unsupported"
|
: category === "unsupported"
|
||||||
? "Этот запрос не подходит под address_query V1."
|
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
|
||||||
: "Не удалось выполнить адресный live-запрос в V1.";
|
: "Не удалось завершить проверку в адресном режиме.";
|
||||||
|
const reasonLine = category === "unsupported"
|
||||||
|
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
|
||||||
|
: category === "missing_anchor"
|
||||||
|
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
|
||||||
|
: category === "recipe_visibility_gap"
|
||||||
|
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
|
||||||
|
: `Коротко: ${normalizeLimitedReason(reason)}.`;
|
||||||
const lines = [
|
const lines = [
|
||||||
heading,
|
heading,
|
||||||
`Причина: ${reason}.`
|
reasonLine
|
||||||
];
|
];
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
lines.push(`Что нужно уточнить: ${nextStep}.`);
|
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
|
||||||
}
|
}
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
@ -842,7 +920,22 @@ class AddressQueryService {
|
||||||
if (!decompose) {
|
if (!decompose) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { mode, shape, intent, filters, baseReasons } = decompose;
|
const { mode, shape, intent, filters } = decompose;
|
||||||
|
const baseReasons = [...decompose.baseReasons];
|
||||||
|
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
|
||||||
|
if (analysisDate) {
|
||||||
|
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
|
||||||
|
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
|
||||||
|
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
|
||||||
|
if (!hasTemporalFilter) {
|
||||||
|
filters.extracted_filters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
as_of_date: analysisDate
|
||||||
|
};
|
||||||
|
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
|
||||||
|
baseReasons.push("as_of_date_from_analysis_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
const composeOptionsFromFilters = (filterSet) => ({
|
const composeOptionsFromFilters = (filterSet) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
||||||
|
|
@ -863,8 +956,8 @@ class AddressQueryService {
|
||||||
rowsFetched: 0,
|
rowsFetched: 0,
|
||||||
rowsMatched: 0,
|
rowsMatched: 0,
|
||||||
category: "unsupported",
|
category: "unsupported",
|
||||||
reasonText: "intent пока не поддержан в address V1",
|
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
|
||||||
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
|
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
|
||||||
limitations: ["intent_not_supported_in_v1"],
|
limitations: ["intent_not_supported_in_v1"],
|
||||||
reasons: baseReasons
|
reasons: baseReasons
|
||||||
});
|
});
|
||||||
|
|
@ -903,8 +996,8 @@ class AddressQueryService {
|
||||||
rowsFetched: 0,
|
rowsFetched: 0,
|
||||||
rowsMatched: 0,
|
rowsMatched: 0,
|
||||||
category: "recipe_visibility_gap",
|
category: "recipe_visibility_gap",
|
||||||
reasonText: "для intent пока нет recipe в address V1",
|
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
|
||||||
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
|
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
|
||||||
limitations: ["recipe_not_available"],
|
limitations: ["recipe_not_available"],
|
||||||
reasons: [...baseReasons, ...recipeSelection.selection_reason]
|
reasons: [...baseReasons, ...recipeSelection.selection_reason]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1172,7 +1172,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
if (intent === "list_open_contracts") {
|
if (intent === "list_open_contracts") {
|
||||||
const contracts = contractCandidatesFromRows(rows);
|
const contracts = contractCandidatesFromRows(rows);
|
||||||
const lines = [
|
const lines = [
|
||||||
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
|
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
|
||||||
`Строк движения: ${rows.length}.`,
|
`Строк движения: ${rows.length}.`,
|
||||||
`Договорных кандидатов: ${contracts.length}.`
|
`Договорных кандидатов: ${contracts.length}.`
|
||||||
];
|
];
|
||||||
|
|
@ -1188,6 +1188,34 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "list_payables_counterparties") {
|
||||||
|
const lines = [
|
||||||
|
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
|
||||||
|
`Строк в выборке: ${rows.length}.`,
|
||||||
|
...(rows.length > 0
|
||||||
|
? ["Ниже примеры строк для ручной проверки."]
|
||||||
|
: ["Явных признаков системной задолженности по доступному срезу не найдено."]),
|
||||||
|
...formatTopRows(rows, 6)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
text: lines.join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (intent === "list_receivables_counterparties") {
|
||||||
|
const lines = [
|
||||||
|
"Проверил покупателей с признаками затянутой оплаты (контур 62/76).",
|
||||||
|
`Строк в выборке: ${rows.length}.`,
|
||||||
|
...(rows.length > 0
|
||||||
|
? ["Ниже примеры строк, которые стоит проверить в первую очередь."]
|
||||||
|
: ["Явных признаков затяжной дебиторки по доступному срезу не найдено."]),
|
||||||
|
...formatTopRows(rows, 6)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
text: lines.join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "open_items_by_counterparty_or_contract") {
|
if (intent === "open_items_by_counterparty_or_contract") {
|
||||||
const lines = [
|
const lines = [
|
||||||
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
|
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
|
||||||
|
|
@ -1279,12 +1307,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const title = intent === "list_payables_counterparties"
|
const lines = ["Срез адресного запроса собран.", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
|
||||||
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
|
|
||||||
: intent === "list_receivables_counterparties"
|
|
||||||
? "Срез требований (receivables) собран по движениям с account scope 62/76."
|
|
||||||
: "Срез адресного запроса собран.";
|
|
||||||
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
|
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantAnswerStructureV11 = buildAssistantAnswerStructureV11;
|
||||||
|
const config_1 = require("../config");
|
||||||
|
const stage1Contracts_1 = require("../types/stage1Contracts");
|
||||||
|
const EVIDENCE_LIMITATION_REASON_CODE_SET = new Set([
|
||||||
|
"snapshot_only",
|
||||||
|
"heuristic_inference",
|
||||||
|
"missing_mechanism",
|
||||||
|
"weak_source_mapping",
|
||||||
|
"insufficient_detail",
|
||||||
|
"unknown"
|
||||||
|
]);
|
||||||
|
function summarizeUnique(values, limit = 6) {
|
||||||
|
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||||
|
}
|
||||||
|
function isEvidenceLimitationReasonCode(value) {
|
||||||
|
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value);
|
||||||
|
}
|
||||||
|
function firstNonEmptyLine(text) {
|
||||||
|
const line = String(text ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.find((item) => item.length > 0);
|
||||||
|
return (line ?? String(text ?? "")).slice(0, 220);
|
||||||
|
}
|
||||||
|
function buildClaimEvidenceLinks(retrievalResults) {
|
||||||
|
const byClaim = new Map();
|
||||||
|
for (const result of retrievalResults) {
|
||||||
|
for (const evidence of result.evidence) {
|
||||||
|
const claimRef = String(evidence.claim_ref ?? "").trim();
|
||||||
|
if (!claimRef) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const evidenceId = String(evidence.evidence_id ?? "").trim();
|
||||||
|
if (!evidenceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const current = byClaim.get(claimRef) ?? [];
|
||||||
|
current.push(evidenceId);
|
||||||
|
byClaim.set(claimRef, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(byClaim.entries())
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([claimRef, evidenceIds]) => ({
|
||||||
|
claim_ref: claimRef,
|
||||||
|
evidence_ids: summarizeUnique(evidenceIds, 10)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function buildAssistantAnswerStructureV11(input) {
|
||||||
|
const evidenceIds = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)), 10);
|
||||||
|
const mechanismNotes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
|
||||||
|
.map((evidence) => evidence.mechanism_note)
|
||||||
|
.filter((note) => typeof note === "string" && note.trim().length > 0)), 6);
|
||||||
|
const sourceRefs = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
|
||||||
|
.map((evidence) => evidence.source_ref?.canonical_ref)
|
||||||
|
.filter((value) => typeof value === "string" && value.trim().length > 0)), 8);
|
||||||
|
const limitationReasonCodes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence.flatMap((evidence) => {
|
||||||
|
const code = evidence.limitation?.reason_code;
|
||||||
|
return typeof code === "string" && code.trim().length > 0 ? [code] : [];
|
||||||
|
})), 8).filter(isEvidenceLimitationReasonCode);
|
||||||
|
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
|
||||||
|
const limitations = summarizeUnique([...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons], 8);
|
||||||
|
const clarificationQuestions = input.coverageReport.clarification_needed_for.map((item) => `Уточните требование ${item}.`);
|
||||||
|
const recommendedActions = summarizeUnique([
|
||||||
|
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
|
||||||
|
...input.coverageReport.requirements_partially_covered.map((item) => `Доуточнить частично покрытое требование ${item}.`)
|
||||||
|
], 6);
|
||||||
|
const mechanismStatus = mechanismNotes.length === 0
|
||||||
|
? "unresolved"
|
||||||
|
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
|
||||||
|
? "limited"
|
||||||
|
: "grounded";
|
||||||
|
const enableEvidenceEnrichment = input.options?.enableEvidenceEnrichment ?? config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
|
||||||
|
return {
|
||||||
|
schema_version: stage1Contracts_1.ANSWER_STRUCTURE_SCHEMA_VERSION,
|
||||||
|
answer_summary: firstNonEmptyLine(input.assistantReply),
|
||||||
|
direct_answer: input.assistantReply,
|
||||||
|
mechanism_block: {
|
||||||
|
status: mechanismStatus,
|
||||||
|
mechanism_notes: mechanismNotes,
|
||||||
|
limitation_reason_codes: limitationReasonCodes
|
||||||
|
},
|
||||||
|
evidence_block: {
|
||||||
|
evidence_ids: evidenceIds,
|
||||||
|
source_refs: sourceRefs,
|
||||||
|
mechanism_notes: mechanismNotes,
|
||||||
|
coverage_note: input.coverageReport.requirements_total === input.coverageReport.requirements_covered
|
||||||
|
? "coverage_full_or_near_full"
|
||||||
|
: "coverage_partial_or_limited",
|
||||||
|
...(enableEvidenceEnrichment && claimEvidenceLinks.length > 0
|
||||||
|
? {
|
||||||
|
claim_evidence_links: claimEvidenceLinks
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
},
|
||||||
|
uncertainty_block: {
|
||||||
|
open_uncertainties: input.groundingCheck.missing_requirements,
|
||||||
|
limitations
|
||||||
|
},
|
||||||
|
next_step_block: {
|
||||||
|
recommended_actions: recommendedActions,
|
||||||
|
clarification_questions: clarificationQuestions
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.assembleAssistantContractsBundleV1 = assembleAssistantContractsBundleV1;
|
||||||
|
const assistantOrchestrationContracts_1 = require("./assistantOrchestrationContracts");
|
||||||
|
function assembleAssistantContractsBundleV1(input) {
|
||||||
|
const queryFrameContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantQueryFrameContractV1)({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalizedQuestion: input.normalizedQuestion,
|
||||||
|
normalized: input.normalized,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
analysisContext: input.analysisContext
|
||||||
|
});
|
||||||
|
const executionPlanContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantExecutionPlanContractV1)({
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
requirements: input.requirements
|
||||||
|
});
|
||||||
|
const outcomeClassV1 = (0, assistantOrchestrationContracts_1.classifyAssistantOutcomeClassV1)({
|
||||||
|
replyType: input.replyType,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
grounding: input.grounding,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const coverageContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantCoverageContractV1)({
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
grounding: input.grounding,
|
||||||
|
outcomeClass: outcomeClassV1
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
queryFrameContractV1,
|
||||||
|
executionPlanContractV1,
|
||||||
|
outcomeClassV1,
|
||||||
|
coverageContractV1,
|
||||||
|
assistantOrchestrationContractsV1: {
|
||||||
|
query_frame: queryFrameContractV1,
|
||||||
|
execution_plan: executionPlanContractV1,
|
||||||
|
evidence_bundle: input.evidenceBundleContractV1,
|
||||||
|
coverage: coverageContractV1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.extractRequirementsForRoute = extractRequirementsForRoute;
|
||||||
|
exports.evaluateCoverageForRequirements = evaluateCoverageForRequirements;
|
||||||
|
exports.checkGroundingForRequirements = checkGroundingForRequirements;
|
||||||
|
function summarizeUnique(values, limit = 6) {
|
||||||
|
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||||
|
}
|
||||||
|
const SUBJECT_TOKEN_RULES = {
|
||||||
|
nds: {
|
||||||
|
critical: true,
|
||||||
|
patterns: [
|
||||||
|
"vat",
|
||||||
|
"accumulationregister",
|
||||||
|
"ндс",
|
||||||
|
"книгипокупок",
|
||||||
|
"книгипродаж",
|
||||||
|
"налогнадобавленнуюстоимость"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
os: {
|
||||||
|
critical: true,
|
||||||
|
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
|
||||||
|
},
|
||||||
|
saldo: {
|
||||||
|
critical: true,
|
||||||
|
patterns: ["balance", "saldo", "сальдо", "остат"]
|
||||||
|
},
|
||||||
|
counterparty: {
|
||||||
|
critical: false,
|
||||||
|
patterns: [
|
||||||
|
"counterparty",
|
||||||
|
"supplier",
|
||||||
|
"buyer",
|
||||||
|
"counterparty_id",
|
||||||
|
"journal_counterparty",
|
||||||
|
"document_has_counterparty",
|
||||||
|
"контрагент",
|
||||||
|
"поставщик",
|
||||||
|
"покупател"
|
||||||
|
],
|
||||||
|
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
critical: false,
|
||||||
|
patterns: [
|
||||||
|
"document",
|
||||||
|
"recorder",
|
||||||
|
"journal",
|
||||||
|
"document_refs_count",
|
||||||
|
"recorded_by_document",
|
||||||
|
"journal_refers_to_document",
|
||||||
|
"документ"
|
||||||
|
],
|
||||||
|
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
|
||||||
|
},
|
||||||
|
anomaly: {
|
||||||
|
critical: false,
|
||||||
|
patterns: [
|
||||||
|
"risk",
|
||||||
|
"risk_score",
|
||||||
|
"unknown_link_count",
|
||||||
|
"zero_guid",
|
||||||
|
"navigation_links",
|
||||||
|
"missing_counterparty_link",
|
||||||
|
"аномал",
|
||||||
|
"риск"
|
||||||
|
],
|
||||||
|
routes: ["store_feature_risk", "batch_refresh_then_store"]
|
||||||
|
},
|
||||||
|
chain: {
|
||||||
|
critical: false,
|
||||||
|
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
|
||||||
|
routes: ["hybrid_store_plus_live"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function hasRegexMatch(corpus, pattern) {
|
||||||
|
try {
|
||||||
|
return pattern.test(corpus);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function evaluateSubjectTokenMatch(token, corpus, executedRoutes) {
|
||||||
|
if (token.startsWith("account_")) {
|
||||||
|
const account = token.slice("account_".length).trim();
|
||||||
|
if (!account) {
|
||||||
|
return { matched: false, critical: true };
|
||||||
|
}
|
||||||
|
const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
|
||||||
|
return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
|
||||||
|
}
|
||||||
|
const rule = SUBJECT_TOKEN_RULES[token];
|
||||||
|
if (rule) {
|
||||||
|
const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
|
||||||
|
const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
|
||||||
|
return { matched: byPattern || byRoute, critical: rule.critical };
|
||||||
|
}
|
||||||
|
return { matched: corpus.includes(token), critical: false };
|
||||||
|
}
|
||||||
|
function evidenceCountForRequirement(requirementId, result) {
|
||||||
|
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||||
|
if (evidence.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const tagged = evidence.filter((item) => {
|
||||||
|
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
|
||||||
|
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
|
||||||
|
}).length;
|
||||||
|
if (tagged > 0) {
|
||||||
|
return tagged;
|
||||||
|
}
|
||||||
|
if (Array.isArray(result.requirement_ids) &&
|
||||||
|
result.requirement_ids.length === 1 &&
|
||||||
|
result.requirement_ids[0] === requirementId) {
|
||||||
|
return evidence.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
function hasSubstantiveCoverageForRequirement(requirementId, result) {
|
||||||
|
const evidenceCount = evidenceCountForRequirement(requirementId, result);
|
||||||
|
if (evidenceCount > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
|
||||||
|
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
|
||||||
|
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
|
||||||
|
if (Array.isArray(result.requirement_ids) &&
|
||||||
|
result.requirement_ids.length === 1 &&
|
||||||
|
result.requirement_ids[0] === requirementId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function extractRequirementsForRoute(input) {
|
||||||
|
const byFragment = new Map();
|
||||||
|
const requirements = [];
|
||||||
|
const pushRequirement = (item) => {
|
||||||
|
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
|
||||||
|
requirements.push({
|
||||||
|
requirement_id: item.requirement_id,
|
||||||
|
source_fragment_id: item.source_fragment_id,
|
||||||
|
requirement_text: item.requirement_text,
|
||||||
|
subject_tokens: subjectTokens,
|
||||||
|
status: item.status,
|
||||||
|
route: item.route
|
||||||
|
});
|
||||||
|
if (item.source_fragment_id) {
|
||||||
|
const current = byFragment.get(item.source_fragment_id) ?? [];
|
||||||
|
current.push(item.requirement_id);
|
||||||
|
byFragment.set(item.source_fragment_id, current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!input.routeSummary) {
|
||||||
|
pushRequirement({
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: null,
|
||||||
|
requirement_text: input.userMessage,
|
||||||
|
status: "clarification_needed",
|
||||||
|
route: null
|
||||||
|
});
|
||||||
|
return { requirements, byFragment };
|
||||||
|
}
|
||||||
|
if (input.routeSummary.mode === "legacy_v1") {
|
||||||
|
pushRequirement({
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: input.userMessage,
|
||||||
|
status: "covered",
|
||||||
|
route: input.routeSummary.route_hint
|
||||||
|
});
|
||||||
|
return { requirements, byFragment };
|
||||||
|
}
|
||||||
|
input.routeSummary.decisions.forEach((decision, index) => {
|
||||||
|
const requirementId = `R${index + 1}`;
|
||||||
|
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
|
||||||
|
let status = "covered";
|
||||||
|
if (decision.route === "no_route") {
|
||||||
|
if (decision.no_route_reason === "out_of_scope") {
|
||||||
|
status = "out_of_scope";
|
||||||
|
}
|
||||||
|
else if (decision.no_route_reason === "insufficient_specificity") {
|
||||||
|
status = "clarification_needed";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
status = "uncovered";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushRequirement({
|
||||||
|
requirement_id: requirementId,
|
||||||
|
source_fragment_id: decision.fragment_id,
|
||||||
|
requirement_text: text,
|
||||||
|
status,
|
||||||
|
route: decision.route === "no_route" ? null : decision.route
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { requirements, byFragment };
|
||||||
|
}
|
||||||
|
function evaluateCoverageForRequirements(requirements, retrievalResults) {
|
||||||
|
const statusByRequirement = new Map();
|
||||||
|
for (const result of retrievalResults) {
|
||||||
|
for (const requirementId of result.requirement_ids) {
|
||||||
|
const list = statusByRequirement.get(requirementId) ?? [];
|
||||||
|
list.push({
|
||||||
|
status: result.status,
|
||||||
|
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
|
||||||
|
});
|
||||||
|
statusByRequirement.set(requirementId, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resolvedRequirements = requirements.map((requirement) => {
|
||||||
|
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
|
||||||
|
return requirement;
|
||||||
|
}
|
||||||
|
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||||||
|
if (states.length === 0) {
|
||||||
|
return { ...requirement, status: "uncovered" };
|
||||||
|
}
|
||||||
|
const hasAnySubstantive = states.some((item) => item.substantive);
|
||||||
|
if (!hasAnySubstantive) {
|
||||||
|
return { ...requirement, status: "uncovered" };
|
||||||
|
}
|
||||||
|
const hasOk = states.some((item) => item.status === "ok");
|
||||||
|
const hasPartial = states.some((item) => item.status === "partial");
|
||||||
|
const hasEmpty = states.some((item) => item.status === "empty");
|
||||||
|
const hasError = states.some((item) => item.status === "error");
|
||||||
|
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
|
||||||
|
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
|
||||||
|
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
|
||||||
|
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
|
||||||
|
return { ...requirement, status: "covered" };
|
||||||
|
}
|
||||||
|
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
|
||||||
|
return { ...requirement, status: "partially_covered" };
|
||||||
|
}
|
||||||
|
return { ...requirement, status: "uncovered" };
|
||||||
|
});
|
||||||
|
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
|
||||||
|
const requirementsUncovered = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "uncovered")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
const requirementsPartiallyCovered = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "partially_covered")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
const clarificationNeededFor = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "clarification_needed")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
const outOfScopeRequirements = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "out_of_scope")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
return {
|
||||||
|
requirements: resolvedRequirements,
|
||||||
|
coverage: {
|
||||||
|
requirements_total: resolvedRequirements.length,
|
||||||
|
requirements_covered: requirementsCovered,
|
||||||
|
requirements_uncovered: requirementsUncovered,
|
||||||
|
requirements_partially_covered: requirementsPartiallyCovered,
|
||||||
|
clarification_needed_for: clarificationNeededFor,
|
||||||
|
out_of_scope_requirements: outOfScopeRequirements
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function checkGroundingForRequirements(input) {
|
||||||
|
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
|
||||||
|
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
|
||||||
|
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
|
||||||
|
const subjectTokens = input.extractSubjectTokens(input.userMessage);
|
||||||
|
const executedRoutes = new Set(input.retrievalResults
|
||||||
|
.filter((item) => item.status !== "error")
|
||||||
|
.map((item) => item.route)
|
||||||
|
.filter(Boolean));
|
||||||
|
const retrievalCorpus = JSON.stringify(input.retrievalResults.map((item) => ({
|
||||||
|
route: item.route,
|
||||||
|
result_type: item.result_type,
|
||||||
|
summary: item.summary,
|
||||||
|
items: item.items,
|
||||||
|
evidence: item.evidence,
|
||||||
|
why_included: item.why_included,
|
||||||
|
selection_reason: item.selection_reason,
|
||||||
|
risk_factors: item.risk_factors,
|
||||||
|
business_interpretation: item.business_interpretation
|
||||||
|
}))).toLowerCase();
|
||||||
|
const missingSubjectTokens = [];
|
||||||
|
const missingCriticalTokens = [];
|
||||||
|
for (const token of subjectTokens) {
|
||||||
|
const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
|
||||||
|
if (!match.matched) {
|
||||||
|
missingSubjectTokens.push(token);
|
||||||
|
if (match.critical) {
|
||||||
|
missingCriticalTokens.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onlyAccountCriticalMissing = missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
|
||||||
|
const accountOnlyMismatchRecoverable = hasMaterialResults &&
|
||||||
|
input.coverage.requirements_covered > 0 &&
|
||||||
|
onlyAccountCriticalMissing &&
|
||||||
|
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
|
||||||
|
const routeSubjectMatch = !hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
|
||||||
|
let status = "grounded";
|
||||||
|
const reasons = [];
|
||||||
|
if (!routeSubjectMatch) {
|
||||||
|
status = "route_mismatch_blocked";
|
||||||
|
reasons.push(`Ключевые ориентиры вопроса не подтверждены в найденных данных: ${missingCriticalTokens.join(", ")}`);
|
||||||
|
}
|
||||||
|
else if (accountOnlyMismatchRecoverable) {
|
||||||
|
status = "partial";
|
||||||
|
reasons.push(`Часть счетных ориентиров не подтвердилась напрямую (${missingCriticalTokens.join(", ")}), но есть опора для ограниченного вывода.`);
|
||||||
|
}
|
||||||
|
else if (input.coverage.requirements_covered === 0) {
|
||||||
|
status = "no_grounded_answer";
|
||||||
|
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
|
||||||
|
}
|
||||||
|
else if (input.coverage.requirements_uncovered.length > 0 ||
|
||||||
|
input.coverage.requirements_partially_covered.length > 0 ||
|
||||||
|
input.coverage.clarification_needed_for.length > 0 ||
|
||||||
|
input.coverage.out_of_scope_requirements.length > 0) {
|
||||||
|
status = "partial";
|
||||||
|
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
|
||||||
|
}
|
||||||
|
if (whyIncludedSummary.length === 0) {
|
||||||
|
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
|
||||||
|
}
|
||||||
|
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
|
||||||
|
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
|
||||||
|
}
|
||||||
|
const missingRequirements = [
|
||||||
|
...input.coverage.requirements_uncovered,
|
||||||
|
...input.coverage.requirements_partially_covered,
|
||||||
|
...input.coverage.clarification_needed_for,
|
||||||
|
...input.coverage.out_of_scope_requirements
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
route_subject_match: routeSubjectMatch,
|
||||||
|
missing_requirements: missingRequirements,
|
||||||
|
reasons,
|
||||||
|
why_included_summary: whyIncludedSummary,
|
||||||
|
selection_reason_summary: selectionReasonSummary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -141,6 +141,29 @@ function formatIsoDateUtc(date) {
|
||||||
const day = String(date.getUTCDate()).padStart(2, "0");
|
const day = String(date.getUTCDate()).padStart(2, "0");
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
function normalizeIsoDate(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
function monthEndFromIso(isoDate) {
|
function monthEndFromIso(isoDate) {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -198,14 +221,25 @@ function hasRbpSignal(text) {
|
||||||
function hasFixedAssetAmortizationSignal(text) {
|
function hasFixedAssetAmortizationSignal(text) {
|
||||||
return /(?:амортиз|основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|depreciat|fixed\s*asset|account\s*0[12]|счет\s*0[12])/i.test(String(text ?? "").toLowerCase());
|
return /(?:амортиз|основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|depreciat|fixed\s*asset|account\s*0[12]|счет\s*0[12])/i.test(String(text ?? "").toLowerCase());
|
||||||
}
|
}
|
||||||
function buildLiveMcpCallPlan(route, fragmentText) {
|
function buildLiveMcpCallPlan(route, fragmentText, temporalHint) {
|
||||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||||
const preferredDomainHint = (0, investigationState_1.inferP0DomainFromMessage)(fragmentText);
|
const preferredDomainHint = (0, investigationState_1.inferP0DomainFromMessage)(fragmentText);
|
||||||
const periodScope = inferPeriodScope(fragmentText);
|
const periodScope = inferPeriodScope(fragmentText);
|
||||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
const hintedAsOfDate = normalizeIsoDate(temporalHint?.as_of_date);
|
||||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
const hintedPeriodFrom = normalizeIsoDate(temporalHint?.period_from);
|
||||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
const hintedPeriodTo = normalizeIsoDate(temporalHint?.period_to);
|
||||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
const primaryFrom = periodScope.from ?? hintedPeriodFrom ?? hintedAsOfDate;
|
||||||
|
const primaryTo = periodScope.to ??
|
||||||
|
hintedPeriodTo ??
|
||||||
|
(!periodScope.from && !hintedPeriodFrom && hintedAsOfDate ? hintedAsOfDate : primaryFrom ? monthEndFromIso(primaryFrom) ?? primaryFrom : null);
|
||||||
|
const carryFrom = primaryFrom ? shiftIsoDate(primaryFrom, -31) ?? primaryFrom : null;
|
||||||
|
const carryTo = primaryTo ? shiftIsoDate(primaryTo, 31) ?? primaryTo : null;
|
||||||
|
const buildPrimaryQuery = (limit) => primaryFrom && primaryTo
|
||||||
|
? buildLiveRangeQuery(primaryFrom, primaryTo, limit)
|
||||||
|
: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
|
||||||
|
const buildCarryQuery = (limit) => carryFrom && carryTo
|
||||||
|
? buildLiveRangeQuery(carryFrom, carryTo, limit)
|
||||||
|
: buildPrimaryQuery(limit);
|
||||||
const faClaim = preferredDomainHint === "fixed_asset_amortization" ||
|
const faClaim = preferredDomainHint === "fixed_asset_amortization" ||
|
||||||
hasFixedAssetAmortizationSignal(fragmentText) ||
|
hasFixedAssetAmortizationSignal(fragmentText) ||
|
||||||
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
|
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
|
||||||
|
|
@ -219,7 +253,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_amortization_documents_in_period",
|
call_id: "find_amortization_documents_in_period",
|
||||||
purpose: "seed_amortization_documents",
|
purpose: "seed_amortization_documents",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -227,7 +261,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_fixed_asset_movements_accounts_01_02",
|
call_id: "find_fixed_asset_movements_accounts_01_02",
|
||||||
purpose: "collect_fa_object_movements",
|
purpose: "collect_fa_object_movements",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -235,7 +269,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_fixed_asset_cards_expected_for_period",
|
call_id: "find_fixed_asset_cards_expected_for_period",
|
||||||
purpose: "build_expected_fa_set",
|
purpose: "build_expected_fa_set",
|
||||||
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -243,7 +277,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "match_expected_vs_actual_fa_coverage",
|
call_id: "match_expected_vs_actual_fa_coverage",
|
||||||
purpose: "compare_expected_vs_actual_fa_coverage",
|
purpose: "compare_expected_vs_actual_fa_coverage",
|
||||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -265,7 +299,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_vat_source_documents_in_period",
|
call_id: "find_vat_source_documents_in_period",
|
||||||
purpose: "seed_vat_source_documents",
|
purpose: "seed_vat_source_documents",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -273,7 +307,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_vat_invoice_links_in_period",
|
call_id: "find_vat_invoice_links_in_period",
|
||||||
purpose: "collect_invoice_links",
|
purpose: "collect_invoice_links",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -281,7 +315,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_vat_register_entries_in_period",
|
call_id: "find_vat_register_entries_in_period",
|
||||||
purpose: "collect_vat_register_entries",
|
purpose: "collect_vat_register_entries",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -289,7 +323,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_vat_book_entries_in_period",
|
call_id: "find_vat_book_entries_in_period",
|
||||||
purpose: "collect_vat_book_entries",
|
purpose: "collect_vat_book_entries",
|
||||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -326,7 +360,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_rbp_writeoff_documents_in_period",
|
call_id: "find_rbp_writeoff_documents_in_period",
|
||||||
purpose: "seed_writeoff_documents",
|
purpose: "seed_writeoff_documents",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||||
|
|
@ -334,7 +368,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_rbp_object_movements_account_97",
|
call_id: "find_rbp_object_movements_account_97",
|
||||||
purpose: "collect_rbp_object_movements",
|
purpose: "collect_rbp_object_movements",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97"]
|
account_scope_override: ["97"]
|
||||||
|
|
@ -342,7 +376,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "find_month_close_entries_linked_to_rbp",
|
call_id: "find_month_close_entries_linked_to_rbp",
|
||||||
purpose: "link_month_close_to_rbp",
|
purpose: "link_month_close_to_rbp",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||||
|
|
@ -350,7 +384,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
||||||
{
|
{
|
||||||
call_id: "compute_end_period_residual_by_rbp_object",
|
call_id: "compute_end_period_residual_by_rbp_object",
|
||||||
purpose: "collect_residual_tail_signals",
|
purpose: "collect_residual_tail_signals",
|
||||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||||
|
|
@ -1410,7 +1444,7 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
||||||
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
||||||
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
|
if (/основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
|
||||||
hasFixedAssetAccountScope) {
|
hasFixedAssetAccountScope) {
|
||||||
pushMany(domainScope, ["fixed_assets"]);
|
pushMany(domainScope, ["fixed_assets"]);
|
||||||
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
||||||
|
|
@ -2243,7 +2277,7 @@ class AssistantDataLayer {
|
||||||
}
|
}
|
||||||
return enforceBroadQueryGuards(route, fragmentText, result);
|
return enforceBroadQueryGuards(route, fragmentText, result);
|
||||||
}
|
}
|
||||||
async executeRouteRuntime(route, fragmentText) {
|
async executeRouteRuntime(route, fragmentText, options) {
|
||||||
const base = this.executeRoute(route, fragmentText);
|
const base = this.executeRoute(route, fragmentText);
|
||||||
if (!config_1.FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
|
if (!config_1.FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
|
||||||
return base;
|
return base;
|
||||||
|
|
@ -2251,7 +2285,7 @@ class AssistantDataLayer {
|
||||||
if (route !== "hybrid_store_plus_live" && route !== "live_mcp_drilldown") {
|
if (route !== "hybrid_store_plus_live" && route !== "live_mcp_drilldown") {
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
|
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText, options?.temporalHint);
|
||||||
return this.mergeWithLiveOverlay(base, liveOverlay);
|
return this.mergeWithLiveOverlay(base, liveOverlay);
|
||||||
}
|
}
|
||||||
cloneRawResult(base) {
|
cloneRawResult(base) {
|
||||||
|
|
@ -2301,9 +2335,9 @@ class AssistantDataLayer {
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
async fetchLiveMcpOverlay(route, fragmentText) {
|
async fetchLiveMcpOverlay(route, fragmentText, temporalHint) {
|
||||||
const endpoint = this.buildMcpUrl("/api/execute_query");
|
const endpoint = this.buildMcpUrl("/api/execute_query");
|
||||||
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
|
const livePlan = buildLiveMcpCallPlan(route, fragmentText, temporalHint);
|
||||||
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
||||||
const accountScope = livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
const accountScope = livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||||
? ["01", "02", "08"]
|
? ["01", "02", "08"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
|
||||||
|
function toAnalysisContext(input) {
|
||||||
|
if (!input.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
as_of_date: input.as_of_date,
|
||||||
|
period_from: input.period_from,
|
||||||
|
period_to: input.period_to,
|
||||||
|
source: input.source,
|
||||||
|
snapshot_mode: input.snapshot_mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildDeepAnalysisDebugPayload(input) {
|
||||||
|
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
return {
|
||||||
|
trace_id: input.traceId,
|
||||||
|
prompt_version: input.promptVersion,
|
||||||
|
schema_version: input.schemaVersion,
|
||||||
|
fallback_type: input.fallbackType,
|
||||||
|
route_summary: input.routeSummary,
|
||||||
|
fragments: input.fragments,
|
||||||
|
requirements_extracted: input.requirementsExtracted,
|
||||||
|
coverage_report: input.coverageReport,
|
||||||
|
routes: input.routes,
|
||||||
|
retrieval_status: input.retrievalStatus,
|
||||||
|
retrieval_results: input.retrievalResults,
|
||||||
|
answer_grounding_check: input.groundingCheck,
|
||||||
|
dropped_intent_segments: input.droppedIntentSegments,
|
||||||
|
question_type_class: input.questionTypeClass,
|
||||||
|
company_anchors: input.companyAnchors,
|
||||||
|
analysis_context_applied: input.runtimeAnalysisContext.active,
|
||||||
|
analysis_context: analysisContext,
|
||||||
|
business_scope_raw: input.businessScopeResolution.business_scope_raw,
|
||||||
|
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
|
||||||
|
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
|
||||||
|
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
raw_time_anchor: input.temporalGuard.raw_time_anchor,
|
||||||
|
raw_time_scope: input.temporalGuard.raw_time_scope,
|
||||||
|
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
|
||||||
|
resolved_primary_period: input.temporalGuard.resolved_primary_period,
|
||||||
|
effective_primary_period: input.temporalGuard.effective_primary_period,
|
||||||
|
temporal_guard_input: input.temporalGuard.temporal_guard_input,
|
||||||
|
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
|
||||||
|
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
|
||||||
|
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
|
||||||
|
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
|
||||||
|
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
|
||||||
|
temporal_guard: input.temporalGuard,
|
||||||
|
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
|
||||||
|
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
|
||||||
|
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
|
||||||
|
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
|
||||||
|
domain_polarity_guard: input.polarityAudit,
|
||||||
|
claim_anchor_audit: input.claimAnchorAudit,
|
||||||
|
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
|
||||||
|
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||||
|
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||||
|
targeted_evidence_acquisition: input.targetedEvidenceAudit,
|
||||||
|
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
|
||||||
|
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
|
||||||
|
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
|
||||||
|
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||||
|
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
|
||||||
|
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
|
||||||
|
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
|
||||||
|
? {
|
||||||
|
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
address_llm_predecompose_attempted: Boolean(input.addressRuntimeMetaForDeep?.attempted),
|
||||||
|
address_llm_predecompose_applied: Boolean(input.addressRuntimeMetaForDeep?.applied),
|
||||||
|
address_llm_predecompose_reason: input.addressRuntimeMetaForDeep?.reason ?? null,
|
||||||
|
address_llm_predecompose_provider: input.addressRuntimeMetaForDeep?.provider ?? null,
|
||||||
|
address_fallback_rule_hit: input.addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
|
||||||
|
address_tool_gate_decision: input.addressRuntimeMetaForDeep?.toolGateDecision ?? null,
|
||||||
|
address_tool_gate_reason: input.addressRuntimeMetaForDeep?.toolGateReason ?? null,
|
||||||
|
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
|
||||||
|
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
|
||||||
|
assistant_outcome_class_v1: input.outcomeClassV1,
|
||||||
|
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
|
||||||
|
answer_structure_v11: input.answerStructureV11,
|
||||||
|
investigation_state_snapshot: input.investigationStateSnapshot,
|
||||||
|
normalized: input.normalizedPayload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildDeepAnswerArtifacts = buildDeepAnswerArtifacts;
|
||||||
|
exports.buildAssistantConversationItem = buildAssistantConversationItem;
|
||||||
|
const assistantAnswerPackageBuilder_1 = require("./assistantAnswerPackageBuilder");
|
||||||
|
function stripTechnicalTail(text) {
|
||||||
|
return String(text ?? "")
|
||||||
|
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||||
|
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function buildDeepAnswerArtifacts(input) {
|
||||||
|
const safeAssistantReply = stripTechnicalTail(input.safeAssistantReplyBase);
|
||||||
|
const answerStructureV11 = input.featureContractsV11
|
||||||
|
? input.featureAnswerPolicyV11 && input.compositionAnswerStructureV11
|
||||||
|
? input.compositionAnswerStructureV11
|
||||||
|
: (0, assistantAnswerPackageBuilder_1.buildAssistantAnswerStructureV11)({
|
||||||
|
assistantReply: safeAssistantReply,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
safeAssistantReply,
|
||||||
|
answerStructureV11
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildAssistantConversationItem(input) {
|
||||||
|
return {
|
||||||
|
message_id: input.messageId,
|
||||||
|
session_id: input.sessionId,
|
||||||
|
role: "assistant",
|
||||||
|
text: input.text,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
trace_id: input.traceId,
|
||||||
|
debug: input.debug
|
||||||
|
};
|
||||||
|
}
|
||||||
40
llm_normalizer/backend/dist/services/assistantDeepTurnCompositionRuntimeAdapter.js
vendored
Normal file
40
llm_normalizer/backend/dist/services/assistantDeepTurnCompositionRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantDeepTurnComposition = buildAssistantDeepTurnComposition;
|
||||||
|
const questionTypeResolver_1 = require("./questionTypeResolver");
|
||||||
|
const answerComposer_1 = require("./answerComposer");
|
||||||
|
function buildAssistantDeepTurnComposition(input) {
|
||||||
|
const resolveQuestionTypeSafe = input.resolveQuestionTypeFn ?? questionTypeResolver_1.resolveQuestionType;
|
||||||
|
const composeAssistantAnswerSafe = input.composeAssistantAnswerFn ?? answerComposer_1.composeAssistantAnswer;
|
||||||
|
const followupApplied = Boolean(input.followupUsage?.applied);
|
||||||
|
const focusDomainHint = followupApplied
|
||||||
|
? input.investigationState?.followup_context?.active_domain ?? input.investigationState?.focus.domain ?? null
|
||||||
|
: null;
|
||||||
|
const questionTypeClass = resolveQuestionTypeSafe(input.userMessage);
|
||||||
|
const companyAnchorSet = input.companyAnchors;
|
||||||
|
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchorSet?.dates) && companyAnchorSet.dates.some((item) => String(item ?? "").trim().length > 0)) ||
|
||||||
|
(Array.isArray(companyAnchorSet?.periods) && companyAnchorSet.periods.some((item) => String(item ?? "").trim().length > 0));
|
||||||
|
const normalizationPeriodExplicit = input.hasExplicitPeriodAnchor(input.normalizedPayload) || hasPeriodInCompanyAnchors;
|
||||||
|
const composition = composeAssistantAnswerSafe({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
requirements: input.requirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
focusDomainHint,
|
||||||
|
questionTypeHint: questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
normalizationPeriodExplicit,
|
||||||
|
enableAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||||
|
enableProblemCentricAnswerV1: input.featureProblemCentricAnswerV1,
|
||||||
|
enableLifecycleAnswerV1: input.featureLifecycleAnswerV1
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
focusDomainHint,
|
||||||
|
questionTypeClass,
|
||||||
|
hasPeriodInCompanyAnchors,
|
||||||
|
normalizationPeriodExplicit,
|
||||||
|
composition
|
||||||
|
};
|
||||||
|
}
|
||||||
72
llm_normalizer/backend/dist/services/assistantDeepTurnContextRuntimeAdapter.js
vendored
Normal file
72
llm_normalizer/backend/dist/services/assistantDeepTurnContextRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantDeepTurnRuntimeContext = buildAssistantDeepTurnRuntimeContext;
|
||||||
|
const KNOWN_P0_DOMAINS = new Set([
|
||||||
|
"settlements_60_62",
|
||||||
|
"vat_document_register_book",
|
||||||
|
"month_close_costs_20_44",
|
||||||
|
"fixed_asset_amortization"
|
||||||
|
]);
|
||||||
|
function toAnalysisContext(runtimeAnalysisContext) {
|
||||||
|
if (!runtimeAnalysisContext.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
as_of_date: runtimeAnalysisContext.as_of_date,
|
||||||
|
period_from: runtimeAnalysisContext.period_from,
|
||||||
|
period_to: runtimeAnalysisContext.period_to,
|
||||||
|
source: runtimeAnalysisContext.source
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildAssistantDeepTurnRuntimeContext(input) {
|
||||||
|
const companyAnchors = input.resolveCompanyAnchors(input.userMessage);
|
||||||
|
const initialBusinessScopeResolution = input.resolveBusinessScopeAlignment({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
normalized: input.normalizedPayload,
|
||||||
|
routeSummary: input.routeSummary
|
||||||
|
});
|
||||||
|
const inferredDomainByMessage = input.inferP0DomainFromMessage(input.userMessage);
|
||||||
|
const focusDomainForGuards = inferredDomainByMessage && KNOWN_P0_DOMAINS.has(inferredDomainByMessage) ? inferredDomainByMessage : null;
|
||||||
|
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
const temporalGuard = input.resolveTemporalGuard({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalized: input.normalizedPayload,
|
||||||
|
companyAnchors,
|
||||||
|
analysisContext
|
||||||
|
});
|
||||||
|
const domainPolarityGuardInitial = input.resolveDomainPolarityGuard({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
focusDomainHint: focusDomainForGuards
|
||||||
|
});
|
||||||
|
const claimAnchorAudit = input.resolveClaimBoundAnchors({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
focusDomainHint: focusDomainForGuards,
|
||||||
|
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
|
||||||
|
});
|
||||||
|
const businessScopeResolution = input.resolveBusinessScopeFromLiveContext({
|
||||||
|
current: initialBusinessScopeResolution,
|
||||||
|
temporalGuard,
|
||||||
|
claimType: claimAnchorAudit.claim_type,
|
||||||
|
focusDomainHint: focusDomainForGuards,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
followupApplied: Boolean(input.followupUsage?.applied)
|
||||||
|
});
|
||||||
|
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
|
||||||
|
const liveTemporalHint = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
return {
|
||||||
|
companyAnchors,
|
||||||
|
initialBusinessScopeResolution,
|
||||||
|
inferredDomainByMessage,
|
||||||
|
focusDomainForGuards,
|
||||||
|
temporalGuard,
|
||||||
|
domainPolarityGuardInitial,
|
||||||
|
claimAnchorAudit,
|
||||||
|
businessScopeResolution,
|
||||||
|
resolvedRouteSummary,
|
||||||
|
liveTemporalHint
|
||||||
|
};
|
||||||
|
}
|
||||||
49
llm_normalizer/backend/dist/services/assistantDeepTurnGroundingRuntimeAdapter.js
vendored
Normal file
49
llm_normalizer/backend/dist/services/assistantDeepTurnGroundingRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.runAssistantDeepTurnGroundingRuntime = runAssistantDeepTurnGroundingRuntime;
|
||||||
|
const assistantOrchestrationRuntimeAdapter_1 = require("./assistantOrchestrationRuntimeAdapter");
|
||||||
|
const assistantDeepTurnGuardRuntimeAdapter_1 = require("./assistantDeepTurnGuardRuntimeAdapter");
|
||||||
|
function runAssistantDeepTurnGroundingRuntime(input) {
|
||||||
|
const runCoverageGroundingPipelineSafe = input.runCoverageGroundingPipelineFn ?? assistantOrchestrationRuntimeAdapter_1.runAssistantCoverageGroundingPipeline;
|
||||||
|
const applyGroundingEligibilitySafe = input.applyGroundingEligibilityFn ??
|
||||||
|
((payload) => (0, assistantDeepTurnGuardRuntimeAdapter_1.applyAssistantDeepTurnGroundingEligibility)(payload));
|
||||||
|
const rbpLiveRouteAudit = input.collectRbpLiveRouteAudit({
|
||||||
|
claimType: input.claimType,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
planAudit: input.rbpPlanAudit
|
||||||
|
});
|
||||||
|
const faLiveRouteAudit = input.collectFaLiveRouteAudit({
|
||||||
|
claimType: input.claimType,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
planAudit: input.faPlanAudit
|
||||||
|
});
|
||||||
|
const orchestrationRuntime = runCoverageGroundingPipelineSafe({
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
normalized: input.normalizedPayload,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
requirementExtraction: input.requirementExtraction,
|
||||||
|
extractRequirements: input.extractRequirements,
|
||||||
|
evaluateCoverage: input.evaluateCoverage,
|
||||||
|
checkGrounding: input.checkGrounding
|
||||||
|
});
|
||||||
|
const coverageEvaluation = orchestrationRuntime.coverageEvaluation;
|
||||||
|
const groundingCheckBase = orchestrationRuntime.groundingCheckBase;
|
||||||
|
const groundingEligibilityRuntime = applyGroundingEligibilitySafe({
|
||||||
|
groundingCheckBase,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
evidenceAudit: input.evidenceAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
|
||||||
|
businessScopeResolved: input.businessScopeResolved
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit,
|
||||||
|
coverageEvaluation,
|
||||||
|
groundingCheckBase,
|
||||||
|
groundedAnswerEligibilityGuard: groundingEligibilityRuntime.groundedAnswerEligibilityGuard,
|
||||||
|
groundingCheck: groundingEligibilityRuntime.groundingCheck
|
||||||
|
};
|
||||||
|
}
|
||||||
51
llm_normalizer/backend/dist/services/assistantDeepTurnGuardRuntimeAdapter.js
vendored
Normal file
51
llm_normalizer/backend/dist/services/assistantDeepTurnGuardRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.applyAssistantDeepTurnRetrievalGuards = applyAssistantDeepTurnRetrievalGuards;
|
||||||
|
exports.applyAssistantDeepTurnGroundingEligibility = applyAssistantDeepTurnGroundingEligibility;
|
||||||
|
const assistantClaimBoundEvidence_1 = require("./assistantClaimBoundEvidence");
|
||||||
|
const assistantRuntimeGuards_1 = require("./assistantRuntimeGuards");
|
||||||
|
function applyAssistantDeepTurnRetrievalGuards(input) {
|
||||||
|
const applyDomainPolarityGuardSafe = input.applyDomainPolarityGuardFn ?? assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults;
|
||||||
|
const applyTargetedEvidenceSafe = input.applyTargetedEvidenceFn ?? assistantClaimBoundEvidence_1.applyTargetedEvidenceAcquisition;
|
||||||
|
const applyEvidenceAdmissibilityGateSafe = input.applyEvidenceAdmissibilityGateFn ?? assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate;
|
||||||
|
const polarityGuardResult = applyDomainPolarityGuardSafe({
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
guard: input.domainPolarityGuardInitial
|
||||||
|
});
|
||||||
|
const targetedEvidenceResult = applyTargetedEvidenceSafe({
|
||||||
|
retrievalResults: polarityGuardResult.retrievalResults,
|
||||||
|
claimAudit: input.claimAnchorAudit
|
||||||
|
});
|
||||||
|
const evidenceGateResult = applyEvidenceAdmissibilityGateSafe({
|
||||||
|
retrievalResults: targetedEvidenceResult.retrievalResults,
|
||||||
|
temporal: input.temporalGuard,
|
||||||
|
focusDomainHint: input.focusDomainForGuards,
|
||||||
|
polarity: polarityGuardResult.audit.polarity,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
userMessage: input.userMessage
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
retrievalResults: evidenceGateResult.retrievalResults,
|
||||||
|
polarityGuardResult,
|
||||||
|
targetedEvidenceResult,
|
||||||
|
evidenceGateResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function applyAssistantDeepTurnGroundingEligibility(input) {
|
||||||
|
const evaluateGroundedAnswerEligibilitySafe = input.evaluateGroundedAnswerEligibilityFn ?? assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility;
|
||||||
|
const applyEligibilityToGroundingCheckSafe = input.applyEligibilityToGroundingCheckFn ??
|
||||||
|
((groundingCheck, eligibility) => (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheck, eligibility));
|
||||||
|
const groundedAnswerEligibilityGuard = evaluateGroundedAnswerEligibilitySafe({
|
||||||
|
temporal: input.temporalGuard,
|
||||||
|
polarity: input.polarityAudit,
|
||||||
|
evidence: input.evidenceAudit,
|
||||||
|
claimAnchors: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
|
||||||
|
businessScopeResolved: input.businessScopeResolved
|
||||||
|
});
|
||||||
|
const groundingCheck = applyEligibilityToGroundingCheckSafe(input.groundingCheckBase, groundedAnswerEligibilityGuard);
|
||||||
|
return {
|
||||||
|
groundedAnswerEligibilityGuard,
|
||||||
|
groundingCheck
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantDeepTurnPackagingInput = buildAssistantDeepTurnPackagingInput;
|
||||||
|
function buildAssistantDeepTurnPackagingInput(args) {
|
||||||
|
return {
|
||||||
|
...args,
|
||||||
|
routesForDebug: Array.isArray(args.routesForDebug) ? args.routesForDebug : [],
|
||||||
|
followupStateUsage: args.followupStateUsage ?? null,
|
||||||
|
composition: {
|
||||||
|
reply_type: args.composition.reply_type,
|
||||||
|
fallback_type: args.composition.fallback_type,
|
||||||
|
answer_structure_v11: args.composition.answer_structure_v11 ?? null,
|
||||||
|
problem_centric_answer_applied: args.composition.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: args.composition.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: args.composition.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
problem_unit_ids_used: Array.isArray(args.composition.problem_unit_ids_used) ? args.composition.problem_unit_ids_used : []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.assembleAssistantDeepTurnPackaging = assembleAssistantDeepTurnPackaging;
|
||||||
|
const assistantEvidenceBundleAssembler_1 = require("./assistantEvidenceBundleAssembler");
|
||||||
|
const assistantContractsBundleAssembler_1 = require("./assistantContractsBundleAssembler");
|
||||||
|
const assistantDeepResponseAssembler_1 = require("./assistantDeepResponseAssembler");
|
||||||
|
const assistantDebugPayloadAssembler_1 = require("./assistantDebugPayloadAssembler");
|
||||||
|
const assistantMessageLogAssembler_1 = require("./assistantMessageLogAssembler");
|
||||||
|
function assembleAssistantDeepTurnPackaging(input) {
|
||||||
|
const normalizedPayload = (input.normalized.normalized ?? null);
|
||||||
|
const normalizedFragments = Array.isArray(normalizedPayload?.["fragments"]) ? normalizedPayload?.["fragments"] : [];
|
||||||
|
const evidenceBundleAssembly = (0, assistantEvidenceBundleAssembler_1.assembleAssistantEvidenceBundle)({
|
||||||
|
retrievalCalls: input.retrievalCalls,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const contractsBundleV1 = (0, assistantContractsBundleAssembler_1.assembleAssistantContractsBundleV1)({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalizedQuestion: input.normalizedQuestion,
|
||||||
|
normalized: input.normalized.normalized,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
analysisContext: input.analysisContextForContract,
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
requirements: input.requirementExtractionRequirements,
|
||||||
|
evidenceBundleContractV1: evidenceBundleAssembly.evidenceBundleContractV1,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
grounding: input.groundingCheck,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const deepAnswerArtifacts = (0, assistantDeepResponseAssembler_1.buildDeepAnswerArtifacts)({
|
||||||
|
safeAssistantReplyBase: input.safeAssistantReplyBase,
|
||||||
|
featureContractsV11: input.featureContractsV11,
|
||||||
|
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||||
|
compositionAnswerStructureV11: input.composition.answer_structure_v11 ?? null,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const debug = (0, assistantDebugPayloadAssembler_1.buildDeepAnalysisDebugPayload)({
|
||||||
|
traceId: input.normalized.trace_id,
|
||||||
|
promptVersion: input.normalized.prompt_version,
|
||||||
|
schemaVersion: input.normalized.schema_version,
|
||||||
|
fallbackType: input.composition.fallback_type,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
fragments: normalizedFragments,
|
||||||
|
requirementsExtracted: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
routes: input.routesForDebug,
|
||||||
|
retrievalStatus: evidenceBundleAssembly.retrievalStatus,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
questionTypeClass: input.questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
businessScopeResolution: input.businessScopeResolution,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceAudit: input.targetedEvidenceAudit,
|
||||||
|
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
|
||||||
|
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit: input.faLiveRouteAudit,
|
||||||
|
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
|
||||||
|
followupStateUsage: input.followupStateUsage,
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : []
|
||||||
|
},
|
||||||
|
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep,
|
||||||
|
outcomeClassV1: contractsBundleV1.outcomeClassV1,
|
||||||
|
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
|
||||||
|
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
|
||||||
|
investigationStateSnapshot: input.investigationStateSnapshot,
|
||||||
|
normalizedPayload: normalizedPayload
|
||||||
|
});
|
||||||
|
const assistantItem = (0, assistantDeepResponseAssembler_1.buildAssistantConversationItem)({
|
||||||
|
messageId: input.messageId,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
text: deepAnswerArtifacts.safeAssistantReply,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
traceId: input.normalized.trace_id,
|
||||||
|
debug: debug
|
||||||
|
});
|
||||||
|
const deepAnalysisLogDetails = (0, assistantMessageLogAssembler_1.buildDeepAnalysisProcessedLogDetails)({
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
messageId: input.messageId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalizerOutput: input.normalized.normalized,
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
resolvedExecutionState: input.resolvedExecutionState,
|
||||||
|
routes: input.routesForDebug,
|
||||||
|
retrievalCalls: input.retrievalCalls,
|
||||||
|
retrievalResultsRaw: input.retrievalResultsRaw,
|
||||||
|
retrievalResultsNormalized: input.retrievalResults,
|
||||||
|
requirementsExtracted: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
questionTypeClass: input.questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
businessScopeResolution: input.businessScopeResolution,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceAudit: input.targetedEvidenceAudit,
|
||||||
|
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
|
||||||
|
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit: input.faLiveRouteAudit,
|
||||||
|
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
|
||||||
|
followupStateUsage: input.followupStateUsage,
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : [],
|
||||||
|
fallback_type: input.composition.fallback_type
|
||||||
|
},
|
||||||
|
outcomeClassV1: contractsBundleV1.outcomeClassV1,
|
||||||
|
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
|
||||||
|
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
|
||||||
|
investigationStateSnapshot: input.investigationStateSnapshot,
|
||||||
|
assistantReply: deepAnswerArtifacts.safeAssistantReply,
|
||||||
|
traceId: input.normalized.trace_id
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
evidenceBundleAssembly,
|
||||||
|
contractsBundleV1,
|
||||||
|
deepAnswerArtifacts,
|
||||||
|
debug,
|
||||||
|
assistantItem,
|
||||||
|
deepAnalysisLogDetails
|
||||||
|
};
|
||||||
|
}
|
||||||
106
llm_normalizer/backend/dist/services/assistantDeepTurnPackagingRuntimeAdapter.js
vendored
Normal file
106
llm_normalizer/backend/dist/services/assistantDeepTurnPackagingRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.runAssistantDeepTurnPackagingRuntime = runAssistantDeepTurnPackagingRuntime;
|
||||||
|
const nanoid_1 = require("nanoid");
|
||||||
|
const assistantDeepTurnInputBuilder_1 = require("./assistantDeepTurnInputBuilder");
|
||||||
|
const assistantDeepTurnPackaging_1 = require("./assistantDeepTurnPackaging");
|
||||||
|
const assistantDeepTurnPrePackagingContext_1 = require("./assistantDeepTurnPrePackagingContext");
|
||||||
|
const assistantInvestigationStateRuntimeAdapter_1 = require("./assistantInvestigationStateRuntimeAdapter");
|
||||||
|
function runAssistantDeepTurnPackagingRuntime(input) {
|
||||||
|
const buildPrePackagingContextSafe = input.buildPrePackagingContextFn ?? assistantDeepTurnPrePackagingContext_1.buildAssistantDeepTurnPrePackagingContext;
|
||||||
|
const buildInvestigationStateSnapshotSafe = input.buildInvestigationStateSnapshotFn ?? assistantInvestigationStateRuntimeAdapter_1.buildAssistantInvestigationStateSnapshot;
|
||||||
|
const persistInvestigationStateSnapshotSafe = input.persistInvestigationStateSnapshotFn ?? assistantInvestigationStateRuntimeAdapter_1.persistAssistantInvestigationStateSnapshot;
|
||||||
|
const buildDeepTurnPackagingInputSafe = input.buildDeepTurnPackagingInputFn ?? assistantDeepTurnInputBuilder_1.buildAssistantDeepTurnPackagingInput;
|
||||||
|
const assembleDeepTurnPackagingSafe = input.assembleDeepTurnPackagingFn ?? assistantDeepTurnPackaging_1.assembleAssistantDeepTurnPackaging;
|
||||||
|
const deepTurnPrePackagingContext = buildPrePackagingContextSafe({
|
||||||
|
normalizedPayload: input.normalized.normalized,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
assistantReply: input.composition.assistant_reply,
|
||||||
|
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
|
||||||
|
buildDebugRoutes: input.buildDebugRoutes,
|
||||||
|
extractExecutionState: input.extractExecutionState,
|
||||||
|
sanitizeReply: input.sanitizeReply
|
||||||
|
});
|
||||||
|
const investigationStateSnapshot = buildInvestigationStateSnapshotSafe({
|
||||||
|
featureEnabled: input.featureInvestigationStateV1,
|
||||||
|
previousState: input.previousInvestigationState,
|
||||||
|
timestamp: input.nowIso ? input.nowIso() : new Date().toISOString(),
|
||||||
|
questionId: input.questionId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
requirements: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
followupApplied: input.followupApplied
|
||||||
|
});
|
||||||
|
persistInvestigationStateSnapshotSafe({
|
||||||
|
featureEnabled: input.featureInvestigationStateV1,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
snapshot: investigationStateSnapshot,
|
||||||
|
persist: input.persistInvestigationState
|
||||||
|
});
|
||||||
|
const messageId = input.messageIdFactory ? input.messageIdFactory() : `msg-${(0, nanoid_1.nanoid)(10)}`;
|
||||||
|
const deepTurnPackagingInput = buildDeepTurnPackagingInputSafe({
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
messageId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalized: input.normalized,
|
||||||
|
normalizedQuestion: input.normalizedQuestion,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
|
||||||
|
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
requirementExtractionRequirements: input.requirementExtractionRequirements,
|
||||||
|
coverageEvaluationRequirements: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
retrievalCalls: input.retrievalCalls,
|
||||||
|
retrievalResultsRaw: input.retrievalResultsRaw,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
|
||||||
|
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
|
||||||
|
questionTypeClass: input.questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
businessScopeResolution: input.businessScopeResolution,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceAudit: input.targetedEvidenceAudit,
|
||||||
|
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
|
||||||
|
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit: input.faLiveRouteAudit,
|
||||||
|
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
|
||||||
|
followupStateUsage: input.followupStateUsage,
|
||||||
|
composition: {
|
||||||
|
reply_type: input.composition.reply_type,
|
||||||
|
fallback_type: input.composition.fallback_type,
|
||||||
|
answer_structure_v11: input.composition.answer_structure_v11,
|
||||||
|
problem_centric_answer_applied: input.composition.problem_centric_answer_applied,
|
||||||
|
problem_units_used_count: input.composition.problem_units_used_count,
|
||||||
|
problem_answer_mode: input.composition.problem_answer_mode,
|
||||||
|
problem_unit_ids_used: input.composition.problem_unit_ids_used
|
||||||
|
},
|
||||||
|
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
|
||||||
|
featureContractsV11: input.featureContractsV11,
|
||||||
|
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||||
|
investigationStateSnapshot,
|
||||||
|
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep
|
||||||
|
});
|
||||||
|
const deepTurnPackaging = assembleDeepTurnPackagingSafe(deepTurnPackagingInput);
|
||||||
|
return {
|
||||||
|
messageId,
|
||||||
|
investigationStateSnapshot,
|
||||||
|
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
|
||||||
|
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
|
||||||
|
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
|
||||||
|
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
|
||||||
|
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
|
||||||
|
safeAssistantReply: deepTurnPackaging.deepAnswerArtifacts.safeAssistantReply,
|
||||||
|
debug: deepTurnPackaging.debug,
|
||||||
|
assistantItem: deepTurnPackaging.assistantItem,
|
||||||
|
deepAnalysisLogDetails: deepTurnPackaging.deepAnalysisLogDetails
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantDeepTurnExecutionPlan = buildAssistantDeepTurnExecutionPlan;
|
||||||
|
function buildAssistantDeepTurnExecutionPlan(input) {
|
||||||
|
const requirementExtraction = input.extractRequirements(input.routeSummary, input.normalizedPayload, input.userMessage);
|
||||||
|
let executionPlan = input.toExecutionPlan(input.routeSummary, input.normalizedPayload, input.userMessage, requirementExtraction.byFragment);
|
||||||
|
const rbpRoutePlanEnforcement = input.enforceRbpLiveRoutePlan({
|
||||||
|
executionPlan,
|
||||||
|
claimType: input.claimType,
|
||||||
|
temporalGuard: input.temporalGuard
|
||||||
|
});
|
||||||
|
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||||||
|
const faRoutePlanEnforcement = input.enforceFaLiveRoutePlan({
|
||||||
|
executionPlan,
|
||||||
|
claimType: input.claimType,
|
||||||
|
temporalGuard: input.temporalGuard
|
||||||
|
});
|
||||||
|
executionPlan = faRoutePlanEnforcement.executionPlan;
|
||||||
|
executionPlan = input.applyTemporalHintToExecutionPlan(executionPlan, input.temporalGuard);
|
||||||
|
executionPlan = input.applyPolarityHintToExecutionPlan(executionPlan, input.domainPolarityGuardInitial);
|
||||||
|
return {
|
||||||
|
requirementExtraction,
|
||||||
|
executionPlan,
|
||||||
|
rbpRoutePlanEnforcement,
|
||||||
|
faRoutePlanEnforcement
|
||||||
|
};
|
||||||
|
}
|
||||||
20
llm_normalizer/backend/dist/services/assistantDeepTurnPrePackagingContext.js
vendored
Normal file
20
llm_normalizer/backend/dist/services/assistantDeepTurnPrePackagingContext.js
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantDeepTurnPrePackagingContext = buildAssistantDeepTurnPrePackagingContext;
|
||||||
|
function buildAssistantDeepTurnPrePackagingContext(input) {
|
||||||
|
return {
|
||||||
|
droppedIntentSegments: input.extractDroppedIntentSegments(input.normalizedPayload),
|
||||||
|
analysisContextForContract: input.runtimeAnalysisContext.active
|
||||||
|
? {
|
||||||
|
as_of_date: input.runtimeAnalysisContext.as_of_date,
|
||||||
|
period_from: input.runtimeAnalysisContext.period_from,
|
||||||
|
period_to: input.runtimeAnalysisContext.period_to,
|
||||||
|
source: input.runtimeAnalysisContext.source,
|
||||||
|
snapshot_mode: input.runtimeAnalysisContext.snapshot_mode
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
routesForDebug: input.buildDebugRoutes(input.routeSummary),
|
||||||
|
resolvedExecutionState: input.extractExecutionState(input.normalizedPayload),
|
||||||
|
safeAssistantReplyBase: input.sanitizeReply(input.assistantReply, "Нужны уточнения для надежного ответа.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantDeepTurnSuccessResponse = buildAssistantDeepTurnSuccessResponse;
|
||||||
|
function buildAssistantDeepTurnSuccessResponse(input) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
session_id: input.sessionId,
|
||||||
|
assistant_reply: input.assistantReply,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
conversation_item: input.conversationItem,
|
||||||
|
debug: input.debug,
|
||||||
|
conversation: input.conversation
|
||||||
|
};
|
||||||
|
}
|
||||||
78
llm_normalizer/backend/dist/services/assistantDeepTurnRetrievalRuntimeAdapter.js
vendored
Normal file
78
llm_normalizer/backend/dist/services/assistantDeepTurnRetrievalRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.executeAssistantDeepTurnRetrievalPlan = executeAssistantDeepTurnRetrievalPlan;
|
||||||
|
const retrievalResultNormalizer_1 = require("./retrievalResultNormalizer");
|
||||||
|
function buildRouteExecutorErrorRawResult(route, message) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {
|
||||||
|
route
|
||||||
|
},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "low",
|
||||||
|
limitations: ["Route executor failed."],
|
||||||
|
errors: [message]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function executeAssistantDeepTurnRetrievalPlan(input) {
|
||||||
|
const normalizeRetrievalResultSafe = input.normalizeRetrievalResultFn ?? retrievalResultNormalizer_1.normalizeRetrievalResult;
|
||||||
|
const retrievalCalls = [];
|
||||||
|
const retrievalResultsRaw = [];
|
||||||
|
const retrievalResults = [];
|
||||||
|
for (const planItem of input.executionPlan) {
|
||||||
|
if (!planItem.should_execute) {
|
||||||
|
retrievalCalls.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
requirement_ids: planItem.requirement_ids,
|
||||||
|
route: planItem.route,
|
||||||
|
status: "skipped",
|
||||||
|
query_text: planItem.fragment_text,
|
||||||
|
reason: input.mapNoRouteReason(planItem.no_route_reason)
|
||||||
|
});
|
||||||
|
retrievalResults.push(input.buildSkippedResult(planItem));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
retrievalCalls.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
requirement_ids: planItem.requirement_ids,
|
||||||
|
route: planItem.route,
|
||||||
|
status: "executed",
|
||||||
|
query_text: planItem.fragment_text,
|
||||||
|
reason: null
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const raw = await input.executeRouteRuntime(planItem.route, planItem.fragment_text, {
|
||||||
|
temporalHint: input.liveTemporalHint
|
||||||
|
});
|
||||||
|
retrievalResultsRaw.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
route: planItem.route,
|
||||||
|
raw_result: raw
|
||||||
|
});
|
||||||
|
retrievalResults.push(normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
retrievalCalls[retrievalCalls.length - 1].status = "failed";
|
||||||
|
retrievalCalls[retrievalCalls.length - 1].reason = message;
|
||||||
|
const rawError = buildRouteExecutorErrorRawResult(planItem.route, message);
|
||||||
|
retrievalResultsRaw.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
route: planItem.route,
|
||||||
|
raw_result: rawError
|
||||||
|
});
|
||||||
|
retrievalResults.push(normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
retrievalCalls,
|
||||||
|
retrievalResultsRaw,
|
||||||
|
retrievalResults
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.assembleAssistantEvidenceBundle = assembleAssistantEvidenceBundle;
|
||||||
|
const assistantOrchestrationContracts_1 = require("./assistantOrchestrationContracts");
|
||||||
|
function buildRetrievalStatus(retrievalResults) {
|
||||||
|
return retrievalResults.map((item) => ({
|
||||||
|
fragment_id: item.fragment_id,
|
||||||
|
requirement_ids: item.requirement_ids,
|
||||||
|
route: item.route,
|
||||||
|
status: item.status,
|
||||||
|
result_type: item.result_type
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function assembleAssistantEvidenceBundle(input) {
|
||||||
|
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
|
||||||
|
return {
|
||||||
|
evidenceBundleContractV1: (0, assistantOrchestrationContracts_1.buildAssistantEvidenceBundleContractV1)({
|
||||||
|
retrievalCalls: Array.isArray(input.retrievalCalls) ? input.retrievalCalls : [],
|
||||||
|
retrievalResults
|
||||||
|
}),
|
||||||
|
retrievalStatus: buildRetrievalStatus(retrievalResults)
|
||||||
|
};
|
||||||
|
}
|
||||||
29
llm_normalizer/backend/dist/services/assistantInvestigationStateRuntimeAdapter.js
vendored
Normal file
29
llm_normalizer/backend/dist/services/assistantInvestigationStateRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantInvestigationStateSnapshot = buildAssistantInvestigationStateSnapshot;
|
||||||
|
exports.persistAssistantInvestigationStateSnapshot = persistAssistantInvestigationStateSnapshot;
|
||||||
|
const investigationState_1 = require("./investigationState");
|
||||||
|
function buildAssistantInvestigationStateSnapshot(input) {
|
||||||
|
if (!input.featureEnabled || !input.previousState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (0, investigationState_1.updateInvestigationState)({
|
||||||
|
previous: input.previousState,
|
||||||
|
timestamp: input.timestamp,
|
||||||
|
questionId: input.questionId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
requirements: input.requirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
replyType: input.replyType,
|
||||||
|
followupApplied: input.followupApplied
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function persistAssistantInvestigationStateSnapshot(input) {
|
||||||
|
if (!input.featureEnabled || !input.snapshot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
input.persist(input.sessionId, input.snapshot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildDeepAnalysisProcessedLogDetails = buildDeepAnalysisProcessedLogDetails;
|
||||||
|
function toAnalysisContext(input) {
|
||||||
|
if (!input.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
as_of_date: input.as_of_date,
|
||||||
|
period_from: input.period_from,
|
||||||
|
period_to: input.period_to,
|
||||||
|
source: input.source,
|
||||||
|
snapshot_mode: input.snapshot_mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveCoverageStatus(coverageReport) {
|
||||||
|
return coverageReport.requirements_total === coverageReport.requirements_covered &&
|
||||||
|
coverageReport.requirements_uncovered.length === 0 &&
|
||||||
|
coverageReport.requirements_partially_covered.length === 0
|
||||||
|
? "full"
|
||||||
|
: "partial_or_limited";
|
||||||
|
}
|
||||||
|
function buildDeepAnalysisProcessedLogDetails(input) {
|
||||||
|
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
return {
|
||||||
|
session_id: input.sessionId,
|
||||||
|
message_id: input.messageId,
|
||||||
|
user_message: input.userMessage,
|
||||||
|
normalizer_output: input.normalizerOutput,
|
||||||
|
execution_plan: input.executionPlan,
|
||||||
|
resolved_execution_state: input.resolvedExecutionState,
|
||||||
|
routes: input.routes,
|
||||||
|
retrieval_calls: input.retrievalCalls,
|
||||||
|
retrieval_results_raw: input.retrievalResultsRaw,
|
||||||
|
retrieval_results_normalized: input.retrievalResultsNormalized,
|
||||||
|
requirements_extracted: input.requirementsExtracted,
|
||||||
|
requirements_total: input.coverageReport.requirements_total,
|
||||||
|
requirements_covered: input.coverageReport.requirements_covered,
|
||||||
|
requirements_uncovered: input.coverageReport.requirements_uncovered,
|
||||||
|
coverage_status: resolveCoverageStatus(input.coverageReport),
|
||||||
|
answer_grounding_status: input.groundingCheck.status,
|
||||||
|
reply_semantic_type: input.replyType,
|
||||||
|
why_included_summary: input.groundingCheck.why_included_summary,
|
||||||
|
selection_reason_summary: input.groundingCheck.selection_reason_summary,
|
||||||
|
route_subject_match: input.groundingCheck.route_subject_match,
|
||||||
|
clarification_target: input.coverageReport.clarification_needed_for,
|
||||||
|
dropped_intent_segments: input.droppedIntentSegments,
|
||||||
|
question_type_class: input.questionTypeClass,
|
||||||
|
company_anchors: input.companyAnchors,
|
||||||
|
analysis_context_applied: input.runtimeAnalysisContext.active,
|
||||||
|
analysis_context: analysisContext,
|
||||||
|
business_scope_raw: input.businessScopeResolution.business_scope_raw,
|
||||||
|
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
|
||||||
|
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
|
||||||
|
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
raw_time_anchor: input.temporalGuard.raw_time_anchor,
|
||||||
|
raw_time_scope: input.temporalGuard.raw_time_scope,
|
||||||
|
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
|
||||||
|
resolved_primary_period: input.temporalGuard.resolved_primary_period,
|
||||||
|
effective_primary_period: input.temporalGuard.effective_primary_period,
|
||||||
|
temporal_guard_input: input.temporalGuard.temporal_guard_input,
|
||||||
|
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
|
||||||
|
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
|
||||||
|
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
|
||||||
|
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
|
||||||
|
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
|
||||||
|
temporal_guard: input.temporalGuard,
|
||||||
|
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
|
||||||
|
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
|
||||||
|
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
|
||||||
|
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
|
||||||
|
domain_polarity_guard: input.polarityAudit,
|
||||||
|
claim_anchor_audit: input.claimAnchorAudit,
|
||||||
|
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
|
||||||
|
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||||
|
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||||
|
targeted_evidence_acquisition: input.targetedEvidenceAudit,
|
||||||
|
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
|
||||||
|
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
|
||||||
|
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
|
||||||
|
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||||
|
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
|
||||||
|
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
|
||||||
|
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
|
||||||
|
? {
|
||||||
|
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
assistant_outcome_class_v1: input.outcomeClassV1,
|
||||||
|
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
|
||||||
|
answer_structure_v11: input.answerStructureV11,
|
||||||
|
investigation_state_snapshot: input.investigationStateSnapshot,
|
||||||
|
fallback_type: input.compositionDebug.fallback_type,
|
||||||
|
assistant_reply: input.assistantReply,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
trace_id: input.traceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.classifyAssistantOutcomeClassV1 = classifyAssistantOutcomeClassV1;
|
||||||
|
exports.buildAssistantQueryFrameContractV1 = buildAssistantQueryFrameContractV1;
|
||||||
|
exports.buildAssistantExecutionPlanContractV1 = buildAssistantExecutionPlanContractV1;
|
||||||
|
exports.buildAssistantEvidenceBundleContractV1 = buildAssistantEvidenceBundleContractV1;
|
||||||
|
exports.buildAssistantCoverageContractV1 = buildAssistantCoverageContractV1;
|
||||||
|
function normalizeSnapshotMode(value) {
|
||||||
|
const token = String(value ?? "").trim();
|
||||||
|
if (token === "force_snapshot" || token === "force_live") {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return "auto";
|
||||||
|
}
|
||||||
|
function extractFragmentsTotal(normalized) {
|
||||||
|
if (!normalized || typeof normalized !== "object") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const source = normalized;
|
||||||
|
const fragments = source.fragments;
|
||||||
|
return Array.isArray(fragments) ? fragments.length : 0;
|
||||||
|
}
|
||||||
|
function collectEvidenceTotals(retrievalResults) {
|
||||||
|
let evidenceTotal = 0;
|
||||||
|
const sourceRefs = new Set();
|
||||||
|
let limitationTotal = 0;
|
||||||
|
let errorTotal = 0;
|
||||||
|
for (const result of retrievalResults) {
|
||||||
|
evidenceTotal += Array.isArray(result.evidence) ? result.evidence.length : 0;
|
||||||
|
limitationTotal += Array.isArray(result.limitations) ? result.limitations.length : 0;
|
||||||
|
errorTotal += Array.isArray(result.errors) ? result.errors.length : 0;
|
||||||
|
for (const evidence of result.evidence ?? []) {
|
||||||
|
const ref = String(evidence?.source_ref?.canonical_ref ?? "").trim();
|
||||||
|
if (ref) {
|
||||||
|
sourceRefs.add(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
evidence_total: evidenceTotal,
|
||||||
|
source_refs_total: sourceRefs.size,
|
||||||
|
limitation_total: limitationTotal,
|
||||||
|
error_total: errorTotal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function classifyAssistantOutcomeClassV1(input) {
|
||||||
|
const replyType = input.replyType;
|
||||||
|
const grounding = input.grounding;
|
||||||
|
const coverage = input.coverageReport;
|
||||||
|
const hasOnlyErrors = input.retrievalResults.length > 0 &&
|
||||||
|
input.retrievalResults.every((item) => item.status === "error");
|
||||||
|
if (replyType === "backend_error" || hasOnlyErrors) {
|
||||||
|
return "BLOCKED_BY_TOOLING";
|
||||||
|
}
|
||||||
|
if (grounding.status === "route_mismatch_blocked" || replyType === "route_mismatch_blocked") {
|
||||||
|
return "MISROUTED";
|
||||||
|
}
|
||||||
|
if (replyType === "clarification_required" || coverage.clarification_needed_for.length > 0) {
|
||||||
|
return "BLOCKED_BY_AMBIGUITY";
|
||||||
|
}
|
||||||
|
if (replyType === "out_of_scope") {
|
||||||
|
return "BLOCKED_BY_AMBIGUITY";
|
||||||
|
}
|
||||||
|
const fullCoverage = coverage.requirements_total > 0 &&
|
||||||
|
coverage.requirements_total === coverage.requirements_covered &&
|
||||||
|
coverage.requirements_uncovered.length === 0 &&
|
||||||
|
coverage.requirements_partially_covered.length === 0 &&
|
||||||
|
coverage.clarification_needed_for.length === 0 &&
|
||||||
|
coverage.out_of_scope_requirements.length === 0;
|
||||||
|
if (fullCoverage && grounding.status === "grounded") {
|
||||||
|
return "FULLY_ANSWERED";
|
||||||
|
}
|
||||||
|
const hasAnyCoverage = coverage.requirements_covered > 0 ||
|
||||||
|
coverage.requirements_partially_covered.length > 0 ||
|
||||||
|
grounding.status === "partial";
|
||||||
|
if (hasAnyCoverage) {
|
||||||
|
return "PARTIALLY_ANSWERED";
|
||||||
|
}
|
||||||
|
const missingRequirementSignal = grounding.missing_requirements.length > 0 ||
|
||||||
|
coverage.requirements_uncovered.length > 0 ||
|
||||||
|
coverage.requirements_total > 0;
|
||||||
|
const possibleBindingFailure = replyType === "no_grounded_answer" &&
|
||||||
|
missingRequirementSignal &&
|
||||||
|
grounding.route_subject_match;
|
||||||
|
if (possibleBindingFailure) {
|
||||||
|
return "FAILED_TO_BIND_ENTITIES";
|
||||||
|
}
|
||||||
|
return "BLOCKED_BY_MISSING_DATA";
|
||||||
|
}
|
||||||
|
function buildAssistantQueryFrameContractV1(input) {
|
||||||
|
const analysis = input.analysisContext
|
||||||
|
? {
|
||||||
|
as_of_date: input.analysisContext.as_of_date ?? null,
|
||||||
|
period_from: input.analysisContext.period_from ?? null,
|
||||||
|
period_to: input.analysisContext.period_to ?? null,
|
||||||
|
source: input.analysisContext.source ?? null,
|
||||||
|
snapshot_mode: normalizeSnapshotMode(input.analysisContext.snapshot_mode)
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_query_frame_v1",
|
||||||
|
original_user_question: String(input.userMessage ?? ""),
|
||||||
|
normalized_question: String(input.normalizedQuestion ?? ""),
|
||||||
|
route_summary_mode: input.routeSummary?.mode ?? "none",
|
||||||
|
fragments_total: extractFragmentsTotal(input.normalized),
|
||||||
|
dropped_intent_segments: Array.isArray(input.droppedIntentSegments) ? [...input.droppedIntentSegments] : [],
|
||||||
|
analysis_context: analysis
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildAssistantExecutionPlanContractV1(input) {
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_execution_plan_v1",
|
||||||
|
steps: (Array.isArray(input.executionPlan) ? input.executionPlan : []).map((item) => ({
|
||||||
|
fragment_id: String(item.fragment_id ?? ""),
|
||||||
|
route: String(item.route ?? ""),
|
||||||
|
should_execute: Boolean(item.should_execute),
|
||||||
|
requirement_ids: Array.isArray(item.requirement_ids) ? [...item.requirement_ids] : [],
|
||||||
|
no_route_reason: item.no_route_reason ?? null,
|
||||||
|
clarification_reason: item.clarification_reason ?? null
|
||||||
|
})),
|
||||||
|
requirements_total: Array.isArray(input.requirements) ? input.requirements.length : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildAssistantEvidenceBundleContractV1(input) {
|
||||||
|
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
|
||||||
|
const breakdown = {
|
||||||
|
ok: retrievalResults.filter((item) => item.status === "ok").length,
|
||||||
|
partial: retrievalResults.filter((item) => item.status === "partial").length,
|
||||||
|
empty: retrievalResults.filter((item) => item.status === "empty").length,
|
||||||
|
error: retrievalResults.filter((item) => item.status === "error").length
|
||||||
|
};
|
||||||
|
const totals = collectEvidenceTotals(retrievalResults);
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_evidence_bundle_v1",
|
||||||
|
retrieval_calls_total: Array.isArray(input.retrievalCalls) ? input.retrievalCalls.length : 0,
|
||||||
|
retrieval_results_total: retrievalResults.length,
|
||||||
|
retrieval_status_breakdown: breakdown,
|
||||||
|
...totals
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildAssistantCoverageContractV1(input) {
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_coverage_contract_v1",
|
||||||
|
coverage_report: input.coverageReport,
|
||||||
|
grounding: input.grounding,
|
||||||
|
outcome_class: input.outcomeClass
|
||||||
|
};
|
||||||
|
}
|
||||||
13
llm_normalizer/backend/dist/services/assistantOrchestrationRuntimeAdapter.js
vendored
Normal file
13
llm_normalizer/backend/dist/services/assistantOrchestrationRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.runAssistantCoverageGroundingPipeline = runAssistantCoverageGroundingPipeline;
|
||||||
|
function runAssistantCoverageGroundingPipeline(input) {
|
||||||
|
const requirementExtraction = input.requirementExtraction ?? input.extractRequirements(input.routeSummary, input.normalized, input.userMessage);
|
||||||
|
const coverageEvaluation = input.evaluateCoverage(requirementExtraction.requirements, input.retrievalResults);
|
||||||
|
const groundingCheckBase = input.checkGrounding(input.userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, input.retrievalResults);
|
||||||
|
return {
|
||||||
|
requirementExtraction,
|
||||||
|
coverageEvaluation,
|
||||||
|
groundingCheckBase
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildFragmentTextById = buildFragmentTextById;
|
||||||
|
exports.buildExecutionPlanFromRoute = buildExecutionPlanFromRoute;
|
||||||
|
exports.buildDebugRoutesFromRoute = buildDebugRoutesFromRoute;
|
||||||
|
function escapeRegex(value) {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
function enrichFragmentTextWithHints(fragment, text) {
|
||||||
|
const baseText = String(text ?? "").trim();
|
||||||
|
const accountHints = Array.isArray(fragment.account_hints)
|
||||||
|
? Array.from(new Set(fragment.account_hints.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0)))
|
||||||
|
: [];
|
||||||
|
if (accountHints.length === 0) {
|
||||||
|
return baseText;
|
||||||
|
}
|
||||||
|
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
|
||||||
|
if (hasAccountInText) {
|
||||||
|
return baseText;
|
||||||
|
}
|
||||||
|
return `${baseText}, по счету ${accountHints.join(", ")}`;
|
||||||
|
}
|
||||||
|
function buildFragmentTextById(fragments) {
|
||||||
|
const result = new Map();
|
||||||
|
for (const item of fragments) {
|
||||||
|
if (!item || typeof item !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fragment = item;
|
||||||
|
const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : "";
|
||||||
|
if (!fragmentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const text = (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
|
||||||
|
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
|
||||||
|
"";
|
||||||
|
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function buildExecutionPlanFromRoute(input) {
|
||||||
|
if (!input.routeSummary) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (input.routeSummary.mode === "legacy_v1") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: input.requirementByFragment.get("F1") ?? ["R1"],
|
||||||
|
route: input.routeSummary.route_hint,
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: input.userMessage,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return input.routeSummary.decisions.map((decision) => {
|
||||||
|
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
|
||||||
|
if (decision.route === "no_route") {
|
||||||
|
return {
|
||||||
|
fragment_id: decision.fragment_id,
|
||||||
|
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
|
||||||
|
route: "no_route",
|
||||||
|
should_execute: false,
|
||||||
|
fragment_text: text,
|
||||||
|
no_route_reason: decision.no_route_reason ?? null,
|
||||||
|
clarification_reason: decision.clarification_reason ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fragment_id: decision.fragment_id,
|
||||||
|
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
|
||||||
|
route: decision.route,
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: text,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: decision.clarification_reason ?? null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function buildDebugRoutesFromRoute(input) {
|
||||||
|
if (!input.routeSummary) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (input.routeSummary.mode === "legacy_v1") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: input.routeSummary.route_hint,
|
||||||
|
reason: input.resolveLegacyRouteReason(input.routeSummary.route_hint),
|
||||||
|
confidence: input.routeSummary.confidence,
|
||||||
|
intent_class: input.routeSummary.intent_class
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return input.routeSummary.decisions.map((decision) => ({
|
||||||
|
fragment_id: decision.fragment_id,
|
||||||
|
route: decision.route,
|
||||||
|
reason: decision.reason,
|
||||||
|
route_status: decision.route_status ?? null,
|
||||||
|
no_route_reason: decision.no_route_reason ?? null,
|
||||||
|
clarification_reason: decision.clarification_reason ?? null,
|
||||||
|
execution_readiness: decision.execution_readiness ?? null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
@ -584,7 +584,78 @@ function toTemporalGuardInput(window, fallback) {
|
||||||
const value = String(fallback ?? "").trim();
|
const value = String(fallback ?? "").trim();
|
||||||
return value || null;
|
return value || null;
|
||||||
}
|
}
|
||||||
|
function normalizeIsoDate(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
|
function normalizeTemporalWindow(input) {
|
||||||
|
const asOfDate = normalizeIsoDate(input.asOfDate);
|
||||||
|
if (asOfDate) {
|
||||||
|
return {
|
||||||
|
from: asOfDate,
|
||||||
|
to: asOfDate,
|
||||||
|
granularity: "day"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const from = normalizeIsoDate(input.periodFrom);
|
||||||
|
const to = normalizeIsoDate(input.periodTo);
|
||||||
|
if (!from || !to) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
granularity: from === to ? "day" : "month"
|
||||||
|
};
|
||||||
|
}
|
||||||
function resolveTemporalGuard(input) {
|
function resolveTemporalGuard(input) {
|
||||||
|
const analysisWindow = normalizeTemporalWindow({
|
||||||
|
asOfDate: input.analysisContext?.as_of_date,
|
||||||
|
periodFrom: input.analysisContext?.period_from,
|
||||||
|
periodTo: input.analysisContext?.period_to
|
||||||
|
});
|
||||||
|
if (analysisWindow) {
|
||||||
|
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
|
||||||
|
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
|
||||||
|
return {
|
||||||
|
raw_time_anchor: analysisWindow.from,
|
||||||
|
raw_time_scope: guardInput,
|
||||||
|
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
|
||||||
|
resolved_primary_period: analysisWindow,
|
||||||
|
effective_primary_period: analysisWindow,
|
||||||
|
temporal_guard_input: guardInput,
|
||||||
|
temporal_alignment_status: "aligned",
|
||||||
|
temporal_resolution_source: source,
|
||||||
|
temporal_guard_basis: "raw_time_scope_unlocked",
|
||||||
|
temporal_guard_applied: false,
|
||||||
|
temporal_guard_outcome: "passed",
|
||||||
|
primary_period_window: null,
|
||||||
|
allowed_context_window: null,
|
||||||
|
controlled_temporal_expansion_enabled: false,
|
||||||
|
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
|
||||||
|
normalized_anchor_drift_detected: false,
|
||||||
|
reason_codes: ["analysis_context_applied"]
|
||||||
|
};
|
||||||
|
}
|
||||||
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
|
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
|
||||||
const julyAnchor = resolveJulyAnchor(rawAnchorText);
|
const julyAnchor = resolveJulyAnchor(rawAnchorText);
|
||||||
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
|
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
|
||||||
|
|
@ -654,9 +725,14 @@ function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
|
||||||
return executionPlan;
|
return executionPlan;
|
||||||
}
|
}
|
||||||
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
|
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
|
||||||
|
const periodLabel = primaryWindow
|
||||||
|
? `${primaryWindow.from}..${primaryWindow.to}`
|
||||||
|
: temporal.resolved_time_anchor
|
||||||
|
? temporal.resolved_time_anchor
|
||||||
|
: "active_period";
|
||||||
const hint = primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
|
const hint = primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
|
||||||
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
|
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
|
||||||
: `primary period July 2020 (${primaryWindow?.from ?? JULY_WINDOW.from}..${primaryWindow?.to ?? JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
|
: `primary period ${periodLabel}; controlled temporal expansion only for linked entities`;
|
||||||
return executionPlan.map((item) => {
|
return executionPlan.map((item) => {
|
||||||
if (!item.should_execute) {
|
if (!item.should_execute) {
|
||||||
return item;
|
return item;
|
||||||
|
|
@ -1319,15 +1395,15 @@ function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
|
||||||
? "no_grounded_answer"
|
? "no_grounded_answer"
|
||||||
: "partial";
|
: "partial";
|
||||||
const reasonMap = {
|
const reasonMap = {
|
||||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
|
||||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
|
||||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
|
||||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
|
||||||
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
|
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
|
||||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
|
||||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
|
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
|
||||||
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
|
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
|
||||||
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
|
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
|
||||||
};
|
};
|
||||||
const reasons = [
|
const reasons = [
|
||||||
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.commitAssistantTurnAndLog = commitAssistantTurnAndLog;
|
||||||
|
function commitAssistantTurnAndLog(input) {
|
||||||
|
input.appendItem(input.sessionId, input.assistantItem);
|
||||||
|
const currentSession = input.getSession(input.sessionId);
|
||||||
|
if (currentSession) {
|
||||||
|
input.persistSession(currentSession);
|
||||||
|
}
|
||||||
|
const conversation = input.cloneConversation(currentSession?.items ?? []);
|
||||||
|
input.logEvent({
|
||||||
|
timestamp: (input.nowIso ?? (() => new Date().toISOString()))(),
|
||||||
|
level: "info",
|
||||||
|
service: "assistant_loop",
|
||||||
|
message: "assistant_message_processed",
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
eventType: input.eventType,
|
||||||
|
details: input.logDetails
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
currentSession,
|
||||||
|
conversation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -169,6 +169,29 @@ function parseRawQuestions(rawQuestions) {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return byLine.length > 0 ? byLine : [text];
|
return byLine.length > 0 ? byLine : [text];
|
||||||
}
|
}
|
||||||
|
function normalizeAnalysisDate(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
function executionReadinessOf(fragment) {
|
function executionReadinessOf(fragment) {
|
||||||
return "execution_readiness" in fragment ? fragment.execution_readiness : "executable";
|
return "execution_readiness" in fragment ? fragment.execution_readiness : "executable";
|
||||||
}
|
}
|
||||||
|
|
@ -759,6 +782,13 @@ class EvalService {
|
||||||
...payload.normalizeConfig,
|
...payload.normalizeConfig,
|
||||||
userQuestion: item.raw_question,
|
userQuestion: item.raw_question,
|
||||||
context: {
|
context: {
|
||||||
|
period_hint: payload.analysisDate ?? undefined,
|
||||||
|
analysis_context: payload.analysisDate
|
||||||
|
? {
|
||||||
|
as_of_date: payload.analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
eval_label: runId,
|
eval_label: runId,
|
||||||
case_id: item.case_id,
|
case_id: item.case_id,
|
||||||
eval_mode: payload.mode
|
eval_mode: payload.mode
|
||||||
|
|
@ -1553,6 +1583,7 @@ class EvalService {
|
||||||
const suite = parseAssistantSuiteFile(payload.caseSetFile);
|
const suite = parseAssistantSuiteFile(payload.caseSetFile);
|
||||||
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
||||||
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
|
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
|
||||||
|
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
|
||||||
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
|
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
|
||||||
const diagnostics = [];
|
const diagnostics = [];
|
||||||
let requestsTotal = 0;
|
let requestsTotal = 0;
|
||||||
|
|
@ -1579,6 +1610,15 @@ class EvalService {
|
||||||
developerPrompt: payload.normalizeConfig.developerPrompt,
|
developerPrompt: payload.normalizeConfig.developerPrompt,
|
||||||
domainPrompt: payload.normalizeConfig.domainPrompt,
|
domainPrompt: payload.normalizeConfig.domainPrompt,
|
||||||
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
||||||
|
context: analysisDate
|
||||||
|
? {
|
||||||
|
period_hint: analysisDate,
|
||||||
|
analysis_context: {
|
||||||
|
as_of_date: analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
useMock: payload.useMock
|
useMock: payload.useMock
|
||||||
}));
|
}));
|
||||||
turnResponses.push(response);
|
turnResponses.push(response);
|
||||||
|
|
@ -1820,6 +1860,7 @@ class EvalService {
|
||||||
eval_target: "assistant_stage1",
|
eval_target: "assistant_stage1",
|
||||||
mode: payload.mode,
|
mode: payload.mode,
|
||||||
use_mock: Boolean(payload.useMock),
|
use_mock: Boolean(payload.useMock),
|
||||||
|
analysis_date: analysisDate,
|
||||||
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
||||||
suite_id: suite.suite_id,
|
suite_id: suite.suite_id,
|
||||||
suite_version: suite.suite_version,
|
suite_version: suite.suite_version,
|
||||||
|
|
@ -1887,6 +1928,7 @@ class EvalService {
|
||||||
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
|
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
|
||||||
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
||||||
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${(0, nanoid_1.nanoid)(10)}`;
|
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${(0, nanoid_1.nanoid)(10)}`;
|
||||||
|
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
|
||||||
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
|
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
|
||||||
const diagnostics = [];
|
const diagnostics = [];
|
||||||
let requestsTotal = 0;
|
let requestsTotal = 0;
|
||||||
|
|
@ -1915,6 +1957,15 @@ class EvalService {
|
||||||
developerPrompt: payload.normalizeConfig.developerPrompt,
|
developerPrompt: payload.normalizeConfig.developerPrompt,
|
||||||
domainPrompt: payload.normalizeConfig.domainPrompt,
|
domainPrompt: payload.normalizeConfig.domainPrompt,
|
||||||
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
||||||
|
context: analysisDate
|
||||||
|
? {
|
||||||
|
period_hint: analysisDate,
|
||||||
|
analysis_context: {
|
||||||
|
as_of_date: analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
useMock: payload.useMock
|
useMock: payload.useMock
|
||||||
}));
|
}));
|
||||||
turnResponses.push(response);
|
turnResponses.push(response);
|
||||||
|
|
@ -2090,6 +2141,7 @@ class EvalService {
|
||||||
eval_target: "assistant_stage2",
|
eval_target: "assistant_stage2",
|
||||||
mode: payload.mode,
|
mode: payload.mode,
|
||||||
use_mock: Boolean(payload.useMock),
|
use_mock: Boolean(payload.useMock),
|
||||||
|
analysis_date: analysisDate,
|
||||||
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
||||||
suite_id: suite.suite_id,
|
suite_id: suite.suite_id,
|
||||||
suite_version: suite.suite_version,
|
suite_version: suite.suite_version,
|
||||||
|
|
@ -2172,6 +2224,7 @@ class EvalService {
|
||||||
async run(payload) {
|
async run(payload) {
|
||||||
const mode = payload.mode ?? "standard";
|
const mode = payload.mode ?? "standard";
|
||||||
const evalTarget = payload.evalTarget ?? "normalizer";
|
const evalTarget = payload.evalTarget ?? "normalizer";
|
||||||
|
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
|
||||||
if (evalTarget === "assistant_stage1") {
|
if (evalTarget === "assistant_stage1") {
|
||||||
return this.runAssistantStage1({
|
return this.runAssistantStage1({
|
||||||
normalizeConfig: payload.normalizeConfig,
|
normalizeConfig: payload.normalizeConfig,
|
||||||
|
|
@ -2180,6 +2233,7 @@ class EvalService {
|
||||||
mode,
|
mode,
|
||||||
caseSetFile: payload.caseSetFile,
|
caseSetFile: payload.caseSetFile,
|
||||||
compareWithReportFile: payload.compareWithReportFile,
|
compareWithReportFile: payload.compareWithReportFile,
|
||||||
|
analysisDate: analysisDate ?? undefined,
|
||||||
runId: payload.runId
|
runId: payload.runId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2191,6 +2245,7 @@ class EvalService {
|
||||||
mode,
|
mode,
|
||||||
caseSetFile: payload.caseSetFile,
|
caseSetFile: payload.caseSetFile,
|
||||||
compareWithReportFile: payload.compareWithReportFile,
|
compareWithReportFile: payload.compareWithReportFile,
|
||||||
|
analysisDate: analysisDate ?? undefined,
|
||||||
runId: payload.runId
|
runId: payload.runId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2231,6 +2286,7 @@ class EvalService {
|
||||||
return this.runV2({
|
return this.runV2({
|
||||||
...payload,
|
...payload,
|
||||||
mode,
|
mode,
|
||||||
|
analysisDate: analysisDate ?? undefined,
|
||||||
cases: filtered
|
cases: filtered
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2256,6 +2312,13 @@ class EvalService {
|
||||||
...payload.normalizeConfig,
|
...payload.normalizeConfig,
|
||||||
userQuestion: item.raw_question,
|
userQuestion: item.raw_question,
|
||||||
context: {
|
context: {
|
||||||
|
period_hint: analysisDate ?? undefined,
|
||||||
|
analysis_context: analysisDate
|
||||||
|
? {
|
||||||
|
as_of_date: analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
expected_route: item.expected.route_hint,
|
expected_route: item.expected.route_hint,
|
||||||
eval_label: runId,
|
eval_label: runId,
|
||||||
case_id: item.case_id,
|
case_id: item.case_id,
|
||||||
|
|
@ -2366,6 +2429,7 @@ class EvalService {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
mode,
|
mode,
|
||||||
use_mock: Boolean(payload.useMock),
|
use_mock: Boolean(payload.useMock),
|
||||||
|
analysis_date: analysisDate,
|
||||||
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
||||||
dataset: {
|
dataset: {
|
||||||
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",
|
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ function resolveQuestionType(input) {
|
||||||
if (bestType !== "unknown") {
|
if (bestType !== "unknown") {
|
||||||
return bestType;
|
return bestType;
|
||||||
}
|
}
|
||||||
if (/[?пјџ]/u.test(text)) {
|
if (/(?:\bwhy\b|почему|из-?за\s+чего|в\s+ч(?:е|ё)м\s+причина)/iu.test(text)) {
|
||||||
return "why_breaks";
|
return "why_breaks";
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ interface RunSummary {
|
||||||
llm_provider: string | null;
|
llm_provider: string | null;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
use_mock: boolean | null;
|
use_mock: boolean | null;
|
||||||
|
analysis_date: string | null;
|
||||||
prompt_version: string | null;
|
prompt_version: string | null;
|
||||||
schema_version: string | null;
|
schema_version: string | null;
|
||||||
suite_id: string | null;
|
suite_id: string | null;
|
||||||
|
|
@ -1012,6 +1013,7 @@ function buildRunSummary(run: IndexedRun): RunSummary {
|
||||||
llm_provider: llmProvider,
|
llm_provider: llmProvider,
|
||||||
model,
|
model,
|
||||||
use_mock: toBooleanSafe(run.report.use_mock),
|
use_mock: toBooleanSafe(run.report.use_mock),
|
||||||
|
analysis_date: toStringSafe(run.report.analysis_date),
|
||||||
prompt_version: toStringSafe(run.report.prompt_version),
|
prompt_version: toStringSafe(run.report.prompt_version),
|
||||||
schema_version: toStringSafe(run.report.schema_version),
|
schema_version: toStringSafe(run.report.schema_version),
|
||||||
suite_id: toStringSafe(run.report.suite_id),
|
suite_id: toStringSafe(run.report.suite_id),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ interface EvalAsyncJob {
|
||||||
eval_target: EvalTarget;
|
eval_target: EvalTarget;
|
||||||
run_id: string;
|
run_id: string;
|
||||||
case_set_file: string | null;
|
case_set_file: string | null;
|
||||||
|
analysis_date: string | null;
|
||||||
total_cases: number;
|
total_cases: number;
|
||||||
completed_cases: number;
|
completed_cases: number;
|
||||||
cases: EvalAsyncCaseInfo[];
|
cases: EvalAsyncCaseInfo[];
|
||||||
|
|
@ -131,6 +132,32 @@ function normalizeCaseIds(value: unknown): string[] | undefined {
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAnalysisDate(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildEvalPayloadFromBody(body: Record<string, unknown>): {
|
function buildEvalPayloadFromBody(body: Record<string, unknown>): {
|
||||||
normalizeConfig: Omit<NormalizeRequestPayload, "userQuestion" | "context">;
|
normalizeConfig: Omit<NormalizeRequestPayload, "userQuestion" | "context">;
|
||||||
caseIds?: string[];
|
caseIds?: string[];
|
||||||
|
|
@ -140,7 +167,11 @@ function buildEvalPayloadFromBody(body: Record<string, unknown>): {
|
||||||
rawQuestions?: string;
|
rawQuestions?: string;
|
||||||
evalTarget: EvalTarget;
|
evalTarget: EvalTarget;
|
||||||
compareWithReportFile?: string;
|
compareWithReportFile?: string;
|
||||||
|
analysisDate?: string;
|
||||||
} {
|
} {
|
||||||
|
const analysisDate =
|
||||||
|
normalizeAnalysisDate(body.analysis_date) ??
|
||||||
|
normalizeAnalysisDate(body.analysisDate);
|
||||||
return {
|
return {
|
||||||
normalizeConfig: (body.normalizeConfig ?? {}) as Omit<NormalizeRequestPayload, "userQuestion" | "context">,
|
normalizeConfig: (body.normalizeConfig ?? {}) as Omit<NormalizeRequestPayload, "userQuestion" | "context">,
|
||||||
caseIds: normalizeCaseIds(body.caseIds),
|
caseIds: normalizeCaseIds(body.caseIds),
|
||||||
|
|
@ -154,7 +185,8 @@ function buildEvalPayloadFromBody(body: Record<string, unknown>): {
|
||||||
? body.compare_with_report_file
|
? body.compare_with_report_file
|
||||||
: typeof body.comparisonBaselineReportFile === "string"
|
: typeof body.comparisonBaselineReportFile === "string"
|
||||||
? body.comparisonBaselineReportFile
|
? body.comparisonBaselineReportFile
|
||||||
: undefined
|
: undefined,
|
||||||
|
analysisDate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,6 +332,7 @@ function snapshotJob(job: EvalAsyncJob): Record<string, unknown> {
|
||||||
eval_target: job.eval_target,
|
eval_target: job.eval_target,
|
||||||
run_id: job.run_id,
|
run_id: job.run_id,
|
||||||
case_set_file: job.case_set_file,
|
case_set_file: job.case_set_file,
|
||||||
|
analysis_date: job.analysis_date,
|
||||||
total_cases: job.total_cases,
|
total_cases: job.total_cases,
|
||||||
completed_cases: job.completed_cases,
|
completed_cases: job.completed_cases,
|
||||||
error: job.error,
|
error: job.error,
|
||||||
|
|
@ -314,7 +347,8 @@ function snapshotJob(job: EvalAsyncJob): Record<string, unknown> {
|
||||||
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
|
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
|
||||||
? Number(toRecord(job.report.metrics)?.score_index)
|
? Number(toRecord(job.report.metrics)?.score_index)
|
||||||
: null,
|
: null,
|
||||||
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null
|
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null,
|
||||||
|
analysis_date: toStringSafe(job.report.analysis_date) ?? job.analysis_date
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
};
|
};
|
||||||
|
|
@ -377,6 +411,7 @@ export function buildEvalRouter(services: AppServices): Router {
|
||||||
eval_target: payload.evalTarget,
|
eval_target: payload.evalTarget,
|
||||||
run_id: runId,
|
run_id: runId,
|
||||||
case_set_file: runtimeCaseSetFile,
|
case_set_file: runtimeCaseSetFile,
|
||||||
|
analysis_date: payload.analysisDate ?? null,
|
||||||
total_cases: caseSeeds.length,
|
total_cases: caseSeeds.length,
|
||||||
completed_cases: 0,
|
completed_cases: 0,
|
||||||
cases: caseSeeds.map((item) => ({
|
cases: caseSeeds.map((item) => ({
|
||||||
|
|
|
||||||
|
|
@ -490,8 +490,20 @@ function extractLooseByAnchorValue(text: string): string | undefined {
|
||||||
}
|
}
|
||||||
const lowered = token.toLowerCase();
|
const lowered = token.toLowerCase();
|
||||||
const stopWords = new Set([
|
const stopWords = new Set([
|
||||||
|
"какой",
|
||||||
|
"какая",
|
||||||
|
"какие",
|
||||||
|
"каких",
|
||||||
|
"каким",
|
||||||
|
"какими",
|
||||||
|
"каком",
|
||||||
|
"кто",
|
||||||
|
"что",
|
||||||
|
"мы",
|
||||||
|
"видим",
|
||||||
"контрагенту",
|
"контрагенту",
|
||||||
"контрагента",
|
"контрагента",
|
||||||
|
"контрагентам",
|
||||||
"контре",
|
"контре",
|
||||||
"компании",
|
"компании",
|
||||||
"компанию",
|
"компанию",
|
||||||
|
|
@ -499,10 +511,14 @@ function extractLooseByAnchorValue(text: string): string | undefined {
|
||||||
"организацию",
|
"организацию",
|
||||||
"поставщику",
|
"поставщику",
|
||||||
"поставщика",
|
"поставщика",
|
||||||
|
"поставщикам",
|
||||||
"клиенту",
|
"клиенту",
|
||||||
"клиента",
|
"клиента",
|
||||||
|
"клиентам",
|
||||||
"покупателю",
|
"покупателю",
|
||||||
"покупателя",
|
"покупателя",
|
||||||
|
"покупателям",
|
||||||
|
"заказчикам",
|
||||||
"партнеру",
|
"партнеру",
|
||||||
"партнера",
|
"партнера",
|
||||||
"договору",
|
"договору",
|
||||||
|
|
@ -618,6 +634,9 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||||
"какая",
|
"какая",
|
||||||
"какое",
|
"какое",
|
||||||
"каких",
|
"каких",
|
||||||
|
"каким",
|
||||||
|
"какими",
|
||||||
|
"каком",
|
||||||
"какому",
|
"какому",
|
||||||
"какую",
|
"какую",
|
||||||
"кто",
|
"кто",
|
||||||
|
|
@ -632,6 +651,8 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||||
"чья",
|
"чья",
|
||||||
"чей",
|
"чей",
|
||||||
"чью",
|
"чью",
|
||||||
|
"мы",
|
||||||
|
"видим",
|
||||||
"самый",
|
"самый",
|
||||||
"самая",
|
"самая",
|
||||||
"самое",
|
"самое",
|
||||||
|
|
@ -686,10 +707,23 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||||
"контрагент",
|
"контрагент",
|
||||||
"контрагенту",
|
"контрагенту",
|
||||||
"контрагента",
|
"контрагента",
|
||||||
|
"контрагентам",
|
||||||
"компания",
|
"компания",
|
||||||
"компании",
|
"компании",
|
||||||
"организация",
|
"организация",
|
||||||
"организации",
|
"организации",
|
||||||
|
"поставщикам",
|
||||||
|
"клиентам",
|
||||||
|
"покупателям",
|
||||||
|
"заказчикам",
|
||||||
|
"аванс",
|
||||||
|
"авансы",
|
||||||
|
"проблемный",
|
||||||
|
"проблемные",
|
||||||
|
"проблемным",
|
||||||
|
"закрытия",
|
||||||
|
"закрыть",
|
||||||
|
"закрыты",
|
||||||
"год",
|
"год",
|
||||||
"года",
|
"года",
|
||||||
"г",
|
"г",
|
||||||
|
|
@ -795,7 +829,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const questionCue =
|
const questionCue =
|
||||||
/(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
|
/(?:кто|что|какой|какая|какие|какого|каких|каким|какими|каком|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
|
||||||
/[?]/u.test(String(rawValue ?? ""));
|
/[?]/u.test(String(rawValue ?? ""));
|
||||||
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
|
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
|
||||||
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
|
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
|
||||||
|
|
|
||||||
|
|
@ -667,6 +667,13 @@ function hasLifecycleSegmentationSignal(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
|
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
|
||||||
|
const hasPaymentRiskLexeme =
|
||||||
|
/(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|долг|задолж)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
if (hasPaymentRiskLexeme) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
|
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -768,6 +775,10 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||||
);
|
);
|
||||||
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
||||||
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
||||||
|
const asksMajorShare =
|
||||||
|
/(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
const asksValue =
|
const asksValue =
|
||||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
|
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
|
||||||
text
|
text
|
||||||
|
|
@ -797,6 +808,9 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||||
if (asksCounterpartySource && asksValue) {
|
if (asksCounterpartySource && asksValue) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
|
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -920,6 +934,71 @@ function hasOpenContractsListSignal(text: string): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSupplierTailRiskSignal(text: string): boolean {
|
||||||
|
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
|
||||||
|
const hasTail = /(?:хвост|висят|незакрыт|задолж|долг|просроч)/iu.test(text);
|
||||||
|
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
|
||||||
|
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц)/iu.test(text);
|
||||||
|
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReceivablesLatencyRiskSignal(text: string): boolean {
|
||||||
|
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
|
||||||
|
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
|
||||||
|
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
|
||||||
|
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
|
||||||
|
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
|
||||||
|
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
|
||||||
|
const hasPeriodOrRiskCue = /(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн)/iu.test(text);
|
||||||
|
const hasBetweenShipmentAndPayment =
|
||||||
|
/между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
|
||||||
|
if (hasBuyer && hasPayment && ((hasShipment && hasDelay) || hasBetweenShipmentAndPayment)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSettlementGapSignal(text: string): boolean {
|
||||||
|
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
|
||||||
|
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
|
||||||
|
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
|
||||||
|
const hasNoDocumentForClosing =
|
||||||
|
/(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
|
||||||
|
/(?:закрыти|взаиморасч|акт)/iu.test(text);
|
||||||
|
const hasNoDocumentForClosingReversed =
|
||||||
|
/(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
|
||||||
|
/(?:закрыти|взаиморасч|акт)/iu.test(text);
|
||||||
|
const hasNoPayments =
|
||||||
|
/(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
|
||||||
|
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
|
||||||
|
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
|
||||||
|
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
|
||||||
|
const hasUnclosedAdvanceGap =
|
||||||
|
hasAdvance &&
|
||||||
|
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт)/iu.test(text) ||
|
||||||
|
hasNoDocumentForClosing ||
|
||||||
|
hasNoDocumentForClosingReversed);
|
||||||
|
return hasPaymentsWithoutClosingDocs || hasDocsWithoutPayments || hasUnclosedAdvanceGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReconciliationMismatchSignal(text: string): boolean {
|
||||||
|
const hasCounterparty =
|
||||||
|
/(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
|
||||||
|
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
|
||||||
|
const hasMismatchLexeme =
|
||||||
|
/(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
|
||||||
|
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
|
||||||
|
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
|
||||||
|
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
|
||||||
|
return (
|
||||||
|
hasCounterparty &&
|
||||||
|
hasReconciliationLexeme &&
|
||||||
|
hasMismatchLexeme &&
|
||||||
|
hasBalanceLexeme &&
|
||||||
|
(hasLookupVerb || hasInterrogativeLookup)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||||
const token = String(rawToken ?? "").trim().toLowerCase();
|
const token = String(rawToken ?? "").trim().toLowerCase();
|
||||||
if (!token || token.length < 2) {
|
if (!token || token.length < 2) {
|
||||||
|
|
@ -1273,6 +1352,38 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasSettlementGapSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_open_contracts",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["settlement_gap_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasReconciliationMismatchSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_open_contracts",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["reconciliation_mismatch_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasReceivablesLatencyRiskSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_receivables_counterparties",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["receivables_payment_lag_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSupplierTailRiskSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "list_payables_counterparties",
|
||||||
|
confidence: "medium",
|
||||||
|
reasons: ["supplier_tail_risk_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
|
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "documents_forming_balance",
|
intent: "documents_forming_balance",
|
||||||
|
|
@ -1299,7 +1410,9 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasAny(text, OPEN_ITEMS_HINTS) &&
|
hasAny(text, OPEN_ITEMS_HINTS) &&
|
||||||
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))
|
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
intent: "open_items_by_counterparty_or_contract",
|
intent: "open_items_by_counterparty_or_contract",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ interface NormalizedAddressRow {
|
||||||
|
|
||||||
interface AddressTryHandleOptions {
|
interface AddressTryHandleOptions {
|
||||||
followupContext?: AddressFollowupContext | null;
|
followupContext?: AddressFollowupContext | null;
|
||||||
|
analysisDateHint?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
|
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
|
||||||
|
|
@ -121,6 +122,36 @@ function parseFiniteNumber(value: unknown): number | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAnalysisDateHint(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
|
||||||
|
if (!isoPrefix) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(isoPrefix[1]);
|
||||||
|
const month = Number(isoPrefix[2]);
|
||||||
|
const day = Number(isoPrefix[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
function valueAsString(value: unknown): string {
|
function valueAsString(value: unknown): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -788,6 +819,58 @@ function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCatego
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLimitedReason(reason: string): string {
|
||||||
|
let normalized = String(reason ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return "не хватает подтвержденных данных для уверенного вывода";
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacements: Array<[RegExp, string]> = [
|
||||||
|
[/address_query\s*v?1/giu, "текущий адресный режим"],
|
||||||
|
[/address\s*v1/giu, "текущий адресный режим"],
|
||||||
|
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
|
||||||
|
[/live\s+recipe/giu, "текущий сценарий выборки"],
|
||||||
|
[/materialized\s+live-строках/giu, "доступном срезе данных"],
|
||||||
|
[/live-выборке/giu, "выборке данных"],
|
||||||
|
[/live-данных/giu, "данных"],
|
||||||
|
[/deep-analysis/giu, "режим расширенной проверки"],
|
||||||
|
[/\blookup\b/giu, "поиск"],
|
||||||
|
[/\bintent\b/giu, "сценария"],
|
||||||
|
[/\brecipe\b/giu, "шаблон выборки"],
|
||||||
|
[/\byakor\b/giu, "ориентир"],
|
||||||
|
[/\banchor\b/giu, "ориентир"],
|
||||||
|
[/\s+/gu, " "]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [pattern, value] of replacements) {
|
||||||
|
normalized = normalized.replace(pattern, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLimitedNextStep(nextStep: string): string {
|
||||||
|
let normalized = String(nextStep ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacements: Array<[RegExp, string]> = [
|
||||||
|
[/address_query\s*v?1/giu, "текущий адресный режим"],
|
||||||
|
[/deep-analysis/giu, "режим расширенной проверки"],
|
||||||
|
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
|
||||||
|
[/\bintent\b/giu, "сценарий"],
|
||||||
|
[/\blookup\b/giu, "поиск"],
|
||||||
|
[/\s+/gu, " "]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [pattern, value] of replacements) {
|
||||||
|
normalized = normalized.replace(pattern, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.trim();
|
||||||
|
}
|
||||||
|
|
||||||
interface RowStageDiagnostics {
|
interface RowStageDiagnostics {
|
||||||
rawRowKeysSample: string[];
|
rawRowKeysSample: string[];
|
||||||
materializationDropReason:
|
materializationDropReason:
|
||||||
|
|
@ -945,20 +1028,28 @@ function toLegacyMcpStatus(
|
||||||
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
|
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
|
||||||
const heading =
|
const heading =
|
||||||
category === "empty_match"
|
category === "empty_match"
|
||||||
? "В live-данных по текущему фильтру записи не найдены."
|
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
|
||||||
: category === "missing_anchor"
|
: category === "missing_anchor"
|
||||||
? "Для точного адресного поиска не хватает обязательного якоря."
|
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
|
||||||
: category === "recipe_visibility_gap"
|
: category === "recipe_visibility_gap"
|
||||||
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
|
? "Запрос понятен, но текущий режим не дает нужной детализации."
|
||||||
: category === "unsupported"
|
: category === "unsupported"
|
||||||
? "Этот запрос не подходит под address_query V1."
|
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
|
||||||
: "Не удалось выполнить адресный live-запрос в V1.";
|
: "Не удалось завершить проверку в адресном режиме.";
|
||||||
|
const reasonLine =
|
||||||
|
category === "unsupported"
|
||||||
|
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
|
||||||
|
: category === "missing_anchor"
|
||||||
|
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
|
||||||
|
: category === "recipe_visibility_gap"
|
||||||
|
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
|
||||||
|
: `Коротко: ${normalizeLimitedReason(reason)}.`;
|
||||||
const lines = [
|
const lines = [
|
||||||
heading,
|
heading,
|
||||||
`Причина: ${reason}.`
|
reasonLine
|
||||||
];
|
];
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
lines.push(`Что нужно уточнить: ${nextStep}.`);
|
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
|
||||||
}
|
}
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
@ -1057,7 +1148,24 @@ export class AddressQueryService {
|
||||||
if (!decompose) {
|
if (!decompose) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { mode, shape, intent, filters, baseReasons } = decompose;
|
const { mode, shape, intent, filters } = decompose;
|
||||||
|
const baseReasons = [...decompose.baseReasons];
|
||||||
|
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
|
||||||
|
if (analysisDate) {
|
||||||
|
const hasTemporalFilter = Boolean(
|
||||||
|
(typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
|
||||||
|
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
|
||||||
|
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||||
|
);
|
||||||
|
if (!hasTemporalFilter) {
|
||||||
|
filters.extracted_filters = {
|
||||||
|
...filters.extracted_filters,
|
||||||
|
as_of_date: analysisDate
|
||||||
|
};
|
||||||
|
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
|
||||||
|
baseReasons.push("as_of_date_from_analysis_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
|
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
||||||
|
|
@ -1079,8 +1187,8 @@ export class AddressQueryService {
|
||||||
rowsFetched: 0,
|
rowsFetched: 0,
|
||||||
rowsMatched: 0,
|
rowsMatched: 0,
|
||||||
category: "unsupported",
|
category: "unsupported",
|
||||||
reasonText: "intent пока не поддержан в address V1",
|
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
|
||||||
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
|
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
|
||||||
limitations: ["intent_not_supported_in_v1"],
|
limitations: ["intent_not_supported_in_v1"],
|
||||||
reasons: baseReasons
|
reasons: baseReasons
|
||||||
});
|
});
|
||||||
|
|
@ -1123,8 +1231,8 @@ export class AddressQueryService {
|
||||||
rowsFetched: 0,
|
rowsFetched: 0,
|
||||||
rowsMatched: 0,
|
rowsMatched: 0,
|
||||||
category: "recipe_visibility_gap",
|
category: "recipe_visibility_gap",
|
||||||
reasonText: "для intent пока нет recipe в address V1",
|
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
|
||||||
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
|
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
|
||||||
limitations: ["recipe_not_available"],
|
limitations: ["recipe_not_available"],
|
||||||
reasons: [...baseReasons, ...recipeSelection.selection_reason]
|
reasons: [...baseReasons, ...recipeSelection.selection_reason]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1509,7 +1509,7 @@ export function composeFactualReply(
|
||||||
if (intent === "list_open_contracts") {
|
if (intent === "list_open_contracts") {
|
||||||
const contracts = contractCandidatesFromRows(rows);
|
const contracts = contractCandidatesFromRows(rows);
|
||||||
const lines = [
|
const lines = [
|
||||||
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
|
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
|
||||||
`Строк движения: ${rows.length}.`,
|
`Строк движения: ${rows.length}.`,
|
||||||
`Договорных кандидатов: ${contracts.length}.`
|
`Договорных кандидатов: ${contracts.length}.`
|
||||||
];
|
];
|
||||||
|
|
@ -1525,6 +1525,36 @@ export function composeFactualReply(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "list_payables_counterparties") {
|
||||||
|
const lines = [
|
||||||
|
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
|
||||||
|
`Строк в выборке: ${rows.length}.`,
|
||||||
|
...(rows.length > 0
|
||||||
|
? ["Ниже примеры строк для ручной проверки."]
|
||||||
|
: ["Явных признаков системной задолженности по доступному срезу не найдено."]),
|
||||||
|
...formatTopRows(rows, 6)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
text: lines.join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent === "list_receivables_counterparties") {
|
||||||
|
const lines = [
|
||||||
|
"Проверил покупателей с признаками затянутой оплаты (контур 62/76).",
|
||||||
|
`Строк в выборке: ${rows.length}.`,
|
||||||
|
...(rows.length > 0
|
||||||
|
? ["Ниже примеры строк, которые стоит проверить в первую очередь."]
|
||||||
|
: ["Явных признаков затяжной дебиторки по доступному срезу не найдено."]),
|
||||||
|
...formatTopRows(rows, 6)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_LIST",
|
||||||
|
text: lines.join("\n")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (intent === "open_items_by_counterparty_or_contract") {
|
if (intent === "open_items_by_counterparty_or_contract") {
|
||||||
const lines = [
|
const lines = [
|
||||||
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
|
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
|
||||||
|
|
@ -1628,14 +1658,7 @@ export function composeFactualReply(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const title =
|
const lines = ["Срез адресного запроса собран.", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
|
||||||
intent === "list_payables_counterparties"
|
|
||||||
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
|
|
||||||
: intent === "list_receivables_counterparties"
|
|
||||||
? "Срез требований (receivables) собран по движениям с account scope 62/76."
|
|
||||||
: "Срез адресного запроса собран.";
|
|
||||||
|
|
||||||
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
|
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n")
|
text: lines.join("\n")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 } from "../config";
|
||||||
|
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import {
|
||||||
|
ANSWER_STRUCTURE_SCHEMA_VERSION,
|
||||||
|
type AnswerStructureV11,
|
||||||
|
type EvidenceLimitationReasonCode
|
||||||
|
} from "../types/stage1Contracts";
|
||||||
|
|
||||||
|
export interface BuildAssistantAnswerStructureV11Input {
|
||||||
|
assistantReply: string;
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
options?: {
|
||||||
|
enableEvidenceEnrichment?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVIDENCE_LIMITATION_REASON_CODE_SET: ReadonlySet<EvidenceLimitationReasonCode> = new Set([
|
||||||
|
"snapshot_only",
|
||||||
|
"heuristic_inference",
|
||||||
|
"missing_mechanism",
|
||||||
|
"weak_source_mapping",
|
||||||
|
"insufficient_detail",
|
||||||
|
"unknown"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
|
||||||
|
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEvidenceLimitationReasonCode(value: string): value is EvidenceLimitationReasonCode {
|
||||||
|
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value as EvidenceLimitationReasonCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstNonEmptyLine(text: string): string {
|
||||||
|
const line = String(text ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.find((item) => item.length > 0);
|
||||||
|
return (line ?? String(text ?? "")).slice(0, 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildClaimEvidenceLinks(
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
): NonNullable<AnswerStructureV11["evidence_block"]["claim_evidence_links"]> {
|
||||||
|
const byClaim = new Map<string, string[]>();
|
||||||
|
for (const result of retrievalResults) {
|
||||||
|
for (const evidence of result.evidence) {
|
||||||
|
const claimRef = String(evidence.claim_ref ?? "").trim();
|
||||||
|
if (!claimRef) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const evidenceId = String(evidence.evidence_id ?? "").trim();
|
||||||
|
if (!evidenceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const current = byClaim.get(claimRef) ?? [];
|
||||||
|
current.push(evidenceId);
|
||||||
|
byClaim.set(claimRef, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(byClaim.entries())
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([claimRef, evidenceIds]) => ({
|
||||||
|
claim_ref: claimRef,
|
||||||
|
evidence_ids: summarizeUnique(evidenceIds, 10)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantAnswerStructureV11(input: BuildAssistantAnswerStructureV11Input): AnswerStructureV11 {
|
||||||
|
const evidenceIds = summarizeUnique(
|
||||||
|
input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
const mechanismNotes = summarizeUnique(
|
||||||
|
input.retrievalResults.flatMap((item) =>
|
||||||
|
item.evidence
|
||||||
|
.map((evidence) => evidence.mechanism_note)
|
||||||
|
.filter((note): note is string => typeof note === "string" && note.trim().length > 0)
|
||||||
|
),
|
||||||
|
6
|
||||||
|
);
|
||||||
|
const sourceRefs = summarizeUnique(
|
||||||
|
input.retrievalResults.flatMap((item) =>
|
||||||
|
item.evidence
|
||||||
|
.map((evidence) => evidence.source_ref?.canonical_ref)
|
||||||
|
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||||
|
),
|
||||||
|
8
|
||||||
|
);
|
||||||
|
const limitationReasonCodes: EvidenceLimitationReasonCode[] = summarizeUnique(
|
||||||
|
input.retrievalResults.flatMap((item) =>
|
||||||
|
item.evidence.flatMap((evidence) => {
|
||||||
|
const code = evidence.limitation?.reason_code;
|
||||||
|
return typeof code === "string" && code.trim().length > 0 ? [code] : [];
|
||||||
|
})
|
||||||
|
),
|
||||||
|
8
|
||||||
|
).filter(isEvidenceLimitationReasonCode);
|
||||||
|
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
|
||||||
|
const limitations = summarizeUnique(
|
||||||
|
[...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons],
|
||||||
|
8
|
||||||
|
);
|
||||||
|
const clarificationQuestions = input.coverageReport.clarification_needed_for.map(
|
||||||
|
(item) => `Уточните требование ${item}.`
|
||||||
|
);
|
||||||
|
const recommendedActions = summarizeUnique(
|
||||||
|
[
|
||||||
|
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
|
||||||
|
...input.coverageReport.requirements_partially_covered.map(
|
||||||
|
(item) => `Доуточнить частично покрытое требование ${item}.`
|
||||||
|
)
|
||||||
|
],
|
||||||
|
6
|
||||||
|
);
|
||||||
|
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
||||||
|
mechanismNotes.length === 0
|
||||||
|
? "unresolved"
|
||||||
|
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
|
||||||
|
? "limited"
|
||||||
|
: "grounded";
|
||||||
|
const enableEvidenceEnrichment =
|
||||||
|
input.options?.enableEvidenceEnrichment ?? FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ANSWER_STRUCTURE_SCHEMA_VERSION,
|
||||||
|
answer_summary: firstNonEmptyLine(input.assistantReply),
|
||||||
|
direct_answer: input.assistantReply,
|
||||||
|
mechanism_block: {
|
||||||
|
status: mechanismStatus,
|
||||||
|
mechanism_notes: mechanismNotes,
|
||||||
|
limitation_reason_codes: limitationReasonCodes
|
||||||
|
},
|
||||||
|
evidence_block: {
|
||||||
|
evidence_ids: evidenceIds,
|
||||||
|
source_refs: sourceRefs,
|
||||||
|
mechanism_notes: mechanismNotes,
|
||||||
|
coverage_note:
|
||||||
|
input.coverageReport.requirements_total === input.coverageReport.requirements_covered
|
||||||
|
? "coverage_full_or_near_full"
|
||||||
|
: "coverage_partial_or_limited",
|
||||||
|
...(enableEvidenceEnrichment && claimEvidenceLinks.length > 0
|
||||||
|
? {
|
||||||
|
claim_evidence_links: claimEvidenceLinks
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
},
|
||||||
|
uncertainty_block: {
|
||||||
|
open_uncertainties: input.groundingCheck.missing_requirements,
|
||||||
|
limitations
|
||||||
|
},
|
||||||
|
next_step_block: {
|
||||||
|
recommended_actions: recommendedActions,
|
||||||
|
clarification_questions: clarificationQuestions
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import type {
|
||||||
|
AssistantReplyType,
|
||||||
|
AssistantRequirement,
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import {
|
||||||
|
buildAssistantCoverageContractV1,
|
||||||
|
buildAssistantExecutionPlanContractV1,
|
||||||
|
buildAssistantQueryFrameContractV1,
|
||||||
|
classifyAssistantOutcomeClassV1,
|
||||||
|
type AssistantCoverageContractV1,
|
||||||
|
type AssistantEvidenceBundleContractV1,
|
||||||
|
type AssistantExecutionPlanContractV1,
|
||||||
|
type AssistantOutcomeClassV1,
|
||||||
|
type AssistantQueryFrameContractV1
|
||||||
|
} from "./assistantOrchestrationContracts";
|
||||||
|
|
||||||
|
export interface AssistantContractsBundleV1 {
|
||||||
|
queryFrameContractV1: AssistantQueryFrameContractV1;
|
||||||
|
executionPlanContractV1: AssistantExecutionPlanContractV1;
|
||||||
|
outcomeClassV1: AssistantOutcomeClassV1;
|
||||||
|
coverageContractV1: AssistantCoverageContractV1;
|
||||||
|
assistantOrchestrationContractsV1: {
|
||||||
|
query_frame: AssistantQueryFrameContractV1;
|
||||||
|
execution_plan: AssistantExecutionPlanContractV1;
|
||||||
|
evidence_bundle: AssistantEvidenceBundleContractV1;
|
||||||
|
coverage: AssistantCoverageContractV1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assembleAssistantContractsBundleV1(input: {
|
||||||
|
userMessage: string;
|
||||||
|
normalizedQuestion: string;
|
||||||
|
normalized: NormalizedPayload | null;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
analysisContext: {
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
} | null;
|
||||||
|
executionPlan: Array<{
|
||||||
|
fragment_id: string;
|
||||||
|
requirement_ids: string[];
|
||||||
|
route: string;
|
||||||
|
should_execute: boolean;
|
||||||
|
no_route_reason?: string | null;
|
||||||
|
clarification_reason?: string | null;
|
||||||
|
}>;
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
evidenceBundleContractV1: AssistantEvidenceBundleContractV1;
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
grounding: AnswerGroundingCheck;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
}): AssistantContractsBundleV1 {
|
||||||
|
const queryFrameContractV1 = buildAssistantQueryFrameContractV1({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalizedQuestion: input.normalizedQuestion,
|
||||||
|
normalized: input.normalized,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
analysisContext: input.analysisContext
|
||||||
|
});
|
||||||
|
const executionPlanContractV1 = buildAssistantExecutionPlanContractV1({
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
requirements: input.requirements
|
||||||
|
});
|
||||||
|
const outcomeClassV1 = classifyAssistantOutcomeClassV1({
|
||||||
|
replyType: input.replyType,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
grounding: input.grounding,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const coverageContractV1 = buildAssistantCoverageContractV1({
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
grounding: input.grounding,
|
||||||
|
outcomeClass: outcomeClassV1
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
queryFrameContractV1,
|
||||||
|
executionPlanContractV1,
|
||||||
|
outcomeClassV1,
|
||||||
|
coverageContractV1,
|
||||||
|
assistantOrchestrationContractsV1: {
|
||||||
|
query_frame: queryFrameContractV1,
|
||||||
|
execution_plan: executionPlanContractV1,
|
||||||
|
evidence_bundle: input.evidenceBundleContractV1,
|
||||||
|
coverage: coverageContractV1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,421 @@
|
||||||
|
import type {
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
AssistantRequirement,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { RouteHintSummary } from "../types/normalizer";
|
||||||
|
|
||||||
|
interface SubjectTokenRule {
|
||||||
|
critical: boolean;
|
||||||
|
patterns: string[];
|
||||||
|
routes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantRequirementExtractionResult {
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
byFragment: Map<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
|
||||||
|
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUBJECT_TOKEN_RULES: Record<string, SubjectTokenRule> = {
|
||||||
|
nds: {
|
||||||
|
critical: true,
|
||||||
|
patterns: [
|
||||||
|
"vat",
|
||||||
|
"accumulationregister",
|
||||||
|
"ндс",
|
||||||
|
"книгипокупок",
|
||||||
|
"книгипродаж",
|
||||||
|
"налогнадобавленнуюстоимость"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
os: {
|
||||||
|
critical: true,
|
||||||
|
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
|
||||||
|
},
|
||||||
|
saldo: {
|
||||||
|
critical: true,
|
||||||
|
patterns: ["balance", "saldo", "сальдо", "остат"]
|
||||||
|
},
|
||||||
|
counterparty: {
|
||||||
|
critical: false,
|
||||||
|
patterns: [
|
||||||
|
"counterparty",
|
||||||
|
"supplier",
|
||||||
|
"buyer",
|
||||||
|
"counterparty_id",
|
||||||
|
"journal_counterparty",
|
||||||
|
"document_has_counterparty",
|
||||||
|
"контрагент",
|
||||||
|
"поставщик",
|
||||||
|
"покупател"
|
||||||
|
],
|
||||||
|
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
critical: false,
|
||||||
|
patterns: [
|
||||||
|
"document",
|
||||||
|
"recorder",
|
||||||
|
"journal",
|
||||||
|
"document_refs_count",
|
||||||
|
"recorded_by_document",
|
||||||
|
"journal_refers_to_document",
|
||||||
|
"документ"
|
||||||
|
],
|
||||||
|
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
|
||||||
|
},
|
||||||
|
anomaly: {
|
||||||
|
critical: false,
|
||||||
|
patterns: [
|
||||||
|
"risk",
|
||||||
|
"risk_score",
|
||||||
|
"unknown_link_count",
|
||||||
|
"zero_guid",
|
||||||
|
"navigation_links",
|
||||||
|
"missing_counterparty_link",
|
||||||
|
"аномал",
|
||||||
|
"риск"
|
||||||
|
],
|
||||||
|
routes: ["store_feature_risk", "batch_refresh_then_store"]
|
||||||
|
},
|
||||||
|
chain: {
|
||||||
|
critical: false,
|
||||||
|
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
|
||||||
|
routes: ["hybrid_store_plus_live"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function hasRegexMatch(corpus: string, pattern: RegExp): boolean {
|
||||||
|
try {
|
||||||
|
return pattern.test(corpus);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateSubjectTokenMatch(
|
||||||
|
token: string,
|
||||||
|
corpus: string,
|
||||||
|
executedRoutes: Set<string>
|
||||||
|
): {
|
||||||
|
matched: boolean;
|
||||||
|
critical: boolean;
|
||||||
|
} {
|
||||||
|
if (token.startsWith("account_")) {
|
||||||
|
const account = token.slice("account_".length).trim();
|
||||||
|
if (!account) {
|
||||||
|
return { matched: false, critical: true };
|
||||||
|
}
|
||||||
|
const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
|
||||||
|
return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
|
||||||
|
}
|
||||||
|
const rule = SUBJECT_TOKEN_RULES[token];
|
||||||
|
if (rule) {
|
||||||
|
const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
|
||||||
|
const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
|
||||||
|
return { matched: byPattern || byRoute, critical: rule.critical };
|
||||||
|
}
|
||||||
|
return { matched: corpus.includes(token), critical: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function evidenceCountForRequirement(requirementId: string, result: UnifiedRetrievalResult): number {
|
||||||
|
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||||
|
if (evidence.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const tagged = evidence.filter((item) => {
|
||||||
|
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
|
||||||
|
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
|
||||||
|
}).length;
|
||||||
|
if (tagged > 0) {
|
||||||
|
return tagged;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Array.isArray(result.requirement_ids) &&
|
||||||
|
result.requirement_ids.length === 1 &&
|
||||||
|
result.requirement_ids[0] === requirementId
|
||||||
|
) {
|
||||||
|
return evidence.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSubstantiveCoverageForRequirement(requirementId: string, result: UnifiedRetrievalResult): boolean {
|
||||||
|
const evidenceCount = evidenceCountForRequirement(requirementId, result);
|
||||||
|
if (evidenceCount > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
|
||||||
|
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
|
||||||
|
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
|
||||||
|
if (
|
||||||
|
Array.isArray(result.requirement_ids) &&
|
||||||
|
result.requirement_ids.length === 1 &&
|
||||||
|
result.requirement_ids[0] === requirementId
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractRequirementsForRoute(input: {
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
userMessage: string;
|
||||||
|
fragmentTextById: Map<string, string>;
|
||||||
|
extractSubjectTokens: (text: string) => string[];
|
||||||
|
}): AssistantRequirementExtractionResult {
|
||||||
|
const byFragment = new Map<string, string[]>();
|
||||||
|
const requirements: AssistantRequirement[] = [];
|
||||||
|
|
||||||
|
const pushRequirement = (item: {
|
||||||
|
requirement_id: string;
|
||||||
|
source_fragment_id: string | null;
|
||||||
|
requirement_text: string;
|
||||||
|
status: AssistantRequirement["status"];
|
||||||
|
route: string | null;
|
||||||
|
}): void => {
|
||||||
|
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
|
||||||
|
requirements.push({
|
||||||
|
requirement_id: item.requirement_id,
|
||||||
|
source_fragment_id: item.source_fragment_id,
|
||||||
|
requirement_text: item.requirement_text,
|
||||||
|
subject_tokens: subjectTokens,
|
||||||
|
status: item.status,
|
||||||
|
route: item.route
|
||||||
|
});
|
||||||
|
if (item.source_fragment_id) {
|
||||||
|
const current = byFragment.get(item.source_fragment_id) ?? [];
|
||||||
|
current.push(item.requirement_id);
|
||||||
|
byFragment.set(item.source_fragment_id, current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!input.routeSummary) {
|
||||||
|
pushRequirement({
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: null,
|
||||||
|
requirement_text: input.userMessage,
|
||||||
|
status: "clarification_needed",
|
||||||
|
route: null
|
||||||
|
});
|
||||||
|
return { requirements, byFragment };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.routeSummary.mode === "legacy_v1") {
|
||||||
|
pushRequirement({
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: input.userMessage,
|
||||||
|
status: "covered",
|
||||||
|
route: input.routeSummary.route_hint
|
||||||
|
});
|
||||||
|
return { requirements, byFragment };
|
||||||
|
}
|
||||||
|
|
||||||
|
input.routeSummary.decisions.forEach((decision, index) => {
|
||||||
|
const requirementId = `R${index + 1}`;
|
||||||
|
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
|
||||||
|
let status: AssistantRequirement["status"] = "covered";
|
||||||
|
if (decision.route === "no_route") {
|
||||||
|
if (decision.no_route_reason === "out_of_scope") {
|
||||||
|
status = "out_of_scope";
|
||||||
|
} else if (decision.no_route_reason === "insufficient_specificity") {
|
||||||
|
status = "clarification_needed";
|
||||||
|
} else {
|
||||||
|
status = "uncovered";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushRequirement({
|
||||||
|
requirement_id: requirementId,
|
||||||
|
source_fragment_id: decision.fragment_id,
|
||||||
|
requirement_text: text,
|
||||||
|
status,
|
||||||
|
route: decision.route === "no_route" ? null : decision.route
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { requirements, byFragment };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateCoverageForRequirements(
|
||||||
|
requirements: AssistantRequirement[],
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
): {
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
coverage: RequirementCoverageReport;
|
||||||
|
} {
|
||||||
|
const statusByRequirement = new Map<string, Array<{ status: UnifiedRetrievalResult["status"]; substantive: boolean }>>();
|
||||||
|
for (const result of retrievalResults) {
|
||||||
|
for (const requirementId of result.requirement_ids) {
|
||||||
|
const list = statusByRequirement.get(requirementId) ?? [];
|
||||||
|
list.push({
|
||||||
|
status: result.status,
|
||||||
|
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
|
||||||
|
});
|
||||||
|
statusByRequirement.set(requirementId, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRequirements = requirements.map((requirement) => {
|
||||||
|
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
|
||||||
|
return requirement;
|
||||||
|
}
|
||||||
|
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||||||
|
if (states.length === 0) {
|
||||||
|
return { ...requirement, status: "uncovered" as const };
|
||||||
|
}
|
||||||
|
const hasAnySubstantive = states.some((item) => item.substantive);
|
||||||
|
if (!hasAnySubstantive) {
|
||||||
|
return { ...requirement, status: "uncovered" as const };
|
||||||
|
}
|
||||||
|
const hasOk = states.some((item) => item.status === "ok");
|
||||||
|
const hasPartial = states.some((item) => item.status === "partial");
|
||||||
|
const hasEmpty = states.some((item) => item.status === "empty");
|
||||||
|
const hasError = states.some((item) => item.status === "error");
|
||||||
|
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
|
||||||
|
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
|
||||||
|
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
|
||||||
|
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
|
||||||
|
return { ...requirement, status: "covered" as const };
|
||||||
|
}
|
||||||
|
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
|
||||||
|
return { ...requirement, status: "partially_covered" as const };
|
||||||
|
}
|
||||||
|
return { ...requirement, status: "uncovered" as const };
|
||||||
|
});
|
||||||
|
|
||||||
|
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
|
||||||
|
const requirementsUncovered = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "uncovered")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
const requirementsPartiallyCovered = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "partially_covered")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
const clarificationNeededFor = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "clarification_needed")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
const outOfScopeRequirements = resolvedRequirements
|
||||||
|
.filter((item) => item.status === "out_of_scope")
|
||||||
|
.map((item) => item.requirement_id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requirements: resolvedRequirements,
|
||||||
|
coverage: {
|
||||||
|
requirements_total: resolvedRequirements.length,
|
||||||
|
requirements_covered: requirementsCovered,
|
||||||
|
requirements_uncovered: requirementsUncovered,
|
||||||
|
requirements_partially_covered: requirementsPartiallyCovered,
|
||||||
|
clarification_needed_for: clarificationNeededFor,
|
||||||
|
out_of_scope_requirements: outOfScopeRequirements
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkGroundingForRequirements(input: {
|
||||||
|
userMessage: string;
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
coverage: RequirementCoverageReport;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
extractSubjectTokens: (text: string) => string[];
|
||||||
|
}): AnswerGroundingCheck {
|
||||||
|
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
|
||||||
|
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
|
||||||
|
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
|
||||||
|
const subjectTokens = input.extractSubjectTokens(input.userMessage);
|
||||||
|
const executedRoutes = new Set(
|
||||||
|
input.retrievalResults
|
||||||
|
.filter((item) => item.status !== "error")
|
||||||
|
.map((item) => item.route)
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
const retrievalCorpus = JSON.stringify(
|
||||||
|
input.retrievalResults.map((item) => ({
|
||||||
|
route: item.route,
|
||||||
|
result_type: item.result_type,
|
||||||
|
summary: item.summary,
|
||||||
|
items: item.items,
|
||||||
|
evidence: item.evidence,
|
||||||
|
why_included: item.why_included,
|
||||||
|
selection_reason: item.selection_reason,
|
||||||
|
risk_factors: item.risk_factors,
|
||||||
|
business_interpretation: item.business_interpretation
|
||||||
|
}))
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
|
const missingSubjectTokens: string[] = [];
|
||||||
|
const missingCriticalTokens: string[] = [];
|
||||||
|
for (const token of subjectTokens) {
|
||||||
|
const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
|
||||||
|
if (!match.matched) {
|
||||||
|
missingSubjectTokens.push(token);
|
||||||
|
if (match.critical) {
|
||||||
|
missingCriticalTokens.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlyAccountCriticalMissing =
|
||||||
|
missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
|
||||||
|
const accountOnlyMismatchRecoverable =
|
||||||
|
hasMaterialResults &&
|
||||||
|
input.coverage.requirements_covered > 0 &&
|
||||||
|
onlyAccountCriticalMissing &&
|
||||||
|
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
|
||||||
|
const routeSubjectMatch =
|
||||||
|
!hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
|
||||||
|
|
||||||
|
let status: AnswerGroundingCheck["status"] = "grounded";
|
||||||
|
const reasons: string[] = [];
|
||||||
|
if (!routeSubjectMatch) {
|
||||||
|
status = "route_mismatch_blocked";
|
||||||
|
reasons.push(
|
||||||
|
`Ключевые ориентиры вопроса не подтверждены в найденных данных: ${missingCriticalTokens.join(", ")}`
|
||||||
|
);
|
||||||
|
} else if (accountOnlyMismatchRecoverable) {
|
||||||
|
status = "partial";
|
||||||
|
reasons.push(
|
||||||
|
`Часть счетных ориентиров не подтвердилась напрямую (${missingCriticalTokens.join(", ")}), но есть опора для ограниченного вывода.`
|
||||||
|
);
|
||||||
|
} else if (input.coverage.requirements_covered === 0) {
|
||||||
|
status = "no_grounded_answer";
|
||||||
|
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
|
||||||
|
} else if (
|
||||||
|
input.coverage.requirements_uncovered.length > 0 ||
|
||||||
|
input.coverage.requirements_partially_covered.length > 0 ||
|
||||||
|
input.coverage.clarification_needed_for.length > 0 ||
|
||||||
|
input.coverage.out_of_scope_requirements.length > 0
|
||||||
|
) {
|
||||||
|
status = "partial";
|
||||||
|
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (whyIncludedSummary.length === 0) {
|
||||||
|
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
|
||||||
|
}
|
||||||
|
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
|
||||||
|
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingRequirements = [
|
||||||
|
...input.coverage.requirements_uncovered,
|
||||||
|
...input.coverage.requirements_partially_covered,
|
||||||
|
...input.coverage.clarification_needed_for,
|
||||||
|
...input.coverage.out_of_scope_requirements
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
route_subject_match: routeSubjectMatch,
|
||||||
|
missing_requirements: missingRequirements,
|
||||||
|
reasons,
|
||||||
|
why_included_summary: whyIncludedSummary,
|
||||||
|
selection_reason_summary: selectionReasonSummary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,13 @@ interface LiveMcpCallExecution {
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LiveTemporalHint {
|
||||||
|
as_of_date?: string | null;
|
||||||
|
period_from?: string | null;
|
||||||
|
period_to?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
type BroadnessLevel = "low" | "medium" | "high";
|
type BroadnessLevel = "low" | "medium" | "high";
|
||||||
|
|
||||||
interface BroadQueryAssessment {
|
interface BroadQueryAssessment {
|
||||||
|
|
@ -262,6 +269,32 @@ function formatIsoDateUtc(date: Date): string {
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeIsoDate(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
function monthEndFromIso(isoDate: string): string | null {
|
function monthEndFromIso(isoDate: string): string | null {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -329,14 +362,28 @@ function hasFixedAssetAmortizationSignal(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallPlan {
|
function buildLiveMcpCallPlan(route: string, fragmentText: string, temporalHint?: LiveTemporalHint | null): LiveMcpCallPlan {
|
||||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||||
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
|
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
|
||||||
const periodScope = inferPeriodScope(fragmentText);
|
const periodScope = inferPeriodScope(fragmentText);
|
||||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
const hintedAsOfDate = normalizeIsoDate(temporalHint?.as_of_date);
|
||||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
const hintedPeriodFrom = normalizeIsoDate(temporalHint?.period_from);
|
||||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
const hintedPeriodTo = normalizeIsoDate(temporalHint?.period_to);
|
||||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
const primaryFrom = periodScope.from ?? hintedPeriodFrom ?? hintedAsOfDate;
|
||||||
|
const primaryTo =
|
||||||
|
periodScope.to ??
|
||||||
|
hintedPeriodTo ??
|
||||||
|
(!periodScope.from && !hintedPeriodFrom && hintedAsOfDate ? hintedAsOfDate : primaryFrom ? monthEndFromIso(primaryFrom) ?? primaryFrom : null);
|
||||||
|
const carryFrom = primaryFrom ? shiftIsoDate(primaryFrom, -31) ?? primaryFrom : null;
|
||||||
|
const carryTo = primaryTo ? shiftIsoDate(primaryTo, 31) ?? primaryTo : null;
|
||||||
|
const buildPrimaryQuery = (limit: number): string =>
|
||||||
|
primaryFrom && primaryTo
|
||||||
|
? buildLiveRangeQuery(primaryFrom, primaryTo, limit)
|
||||||
|
: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
|
||||||
|
const buildCarryQuery = (limit: number): string =>
|
||||||
|
carryFrom && carryTo
|
||||||
|
? buildLiveRangeQuery(carryFrom, carryTo, limit)
|
||||||
|
: buildPrimaryQuery(limit);
|
||||||
|
|
||||||
const faClaim =
|
const faClaim =
|
||||||
preferredDomainHint === "fixed_asset_amortization" ||
|
preferredDomainHint === "fixed_asset_amortization" ||
|
||||||
|
|
@ -352,7 +399,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_amortization_documents_in_period",
|
call_id: "find_amortization_documents_in_period",
|
||||||
purpose: "seed_amortization_documents",
|
purpose: "seed_amortization_documents",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -360,7 +407,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_fixed_asset_movements_accounts_01_02",
|
call_id: "find_fixed_asset_movements_accounts_01_02",
|
||||||
purpose: "collect_fa_object_movements",
|
purpose: "collect_fa_object_movements",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -368,7 +415,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_fixed_asset_cards_expected_for_period",
|
call_id: "find_fixed_asset_cards_expected_for_period",
|
||||||
purpose: "build_expected_fa_set",
|
purpose: "build_expected_fa_set",
|
||||||
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -376,7 +423,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "match_expected_vs_actual_fa_coverage",
|
call_id: "match_expected_vs_actual_fa_coverage",
|
||||||
purpose: "compare_expected_vs_actual_fa_coverage",
|
purpose: "compare_expected_vs_actual_fa_coverage",
|
||||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["01", "02", "08"]
|
account_scope_override: ["01", "02", "08"]
|
||||||
|
|
@ -400,7 +447,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_vat_source_documents_in_period",
|
call_id: "find_vat_source_documents_in_period",
|
||||||
purpose: "seed_vat_source_documents",
|
purpose: "seed_vat_source_documents",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -408,7 +455,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_vat_invoice_links_in_period",
|
call_id: "find_vat_invoice_links_in_period",
|
||||||
purpose: "collect_invoice_links",
|
purpose: "collect_invoice_links",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -416,7 +463,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_vat_register_entries_in_period",
|
call_id: "find_vat_register_entries_in_period",
|
||||||
purpose: "collect_vat_register_entries",
|
purpose: "collect_vat_register_entries",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -424,7 +471,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_vat_book_entries_in_period",
|
call_id: "find_vat_book_entries_in_period",
|
||||||
purpose: "collect_vat_book_entries",
|
purpose: "collect_vat_book_entries",
|
||||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["19", "68"]
|
account_scope_override: ["19", "68"]
|
||||||
|
|
@ -464,7 +511,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_rbp_writeoff_documents_in_period",
|
call_id: "find_rbp_writeoff_documents_in_period",
|
||||||
purpose: "seed_writeoff_documents",
|
purpose: "seed_writeoff_documents",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||||
|
|
@ -472,7 +519,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_rbp_object_movements_account_97",
|
call_id: "find_rbp_object_movements_account_97",
|
||||||
purpose: "collect_rbp_object_movements",
|
purpose: "collect_rbp_object_movements",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97"]
|
account_scope_override: ["97"]
|
||||||
|
|
@ -480,7 +527,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "find_month_close_entries_linked_to_rbp",
|
call_id: "find_month_close_entries_linked_to_rbp",
|
||||||
purpose: "link_month_close_to_rbp",
|
purpose: "link_month_close_to_rbp",
|
||||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||||
|
|
@ -488,7 +535,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||||
{
|
{
|
||||||
call_id: "compute_end_period_residual_by_rbp_object",
|
call_id: "compute_end_period_residual_by_rbp_object",
|
||||||
purpose: "collect_residual_tail_signals",
|
purpose: "collect_residual_tail_signals",
|
||||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||||
required_for_claim: true,
|
required_for_claim: true,
|
||||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||||
|
|
@ -1849,7 +1896,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||||
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
|
/основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
|
||||||
lower
|
lower
|
||||||
) ||
|
) ||
|
||||||
hasFixedAssetAccountScope
|
hasFixedAssetAccountScope
|
||||||
|
|
@ -2855,7 +2902,13 @@ export class AssistantDataLayer {
|
||||||
return enforceBroadQueryGuards(route, fragmentText, result);
|
return enforceBroadQueryGuards(route, fragmentText, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async executeRouteRuntime(route: string, fragmentText: string): Promise<RawRetrievalResult> {
|
public async executeRouteRuntime(
|
||||||
|
route: string,
|
||||||
|
fragmentText: string,
|
||||||
|
options?: {
|
||||||
|
temporalHint?: LiveTemporalHint | null;
|
||||||
|
}
|
||||||
|
): Promise<RawRetrievalResult> {
|
||||||
const base = this.executeRoute(route, fragmentText);
|
const base = this.executeRoute(route, fragmentText);
|
||||||
if (!FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
|
if (!FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
|
||||||
return base;
|
return base;
|
||||||
|
|
@ -2864,7 +2917,7 @@ export class AssistantDataLayer {
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
|
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText, options?.temporalHint);
|
||||||
return this.mergeWithLiveOverlay(base, liveOverlay);
|
return this.mergeWithLiveOverlay(base, liveOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2922,9 +2975,13 @@ export class AssistantDataLayer {
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchLiveMcpOverlay(route: string, fragmentText: string): Promise<LiveMcpOverlay> {
|
private async fetchLiveMcpOverlay(
|
||||||
|
route: string,
|
||||||
|
fragmentText: string,
|
||||||
|
temporalHint?: LiveTemporalHint | null
|
||||||
|
): Promise<LiveMcpOverlay> {
|
||||||
const endpoint = this.buildMcpUrl("/api/execute_query");
|
const endpoint = this.buildMcpUrl("/api/execute_query");
|
||||||
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
|
const livePlan = buildLiveMcpCallPlan(route, fragmentText, temporalHint);
|
||||||
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
||||||
const accountScope =
|
const accountScope =
|
||||||
livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import type { AssistantDebugPayload } from "../types/assistant";
|
||||||
|
|
||||||
|
type RetrievalStatusItem = AssistantDebugPayload["retrieval_status"][number];
|
||||||
|
|
||||||
|
export interface DeepAnalysisDebugPayloadInput {
|
||||||
|
traceId: string;
|
||||||
|
promptVersion: string;
|
||||||
|
schemaVersion: string;
|
||||||
|
fallbackType: unknown;
|
||||||
|
routeSummary: unknown;
|
||||||
|
fragments: unknown[];
|
||||||
|
requirementsExtracted: unknown[];
|
||||||
|
coverageReport: unknown;
|
||||||
|
routes: Array<Record<string, unknown>>;
|
||||||
|
retrievalStatus: RetrievalStatusItem[];
|
||||||
|
retrievalResults: unknown[];
|
||||||
|
groundingCheck: unknown;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
questionTypeClass: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: boolean;
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
};
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw?: string[];
|
||||||
|
business_scope_resolved?: string[];
|
||||||
|
company_grounding_applied?: boolean;
|
||||||
|
scope_resolution_reason?: string[];
|
||||||
|
};
|
||||||
|
temporalGuard: Record<string, unknown>;
|
||||||
|
polarityAudit: Record<string, unknown>;
|
||||||
|
claimAnchorAudit: Record<string, unknown>;
|
||||||
|
targetedEvidenceAudit: unknown;
|
||||||
|
evidenceAdmissibilityGateAudit: unknown;
|
||||||
|
rbpLiveRouteAudit: unknown | null;
|
||||||
|
faLiveRouteAudit: unknown | null;
|
||||||
|
groundedAnswerEligibilityGuard: Record<string, unknown>;
|
||||||
|
followupStateUsage: unknown | null;
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied?: boolean;
|
||||||
|
problem_units_used_count?: number;
|
||||||
|
problem_answer_mode?: string;
|
||||||
|
problem_unit_ids_used?: string[];
|
||||||
|
};
|
||||||
|
addressRuntimeMetaForDeep:
|
||||||
|
| {
|
||||||
|
attempted?: boolean;
|
||||||
|
applied?: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
provider?: string | null;
|
||||||
|
fallbackRuleHit?: string | null;
|
||||||
|
toolGateDecision?: string | null;
|
||||||
|
toolGateReason?: string | null;
|
||||||
|
predecomposeContract?: unknown;
|
||||||
|
orchestrationContract?: unknown;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
outcomeClassV1: unknown;
|
||||||
|
assistantOrchestrationContractsV1: unknown;
|
||||||
|
answerStructureV11: unknown;
|
||||||
|
investigationStateSnapshot: unknown;
|
||||||
|
normalizedPayload: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAnalysisContext(input: DeepAnalysisDebugPayloadInput["runtimeAnalysisContext"]): Record<string, unknown> | null {
|
||||||
|
if (!input.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
as_of_date: input.as_of_date,
|
||||||
|
period_from: input.period_from,
|
||||||
|
period_to: input.period_to,
|
||||||
|
source: input.source,
|
||||||
|
snapshot_mode: input.snapshot_mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInput): Record<string, unknown> {
|
||||||
|
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
return {
|
||||||
|
trace_id: input.traceId,
|
||||||
|
prompt_version: input.promptVersion,
|
||||||
|
schema_version: input.schemaVersion,
|
||||||
|
fallback_type: input.fallbackType,
|
||||||
|
route_summary: input.routeSummary,
|
||||||
|
fragments: input.fragments,
|
||||||
|
requirements_extracted: input.requirementsExtracted,
|
||||||
|
coverage_report: input.coverageReport,
|
||||||
|
routes: input.routes,
|
||||||
|
retrieval_status: input.retrievalStatus,
|
||||||
|
retrieval_results: input.retrievalResults,
|
||||||
|
answer_grounding_check: input.groundingCheck,
|
||||||
|
dropped_intent_segments: input.droppedIntentSegments,
|
||||||
|
question_type_class: input.questionTypeClass,
|
||||||
|
company_anchors: input.companyAnchors,
|
||||||
|
analysis_context_applied: input.runtimeAnalysisContext.active,
|
||||||
|
analysis_context: analysisContext,
|
||||||
|
business_scope_raw: input.businessScopeResolution.business_scope_raw,
|
||||||
|
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
|
||||||
|
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
|
||||||
|
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
raw_time_anchor: input.temporalGuard.raw_time_anchor,
|
||||||
|
raw_time_scope: input.temporalGuard.raw_time_scope,
|
||||||
|
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
|
||||||
|
resolved_primary_period: input.temporalGuard.resolved_primary_period,
|
||||||
|
effective_primary_period: input.temporalGuard.effective_primary_period,
|
||||||
|
temporal_guard_input: input.temporalGuard.temporal_guard_input,
|
||||||
|
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
|
||||||
|
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
|
||||||
|
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
|
||||||
|
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
|
||||||
|
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
|
||||||
|
temporal_guard: input.temporalGuard,
|
||||||
|
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
|
||||||
|
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
|
||||||
|
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
|
||||||
|
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
|
||||||
|
domain_polarity_guard: input.polarityAudit,
|
||||||
|
claim_anchor_audit: input.claimAnchorAudit,
|
||||||
|
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
|
||||||
|
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||||
|
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||||
|
targeted_evidence_acquisition: input.targetedEvidenceAudit,
|
||||||
|
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
|
||||||
|
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
|
||||||
|
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
|
||||||
|
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||||
|
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
|
||||||
|
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
|
||||||
|
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
|
||||||
|
? {
|
||||||
|
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
address_llm_predecompose_attempted: Boolean(input.addressRuntimeMetaForDeep?.attempted),
|
||||||
|
address_llm_predecompose_applied: Boolean(input.addressRuntimeMetaForDeep?.applied),
|
||||||
|
address_llm_predecompose_reason: input.addressRuntimeMetaForDeep?.reason ?? null,
|
||||||
|
address_llm_predecompose_provider: input.addressRuntimeMetaForDeep?.provider ?? null,
|
||||||
|
address_fallback_rule_hit: input.addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
|
||||||
|
address_tool_gate_decision: input.addressRuntimeMetaForDeep?.toolGateDecision ?? null,
|
||||||
|
address_tool_gate_reason: input.addressRuntimeMetaForDeep?.toolGateReason ?? null,
|
||||||
|
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
|
||||||
|
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
|
||||||
|
assistant_outcome_class_v1: input.outcomeClassV1,
|
||||||
|
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
|
||||||
|
answer_structure_v11: input.answerStructureV11,
|
||||||
|
investigation_state_snapshot: input.investigationStateSnapshot,
|
||||||
|
normalized: input.normalizedPayload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { buildAssistantAnswerStructureV11 } from "./assistantAnswerPackageBuilder";
|
||||||
|
import type {
|
||||||
|
AssistantConversationItem,
|
||||||
|
AssistantDebugPayload,
|
||||||
|
AssistantReplyType,
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { AnswerStructureV11 } from "../types/stage1Contracts";
|
||||||
|
|
||||||
|
export interface DeepAnswerArtifacts {
|
||||||
|
safeAssistantReply: string;
|
||||||
|
answerStructureV11: AnswerStructureV11 | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripTechnicalTail(text: string): string {
|
||||||
|
return String(text ?? "")
|
||||||
|
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||||
|
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeepAnswerArtifacts(input: {
|
||||||
|
safeAssistantReplyBase: string;
|
||||||
|
featureContractsV11: boolean;
|
||||||
|
featureAnswerPolicyV11: boolean;
|
||||||
|
compositionAnswerStructureV11: AnswerStructureV11 | null | undefined;
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
}): DeepAnswerArtifacts {
|
||||||
|
const safeAssistantReply = stripTechnicalTail(input.safeAssistantReplyBase);
|
||||||
|
const answerStructureV11 = input.featureContractsV11
|
||||||
|
? input.featureAnswerPolicyV11 && input.compositionAnswerStructureV11
|
||||||
|
? input.compositionAnswerStructureV11
|
||||||
|
: buildAssistantAnswerStructureV11({
|
||||||
|
assistantReply: safeAssistantReply,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
safeAssistantReply,
|
||||||
|
answerStructureV11
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantConversationItem(input: {
|
||||||
|
messageId: string;
|
||||||
|
sessionId: string;
|
||||||
|
text: string;
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
traceId: string | null;
|
||||||
|
debug: AssistantDebugPayload;
|
||||||
|
}): AssistantConversationItem {
|
||||||
|
return {
|
||||||
|
message_id: input.messageId,
|
||||||
|
session_id: input.sessionId,
|
||||||
|
role: "assistant",
|
||||||
|
text: input.text,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
trace_id: input.traceId,
|
||||||
|
debug: input.debug
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { AssistantRequirement, AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
|
||||||
|
import type { QuestionTypeClass } from "./questionTypeResolver";
|
||||||
|
import { resolveQuestionType } from "./questionTypeResolver";
|
||||||
|
import { composeAssistantAnswer } from "./answerComposer";
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnCompositionInput {
|
||||||
|
userMessage: string;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
followupUsage: unknown | null | undefined;
|
||||||
|
investigationState: InvestigationStateWithProblemUnits | null | undefined;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"];
|
||||||
|
featureAnswerPolicyV11: boolean;
|
||||||
|
featureProblemCentricAnswerV1: boolean;
|
||||||
|
featureLifecycleAnswerV1: boolean;
|
||||||
|
hasExplicitPeriodAnchor: (normalizedPayload: NormalizeResponsePayload["normalized"]) => boolean;
|
||||||
|
resolveQuestionTypeFn?: (input: string) => QuestionTypeClass;
|
||||||
|
composeAssistantAnswerFn?: typeof composeAssistantAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnCompositionOutput {
|
||||||
|
focusDomainHint: string | null;
|
||||||
|
questionTypeClass: QuestionTypeClass;
|
||||||
|
hasPeriodInCompanyAnchors: boolean;
|
||||||
|
normalizationPeriodExplicit: boolean;
|
||||||
|
composition: ReturnType<typeof composeAssistantAnswer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantDeepTurnComposition(
|
||||||
|
input: BuildAssistantDeepTurnCompositionInput
|
||||||
|
): AssistantDeepTurnCompositionOutput {
|
||||||
|
const resolveQuestionTypeSafe = input.resolveQuestionTypeFn ?? resolveQuestionType;
|
||||||
|
const composeAssistantAnswerSafe = input.composeAssistantAnswerFn ?? composeAssistantAnswer;
|
||||||
|
|
||||||
|
const followupApplied = Boolean((input.followupUsage as { applied?: unknown } | null)?.applied);
|
||||||
|
const focusDomainHint = followupApplied
|
||||||
|
? input.investigationState?.followup_context?.active_domain ?? input.investigationState?.focus.domain ?? null
|
||||||
|
: null;
|
||||||
|
const questionTypeClass = resolveQuestionTypeSafe(input.userMessage);
|
||||||
|
const companyAnchorSet = input.companyAnchors as {
|
||||||
|
dates?: unknown[];
|
||||||
|
periods?: unknown[];
|
||||||
|
} | null;
|
||||||
|
const hasPeriodInCompanyAnchors =
|
||||||
|
(Array.isArray(companyAnchorSet?.dates) && companyAnchorSet.dates.some((item) => String(item ?? "").trim().length > 0)) ||
|
||||||
|
(Array.isArray(companyAnchorSet?.periods) && companyAnchorSet.periods.some((item) => String(item ?? "").trim().length > 0));
|
||||||
|
const normalizationPeriodExplicit = input.hasExplicitPeriodAnchor(input.normalizedPayload) || hasPeriodInCompanyAnchors;
|
||||||
|
const composition = composeAssistantAnswerSafe({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
requirements: input.requirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
focusDomainHint,
|
||||||
|
questionTypeHint: questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors as any,
|
||||||
|
normalizationPeriodExplicit,
|
||||||
|
enableAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||||
|
enableProblemCentricAnswerV1: input.featureProblemCentricAnswerV1,
|
||||||
|
enableLifecycleAnswerV1: input.featureLifecycleAnswerV1
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
focusDomainHint,
|
||||||
|
questionTypeClass,
|
||||||
|
hasPeriodInCompanyAnchors,
|
||||||
|
normalizationPeriodExplicit,
|
||||||
|
composition
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
|
||||||
|
const KNOWN_P0_DOMAINS = new Set([
|
||||||
|
"settlements_60_62",
|
||||||
|
"vat_document_register_book",
|
||||||
|
"month_close_costs_20_44",
|
||||||
|
"fixed_asset_amortization"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function toAnalysisContext(
|
||||||
|
runtimeAnalysisContext: BuildAssistantDeepTurnRuntimeContextInput["runtimeAnalysisContext"]
|
||||||
|
): Record<string, string | null> | null {
|
||||||
|
if (!runtimeAnalysisContext.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
as_of_date: runtimeAnalysisContext.as_of_date,
|
||||||
|
period_from: runtimeAnalysisContext.period_from,
|
||||||
|
period_to: runtimeAnalysisContext.period_to,
|
||||||
|
source: runtimeAnalysisContext.source
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnRuntimeContextInput {
|
||||||
|
userMessage: string;
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"];
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: boolean;
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
};
|
||||||
|
followupUsage: unknown | null | undefined;
|
||||||
|
resolveCompanyAnchors: (userMessage: string) => unknown;
|
||||||
|
resolveBusinessScopeAlignment: (input: {
|
||||||
|
userMessage: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
normalized: NormalizeResponsePayload["normalized"];
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
}) => {
|
||||||
|
route_summary_resolved: RouteHintSummary | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
inferP0DomainFromMessage: (userMessage: string) => string | null;
|
||||||
|
resolveTemporalGuard: (input: {
|
||||||
|
userMessage: string;
|
||||||
|
normalized: NormalizeResponsePayload["normalized"];
|
||||||
|
companyAnchors: unknown;
|
||||||
|
analysisContext: Record<string, string | null> | null;
|
||||||
|
}) => {
|
||||||
|
effective_primary_period?: unknown;
|
||||||
|
primary_period_window?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
resolveDomainPolarityGuard: (input: {
|
||||||
|
userMessage: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
focusDomainHint: string | null;
|
||||||
|
}) => unknown;
|
||||||
|
resolveClaimBoundAnchors: (input: {
|
||||||
|
userMessage: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
focusDomainHint: string | null;
|
||||||
|
primaryPeriod: unknown;
|
||||||
|
}) => {
|
||||||
|
claim_type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
resolveBusinessScopeFromLiveContext: (input: {
|
||||||
|
current: {
|
||||||
|
route_summary_resolved: RouteHintSummary | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
temporalGuard: unknown;
|
||||||
|
claimType: string;
|
||||||
|
focusDomainHint: string | null;
|
||||||
|
userMessage: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
followupApplied: boolean;
|
||||||
|
}) => {
|
||||||
|
route_summary_resolved: RouteHintSummary | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnRuntimeContextOutput {
|
||||||
|
companyAnchors: unknown;
|
||||||
|
initialBusinessScopeResolution: {
|
||||||
|
route_summary_resolved: RouteHintSummary | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
inferredDomainByMessage: string | null;
|
||||||
|
focusDomainForGuards: string | null;
|
||||||
|
temporalGuard: {
|
||||||
|
effective_primary_period?: unknown;
|
||||||
|
primary_period_window?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
domainPolarityGuardInitial: unknown;
|
||||||
|
claimAnchorAudit: {
|
||||||
|
claim_type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
businessScopeResolution: {
|
||||||
|
route_summary_resolved: RouteHintSummary | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
resolvedRouteSummary: RouteHintSummary | null;
|
||||||
|
liveTemporalHint: Record<string, string | null> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantDeepTurnRuntimeContext(
|
||||||
|
input: BuildAssistantDeepTurnRuntimeContextInput
|
||||||
|
): BuildAssistantDeepTurnRuntimeContextOutput {
|
||||||
|
const companyAnchors = input.resolveCompanyAnchors(input.userMessage);
|
||||||
|
const initialBusinessScopeResolution = input.resolveBusinessScopeAlignment({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
normalized: input.normalizedPayload,
|
||||||
|
routeSummary: input.routeSummary
|
||||||
|
});
|
||||||
|
const inferredDomainByMessage = input.inferP0DomainFromMessage(input.userMessage);
|
||||||
|
const focusDomainForGuards =
|
||||||
|
inferredDomainByMessage && KNOWN_P0_DOMAINS.has(inferredDomainByMessage) ? inferredDomainByMessage : null;
|
||||||
|
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
const temporalGuard = input.resolveTemporalGuard({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalized: input.normalizedPayload,
|
||||||
|
companyAnchors,
|
||||||
|
analysisContext
|
||||||
|
});
|
||||||
|
const domainPolarityGuardInitial = input.resolveDomainPolarityGuard({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
focusDomainHint: focusDomainForGuards
|
||||||
|
});
|
||||||
|
const claimAnchorAudit = input.resolveClaimBoundAnchors({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
focusDomainHint: focusDomainForGuards,
|
||||||
|
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
|
||||||
|
});
|
||||||
|
const businessScopeResolution = input.resolveBusinessScopeFromLiveContext({
|
||||||
|
current: initialBusinessScopeResolution,
|
||||||
|
temporalGuard,
|
||||||
|
claimType: claimAnchorAudit.claim_type,
|
||||||
|
focusDomainHint: focusDomainForGuards,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
companyAnchors,
|
||||||
|
followupApplied: Boolean((input.followupUsage as { applied?: unknown } | null)?.applied)
|
||||||
|
});
|
||||||
|
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
|
||||||
|
const liveTemporalHint = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
companyAnchors,
|
||||||
|
initialBusinessScopeResolution,
|
||||||
|
inferredDomainByMessage,
|
||||||
|
focusDomainForGuards,
|
||||||
|
temporalGuard,
|
||||||
|
domainPolarityGuardInitial,
|
||||||
|
claimAnchorAudit,
|
||||||
|
businessScopeResolution,
|
||||||
|
resolvedRouteSummary,
|
||||||
|
liveTemporalHint
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import type {
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
AssistantRequirement,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { AssistantRequirementExtractionResult, AssistantCoverageEvaluationResult } from "./assistantOrchestrationRuntimeAdapter";
|
||||||
|
import { runAssistantCoverageGroundingPipeline } from "./assistantOrchestrationRuntimeAdapter";
|
||||||
|
import type { AssistantDeepTurnGroundingEligibilityOutput } from "./assistantDeepTurnGuardRuntimeAdapter";
|
||||||
|
import { applyAssistantDeepTurnGroundingEligibility } from "./assistantDeepTurnGuardRuntimeAdapter";
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnGroundingRuntimeInput {
|
||||||
|
claimType: string;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
rbpPlanAudit: unknown;
|
||||||
|
faPlanAudit: unknown;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"] | null | undefined;
|
||||||
|
userMessage: string;
|
||||||
|
requirementExtraction: AssistantRequirementExtractionResult;
|
||||||
|
extractRequirements: (
|
||||||
|
routeSummary: RouteHintSummary | null,
|
||||||
|
normalized: NormalizeResponsePayload["normalized"] | null | undefined,
|
||||||
|
userMessage: string
|
||||||
|
) => AssistantRequirementExtractionResult;
|
||||||
|
evaluateCoverage: (
|
||||||
|
requirements: AssistantRequirement[],
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
) => AssistantCoverageEvaluationResult;
|
||||||
|
checkGrounding: (
|
||||||
|
userMessage: string,
|
||||||
|
requirements: AssistantRequirement[],
|
||||||
|
coverage: RequirementCoverageReport,
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
) => AnswerGroundingCheck;
|
||||||
|
temporalGuard: unknown;
|
||||||
|
polarityAudit: unknown;
|
||||||
|
evidenceAudit: unknown;
|
||||||
|
claimAnchorAudit: unknown;
|
||||||
|
targetedEvidenceHitRate?: number | null;
|
||||||
|
businessScopeResolved?: string[] | null;
|
||||||
|
collectRbpLiveRouteAudit: (input: {
|
||||||
|
claimType: string;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
planAudit: unknown;
|
||||||
|
}) => unknown;
|
||||||
|
collectFaLiveRouteAudit: (input: {
|
||||||
|
claimType: string;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
planAudit: unknown;
|
||||||
|
}) => unknown;
|
||||||
|
runCoverageGroundingPipelineFn?: typeof runAssistantCoverageGroundingPipeline;
|
||||||
|
applyGroundingEligibilityFn?: (input: {
|
||||||
|
groundingCheckBase: AnswerGroundingCheck;
|
||||||
|
temporalGuard: unknown;
|
||||||
|
polarityAudit: unknown;
|
||||||
|
evidenceAudit: unknown;
|
||||||
|
claimAnchorAudit?: unknown;
|
||||||
|
targetedEvidenceHitRate?: number | null;
|
||||||
|
businessScopeResolved?: string[] | null;
|
||||||
|
}) => AssistantDeepTurnGroundingEligibilityOutput<AnswerGroundingCheck>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnGroundingRuntimeOutput {
|
||||||
|
rbpLiveRouteAudit: unknown;
|
||||||
|
faLiveRouteAudit: unknown;
|
||||||
|
coverageEvaluation: AssistantCoverageEvaluationResult;
|
||||||
|
groundingCheckBase: AnswerGroundingCheck;
|
||||||
|
groundedAnswerEligibilityGuard: unknown;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runAssistantDeepTurnGroundingRuntime(
|
||||||
|
input: AssistantDeepTurnGroundingRuntimeInput
|
||||||
|
): AssistantDeepTurnGroundingRuntimeOutput {
|
||||||
|
const runCoverageGroundingPipelineSafe = input.runCoverageGroundingPipelineFn ?? runAssistantCoverageGroundingPipeline;
|
||||||
|
const applyGroundingEligibilitySafe =
|
||||||
|
input.applyGroundingEligibilityFn ??
|
||||||
|
((payload) => applyAssistantDeepTurnGroundingEligibility(payload as any) as any);
|
||||||
|
|
||||||
|
const rbpLiveRouteAudit = input.collectRbpLiveRouteAudit({
|
||||||
|
claimType: input.claimType,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
planAudit: input.rbpPlanAudit
|
||||||
|
});
|
||||||
|
const faLiveRouteAudit = input.collectFaLiveRouteAudit({
|
||||||
|
claimType: input.claimType,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
planAudit: input.faPlanAudit
|
||||||
|
});
|
||||||
|
const orchestrationRuntime = runCoverageGroundingPipelineSafe({
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
normalized: input.normalizedPayload,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
requirementExtraction: input.requirementExtraction,
|
||||||
|
extractRequirements: input.extractRequirements,
|
||||||
|
evaluateCoverage: input.evaluateCoverage,
|
||||||
|
checkGrounding: input.checkGrounding
|
||||||
|
});
|
||||||
|
const coverageEvaluation = orchestrationRuntime.coverageEvaluation;
|
||||||
|
const groundingCheckBase = orchestrationRuntime.groundingCheckBase;
|
||||||
|
const groundingEligibilityRuntime = applyGroundingEligibilitySafe({
|
||||||
|
groundingCheckBase,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
evidenceAudit: input.evidenceAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
|
||||||
|
businessScopeResolved: input.businessScopeResolved
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit,
|
||||||
|
coverageEvaluation,
|
||||||
|
groundingCheckBase,
|
||||||
|
groundedAnswerEligibilityGuard: groundingEligibilityRuntime.groundedAnswerEligibilityGuard,
|
||||||
|
groundingCheck: groundingEligibilityRuntime.groundingCheck
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import type { UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import { applyTargetedEvidenceAcquisition } from "./assistantClaimBoundEvidence";
|
||||||
|
import {
|
||||||
|
applyDomainPolarityGuardToRetrievalResults,
|
||||||
|
applyEvidenceAdmissibilityGate,
|
||||||
|
applyEligibilityToGroundingCheck,
|
||||||
|
evaluateGroundedAnswerEligibility
|
||||||
|
} from "./assistantRuntimeGuards";
|
||||||
|
|
||||||
|
type GroundingCheckLike = {
|
||||||
|
status: string;
|
||||||
|
reasons: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApplyEligibilityToGroundingCheckFn = <T extends GroundingCheckLike>(
|
||||||
|
groundingCheck: T,
|
||||||
|
eligibility: ReturnType<typeof evaluateGroundedAnswerEligibility>
|
||||||
|
) => T;
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnRetrievalGuardPipelineInput {
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
domainPolarityGuardInitial: Parameters<typeof applyDomainPolarityGuardToRetrievalResults>[0]["guard"];
|
||||||
|
claimAnchorAudit: Parameters<typeof applyTargetedEvidenceAcquisition>[0]["claimAudit"];
|
||||||
|
temporalGuard: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["temporal"];
|
||||||
|
focusDomainForGuards: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["focusDomainHint"];
|
||||||
|
companyAnchors?: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["companyAnchors"];
|
||||||
|
userMessage: string;
|
||||||
|
applyDomainPolarityGuardFn?: typeof applyDomainPolarityGuardToRetrievalResults;
|
||||||
|
applyTargetedEvidenceFn?: typeof applyTargetedEvidenceAcquisition;
|
||||||
|
applyEvidenceAdmissibilityGateFn?: typeof applyEvidenceAdmissibilityGate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnRetrievalGuardPipelineOutput {
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
polarityGuardResult: ReturnType<typeof applyDomainPolarityGuardToRetrievalResults>;
|
||||||
|
targetedEvidenceResult: ReturnType<typeof applyTargetedEvidenceAcquisition>;
|
||||||
|
evidenceGateResult: ReturnType<typeof applyEvidenceAdmissibilityGate>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAssistantDeepTurnRetrievalGuards(
|
||||||
|
input: AssistantDeepTurnRetrievalGuardPipelineInput
|
||||||
|
): AssistantDeepTurnRetrievalGuardPipelineOutput {
|
||||||
|
const applyDomainPolarityGuardSafe = input.applyDomainPolarityGuardFn ?? applyDomainPolarityGuardToRetrievalResults;
|
||||||
|
const applyTargetedEvidenceSafe = input.applyTargetedEvidenceFn ?? applyTargetedEvidenceAcquisition;
|
||||||
|
const applyEvidenceAdmissibilityGateSafe = input.applyEvidenceAdmissibilityGateFn ?? applyEvidenceAdmissibilityGate;
|
||||||
|
|
||||||
|
const polarityGuardResult = applyDomainPolarityGuardSafe({
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
guard: input.domainPolarityGuardInitial
|
||||||
|
});
|
||||||
|
const targetedEvidenceResult = applyTargetedEvidenceSafe({
|
||||||
|
retrievalResults: polarityGuardResult.retrievalResults,
|
||||||
|
claimAudit: input.claimAnchorAudit
|
||||||
|
});
|
||||||
|
const evidenceGateResult = applyEvidenceAdmissibilityGateSafe({
|
||||||
|
retrievalResults: targetedEvidenceResult.retrievalResults,
|
||||||
|
temporal: input.temporalGuard,
|
||||||
|
focusDomainHint: input.focusDomainForGuards,
|
||||||
|
polarity: polarityGuardResult.audit.polarity,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
userMessage: input.userMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
retrievalResults: evidenceGateResult.retrievalResults,
|
||||||
|
polarityGuardResult,
|
||||||
|
targetedEvidenceResult,
|
||||||
|
evidenceGateResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnGroundingEligibilityInput<T extends GroundingCheckLike> {
|
||||||
|
groundingCheckBase: T;
|
||||||
|
temporalGuard: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["temporal"];
|
||||||
|
polarityAudit: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["polarity"];
|
||||||
|
evidenceAudit: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["evidence"];
|
||||||
|
claimAnchorAudit?: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["claimAnchors"];
|
||||||
|
targetedEvidenceHitRate?: number | null;
|
||||||
|
businessScopeResolved?: string[] | null;
|
||||||
|
evaluateGroundedAnswerEligibilityFn?: typeof evaluateGroundedAnswerEligibility;
|
||||||
|
applyEligibilityToGroundingCheckFn?: ApplyEligibilityToGroundingCheckFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnGroundingEligibilityOutput<T extends GroundingCheckLike> {
|
||||||
|
groundedAnswerEligibilityGuard: ReturnType<typeof evaluateGroundedAnswerEligibility>;
|
||||||
|
groundingCheck: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAssistantDeepTurnGroundingEligibility<T extends GroundingCheckLike>(
|
||||||
|
input: AssistantDeepTurnGroundingEligibilityInput<T>
|
||||||
|
): AssistantDeepTurnGroundingEligibilityOutput<T> {
|
||||||
|
const evaluateGroundedAnswerEligibilitySafe =
|
||||||
|
input.evaluateGroundedAnswerEligibilityFn ?? evaluateGroundedAnswerEligibility;
|
||||||
|
const applyEligibilityToGroundingCheckSafe =
|
||||||
|
input.applyEligibilityToGroundingCheckFn ??
|
||||||
|
((groundingCheck, eligibility) => applyEligibilityToGroundingCheck(groundingCheck, eligibility));
|
||||||
|
|
||||||
|
const groundedAnswerEligibilityGuard = evaluateGroundedAnswerEligibilitySafe({
|
||||||
|
temporal: input.temporalGuard,
|
||||||
|
polarity: input.polarityAudit,
|
||||||
|
evidence: input.evidenceAudit,
|
||||||
|
claimAnchors: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
|
||||||
|
businessScopeResolved: input.businessScopeResolved
|
||||||
|
});
|
||||||
|
const groundingCheck = applyEligibilityToGroundingCheckSafe(input.groundingCheckBase, groundedAnswerEligibilityGuard);
|
||||||
|
|
||||||
|
return {
|
||||||
|
groundedAnswerEligibilityGuard,
|
||||||
|
groundingCheck
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import type { AssistantReplyType, AssistantRequirement, AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { AnswerStructureV11 } from "../types/stage1Contracts";
|
||||||
|
import type { AssistantDeepTurnPackagingInput } from "./assistantDeepTurnPackaging";
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnInputBuilderArgs {
|
||||||
|
sessionId: string;
|
||||||
|
messageId: string;
|
||||||
|
userMessage: string;
|
||||||
|
normalized: {
|
||||||
|
trace_id: string;
|
||||||
|
prompt_version: string;
|
||||||
|
schema_version: string;
|
||||||
|
normalized: NormalizeResponsePayload["normalized"];
|
||||||
|
};
|
||||||
|
normalizedQuestion: string;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
analysisContextForContract: {
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
} | null;
|
||||||
|
executionPlan: Array<{
|
||||||
|
fragment_id: string;
|
||||||
|
requirement_ids: string[];
|
||||||
|
route: string;
|
||||||
|
should_execute: boolean;
|
||||||
|
no_route_reason?: string | null;
|
||||||
|
clarification_reason?: string | null;
|
||||||
|
}>;
|
||||||
|
requirementExtractionRequirements: AssistantRequirement[];
|
||||||
|
coverageEvaluationRequirements: AssistantRequirement[];
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
retrievalCalls: Array<Record<string, unknown>>;
|
||||||
|
retrievalResultsRaw: unknown[];
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
routesForDebug: Array<Record<string, unknown>>;
|
||||||
|
resolvedExecutionState: unknown;
|
||||||
|
questionTypeClass: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: boolean;
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
};
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw?: string[];
|
||||||
|
business_scope_resolved?: string[];
|
||||||
|
company_grounding_applied?: boolean;
|
||||||
|
scope_resolution_reason?: string[];
|
||||||
|
};
|
||||||
|
temporalGuard: Record<string, unknown>;
|
||||||
|
polarityAudit: Record<string, unknown>;
|
||||||
|
claimAnchorAudit: Record<string, unknown>;
|
||||||
|
targetedEvidenceAudit: unknown;
|
||||||
|
evidenceAdmissibilityGateAudit: unknown;
|
||||||
|
rbpLiveRouteAudit: unknown | null;
|
||||||
|
faLiveRouteAudit: unknown | null;
|
||||||
|
groundedAnswerEligibilityGuard: Record<string, unknown>;
|
||||||
|
followupStateUsage?: unknown;
|
||||||
|
composition: {
|
||||||
|
reply_type: AssistantReplyType;
|
||||||
|
fallback_type: unknown;
|
||||||
|
answer_structure_v11?: AnswerStructureV11 | null;
|
||||||
|
problem_centric_answer_applied?: boolean;
|
||||||
|
problem_units_used_count?: number;
|
||||||
|
problem_answer_mode?: string;
|
||||||
|
problem_unit_ids_used?: unknown;
|
||||||
|
};
|
||||||
|
safeAssistantReplyBase: string;
|
||||||
|
featureContractsV11: boolean;
|
||||||
|
featureAnswerPolicyV11: boolean;
|
||||||
|
investigationStateSnapshot: unknown;
|
||||||
|
addressRuntimeMetaForDeep:
|
||||||
|
| {
|
||||||
|
attempted?: boolean;
|
||||||
|
applied?: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
provider?: string | null;
|
||||||
|
fallbackRuleHit?: string | null;
|
||||||
|
toolGateDecision?: string | null;
|
||||||
|
toolGateReason?: string | null;
|
||||||
|
predecomposeContract?: unknown;
|
||||||
|
orchestrationContract?: unknown;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantDeepTurnPackagingInput(args: AssistantDeepTurnInputBuilderArgs): AssistantDeepTurnPackagingInput {
|
||||||
|
return {
|
||||||
|
...args,
|
||||||
|
routesForDebug: Array.isArray(args.routesForDebug) ? args.routesForDebug : [],
|
||||||
|
followupStateUsage: args.followupStateUsage ?? null,
|
||||||
|
composition: {
|
||||||
|
reply_type: args.composition.reply_type,
|
||||||
|
fallback_type: args.composition.fallback_type,
|
||||||
|
answer_structure_v11: args.composition.answer_structure_v11 ?? null,
|
||||||
|
problem_centric_answer_applied: args.composition.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: args.composition.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: args.composition.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
problem_unit_ids_used: Array.isArray(args.composition.problem_unit_ids_used) ? args.composition.problem_unit_ids_used : []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
import type {
|
||||||
|
AssistantConversationItem,
|
||||||
|
AssistantReplyType,
|
||||||
|
AssistantRequirement,
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { AnswerStructureV11 } from "../types/stage1Contracts";
|
||||||
|
import {
|
||||||
|
assembleAssistantEvidenceBundle,
|
||||||
|
type AssistantEvidenceBundleAssembly
|
||||||
|
} from "./assistantEvidenceBundleAssembler";
|
||||||
|
import { assembleAssistantContractsBundleV1, type AssistantContractsBundleV1 } from "./assistantContractsBundleAssembler";
|
||||||
|
import { buildDeepAnswerArtifacts, buildAssistantConversationItem, type DeepAnswerArtifacts } from "./assistantDeepResponseAssembler";
|
||||||
|
import { buildDeepAnalysisDebugPayload } from "./assistantDebugPayloadAssembler";
|
||||||
|
import { buildDeepAnalysisProcessedLogDetails } from "./assistantMessageLogAssembler";
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnPackagingInput {
|
||||||
|
sessionId: string;
|
||||||
|
messageId: string;
|
||||||
|
userMessage: string;
|
||||||
|
normalized: {
|
||||||
|
trace_id: string;
|
||||||
|
prompt_version: string;
|
||||||
|
schema_version: string;
|
||||||
|
normalized: NormalizeResponsePayload["normalized"];
|
||||||
|
};
|
||||||
|
normalizedQuestion: string;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
analysisContextForContract: {
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
} | null;
|
||||||
|
executionPlan: Array<{
|
||||||
|
fragment_id: string;
|
||||||
|
requirement_ids: string[];
|
||||||
|
route: string;
|
||||||
|
should_execute: boolean;
|
||||||
|
no_route_reason?: string | null;
|
||||||
|
clarification_reason?: string | null;
|
||||||
|
}>;
|
||||||
|
requirementExtractionRequirements: AssistantRequirement[];
|
||||||
|
coverageEvaluationRequirements: AssistantRequirement[];
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
retrievalCalls: Array<Record<string, unknown>>;
|
||||||
|
retrievalResultsRaw: unknown[];
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
routesForDebug: Array<Record<string, unknown>>;
|
||||||
|
resolvedExecutionState: unknown;
|
||||||
|
questionTypeClass: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: boolean;
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
};
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw?: string[];
|
||||||
|
business_scope_resolved?: string[];
|
||||||
|
company_grounding_applied?: boolean;
|
||||||
|
scope_resolution_reason?: string[];
|
||||||
|
};
|
||||||
|
temporalGuard: Record<string, unknown>;
|
||||||
|
polarityAudit: Record<string, unknown>;
|
||||||
|
claimAnchorAudit: Record<string, unknown>;
|
||||||
|
targetedEvidenceAudit: unknown;
|
||||||
|
evidenceAdmissibilityGateAudit: unknown;
|
||||||
|
rbpLiveRouteAudit: unknown | null;
|
||||||
|
faLiveRouteAudit: unknown | null;
|
||||||
|
groundedAnswerEligibilityGuard: Record<string, unknown>;
|
||||||
|
followupStateUsage: unknown | null;
|
||||||
|
composition: {
|
||||||
|
reply_type: AssistantReplyType;
|
||||||
|
fallback_type: unknown;
|
||||||
|
answer_structure_v11?: AnswerStructureV11 | null;
|
||||||
|
problem_centric_answer_applied?: boolean;
|
||||||
|
problem_units_used_count?: number;
|
||||||
|
problem_answer_mode?: string;
|
||||||
|
problem_unit_ids_used?: string[];
|
||||||
|
};
|
||||||
|
safeAssistantReplyBase: string;
|
||||||
|
featureContractsV11: boolean;
|
||||||
|
featureAnswerPolicyV11: boolean;
|
||||||
|
investigationStateSnapshot: unknown;
|
||||||
|
addressRuntimeMetaForDeep:
|
||||||
|
| {
|
||||||
|
attempted?: boolean;
|
||||||
|
applied?: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
provider?: string | null;
|
||||||
|
fallbackRuleHit?: string | null;
|
||||||
|
toolGateDecision?: string | null;
|
||||||
|
toolGateReason?: string | null;
|
||||||
|
predecomposeContract?: unknown;
|
||||||
|
orchestrationContract?: unknown;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnPackagingOutput {
|
||||||
|
evidenceBundleAssembly: AssistantEvidenceBundleAssembly;
|
||||||
|
contractsBundleV1: AssistantContractsBundleV1;
|
||||||
|
deepAnswerArtifacts: DeepAnswerArtifacts;
|
||||||
|
debug: Record<string, unknown>;
|
||||||
|
assistantItem: AssistantConversationItem;
|
||||||
|
deepAnalysisLogDetails: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assembleAssistantDeepTurnPackaging(input: AssistantDeepTurnPackagingInput): AssistantDeepTurnPackagingOutput {
|
||||||
|
const normalizedPayload = (input.normalized.normalized ?? null) as Record<string, unknown> | null;
|
||||||
|
const normalizedFragments = Array.isArray(normalizedPayload?.["fragments"]) ? (normalizedPayload?.["fragments"] as unknown[]) : [];
|
||||||
|
const evidenceBundleAssembly = assembleAssistantEvidenceBundle({
|
||||||
|
retrievalCalls: input.retrievalCalls,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const contractsBundleV1 = assembleAssistantContractsBundleV1({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalizedQuestion: input.normalizedQuestion,
|
||||||
|
normalized: input.normalized.normalized,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
analysisContext: input.analysisContextForContract,
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
requirements: input.requirementExtractionRequirements,
|
||||||
|
evidenceBundleContractV1: evidenceBundleAssembly.evidenceBundleContractV1,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
grounding: input.groundingCheck,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const deepAnswerArtifacts = buildDeepAnswerArtifacts({
|
||||||
|
safeAssistantReplyBase: input.safeAssistantReplyBase,
|
||||||
|
featureContractsV11: input.featureContractsV11,
|
||||||
|
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||||
|
compositionAnswerStructureV11: input.composition.answer_structure_v11 ?? null,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
retrievalResults: input.retrievalResults
|
||||||
|
});
|
||||||
|
const debug = buildDeepAnalysisDebugPayload({
|
||||||
|
traceId: input.normalized.trace_id,
|
||||||
|
promptVersion: input.normalized.prompt_version,
|
||||||
|
schemaVersion: input.normalized.schema_version,
|
||||||
|
fallbackType: input.composition.fallback_type,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
fragments: normalizedFragments,
|
||||||
|
requirementsExtracted: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
routes: input.routesForDebug,
|
||||||
|
retrievalStatus: evidenceBundleAssembly.retrievalStatus,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
questionTypeClass: input.questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
businessScopeResolution: input.businessScopeResolution,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceAudit: input.targetedEvidenceAudit,
|
||||||
|
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
|
||||||
|
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit: input.faLiveRouteAudit,
|
||||||
|
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
|
||||||
|
followupStateUsage: input.followupStateUsage,
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : []
|
||||||
|
},
|
||||||
|
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep,
|
||||||
|
outcomeClassV1: contractsBundleV1.outcomeClassV1,
|
||||||
|
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
|
||||||
|
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
|
||||||
|
investigationStateSnapshot: input.investigationStateSnapshot,
|
||||||
|
normalizedPayload: normalizedPayload
|
||||||
|
});
|
||||||
|
const assistantItem = buildAssistantConversationItem({
|
||||||
|
messageId: input.messageId,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
text: deepAnswerArtifacts.safeAssistantReply,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
traceId: input.normalized.trace_id,
|
||||||
|
debug: debug as any
|
||||||
|
});
|
||||||
|
const deepAnalysisLogDetails = buildDeepAnalysisProcessedLogDetails({
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
messageId: input.messageId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalizerOutput: input.normalized.normalized,
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
resolvedExecutionState: input.resolvedExecutionState,
|
||||||
|
routes: input.routesForDebug,
|
||||||
|
retrievalCalls: input.retrievalCalls,
|
||||||
|
retrievalResultsRaw: input.retrievalResultsRaw,
|
||||||
|
retrievalResultsNormalized: input.retrievalResults,
|
||||||
|
requirementsExtracted: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
droppedIntentSegments: input.droppedIntentSegments,
|
||||||
|
questionTypeClass: input.questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
businessScopeResolution: input.businessScopeResolution,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceAudit: input.targetedEvidenceAudit,
|
||||||
|
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
|
||||||
|
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit: input.faLiveRouteAudit,
|
||||||
|
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
|
||||||
|
followupStateUsage: input.followupStateUsage,
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : [],
|
||||||
|
fallback_type: input.composition.fallback_type as string
|
||||||
|
},
|
||||||
|
outcomeClassV1: contractsBundleV1.outcomeClassV1,
|
||||||
|
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
|
||||||
|
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
|
||||||
|
investigationStateSnapshot: input.investigationStateSnapshot,
|
||||||
|
assistantReply: deepAnswerArtifacts.safeAssistantReply,
|
||||||
|
traceId: input.normalized.trace_id
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
evidenceBundleAssembly,
|
||||||
|
contractsBundleV1,
|
||||||
|
deepAnswerArtifacts,
|
||||||
|
debug,
|
||||||
|
assistantItem,
|
||||||
|
deepAnalysisLogDetails
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import type {
|
||||||
|
AssistantConversationItem,
|
||||||
|
AssistantRequirement,
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
|
||||||
|
import type { AssistantDeepTurnInputBuilderArgs } from "./assistantDeepTurnInputBuilder";
|
||||||
|
import { buildAssistantDeepTurnPackagingInput } from "./assistantDeepTurnInputBuilder";
|
||||||
|
import { assembleAssistantDeepTurnPackaging } from "./assistantDeepTurnPackaging";
|
||||||
|
import type {
|
||||||
|
AssistantAnalysisContextForContract,
|
||||||
|
AssistantRuntimeAnalysisContextForPrePackaging
|
||||||
|
} from "./assistantDeepTurnPrePackagingContext";
|
||||||
|
import { buildAssistantDeepTurnPrePackagingContext } from "./assistantDeepTurnPrePackagingContext";
|
||||||
|
import {
|
||||||
|
buildAssistantInvestigationStateSnapshot,
|
||||||
|
persistAssistantInvestigationStateSnapshot
|
||||||
|
} from "./assistantInvestigationStateRuntimeAdapter";
|
||||||
|
|
||||||
|
type AssistantDeepTurnCompositionForPackaging = AssistantDeepTurnInputBuilderArgs["composition"] & {
|
||||||
|
assistant_reply: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnPackagingRuntimeInput {
|
||||||
|
featureInvestigationStateV1: boolean;
|
||||||
|
sessionId: string;
|
||||||
|
questionId: string;
|
||||||
|
userMessage: string;
|
||||||
|
normalized: {
|
||||||
|
trace_id: string;
|
||||||
|
prompt_version: string;
|
||||||
|
schema_version: string;
|
||||||
|
normalized: NormalizeResponsePayload["normalized"];
|
||||||
|
};
|
||||||
|
normalizedQuestion: string;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
executionPlan: AssistantDeepTurnInputBuilderArgs["executionPlan"];
|
||||||
|
requirementExtractionRequirements: AssistantRequirement[];
|
||||||
|
coverageEvaluationRequirements: AssistantRequirement[];
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
retrievalCalls: Array<Record<string, unknown>>;
|
||||||
|
retrievalResultsRaw: unknown[];
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
questionTypeClass: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
runtimeAnalysisContext: AssistantDeepTurnInputBuilderArgs["runtimeAnalysisContext"];
|
||||||
|
businessScopeResolution: AssistantDeepTurnInputBuilderArgs["businessScopeResolution"];
|
||||||
|
temporalGuard: Record<string, unknown>;
|
||||||
|
polarityAudit: Record<string, unknown>;
|
||||||
|
claimAnchorAudit: Record<string, unknown>;
|
||||||
|
targetedEvidenceAudit: unknown;
|
||||||
|
evidenceAdmissibilityGateAudit: unknown;
|
||||||
|
rbpLiveRouteAudit: unknown | null;
|
||||||
|
faLiveRouteAudit: unknown | null;
|
||||||
|
groundedAnswerEligibilityGuard: Record<string, unknown>;
|
||||||
|
followupStateUsage?: unknown;
|
||||||
|
followupApplied: boolean;
|
||||||
|
composition: AssistantDeepTurnCompositionForPackaging;
|
||||||
|
featureContractsV11: boolean;
|
||||||
|
featureAnswerPolicyV11: boolean;
|
||||||
|
previousInvestigationState: InvestigationStateWithProblemUnits | null | undefined;
|
||||||
|
addressRuntimeMetaForDeep:
|
||||||
|
| {
|
||||||
|
attempted?: boolean;
|
||||||
|
applied?: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
provider?: string | null;
|
||||||
|
fallbackRuleHit?: string | null;
|
||||||
|
toolGateDecision?: string | null;
|
||||||
|
toolGateReason?: string | null;
|
||||||
|
predecomposeContract?: unknown;
|
||||||
|
orchestrationContract?: unknown;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
extractDroppedIntentSegments: (normalizedPayload: NormalizeResponsePayload["normalized"]) => string[];
|
||||||
|
buildDebugRoutes: (routeSummary: RouteHintSummary | null) => Array<Record<string, unknown>>;
|
||||||
|
extractExecutionState: (normalizedPayload: NormalizeResponsePayload["normalized"]) => unknown;
|
||||||
|
sanitizeReply: (value: string, fallback?: string) => string;
|
||||||
|
persistInvestigationState: (sessionId: string, snapshot: InvestigationStateWithProblemUnits) => void;
|
||||||
|
nowIso?: () => string;
|
||||||
|
messageIdFactory?: () => string;
|
||||||
|
buildPrePackagingContextFn?: typeof buildAssistantDeepTurnPrePackagingContext;
|
||||||
|
buildInvestigationStateSnapshotFn?: typeof buildAssistantInvestigationStateSnapshot;
|
||||||
|
persistInvestigationStateSnapshotFn?: typeof persistAssistantInvestigationStateSnapshot;
|
||||||
|
buildDeepTurnPackagingInputFn?: typeof buildAssistantDeepTurnPackagingInput;
|
||||||
|
assembleDeepTurnPackagingFn?: typeof assembleAssistantDeepTurnPackaging;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnPackagingRuntimeOutput {
|
||||||
|
messageId: string;
|
||||||
|
investigationStateSnapshot: InvestigationStateWithProblemUnits | null;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
analysisContextForContract: AssistantAnalysisContextForContract | null;
|
||||||
|
routesForDebug: Array<Record<string, unknown>>;
|
||||||
|
resolvedExecutionState: unknown;
|
||||||
|
safeAssistantReplyBase: string;
|
||||||
|
safeAssistantReply: string;
|
||||||
|
debug: Record<string, unknown>;
|
||||||
|
assistantItem: AssistantConversationItem;
|
||||||
|
deepAnalysisLogDetails: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runAssistantDeepTurnPackagingRuntime(
|
||||||
|
input: AssistantDeepTurnPackagingRuntimeInput
|
||||||
|
): AssistantDeepTurnPackagingRuntimeOutput {
|
||||||
|
const buildPrePackagingContextSafe = input.buildPrePackagingContextFn ?? buildAssistantDeepTurnPrePackagingContext;
|
||||||
|
const buildInvestigationStateSnapshotSafe =
|
||||||
|
input.buildInvestigationStateSnapshotFn ?? buildAssistantInvestigationStateSnapshot;
|
||||||
|
const persistInvestigationStateSnapshotSafe =
|
||||||
|
input.persistInvestigationStateSnapshotFn ?? persistAssistantInvestigationStateSnapshot;
|
||||||
|
const buildDeepTurnPackagingInputSafe = input.buildDeepTurnPackagingInputFn ?? buildAssistantDeepTurnPackagingInput;
|
||||||
|
const assembleDeepTurnPackagingSafe = input.assembleDeepTurnPackagingFn ?? assembleAssistantDeepTurnPackaging;
|
||||||
|
|
||||||
|
const deepTurnPrePackagingContext = buildPrePackagingContextSafe({
|
||||||
|
normalizedPayload: input.normalized.normalized,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext as AssistantRuntimeAnalysisContextForPrePackaging,
|
||||||
|
assistantReply: input.composition.assistant_reply,
|
||||||
|
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
|
||||||
|
buildDebugRoutes: input.buildDebugRoutes,
|
||||||
|
extractExecutionState: input.extractExecutionState,
|
||||||
|
sanitizeReply: input.sanitizeReply
|
||||||
|
});
|
||||||
|
const investigationStateSnapshot = buildInvestigationStateSnapshotSafe({
|
||||||
|
featureEnabled: input.featureInvestigationStateV1,
|
||||||
|
previousState: input.previousInvestigationState,
|
||||||
|
timestamp: input.nowIso ? input.nowIso() : new Date().toISOString(),
|
||||||
|
questionId: input.questionId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
requirements: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
replyType: input.composition.reply_type,
|
||||||
|
followupApplied: input.followupApplied
|
||||||
|
});
|
||||||
|
persistInvestigationStateSnapshotSafe({
|
||||||
|
featureEnabled: input.featureInvestigationStateV1,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
snapshot: investigationStateSnapshot,
|
||||||
|
persist: input.persistInvestigationState
|
||||||
|
});
|
||||||
|
const messageId = input.messageIdFactory ? input.messageIdFactory() : `msg-${nanoid(10)}`;
|
||||||
|
const deepTurnPackagingInput = buildDeepTurnPackagingInputSafe({
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
messageId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
normalized: input.normalized,
|
||||||
|
normalizedQuestion: input.normalizedQuestion,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
|
||||||
|
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
|
||||||
|
executionPlan: input.executionPlan,
|
||||||
|
requirementExtractionRequirements: input.requirementExtractionRequirements,
|
||||||
|
coverageEvaluationRequirements: input.coverageEvaluationRequirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
groundingCheck: input.groundingCheck,
|
||||||
|
retrievalCalls: input.retrievalCalls,
|
||||||
|
retrievalResultsRaw: input.retrievalResultsRaw,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
|
||||||
|
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
|
||||||
|
questionTypeClass: input.questionTypeClass,
|
||||||
|
companyAnchors: input.companyAnchors,
|
||||||
|
runtimeAnalysisContext: input.runtimeAnalysisContext,
|
||||||
|
businessScopeResolution: input.businessScopeResolution,
|
||||||
|
temporalGuard: input.temporalGuard,
|
||||||
|
polarityAudit: input.polarityAudit,
|
||||||
|
claimAnchorAudit: input.claimAnchorAudit,
|
||||||
|
targetedEvidenceAudit: input.targetedEvidenceAudit,
|
||||||
|
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
|
||||||
|
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
|
||||||
|
faLiveRouteAudit: input.faLiveRouteAudit,
|
||||||
|
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
|
||||||
|
followupStateUsage: input.followupStateUsage,
|
||||||
|
composition: {
|
||||||
|
reply_type: input.composition.reply_type,
|
||||||
|
fallback_type: input.composition.fallback_type,
|
||||||
|
answer_structure_v11: input.composition.answer_structure_v11,
|
||||||
|
problem_centric_answer_applied: input.composition.problem_centric_answer_applied,
|
||||||
|
problem_units_used_count: input.composition.problem_units_used_count,
|
||||||
|
problem_answer_mode: input.composition.problem_answer_mode,
|
||||||
|
problem_unit_ids_used: input.composition.problem_unit_ids_used
|
||||||
|
},
|
||||||
|
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
|
||||||
|
featureContractsV11: input.featureContractsV11,
|
||||||
|
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||||
|
investigationStateSnapshot,
|
||||||
|
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep
|
||||||
|
});
|
||||||
|
const deepTurnPackaging = assembleDeepTurnPackagingSafe(deepTurnPackagingInput);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId,
|
||||||
|
investigationStateSnapshot,
|
||||||
|
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
|
||||||
|
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
|
||||||
|
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
|
||||||
|
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
|
||||||
|
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
|
||||||
|
safeAssistantReply: deepTurnPackaging.deepAnswerArtifacts.safeAssistantReply,
|
||||||
|
debug: deepTurnPackaging.debug,
|
||||||
|
assistantItem: deepTurnPackaging.assistantItem,
|
||||||
|
deepAnalysisLogDetails: deepTurnPackaging.deepAnalysisLogDetails
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import type { AssistantRequirement } from "../types/assistant";
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { AssistantExecutionPlanItem } from "./assistantQueryPlanning";
|
||||||
|
|
||||||
|
export interface AssistantRequirementExtractionLike {
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
byFragment: Map<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantPlanEnforcementAuditLike {
|
||||||
|
executionPlan: AssistantExecutionPlanItem[];
|
||||||
|
audit: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnExecutionPlanInput {
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"];
|
||||||
|
userMessage: string;
|
||||||
|
claimType: string;
|
||||||
|
temporalGuard: unknown;
|
||||||
|
domainPolarityGuardInitial: unknown;
|
||||||
|
extractRequirements: (
|
||||||
|
routeSummary: RouteHintSummary | null,
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"],
|
||||||
|
userMessage: string
|
||||||
|
) => AssistantRequirementExtractionLike;
|
||||||
|
toExecutionPlan: (
|
||||||
|
routeSummary: RouteHintSummary | null,
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"],
|
||||||
|
userMessage: string,
|
||||||
|
requirementByFragment: Map<string, string[]>
|
||||||
|
) => AssistantExecutionPlanItem[];
|
||||||
|
enforceRbpLiveRoutePlan: (input: {
|
||||||
|
executionPlan: AssistantExecutionPlanItem[];
|
||||||
|
claimType: string;
|
||||||
|
temporalGuard: unknown;
|
||||||
|
}) => AssistantPlanEnforcementAuditLike;
|
||||||
|
enforceFaLiveRoutePlan: (input: {
|
||||||
|
executionPlan: AssistantExecutionPlanItem[];
|
||||||
|
claimType: string;
|
||||||
|
temporalGuard: unknown;
|
||||||
|
}) => AssistantPlanEnforcementAuditLike;
|
||||||
|
applyTemporalHintToExecutionPlan: (
|
||||||
|
executionPlan: AssistantExecutionPlanItem[],
|
||||||
|
temporalGuard: unknown
|
||||||
|
) => AssistantExecutionPlanItem[];
|
||||||
|
applyPolarityHintToExecutionPlan: (
|
||||||
|
executionPlan: AssistantExecutionPlanItem[],
|
||||||
|
domainPolarityGuardInitial: unknown
|
||||||
|
) => AssistantExecutionPlanItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnExecutionPlanOutput {
|
||||||
|
requirementExtraction: AssistantRequirementExtractionLike;
|
||||||
|
executionPlan: AssistantExecutionPlanItem[];
|
||||||
|
rbpRoutePlanEnforcement: AssistantPlanEnforcementAuditLike;
|
||||||
|
faRoutePlanEnforcement: AssistantPlanEnforcementAuditLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantDeepTurnExecutionPlan(
|
||||||
|
input: BuildAssistantDeepTurnExecutionPlanInput
|
||||||
|
): BuildAssistantDeepTurnExecutionPlanOutput {
|
||||||
|
const requirementExtraction = input.extractRequirements(input.routeSummary, input.normalizedPayload, input.userMessage);
|
||||||
|
let executionPlan = input.toExecutionPlan(
|
||||||
|
input.routeSummary,
|
||||||
|
input.normalizedPayload,
|
||||||
|
input.userMessage,
|
||||||
|
requirementExtraction.byFragment
|
||||||
|
);
|
||||||
|
const rbpRoutePlanEnforcement = input.enforceRbpLiveRoutePlan({
|
||||||
|
executionPlan,
|
||||||
|
claimType: input.claimType,
|
||||||
|
temporalGuard: input.temporalGuard
|
||||||
|
});
|
||||||
|
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||||||
|
const faRoutePlanEnforcement = input.enforceFaLiveRoutePlan({
|
||||||
|
executionPlan,
|
||||||
|
claimType: input.claimType,
|
||||||
|
temporalGuard: input.temporalGuard
|
||||||
|
});
|
||||||
|
executionPlan = faRoutePlanEnforcement.executionPlan;
|
||||||
|
executionPlan = input.applyTemporalHintToExecutionPlan(executionPlan, input.temporalGuard);
|
||||||
|
executionPlan = input.applyPolarityHintToExecutionPlan(executionPlan, input.domainPolarityGuardInitial);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requirementExtraction,
|
||||||
|
executionPlan,
|
||||||
|
rbpRoutePlanEnforcement,
|
||||||
|
faRoutePlanEnforcement
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
|
||||||
|
export interface AssistantRuntimeAnalysisContextForPrePackaging {
|
||||||
|
active: boolean;
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantAnalysisContextForContract {
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnPrePackagingContextInput {
|
||||||
|
normalizedPayload: NormalizeResponsePayload["normalized"];
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
runtimeAnalysisContext: AssistantRuntimeAnalysisContextForPrePackaging;
|
||||||
|
assistantReply: string;
|
||||||
|
extractDroppedIntentSegments: (normalizedPayload: NormalizeResponsePayload["normalized"]) => string[];
|
||||||
|
buildDebugRoutes: (routeSummary: RouteHintSummary | null) => Array<Record<string, unknown>>;
|
||||||
|
extractExecutionState: (normalizedPayload: NormalizeResponsePayload["normalized"]) => unknown;
|
||||||
|
sanitizeReply: (value: string, fallback?: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnPrePackagingContext {
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
analysisContextForContract: AssistantAnalysisContextForContract | null;
|
||||||
|
routesForDebug: Array<Record<string, unknown>>;
|
||||||
|
resolvedExecutionState: unknown;
|
||||||
|
safeAssistantReplyBase: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantDeepTurnPrePackagingContext(
|
||||||
|
input: BuildAssistantDeepTurnPrePackagingContextInput
|
||||||
|
): AssistantDeepTurnPrePackagingContext {
|
||||||
|
return {
|
||||||
|
droppedIntentSegments: input.extractDroppedIntentSegments(input.normalizedPayload),
|
||||||
|
analysisContextForContract: input.runtimeAnalysisContext.active
|
||||||
|
? {
|
||||||
|
as_of_date: input.runtimeAnalysisContext.as_of_date,
|
||||||
|
period_from: input.runtimeAnalysisContext.period_from,
|
||||||
|
period_to: input.runtimeAnalysisContext.period_to,
|
||||||
|
source: input.runtimeAnalysisContext.source,
|
||||||
|
snapshot_mode: input.runtimeAnalysisContext.snapshot_mode
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
routesForDebug: input.buildDebugRoutes(input.routeSummary),
|
||||||
|
resolvedExecutionState: input.extractExecutionState(input.normalizedPayload),
|
||||||
|
safeAssistantReplyBase: input.sanitizeReply(input.assistantReply, "Нужны уточнения для надежного ответа.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type {
|
||||||
|
AssistantConversationItem,
|
||||||
|
AssistantDebugPayload,
|
||||||
|
AssistantMessageResponsePayload,
|
||||||
|
AssistantReplyType
|
||||||
|
} from "../types/assistant";
|
||||||
|
|
||||||
|
export interface BuildAssistantDeepTurnSuccessResponseInput {
|
||||||
|
sessionId: string;
|
||||||
|
assistantReply: string;
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
conversationItem: AssistantConversationItem;
|
||||||
|
debug: AssistantDebugPayload | Record<string, unknown>;
|
||||||
|
conversation: AssistantConversationItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantDeepTurnSuccessResponse(
|
||||||
|
input: BuildAssistantDeepTurnSuccessResponseInput
|
||||||
|
): AssistantMessageResponsePayload {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
session_id: input.sessionId,
|
||||||
|
assistant_reply: input.assistantReply,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
conversation_item: input.conversationItem,
|
||||||
|
debug: input.debug as AssistantDebugPayload,
|
||||||
|
conversation: input.conversation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import type { UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import type { AssistantExecutionPlanItem } from "./assistantQueryPlanning";
|
||||||
|
import { normalizeRetrievalResult } from "./retrievalResultNormalizer";
|
||||||
|
|
||||||
|
export interface AssistantLiveTemporalHint {
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantRetrievalCallRecord {
|
||||||
|
fragment_id: string;
|
||||||
|
requirement_ids: string[];
|
||||||
|
route: string;
|
||||||
|
status: "skipped" | "executed" | "failed";
|
||||||
|
query_text: string;
|
||||||
|
reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantRetrievalRawResultRecord {
|
||||||
|
fragment_id: string;
|
||||||
|
route: string;
|
||||||
|
raw_result: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnRetrievalExecutionInput {
|
||||||
|
executionPlan: AssistantExecutionPlanItem[];
|
||||||
|
liveTemporalHint: AssistantLiveTemporalHint | null;
|
||||||
|
executeRouteRuntime: (
|
||||||
|
route: string,
|
||||||
|
fragmentText: string,
|
||||||
|
options: {
|
||||||
|
temporalHint: AssistantLiveTemporalHint | null;
|
||||||
|
}
|
||||||
|
) => Promise<unknown>;
|
||||||
|
mapNoRouteReason: (reason: string | null) => string;
|
||||||
|
buildSkippedResult: (item: AssistantExecutionPlanItem) => UnifiedRetrievalResult;
|
||||||
|
normalizeRetrievalResultFn?: typeof normalizeRetrievalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDeepTurnRetrievalExecutionOutput {
|
||||||
|
retrievalCalls: AssistantRetrievalCallRecord[];
|
||||||
|
retrievalResultsRaw: AssistantRetrievalRawResultRecord[];
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRouteExecutorErrorRawResult(route: string, message: string): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {
|
||||||
|
route
|
||||||
|
},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "low",
|
||||||
|
limitations: ["Route executor failed."],
|
||||||
|
errors: [message]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeAssistantDeepTurnRetrievalPlan(
|
||||||
|
input: AssistantDeepTurnRetrievalExecutionInput
|
||||||
|
): Promise<AssistantDeepTurnRetrievalExecutionOutput> {
|
||||||
|
const normalizeRetrievalResultSafe = input.normalizeRetrievalResultFn ?? normalizeRetrievalResult;
|
||||||
|
const retrievalCalls: AssistantRetrievalCallRecord[] = [];
|
||||||
|
const retrievalResultsRaw: AssistantRetrievalRawResultRecord[] = [];
|
||||||
|
const retrievalResults: UnifiedRetrievalResult[] = [];
|
||||||
|
|
||||||
|
for (const planItem of input.executionPlan) {
|
||||||
|
if (!planItem.should_execute) {
|
||||||
|
retrievalCalls.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
requirement_ids: planItem.requirement_ids,
|
||||||
|
route: planItem.route,
|
||||||
|
status: "skipped",
|
||||||
|
query_text: planItem.fragment_text,
|
||||||
|
reason: input.mapNoRouteReason(planItem.no_route_reason)
|
||||||
|
});
|
||||||
|
retrievalResults.push(input.buildSkippedResult(planItem));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
retrievalCalls.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
requirement_ids: planItem.requirement_ids,
|
||||||
|
route: planItem.route,
|
||||||
|
status: "executed",
|
||||||
|
query_text: planItem.fragment_text,
|
||||||
|
reason: null
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await input.executeRouteRuntime(planItem.route, planItem.fragment_text, {
|
||||||
|
temporalHint: input.liveTemporalHint
|
||||||
|
});
|
||||||
|
retrievalResultsRaw.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
route: planItem.route,
|
||||||
|
raw_result: raw
|
||||||
|
});
|
||||||
|
retrievalResults.push(
|
||||||
|
normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw as any)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
retrievalCalls[retrievalCalls.length - 1].status = "failed";
|
||||||
|
retrievalCalls[retrievalCalls.length - 1].reason = message;
|
||||||
|
const rawError = buildRouteExecutorErrorRawResult(planItem.route, message);
|
||||||
|
retrievalResultsRaw.push({
|
||||||
|
fragment_id: planItem.fragment_id,
|
||||||
|
route: planItem.route,
|
||||||
|
raw_result: rawError
|
||||||
|
});
|
||||||
|
retrievalResults.push(
|
||||||
|
normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError as any)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
retrievalCalls,
|
||||||
|
retrievalResultsRaw,
|
||||||
|
retrievalResults
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { AssistantDebugPayload, UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import {
|
||||||
|
buildAssistantEvidenceBundleContractV1,
|
||||||
|
type AssistantEvidenceBundleContractV1
|
||||||
|
} from "./assistantOrchestrationContracts";
|
||||||
|
|
||||||
|
type RetrievalStatusItem = AssistantDebugPayload["retrieval_status"][number];
|
||||||
|
|
||||||
|
export interface AssistantEvidenceBundleAssembly {
|
||||||
|
evidenceBundleContractV1: AssistantEvidenceBundleContractV1;
|
||||||
|
retrievalStatus: RetrievalStatusItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrievalStatus(retrievalResults: UnifiedRetrievalResult[]): RetrievalStatusItem[] {
|
||||||
|
return retrievalResults.map((item) => ({
|
||||||
|
fragment_id: item.fragment_id,
|
||||||
|
requirement_ids: item.requirement_ids,
|
||||||
|
route: item.route,
|
||||||
|
status: item.status,
|
||||||
|
result_type: item.result_type
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assembleAssistantEvidenceBundle(input: {
|
||||||
|
retrievalCalls: Array<Record<string, unknown>>;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
}): AssistantEvidenceBundleAssembly {
|
||||||
|
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
|
||||||
|
return {
|
||||||
|
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
|
||||||
|
retrievalCalls: Array.isArray(input.retrievalCalls) ? input.retrievalCalls : [],
|
||||||
|
retrievalResults
|
||||||
|
}),
|
||||||
|
retrievalStatus: buildRetrievalStatus(retrievalResults)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { AssistantReplyType, AssistantRequirement, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
|
||||||
|
import type { RouteHintSummary } from "../types/normalizer";
|
||||||
|
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
|
||||||
|
import { updateInvestigationState } from "./investigationState";
|
||||||
|
|
||||||
|
export interface BuildAssistantInvestigationStateSnapshotInput {
|
||||||
|
featureEnabled: boolean;
|
||||||
|
previousState: InvestigationStateWithProblemUnits | null | undefined;
|
||||||
|
timestamp: string;
|
||||||
|
questionId: string;
|
||||||
|
userMessage: string;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
followupApplied: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantInvestigationStateSnapshot(
|
||||||
|
input: BuildAssistantInvestigationStateSnapshotInput
|
||||||
|
): InvestigationStateWithProblemUnits | null {
|
||||||
|
if (!input.featureEnabled || !input.previousState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return updateInvestigationState({
|
||||||
|
previous: input.previousState,
|
||||||
|
timestamp: input.timestamp,
|
||||||
|
questionId: input.questionId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: input.routeSummary,
|
||||||
|
requirements: input.requirements,
|
||||||
|
coverageReport: input.coverageReport,
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
replyType: input.replyType,
|
||||||
|
followupApplied: input.followupApplied
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersistAssistantInvestigationStateSnapshotInput {
|
||||||
|
featureEnabled: boolean;
|
||||||
|
sessionId: string;
|
||||||
|
snapshot: InvestigationStateWithProblemUnits | null;
|
||||||
|
persist: (sessionId: string, snapshot: InvestigationStateWithProblemUnits) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistAssistantInvestigationStateSnapshot(
|
||||||
|
input: PersistAssistantInvestigationStateSnapshotInput
|
||||||
|
): boolean {
|
||||||
|
if (!input.featureEnabled || !input.snapshot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
input.persist(input.sessionId, input.snapshot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import type { AnswerGroundingCheck, RequirementCoverageReport } from "../types/assistant";
|
||||||
|
|
||||||
|
export interface DeepAnalysisMessageLogDetailsInput {
|
||||||
|
sessionId: string;
|
||||||
|
messageId: string;
|
||||||
|
userMessage: string;
|
||||||
|
normalizerOutput: unknown;
|
||||||
|
executionPlan: Array<Record<string, unknown>>;
|
||||||
|
resolvedExecutionState: unknown;
|
||||||
|
routes: Array<Record<string, unknown>>;
|
||||||
|
retrievalCalls: Array<Record<string, unknown>>;
|
||||||
|
retrievalResultsRaw: unknown[];
|
||||||
|
retrievalResultsNormalized: unknown[];
|
||||||
|
requirementsExtracted: unknown[];
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
groundingCheck: AnswerGroundingCheck;
|
||||||
|
replyType: string;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
questionTypeClass: string;
|
||||||
|
companyAnchors: unknown;
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: boolean;
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
};
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw?: string[];
|
||||||
|
business_scope_resolved?: string[];
|
||||||
|
company_grounding_applied?: boolean;
|
||||||
|
scope_resolution_reason?: string[];
|
||||||
|
};
|
||||||
|
temporalGuard: Record<string, unknown>;
|
||||||
|
polarityAudit: Record<string, unknown>;
|
||||||
|
claimAnchorAudit: Record<string, unknown>;
|
||||||
|
targetedEvidenceAudit: unknown;
|
||||||
|
evidenceAdmissibilityGateAudit: unknown;
|
||||||
|
rbpLiveRouteAudit: unknown | null;
|
||||||
|
faLiveRouteAudit: unknown | null;
|
||||||
|
groundedAnswerEligibilityGuard: Record<string, unknown>;
|
||||||
|
followupStateUsage: unknown | null;
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied?: boolean;
|
||||||
|
problem_units_used_count?: number;
|
||||||
|
problem_answer_mode?: string;
|
||||||
|
problem_unit_ids_used?: string[];
|
||||||
|
fallback_type?: string;
|
||||||
|
};
|
||||||
|
outcomeClassV1: unknown;
|
||||||
|
assistantOrchestrationContractsV1: unknown;
|
||||||
|
answerStructureV11: unknown;
|
||||||
|
investigationStateSnapshot: unknown;
|
||||||
|
assistantReply: string;
|
||||||
|
traceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAnalysisContext(input: DeepAnalysisMessageLogDetailsInput["runtimeAnalysisContext"]): Record<string, unknown> | null {
|
||||||
|
if (!input.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
as_of_date: input.as_of_date,
|
||||||
|
period_from: input.period_from,
|
||||||
|
period_to: input.period_to,
|
||||||
|
source: input.source,
|
||||||
|
snapshot_mode: input.snapshot_mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCoverageStatus(coverageReport: RequirementCoverageReport): "full" | "partial_or_limited" {
|
||||||
|
return coverageReport.requirements_total === coverageReport.requirements_covered &&
|
||||||
|
coverageReport.requirements_uncovered.length === 0 &&
|
||||||
|
coverageReport.requirements_partially_covered.length === 0
|
||||||
|
? "full"
|
||||||
|
: "partial_or_limited";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeepAnalysisProcessedLogDetails(input: DeepAnalysisMessageLogDetailsInput): Record<string, unknown> {
|
||||||
|
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
|
||||||
|
return {
|
||||||
|
session_id: input.sessionId,
|
||||||
|
message_id: input.messageId,
|
||||||
|
user_message: input.userMessage,
|
||||||
|
normalizer_output: input.normalizerOutput,
|
||||||
|
execution_plan: input.executionPlan,
|
||||||
|
resolved_execution_state: input.resolvedExecutionState,
|
||||||
|
routes: input.routes,
|
||||||
|
retrieval_calls: input.retrievalCalls,
|
||||||
|
retrieval_results_raw: input.retrievalResultsRaw,
|
||||||
|
retrieval_results_normalized: input.retrievalResultsNormalized,
|
||||||
|
requirements_extracted: input.requirementsExtracted,
|
||||||
|
requirements_total: input.coverageReport.requirements_total,
|
||||||
|
requirements_covered: input.coverageReport.requirements_covered,
|
||||||
|
requirements_uncovered: input.coverageReport.requirements_uncovered,
|
||||||
|
coverage_status: resolveCoverageStatus(input.coverageReport),
|
||||||
|
answer_grounding_status: input.groundingCheck.status,
|
||||||
|
reply_semantic_type: input.replyType,
|
||||||
|
why_included_summary: input.groundingCheck.why_included_summary,
|
||||||
|
selection_reason_summary: input.groundingCheck.selection_reason_summary,
|
||||||
|
route_subject_match: input.groundingCheck.route_subject_match,
|
||||||
|
clarification_target: input.coverageReport.clarification_needed_for,
|
||||||
|
dropped_intent_segments: input.droppedIntentSegments,
|
||||||
|
question_type_class: input.questionTypeClass,
|
||||||
|
company_anchors: input.companyAnchors,
|
||||||
|
analysis_context_applied: input.runtimeAnalysisContext.active,
|
||||||
|
analysis_context: analysisContext,
|
||||||
|
business_scope_raw: input.businessScopeResolution.business_scope_raw,
|
||||||
|
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
|
||||||
|
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
|
||||||
|
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
|
||||||
|
raw_time_anchor: input.temporalGuard.raw_time_anchor,
|
||||||
|
raw_time_scope: input.temporalGuard.raw_time_scope,
|
||||||
|
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
|
||||||
|
resolved_primary_period: input.temporalGuard.resolved_primary_period,
|
||||||
|
effective_primary_period: input.temporalGuard.effective_primary_period,
|
||||||
|
temporal_guard_input: input.temporalGuard.temporal_guard_input,
|
||||||
|
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
|
||||||
|
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
|
||||||
|
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
|
||||||
|
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
|
||||||
|
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
|
||||||
|
temporal_guard: input.temporalGuard,
|
||||||
|
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
|
||||||
|
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
|
||||||
|
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
|
||||||
|
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
|
||||||
|
domain_polarity_guard: input.polarityAudit,
|
||||||
|
claim_anchor_audit: input.claimAnchorAudit,
|
||||||
|
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
|
||||||
|
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||||
|
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||||
|
targeted_evidence_acquisition: input.targetedEvidenceAudit,
|
||||||
|
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
|
||||||
|
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
|
||||||
|
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
|
||||||
|
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||||
|
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
|
||||||
|
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
|
||||||
|
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
|
||||||
|
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
|
||||||
|
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
|
||||||
|
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
|
||||||
|
? {
|
||||||
|
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
assistant_outcome_class_v1: input.outcomeClassV1,
|
||||||
|
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
|
||||||
|
answer_structure_v11: input.answerStructureV11,
|
||||||
|
investigation_state_snapshot: input.investigationStateSnapshot,
|
||||||
|
fallback_type: input.compositionDebug.fallback_type,
|
||||||
|
assistant_reply: input.assistantReply,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
trace_id: input.traceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
import type {
|
||||||
|
AssistantReplyType,
|
||||||
|
AssistantRequirement,
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
|
||||||
|
export type AssistantOutcomeClassV1 =
|
||||||
|
| "FULLY_ANSWERED"
|
||||||
|
| "PARTIALLY_ANSWERED"
|
||||||
|
| "BLOCKED_BY_AMBIGUITY"
|
||||||
|
| "BLOCKED_BY_MISSING_DATA"
|
||||||
|
| "BLOCKED_BY_TOOLING"
|
||||||
|
| "MISROUTED"
|
||||||
|
| "FAILED_TO_BIND_ENTITIES";
|
||||||
|
|
||||||
|
export interface AssistantAnalysisContextContractV1 {
|
||||||
|
as_of_date: string | null;
|
||||||
|
period_from: string | null;
|
||||||
|
period_to: string | null;
|
||||||
|
source: string | null;
|
||||||
|
snapshot_mode: "auto" | "force_snapshot" | "force_live";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantQueryFrameContractV1 {
|
||||||
|
schema_version: "assistant_query_frame_v1";
|
||||||
|
original_user_question: string;
|
||||||
|
normalized_question: string;
|
||||||
|
route_summary_mode: RouteHintSummary["mode"] | "none";
|
||||||
|
fragments_total: number;
|
||||||
|
dropped_intent_segments: string[];
|
||||||
|
analysis_context: AssistantAnalysisContextContractV1 | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantExecutionPlanStepContractV1 {
|
||||||
|
fragment_id: string;
|
||||||
|
route: string;
|
||||||
|
should_execute: boolean;
|
||||||
|
requirement_ids: string[];
|
||||||
|
no_route_reason: string | null;
|
||||||
|
clarification_reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantExecutionPlanContractV1 {
|
||||||
|
schema_version: "assistant_execution_plan_v1";
|
||||||
|
steps: AssistantExecutionPlanStepContractV1[];
|
||||||
|
requirements_total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantEvidenceBundleContractV1 {
|
||||||
|
schema_version: "assistant_evidence_bundle_v1";
|
||||||
|
retrieval_calls_total: number;
|
||||||
|
retrieval_results_total: number;
|
||||||
|
retrieval_status_breakdown: {
|
||||||
|
ok: number;
|
||||||
|
partial: number;
|
||||||
|
empty: number;
|
||||||
|
error: number;
|
||||||
|
};
|
||||||
|
evidence_total: number;
|
||||||
|
source_refs_total: number;
|
||||||
|
limitation_total: number;
|
||||||
|
error_total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantCoverageContractV1 {
|
||||||
|
schema_version: "assistant_coverage_contract_v1";
|
||||||
|
coverage_report: RequirementCoverageReport;
|
||||||
|
grounding: AnswerGroundingCheck;
|
||||||
|
outcome_class: AssistantOutcomeClassV1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSnapshotMode(value: unknown): "auto" | "force_snapshot" | "force_live" {
|
||||||
|
const token = String(value ?? "").trim();
|
||||||
|
if (token === "force_snapshot" || token === "force_live") {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFragmentsTotal(normalized: NormalizedPayload | null | undefined): number {
|
||||||
|
if (!normalized || typeof normalized !== "object") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const source = normalized as unknown as { fragments?: unknown };
|
||||||
|
const fragments = source.fragments;
|
||||||
|
return Array.isArray(fragments) ? fragments.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectEvidenceTotals(retrievalResults: UnifiedRetrievalResult[]): {
|
||||||
|
evidence_total: number;
|
||||||
|
source_refs_total: number;
|
||||||
|
limitation_total: number;
|
||||||
|
error_total: number;
|
||||||
|
} {
|
||||||
|
let evidenceTotal = 0;
|
||||||
|
const sourceRefs = new Set<string>();
|
||||||
|
let limitationTotal = 0;
|
||||||
|
let errorTotal = 0;
|
||||||
|
|
||||||
|
for (const result of retrievalResults) {
|
||||||
|
evidenceTotal += Array.isArray(result.evidence) ? result.evidence.length : 0;
|
||||||
|
limitationTotal += Array.isArray(result.limitations) ? result.limitations.length : 0;
|
||||||
|
errorTotal += Array.isArray(result.errors) ? result.errors.length : 0;
|
||||||
|
for (const evidence of result.evidence ?? []) {
|
||||||
|
const ref = String(evidence?.source_ref?.canonical_ref ?? "").trim();
|
||||||
|
if (ref) {
|
||||||
|
sourceRefs.add(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
evidence_total: evidenceTotal,
|
||||||
|
source_refs_total: sourceRefs.size,
|
||||||
|
limitation_total: limitationTotal,
|
||||||
|
error_total: errorTotal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function classifyAssistantOutcomeClassV1(input: {
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
grounding: AnswerGroundingCheck;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
}): AssistantOutcomeClassV1 {
|
||||||
|
const replyType = input.replyType;
|
||||||
|
const grounding = input.grounding;
|
||||||
|
const coverage = input.coverageReport;
|
||||||
|
const hasOnlyErrors =
|
||||||
|
input.retrievalResults.length > 0 &&
|
||||||
|
input.retrievalResults.every((item) => item.status === "error");
|
||||||
|
|
||||||
|
if (replyType === "backend_error" || hasOnlyErrors) {
|
||||||
|
return "BLOCKED_BY_TOOLING";
|
||||||
|
}
|
||||||
|
if (grounding.status === "route_mismatch_blocked" || replyType === "route_mismatch_blocked") {
|
||||||
|
return "MISROUTED";
|
||||||
|
}
|
||||||
|
if (replyType === "clarification_required" || coverage.clarification_needed_for.length > 0) {
|
||||||
|
return "BLOCKED_BY_AMBIGUITY";
|
||||||
|
}
|
||||||
|
if (replyType === "out_of_scope") {
|
||||||
|
return "BLOCKED_BY_AMBIGUITY";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullCoverage =
|
||||||
|
coverage.requirements_total > 0 &&
|
||||||
|
coverage.requirements_total === coverage.requirements_covered &&
|
||||||
|
coverage.requirements_uncovered.length === 0 &&
|
||||||
|
coverage.requirements_partially_covered.length === 0 &&
|
||||||
|
coverage.clarification_needed_for.length === 0 &&
|
||||||
|
coverage.out_of_scope_requirements.length === 0;
|
||||||
|
if (fullCoverage && grounding.status === "grounded") {
|
||||||
|
return "FULLY_ANSWERED";
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAnyCoverage =
|
||||||
|
coverage.requirements_covered > 0 ||
|
||||||
|
coverage.requirements_partially_covered.length > 0 ||
|
||||||
|
grounding.status === "partial";
|
||||||
|
if (hasAnyCoverage) {
|
||||||
|
return "PARTIALLY_ANSWERED";
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingRequirementSignal =
|
||||||
|
grounding.missing_requirements.length > 0 ||
|
||||||
|
coverage.requirements_uncovered.length > 0 ||
|
||||||
|
coverage.requirements_total > 0;
|
||||||
|
const possibleBindingFailure =
|
||||||
|
replyType === "no_grounded_answer" &&
|
||||||
|
missingRequirementSignal &&
|
||||||
|
grounding.route_subject_match;
|
||||||
|
if (possibleBindingFailure) {
|
||||||
|
return "FAILED_TO_BIND_ENTITIES";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "BLOCKED_BY_MISSING_DATA";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantQueryFrameContractV1(input: {
|
||||||
|
userMessage: string;
|
||||||
|
normalizedQuestion: string;
|
||||||
|
normalized: NormalizedPayload | null | undefined;
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
droppedIntentSegments: string[];
|
||||||
|
analysisContext?: {
|
||||||
|
as_of_date?: string | null;
|
||||||
|
period_from?: string | null;
|
||||||
|
period_to?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
snapshot_mode?: string | null;
|
||||||
|
} | null;
|
||||||
|
}): AssistantQueryFrameContractV1 {
|
||||||
|
const analysis = input.analysisContext
|
||||||
|
? {
|
||||||
|
as_of_date: input.analysisContext.as_of_date ?? null,
|
||||||
|
period_from: input.analysisContext.period_from ?? null,
|
||||||
|
period_to: input.analysisContext.period_to ?? null,
|
||||||
|
source: input.analysisContext.source ?? null,
|
||||||
|
snapshot_mode: normalizeSnapshotMode(input.analysisContext.snapshot_mode)
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_query_frame_v1",
|
||||||
|
original_user_question: String(input.userMessage ?? ""),
|
||||||
|
normalized_question: String(input.normalizedQuestion ?? ""),
|
||||||
|
route_summary_mode: input.routeSummary?.mode ?? "none",
|
||||||
|
fragments_total: extractFragmentsTotal(input.normalized),
|
||||||
|
dropped_intent_segments: Array.isArray(input.droppedIntentSegments) ? [...input.droppedIntentSegments] : [],
|
||||||
|
analysis_context: analysis
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantExecutionPlanContractV1(input: {
|
||||||
|
executionPlan: Array<{
|
||||||
|
fragment_id: string;
|
||||||
|
requirement_ids: string[];
|
||||||
|
route: string;
|
||||||
|
should_execute: boolean;
|
||||||
|
no_route_reason?: string | null;
|
||||||
|
clarification_reason?: string | null;
|
||||||
|
}>;
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
}): AssistantExecutionPlanContractV1 {
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_execution_plan_v1",
|
||||||
|
steps: (Array.isArray(input.executionPlan) ? input.executionPlan : []).map((item) => ({
|
||||||
|
fragment_id: String(item.fragment_id ?? ""),
|
||||||
|
route: String(item.route ?? ""),
|
||||||
|
should_execute: Boolean(item.should_execute),
|
||||||
|
requirement_ids: Array.isArray(item.requirement_ids) ? [...item.requirement_ids] : [],
|
||||||
|
no_route_reason: item.no_route_reason ?? null,
|
||||||
|
clarification_reason: item.clarification_reason ?? null
|
||||||
|
})),
|
||||||
|
requirements_total: Array.isArray(input.requirements) ? input.requirements.length : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantEvidenceBundleContractV1(input: {
|
||||||
|
retrievalCalls: Array<Record<string, unknown>>;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
}): AssistantEvidenceBundleContractV1 {
|
||||||
|
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
|
||||||
|
const breakdown = {
|
||||||
|
ok: retrievalResults.filter((item) => item.status === "ok").length,
|
||||||
|
partial: retrievalResults.filter((item) => item.status === "partial").length,
|
||||||
|
empty: retrievalResults.filter((item) => item.status === "empty").length,
|
||||||
|
error: retrievalResults.filter((item) => item.status === "error").length
|
||||||
|
};
|
||||||
|
const totals = collectEvidenceTotals(retrievalResults);
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_evidence_bundle_v1",
|
||||||
|
retrieval_calls_total: Array.isArray(input.retrievalCalls) ? input.retrievalCalls.length : 0,
|
||||||
|
retrieval_results_total: retrievalResults.length,
|
||||||
|
retrieval_status_breakdown: breakdown,
|
||||||
|
...totals
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantCoverageContractV1(input: {
|
||||||
|
coverageReport: RequirementCoverageReport;
|
||||||
|
grounding: AnswerGroundingCheck;
|
||||||
|
outcomeClass: AssistantOutcomeClassV1;
|
||||||
|
}): AssistantCoverageContractV1 {
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_coverage_contract_v1",
|
||||||
|
coverage_report: input.coverageReport,
|
||||||
|
grounding: input.grounding,
|
||||||
|
outcome_class: input.outcomeClass
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type {
|
||||||
|
AnswerGroundingCheck,
|
||||||
|
AssistantRequirement,
|
||||||
|
RequirementCoverageReport,
|
||||||
|
UnifiedRetrievalResult
|
||||||
|
} from "../types/assistant";
|
||||||
|
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
|
||||||
|
|
||||||
|
export interface AssistantRequirementExtractionResult {
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
byFragment: Map<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantCoverageEvaluationResult {
|
||||||
|
requirements: AssistantRequirement[];
|
||||||
|
coverage: RequirementCoverageReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantCoverageGroundingPipelineInput {
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
normalized: NormalizedPayload | null | undefined;
|
||||||
|
userMessage: string;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
requirementExtraction?: AssistantRequirementExtractionResult;
|
||||||
|
extractRequirements: (
|
||||||
|
routeSummary: RouteHintSummary | null,
|
||||||
|
normalized: NormalizedPayload | null | undefined,
|
||||||
|
userMessage: string
|
||||||
|
) => AssistantRequirementExtractionResult;
|
||||||
|
evaluateCoverage: (
|
||||||
|
requirements: AssistantRequirement[],
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
) => AssistantCoverageEvaluationResult;
|
||||||
|
checkGrounding: (
|
||||||
|
userMessage: string,
|
||||||
|
requirements: AssistantRequirement[],
|
||||||
|
coverage: RequirementCoverageReport,
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
) => AnswerGroundingCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantCoverageGroundingPipelineOutput {
|
||||||
|
requirementExtraction: AssistantRequirementExtractionResult;
|
||||||
|
coverageEvaluation: AssistantCoverageEvaluationResult;
|
||||||
|
groundingCheckBase: AnswerGroundingCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runAssistantCoverageGroundingPipeline(
|
||||||
|
input: AssistantCoverageGroundingPipelineInput
|
||||||
|
): AssistantCoverageGroundingPipelineOutput {
|
||||||
|
const requirementExtraction =
|
||||||
|
input.requirementExtraction ?? input.extractRequirements(input.routeSummary, input.normalized, input.userMessage);
|
||||||
|
const coverageEvaluation = input.evaluateCoverage(requirementExtraction.requirements, input.retrievalResults);
|
||||||
|
const groundingCheckBase = input.checkGrounding(
|
||||||
|
input.userMessage,
|
||||||
|
coverageEvaluation.requirements,
|
||||||
|
coverageEvaluation.coverage,
|
||||||
|
input.retrievalResults
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requirementExtraction,
|
||||||
|
coverageEvaluation,
|
||||||
|
groundingCheckBase
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import type { RouteHintSummary } from "../types/normalizer";
|
||||||
|
|
||||||
|
interface FragmentLike {
|
||||||
|
fragment_id?: string;
|
||||||
|
raw_fragment_text?: string;
|
||||||
|
normalized_fragment_text?: string;
|
||||||
|
account_hints?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantExecutionPlanItem {
|
||||||
|
fragment_id: string;
|
||||||
|
requirement_ids: string[];
|
||||||
|
route: string;
|
||||||
|
should_execute: boolean;
|
||||||
|
fragment_text: string;
|
||||||
|
no_route_reason: string | null;
|
||||||
|
clarification_reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(value: string): string {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichFragmentTextWithHints(fragment: FragmentLike, text: string): string {
|
||||||
|
const baseText = String(text ?? "").trim();
|
||||||
|
const accountHints = Array.isArray(fragment.account_hints)
|
||||||
|
? Array.from(new Set(fragment.account_hints.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0)))
|
||||||
|
: [];
|
||||||
|
if (accountHints.length === 0) {
|
||||||
|
return baseText;
|
||||||
|
}
|
||||||
|
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
|
||||||
|
if (hasAccountInText) {
|
||||||
|
return baseText;
|
||||||
|
}
|
||||||
|
return `${baseText}, по счету ${accountHints.join(", ")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFragmentTextById(fragments: FragmentLike[]): Map<string, string> {
|
||||||
|
const result = new Map<string, string>();
|
||||||
|
for (const item of fragments) {
|
||||||
|
if (!item || typeof item !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fragment = item as FragmentLike;
|
||||||
|
const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : "";
|
||||||
|
if (!fragmentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const text =
|
||||||
|
(typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
|
||||||
|
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
|
||||||
|
"";
|
||||||
|
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildExecutionPlanFromRoute(input: {
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
userMessage: string;
|
||||||
|
fragmentTextById: Map<string, string>;
|
||||||
|
requirementByFragment: Map<string, string[]>;
|
||||||
|
}): AssistantExecutionPlanItem[] {
|
||||||
|
if (!input.routeSummary) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (input.routeSummary.mode === "legacy_v1") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: input.requirementByFragment.get("F1") ?? ["R1"],
|
||||||
|
route: input.routeSummary.route_hint,
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: input.userMessage,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return input.routeSummary.decisions.map((decision) => {
|
||||||
|
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
|
||||||
|
if (decision.route === "no_route") {
|
||||||
|
return {
|
||||||
|
fragment_id: decision.fragment_id,
|
||||||
|
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
|
||||||
|
route: "no_route",
|
||||||
|
should_execute: false,
|
||||||
|
fragment_text: text,
|
||||||
|
no_route_reason: decision.no_route_reason ?? null,
|
||||||
|
clarification_reason: decision.clarification_reason ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fragment_id: decision.fragment_id,
|
||||||
|
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
|
||||||
|
route: decision.route,
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: text,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: decision.clarification_reason ?? null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDebugRoutesFromRoute(input: {
|
||||||
|
routeSummary: RouteHintSummary | null;
|
||||||
|
resolveLegacyRouteReason: (route: string) => string;
|
||||||
|
}): Array<Record<string, unknown>> {
|
||||||
|
if (!input.routeSummary) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (input.routeSummary.mode === "legacy_v1") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: input.routeSummary.route_hint,
|
||||||
|
reason: input.resolveLegacyRouteReason(input.routeSummary.route_hint),
|
||||||
|
confidence: input.routeSummary.confidence,
|
||||||
|
intent_class: input.routeSummary.intent_class
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return input.routeSummary.decisions.map((decision) => ({
|
||||||
|
fragment_id: decision.fragment_id,
|
||||||
|
route: decision.route,
|
||||||
|
reason: decision.reason,
|
||||||
|
route_status: decision.route_status ?? null,
|
||||||
|
no_route_reason: decision.no_route_reason ?? null,
|
||||||
|
clarification_reason: decision.clarification_reason ?? null,
|
||||||
|
execution_readiness: decision.execution_readiness ?? null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
@ -683,11 +683,97 @@ function toTemporalGuardInput(window: TemporalWindow | null, fallback: string |
|
||||||
return value || null;
|
return value || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeIsoDate(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTemporalWindow(input: {
|
||||||
|
asOfDate?: unknown;
|
||||||
|
periodFrom?: unknown;
|
||||||
|
periodTo?: unknown;
|
||||||
|
}): TemporalWindow | null {
|
||||||
|
const asOfDate = normalizeIsoDate(input.asOfDate);
|
||||||
|
if (asOfDate) {
|
||||||
|
return {
|
||||||
|
from: asOfDate,
|
||||||
|
to: asOfDate,
|
||||||
|
granularity: "day"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const from = normalizeIsoDate(input.periodFrom);
|
||||||
|
const to = normalizeIsoDate(input.periodTo);
|
||||||
|
if (!from || !to) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
granularity: from === to ? "day" : "month"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveTemporalGuard(input: {
|
export function resolveTemporalGuard(input: {
|
||||||
userMessage: string;
|
userMessage: string;
|
||||||
normalized: NormalizedPayload | null | undefined;
|
normalized: NormalizedPayload | null | undefined;
|
||||||
companyAnchors?: CompanyAnchorSet | null;
|
companyAnchors?: CompanyAnchorSet | null;
|
||||||
|
analysisContext?: {
|
||||||
|
as_of_date?: string | null;
|
||||||
|
period_from?: string | null;
|
||||||
|
period_to?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
} | null;
|
||||||
}): TemporalGuardAudit {
|
}): TemporalGuardAudit {
|
||||||
|
const analysisWindow = normalizeTemporalWindow({
|
||||||
|
asOfDate: input.analysisContext?.as_of_date,
|
||||||
|
periodFrom: input.analysisContext?.period_from,
|
||||||
|
periodTo: input.analysisContext?.period_to
|
||||||
|
});
|
||||||
|
if (analysisWindow) {
|
||||||
|
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
|
||||||
|
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
|
||||||
|
return {
|
||||||
|
raw_time_anchor: analysisWindow.from,
|
||||||
|
raw_time_scope: guardInput,
|
||||||
|
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
|
||||||
|
resolved_primary_period: analysisWindow,
|
||||||
|
effective_primary_period: analysisWindow,
|
||||||
|
temporal_guard_input: guardInput,
|
||||||
|
temporal_alignment_status: "aligned",
|
||||||
|
temporal_resolution_source: source,
|
||||||
|
temporal_guard_basis: "raw_time_scope_unlocked",
|
||||||
|
temporal_guard_applied: false,
|
||||||
|
temporal_guard_outcome: "passed",
|
||||||
|
primary_period_window: null,
|
||||||
|
allowed_context_window: null,
|
||||||
|
controlled_temporal_expansion_enabled: false,
|
||||||
|
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
|
||||||
|
normalized_anchor_drift_detected: false,
|
||||||
|
reason_codes: ["analysis_context_applied"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
|
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
|
||||||
const julyAnchor = resolveJulyAnchor(rawAnchorText);
|
const julyAnchor = resolveJulyAnchor(rawAnchorText);
|
||||||
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
|
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
|
||||||
|
|
@ -762,10 +848,15 @@ export function applyTemporalHintToExecutionPlan<
|
||||||
return executionPlan;
|
return executionPlan;
|
||||||
}
|
}
|
||||||
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
|
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
|
||||||
|
const periodLabel = primaryWindow
|
||||||
|
? `${primaryWindow.from}..${primaryWindow.to}`
|
||||||
|
: temporal.resolved_time_anchor
|
||||||
|
? temporal.resolved_time_anchor
|
||||||
|
: "active_period";
|
||||||
const hint =
|
const hint =
|
||||||
primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
|
primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
|
||||||
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
|
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
|
||||||
: `primary period July 2020 (${primaryWindow?.from ?? JULY_WINDOW.from}..${primaryWindow?.to ?? JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
|
: `primary period ${periodLabel}; controlled temporal expansion only for linked entities`;
|
||||||
return executionPlan.map((item) => {
|
return executionPlan.map((item) => {
|
||||||
if (!item.should_execute) {
|
if (!item.should_execute) {
|
||||||
return item;
|
return item;
|
||||||
|
|
@ -1590,15 +1681,15 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
|
||||||
? "no_grounded_answer"
|
? "no_grounded_answer"
|
||||||
: "partial";
|
: "partial";
|
||||||
const reasonMap: Record<string, string> = {
|
const reasonMap: Record<string, string> = {
|
||||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
|
||||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
|
||||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
|
||||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
|
||||||
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
|
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
|
||||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
|
||||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
|
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
|
||||||
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
|
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
|
||||||
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
|
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
|
||||||
};
|
};
|
||||||
const reasons = [
|
const reasons = [
|
||||||
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,49 @@
|
||||||
|
import type { AssistantConversationItem, AssistantSessionState } from "../types/assistant";
|
||||||
|
|
||||||
|
export interface CommitAssistantTurnAndLogInput {
|
||||||
|
sessionId: string;
|
||||||
|
assistantItem: AssistantConversationItem;
|
||||||
|
eventType: string;
|
||||||
|
logDetails: Record<string, unknown>;
|
||||||
|
appendItem: (sessionId: string, item: AssistantConversationItem) => void;
|
||||||
|
getSession: (sessionId: string) => AssistantSessionState | null;
|
||||||
|
persistSession: (session: AssistantSessionState) => void;
|
||||||
|
cloneConversation: (items: AssistantConversationItem[]) => AssistantConversationItem[];
|
||||||
|
logEvent: (payload: {
|
||||||
|
timestamp: string;
|
||||||
|
level: "info";
|
||||||
|
service: "assistant_loop";
|
||||||
|
message: "assistant_message_processed";
|
||||||
|
sessionId: string;
|
||||||
|
eventType: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
}) => void;
|
||||||
|
nowIso?: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommitAssistantTurnAndLogOutput {
|
||||||
|
currentSession: AssistantSessionState | null;
|
||||||
|
conversation: AssistantConversationItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commitAssistantTurnAndLog(input: CommitAssistantTurnAndLogInput): CommitAssistantTurnAndLogOutput {
|
||||||
|
input.appendItem(input.sessionId, input.assistantItem);
|
||||||
|
const currentSession = input.getSession(input.sessionId);
|
||||||
|
if (currentSession) {
|
||||||
|
input.persistSession(currentSession);
|
||||||
|
}
|
||||||
|
const conversation = input.cloneConversation(currentSession?.items ?? []);
|
||||||
|
input.logEvent({
|
||||||
|
timestamp: (input.nowIso ?? (() => new Date().toISOString()))(),
|
||||||
|
level: "info",
|
||||||
|
service: "assistant_loop",
|
||||||
|
message: "assistant_message_processed",
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
eventType: input.eventType,
|
||||||
|
details: input.logDetails
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
currentSession,
|
||||||
|
conversation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -264,6 +264,32 @@ function parseRawQuestions(rawQuestions: string): string[] {
|
||||||
return byLine.length > 0 ? byLine : [text];
|
return byLine.length > 0 ? byLine : [text];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAnalysisDate(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
candidate.getUTCFullYear() !== year ||
|
||||||
|
candidate.getUTCMonth() + 1 !== month ||
|
||||||
|
candidate.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
type V2FamilyFragment =
|
type V2FamilyFragment =
|
||||||
| NormalizedQueryV2["fragments"][number]
|
| NormalizedQueryV2["fragments"][number]
|
||||||
| NormalizedQueryV2_0_1["fragments"][number]
|
| NormalizedQueryV2_0_1["fragments"][number]
|
||||||
|
|
@ -936,6 +962,7 @@ export class EvalService {
|
||||||
mode: EvalRunMode;
|
mode: EvalRunMode;
|
||||||
caseSetFile?: string;
|
caseSetFile?: string;
|
||||||
rawQuestions?: string;
|
rawQuestions?: string;
|
||||||
|
analysisDate?: string;
|
||||||
cases: EvalInputCase[];
|
cases: EvalInputCase[];
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
const runId = `eval-${nanoid(10)}`;
|
const runId = `eval-${nanoid(10)}`;
|
||||||
|
|
@ -976,6 +1003,13 @@ export class EvalService {
|
||||||
...payload.normalizeConfig,
|
...payload.normalizeConfig,
|
||||||
userQuestion: item.raw_question,
|
userQuestion: item.raw_question,
|
||||||
context: {
|
context: {
|
||||||
|
period_hint: payload.analysisDate ?? undefined,
|
||||||
|
analysis_context: payload.analysisDate
|
||||||
|
? {
|
||||||
|
as_of_date: payload.analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
eval_label: runId,
|
eval_label: runId,
|
||||||
case_id: item.case_id,
|
case_id: item.case_id,
|
||||||
eval_mode: payload.mode
|
eval_mode: payload.mode
|
||||||
|
|
@ -1876,6 +1910,7 @@ export class EvalService {
|
||||||
mode: EvalRunMode;
|
mode: EvalRunMode;
|
||||||
caseSetFile?: string;
|
caseSetFile?: string;
|
||||||
compareWithReportFile?: string;
|
compareWithReportFile?: string;
|
||||||
|
analysisDate?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
if (!FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1) {
|
if (!FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1) {
|
||||||
|
|
@ -1889,6 +1924,7 @@ export class EvalService {
|
||||||
const suite = parseAssistantSuiteFile(payload.caseSetFile);
|
const suite = parseAssistantSuiteFile(payload.caseSetFile);
|
||||||
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
||||||
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${nanoid(10)}`;
|
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${nanoid(10)}`;
|
||||||
|
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
|
||||||
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
|
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
|
||||||
const diagnostics: AssistantCaseDiagnostics[] = [];
|
const diagnostics: AssistantCaseDiagnostics[] = [];
|
||||||
let requestsTotal = 0;
|
let requestsTotal = 0;
|
||||||
|
|
@ -1917,6 +1953,15 @@ export class EvalService {
|
||||||
developerPrompt: payload.normalizeConfig.developerPrompt,
|
developerPrompt: payload.normalizeConfig.developerPrompt,
|
||||||
domainPrompt: payload.normalizeConfig.domainPrompt,
|
domainPrompt: payload.normalizeConfig.domainPrompt,
|
||||||
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
||||||
|
context: analysisDate
|
||||||
|
? {
|
||||||
|
period_hint: analysisDate,
|
||||||
|
analysis_context: {
|
||||||
|
as_of_date: analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
useMock: payload.useMock
|
useMock: payload.useMock
|
||||||
})) as AssistantMessageResponsePayload;
|
})) as AssistantMessageResponsePayload;
|
||||||
turnResponses.push(response);
|
turnResponses.push(response);
|
||||||
|
|
@ -2153,6 +2198,7 @@ export class EvalService {
|
||||||
eval_target: "assistant_stage1",
|
eval_target: "assistant_stage1",
|
||||||
mode: payload.mode,
|
mode: payload.mode,
|
||||||
use_mock: Boolean(payload.useMock),
|
use_mock: Boolean(payload.useMock),
|
||||||
|
analysis_date: analysisDate,
|
||||||
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
||||||
suite_id: suite.suite_id,
|
suite_id: suite.suite_id,
|
||||||
suite_version: suite.suite_version,
|
suite_version: suite.suite_version,
|
||||||
|
|
@ -2225,6 +2271,7 @@ export class EvalService {
|
||||||
mode: EvalRunMode;
|
mode: EvalRunMode;
|
||||||
caseSetFile?: string;
|
caseSetFile?: string;
|
||||||
compareWithReportFile?: string;
|
compareWithReportFile?: string;
|
||||||
|
analysisDate?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
if (!FEATURE_ASSISTANT_STAGE2_EVAL_V1) {
|
if (!FEATURE_ASSISTANT_STAGE2_EVAL_V1) {
|
||||||
|
|
@ -2238,6 +2285,7 @@ export class EvalService {
|
||||||
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
|
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
|
||||||
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
|
||||||
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${nanoid(10)}`;
|
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${nanoid(10)}`;
|
||||||
|
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
|
||||||
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
|
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
|
||||||
const diagnostics: AssistantStage2CaseDiagnostics[] = [];
|
const diagnostics: AssistantStage2CaseDiagnostics[] = [];
|
||||||
let requestsTotal = 0;
|
let requestsTotal = 0;
|
||||||
|
|
@ -2269,6 +2317,15 @@ export class EvalService {
|
||||||
developerPrompt: payload.normalizeConfig.developerPrompt,
|
developerPrompt: payload.normalizeConfig.developerPrompt,
|
||||||
domainPrompt: payload.normalizeConfig.domainPrompt,
|
domainPrompt: payload.normalizeConfig.domainPrompt,
|
||||||
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
||||||
|
context: analysisDate
|
||||||
|
? {
|
||||||
|
period_hint: analysisDate,
|
||||||
|
analysis_context: {
|
||||||
|
as_of_date: analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
useMock: payload.useMock
|
useMock: payload.useMock
|
||||||
})) as AssistantMessageResponsePayload;
|
})) as AssistantMessageResponsePayload;
|
||||||
turnResponses.push(response);
|
turnResponses.push(response);
|
||||||
|
|
@ -2446,6 +2503,7 @@ export class EvalService {
|
||||||
eval_target: "assistant_stage2",
|
eval_target: "assistant_stage2",
|
||||||
mode: payload.mode,
|
mode: payload.mode,
|
||||||
use_mock: Boolean(payload.useMock),
|
use_mock: Boolean(payload.useMock),
|
||||||
|
analysis_date: analysisDate,
|
||||||
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
||||||
suite_id: suite.suite_id,
|
suite_id: suite.suite_id,
|
||||||
suite_version: suite.suite_version,
|
suite_version: suite.suite_version,
|
||||||
|
|
@ -2552,10 +2610,12 @@ export class EvalService {
|
||||||
rawQuestions?: string;
|
rawQuestions?: string;
|
||||||
evalTarget?: EvalTarget;
|
evalTarget?: EvalTarget;
|
||||||
compareWithReportFile?: string;
|
compareWithReportFile?: string;
|
||||||
|
analysisDate?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
const mode = payload.mode ?? "standard";
|
const mode = payload.mode ?? "standard";
|
||||||
const evalTarget = payload.evalTarget ?? "normalizer";
|
const evalTarget = payload.evalTarget ?? "normalizer";
|
||||||
|
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
|
||||||
|
|
||||||
if (evalTarget === "assistant_stage1") {
|
if (evalTarget === "assistant_stage1") {
|
||||||
return this.runAssistantStage1({
|
return this.runAssistantStage1({
|
||||||
|
|
@ -2565,6 +2625,7 @@ export class EvalService {
|
||||||
mode,
|
mode,
|
||||||
caseSetFile: payload.caseSetFile,
|
caseSetFile: payload.caseSetFile,
|
||||||
compareWithReportFile: payload.compareWithReportFile,
|
compareWithReportFile: payload.compareWithReportFile,
|
||||||
|
analysisDate: analysisDate ?? undefined,
|
||||||
runId: payload.runId
|
runId: payload.runId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2577,6 +2638,7 @@ export class EvalService {
|
||||||
mode,
|
mode,
|
||||||
caseSetFile: payload.caseSetFile,
|
caseSetFile: payload.caseSetFile,
|
||||||
compareWithReportFile: payload.compareWithReportFile,
|
compareWithReportFile: payload.compareWithReportFile,
|
||||||
|
analysisDate: analysisDate ?? undefined,
|
||||||
runId: payload.runId
|
runId: payload.runId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2622,6 +2684,7 @@ export class EvalService {
|
||||||
return this.runV2({
|
return this.runV2({
|
||||||
...payload,
|
...payload,
|
||||||
mode,
|
mode,
|
||||||
|
analysisDate: analysisDate ?? undefined,
|
||||||
cases: filtered
|
cases: filtered
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2651,6 +2714,13 @@ export class EvalService {
|
||||||
...payload.normalizeConfig,
|
...payload.normalizeConfig,
|
||||||
userQuestion: item.raw_question,
|
userQuestion: item.raw_question,
|
||||||
context: {
|
context: {
|
||||||
|
period_hint: analysisDate ?? undefined,
|
||||||
|
analysis_context: analysisDate
|
||||||
|
? {
|
||||||
|
as_of_date: analysisDate,
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
expected_route: item.expected.route_hint as NormalizeRequestPayload["context"] extends infer C
|
expected_route: item.expected.route_hint as NormalizeRequestPayload["context"] extends infer C
|
||||||
? C extends { expected_route?: infer R }
|
? C extends { expected_route?: infer R }
|
||||||
? R
|
? R
|
||||||
|
|
@ -2779,6 +2849,7 @@ export class EvalService {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
mode,
|
mode,
|
||||||
use_mock: Boolean(payload.useMock),
|
use_mock: Boolean(payload.useMock),
|
||||||
|
analysis_date: analysisDate,
|
||||||
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
prompt_version: payload.normalizeConfig.promptVersion ?? null,
|
||||||
dataset: {
|
dataset: {
|
||||||
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",
|
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ export function resolveQuestionType(input: string): QuestionTypeClass {
|
||||||
return bestType;
|
return bestType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/[?пјџ]/u.test(text)) {
|
if (/(?:\bwhy\b|почему|из-?за\s+чего|в\s+ч(?:е|ё)м\s+причина)/iu.test(text)) {
|
||||||
return "why_breaks";
|
return "why_breaks";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,14 @@ export interface RouteHintSummaryV2 {
|
||||||
export type RouteHintSummary = RouteHintSummaryV1 | RouteHintSummaryV2;
|
export type RouteHintSummary = RouteHintSummaryV1 | RouteHintSummaryV2;
|
||||||
export type NormalizedPayload = NormalizedQueryV1 | NormalizedQueryV2 | NormalizedQueryV2_0_1 | NormalizedQueryV2_0_2;
|
export type NormalizedPayload = NormalizedQueryV1 | NormalizedQueryV2 | NormalizedQueryV2_0_1 | NormalizedQueryV2_0_2;
|
||||||
|
|
||||||
|
export interface AnalysisContextV1 {
|
||||||
|
as_of_date?: string;
|
||||||
|
period_from?: string;
|
||||||
|
period_to?: string;
|
||||||
|
snapshot_mode?: "auto" | "force_snapshot" | "force_live";
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NormalizeRequestPayload {
|
export interface NormalizeRequestPayload {
|
||||||
llmProvider?: LlmProvider;
|
llmProvider?: LlmProvider;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
|
@ -250,6 +258,7 @@ export interface NormalizeRequestPayload {
|
||||||
userQuestion: string;
|
userQuestion: string;
|
||||||
context?: {
|
context?: {
|
||||||
period_hint?: string;
|
period_hint?: string;
|
||||||
|
analysis_context?: AnalysisContextV1;
|
||||||
business_context?: string;
|
business_context?: string;
|
||||||
expected_route?: RouteHint;
|
expected_route?: RouteHint;
|
||||||
eval_label?: string;
|
eval_label?: string;
|
||||||
|
|
|
||||||
|
|
@ -1654,6 +1654,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves major-share revenue wording into customer revenue intent", () => {
|
||||||
|
const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?");
|
||||||
|
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves customer revenue intent from highest inflow slang wording", () => {
|
it("resolves customer revenue intent from highest inflow slang wording", () => {
|
||||||
const result = resolveAddressIntent("какие приходы самые высокие за все время");
|
const result = resolveAddressIntent("какие приходы самые высокие за все время");
|
||||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||||
|
|
@ -1725,6 +1730,74 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
const result = resolveAddressIntent("покажи документы по этому же договору");
|
const result = resolveAddressIntent("покажи документы по этому же договору");
|
||||||
expect(result.intent).toBe("list_documents_by_contract");
|
expect(result.intent).toBe("list_documents_by_contract");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes supplier tail-risk wording into payables intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_payables_counterparties");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps out-of-scope supplier control wording as unknown intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes long shipment-to-payment lag wording into receivables intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_receivables_counterparties");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes non-paying counterparties month-risk wording into receivables intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_receivables_counterparties");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes reconciliation mismatch wording into open contracts intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes reconciliation mismatch wording without explicit lookup verb into open contracts intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"По каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes payments-without-closing-docs wording into open contracts intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes documents-without-payments wording into open contracts intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes stale advances without closing docs wording into open contracts intent", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
|
||||||
|
);
|
||||||
|
expect(result.intent).toBe("list_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes buyers with open debt wording into open-items intent", () => {
|
||||||
|
const result = resolveAddressIntent("по каким покупателям у нас есть открытые задолженности на конец месяца?");
|
||||||
|
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("address filter extraction for balance drilldown", () => {
|
describe("address filter extraction for balance drilldown", () => {
|
||||||
|
|
@ -1810,6 +1883,14 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => {
|
||||||
|
const extracted = extractAddressFilters(
|
||||||
|
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
|
||||||
|
"list_open_contracts"
|
||||||
|
);
|
||||||
|
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
|
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
|
||||||
const extracted = extractAddressFilters(
|
const extracted = extractAddressFilters(
|
||||||
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
|
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
|
||||||
|
|
@ -2250,6 +2331,98 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("address query limited taxonomy and stage diagnostics", () => {
|
describe("address query limited taxonomy and stage diagnostics", () => {
|
||||||
|
it("injects as_of_date from analysis context when user message has no explicit period", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("Покажи контрагентов с незакрытыми хвостами", {
|
||||||
|
analysisDateHint: "2020-07-31"
|
||||||
|
});
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-07-31");
|
||||||
|
expect(Array.isArray(result?.debug.reasons)).toBe(true);
|
||||||
|
expect(result?.debug.reasons).toContain("as_of_date_from_analysis_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns soft out-of-scope reply without technical jargon for unsupported supplier-control wording", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
|
||||||
|
expect(result?.debug.detected_intent).toBe("unknown");
|
||||||
|
expect(result?.debug.limited_reason_category).toBe("unsupported");
|
||||||
|
const reply = String(result?.reply_text ?? "");
|
||||||
|
expect(reply.toLowerCase()).toContain("вне поддерживаемого контура");
|
||||||
|
expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes supplier tail-risk wording without forcing missing-anchor fallback", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_payables_counterparties");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes shipment-to-payment lag wording into receivables lane without missing-anchor fallback", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes payments-without-closing-docs wording into open contracts lane", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes stale advances wording into open contracts lane without missing-anchor fallback", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes non-paying counterparties month-risk wording into receivables lane", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes documents-without-payments wording into open contracts lane", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes period coverage profile question into dedicated aggregate recipe", async () => {
|
it("routes period coverage profile question into dedicated aggregate recipe", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const result = await service.tryHandle("За какие годы в базе есть данные?");
|
const result = await service.tryHandle("За какие годы в базе есть данные?");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantAnswerStructureV11 } from "../src/services/assistantAnswerPackageBuilder";
|
||||||
|
|
||||||
|
function buildRetrieval(input?: Partial<any>): any {
|
||||||
|
return {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant answer package builder v11", () => {
|
||||||
|
it("builds baseline answer structure with unresolved mechanism", () => {
|
||||||
|
const structure = buildAssistantAnswerStructureV11({
|
||||||
|
assistantReply: "Первая строка\nВторая строка",
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 2,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: ["R2"],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "partial",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: ["R2"],
|
||||||
|
reasons: ["limited coverage"],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalResults: [buildRetrieval()]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(structure.schema_version).toBe("answer_structure_v1_1");
|
||||||
|
expect(structure.answer_summary).toBe("Первая строка");
|
||||||
|
expect(structure.mechanism_block.status).toBe("unresolved");
|
||||||
|
expect(structure.evidence_block.coverage_note).toBe("coverage_partial_or_limited");
|
||||||
|
expect(structure.uncertainty_block.open_uncertainties).toEqual(["R2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds claim-evidence links when enrichment is explicitly enabled", () => {
|
||||||
|
const structure = buildAssistantAnswerStructureV11({
|
||||||
|
assistantReply: "Ответ",
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
evidence_id: "ev-1",
|
||||||
|
claim_ref: "requirement:R1",
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "amount",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: "trace confirmed",
|
||||||
|
confidence: "high",
|
||||||
|
limitation: null,
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
enableEvidenceEnrichment: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Array.isArray(structure.evidence_block.claim_evidence_links)).toBe(true);
|
||||||
|
expect(structure.evidence_block.claim_evidence_links?.[0]?.claim_ref).toBe("requirement:R1");
|
||||||
|
expect(structure.evidence_block.claim_evidence_links?.[0]?.evidence_ids).toContain("ev-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits claim-evidence links when enrichment is disabled", () => {
|
||||||
|
const structure = buildAssistantAnswerStructureV11({
|
||||||
|
assistantReply: "Ответ",
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
evidence_id: "ev-1",
|
||||||
|
claim_ref: "requirement:R1",
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "amount",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: "trace confirmed",
|
||||||
|
confidence: "high",
|
||||||
|
limitation: null,
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
enableEvidenceEnrichment: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(structure.evidence_block.claim_evidence_links).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AssistantRequirement, UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
import { buildAssistantEvidenceBundleContractV1 } from "../src/services/assistantOrchestrationContracts";
|
||||||
|
import { assembleAssistantContractsBundleV1 } from "../src/services/assistantContractsBundleAssembler";
|
||||||
|
|
||||||
|
function buildRequirement(): AssistantRequirement {
|
||||||
|
return {
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "req1",
|
||||||
|
subject_tokens: ["account_60.01"],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
|
||||||
|
return {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant contracts bundle assembler", () => {
|
||||||
|
it("assembles query/execution/coverage contracts with outcome class", () => {
|
||||||
|
const retrievalResults = [buildRetrieval({ status: "ok" })];
|
||||||
|
const bundle = assembleAssistantContractsBundleV1({
|
||||||
|
userMessage: "проверь хвосты по 60.01",
|
||||||
|
normalizedQuestion: "проверь хвосты по 60.01",
|
||||||
|
normalized: {
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: "проверь хвосты по 60.01",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [{ fragment_id: "F1" }],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
routeSummary: {
|
||||||
|
mode: "deterministic_v2",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
planner: {
|
||||||
|
total_fragments: 1,
|
||||||
|
in_scope_fragments: 1,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [],
|
||||||
|
fallback: {
|
||||||
|
type: "none",
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
analysisContext: {
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto"
|
||||||
|
},
|
||||||
|
executionPlan: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
should_execute: true,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requirements: [buildRequirement()],
|
||||||
|
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
|
||||||
|
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
|
||||||
|
retrievalResults
|
||||||
|
}),
|
||||||
|
replyType: "factual_with_explanation",
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
grounding: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalResults
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(bundle.queryFrameContractV1.schema_version).toBe("assistant_query_frame_v1");
|
||||||
|
expect(bundle.executionPlanContractV1.schema_version).toBe("assistant_execution_plan_v1");
|
||||||
|
expect(bundle.coverageContractV1.schema_version).toBe("assistant_coverage_contract_v1");
|
||||||
|
expect(bundle.outcomeClassV1).toBe("FULLY_ANSWERED");
|
||||||
|
expect(bundle.assistantOrchestrationContractsV1.evidence_bundle.schema_version).toBe("assistant_evidence_bundle_v1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
checkGroundingForRequirements,
|
||||||
|
evaluateCoverageForRequirements,
|
||||||
|
extractRequirementsForRoute
|
||||||
|
} from "../src/services/assistantCoverageGrounding";
|
||||||
|
|
||||||
|
describe("assistant coverage-grounding module", () => {
|
||||||
|
it("extracts requirements from deterministic route summary", () => {
|
||||||
|
const extracted = extractRequirementsForRoute({
|
||||||
|
routeSummary: {
|
||||||
|
mode: "deterministic_v2",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
planner: {
|
||||||
|
total_fragments: 2,
|
||||||
|
in_scope_fragments: 2,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "no_route",
|
||||||
|
no_route_reason: "insufficient_specificity",
|
||||||
|
reason: "missing anchor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
reason: "ok route"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
fallback: {
|
||||||
|
type: "none",
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
userMessage: "base question",
|
||||||
|
fragmentTextById: new Map([
|
||||||
|
["F1", "need more details"],
|
||||||
|
["F2", "check account 60"]
|
||||||
|
]),
|
||||||
|
extractSubjectTokens: (text) => (text.includes("60") ? ["account_60"] : ["counterparty"])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extracted.requirements).toHaveLength(2);
|
||||||
|
expect(extracted.requirements[0].status).toBe("clarification_needed");
|
||||||
|
expect(extracted.requirements[0].route).toBeNull();
|
||||||
|
expect(extracted.requirements[1].status).toBe("covered");
|
||||||
|
expect(extracted.requirements[1].route).toBe("hybrid_store_plus_live");
|
||||||
|
expect(extracted.byFragment.get("F2")).toEqual(["R2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("evaluates coverage from retrieval outcomes", () => {
|
||||||
|
const requirements = [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "req1",
|
||||||
|
subject_tokens: ["account_60"],
|
||||||
|
status: "covered" as const,
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requirement_id: "R2",
|
||||||
|
source_fragment_id: "F2",
|
||||||
|
requirement_text: "req2",
|
||||||
|
subject_tokens: ["counterparty"],
|
||||||
|
status: "covered" as const,
|
||||||
|
route: "store_feature_risk"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const retrievalResults = [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
evidence_id: "ev-1",
|
||||||
|
claim_ref: "requirement:R1",
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "amount",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: "ok",
|
||||||
|
confidence: "high",
|
||||||
|
limitation: null,
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
why_included: ["why"],
|
||||||
|
selection_reason: ["sel"],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "high",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
requirement_ids: ["R2"],
|
||||||
|
route: "store_feature_risk",
|
||||||
|
status: "empty",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "low",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
}
|
||||||
|
] as any;
|
||||||
|
|
||||||
|
const evaluation = evaluateCoverageForRequirements(requirements as any, retrievalResults);
|
||||||
|
expect(evaluation.coverage.requirements_total).toBe(2);
|
||||||
|
expect(evaluation.coverage.requirements_covered).toBe(1);
|
||||||
|
expect(evaluation.coverage.requirements_uncovered).toContain("R2");
|
||||||
|
expect(evaluation.requirements.find((item) => item.requirement_id === "R1")?.status).toBe("covered");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("produces route mismatch grounding when critical subject token is absent", () => {
|
||||||
|
const grounded = checkGroundingForRequirements({
|
||||||
|
userMessage: "Проверь НДС цепочку",
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "vat chain",
|
||||||
|
subject_tokens: ["nds"],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
}
|
||||||
|
] as any,
|
||||||
|
coverage: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
retrievalResults: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: { note: "no tax markers" },
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
}
|
||||||
|
] as any,
|
||||||
|
extractSubjectTokens: () => ["nds"]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(grounded.status).toBe("route_mismatch_blocked");
|
||||||
|
expect(grounded.route_subject_match).toBe(false);
|
||||||
|
expect(grounded.reasons.some((item) => item.includes("Ключевые ориентиры вопроса"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildDeepAnalysisDebugPayload } from "../src/services/assistantDebugPayloadAssembler";
|
||||||
|
|
||||||
|
function baseInput() {
|
||||||
|
return {
|
||||||
|
traceId: "trace-1",
|
||||||
|
promptVersion: "normalizer_v2_0_2",
|
||||||
|
schemaVersion: "normalized_query_v2_0_2",
|
||||||
|
fallbackType: "none",
|
||||||
|
routeSummary: { mode: "deterministic_v2" },
|
||||||
|
fragments: [{ fragment_id: "F1" }],
|
||||||
|
requirementsExtracted: [{ requirement_id: "R1", status: "covered" }],
|
||||||
|
coverageReport: { requirements_total: 1, requirements_covered: 1 },
|
||||||
|
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||||
|
retrievalStatus: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
retrievalResults: [{ fragment_id: "F1", status: "ok" }],
|
||||||
|
groundingCheck: { status: "grounded" },
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
questionTypeClass: "factual_lookup",
|
||||||
|
companyAnchors: { companies: ["demo"] },
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: true,
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto" as const
|
||||||
|
},
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw: ["company_specific_accounting"],
|
||||||
|
business_scope_resolved: ["company_specific_accounting"],
|
||||||
|
company_grounding_applied: true,
|
||||||
|
scope_resolution_reason: ["resolved"]
|
||||||
|
},
|
||||||
|
temporalGuard: {
|
||||||
|
raw_time_anchor: "2020-07",
|
||||||
|
raw_time_scope: "month",
|
||||||
|
resolved_time_anchor: "2020-07",
|
||||||
|
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||||
|
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||||
|
temporal_guard_input: "2020-07",
|
||||||
|
temporal_alignment_status: "aligned",
|
||||||
|
temporal_resolution_source: "analysis_context",
|
||||||
|
temporal_guard_basis: "analysis_context",
|
||||||
|
temporal_guard_applied: true,
|
||||||
|
temporal_guard_outcome: "pass"
|
||||||
|
},
|
||||||
|
polarityAudit: {
|
||||||
|
raw_numeric_tokens: ["60.01"],
|
||||||
|
classified_numeric_tokens: [{ token: "60.01" }],
|
||||||
|
rejected_as_non_accounts: [],
|
||||||
|
resolved_account_anchors: ["60.01"]
|
||||||
|
},
|
||||||
|
claimAnchorAudit: {
|
||||||
|
settlement_role: "supplier",
|
||||||
|
settlement_role_resolution_reason: ["account_60_detected"],
|
||||||
|
polarity_resolution_status: "resolved"
|
||||||
|
},
|
||||||
|
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
|
||||||
|
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
|
||||||
|
rbpLiveRouteAudit: null,
|
||||||
|
faLiveRouteAudit: null,
|
||||||
|
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
|
||||||
|
followupStateUsage: null,
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied: true,
|
||||||
|
problem_units_used_count: 2,
|
||||||
|
problem_answer_mode: "stage3_lifecycle_aware_v1",
|
||||||
|
problem_unit_ids_used: ["pu-1", "pu-2"]
|
||||||
|
},
|
||||||
|
addressRuntimeMetaForDeep: {
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
reason: "ok",
|
||||||
|
provider: "openai",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
toolGateDecision: "run_address_lane",
|
||||||
|
toolGateReason: "detected",
|
||||||
|
predecomposeContract: { schema_version: "x" },
|
||||||
|
orchestrationContract: { schema_version: "y" }
|
||||||
|
},
|
||||||
|
outcomeClassV1: "FULLY_ANSWERED",
|
||||||
|
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
|
||||||
|
answerStructureV11: { schema_version: "answer_structure_v1_1" },
|
||||||
|
investigationStateSnapshot: { status: "active" },
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant debug payload assembler", () => {
|
||||||
|
it("builds deep debug payload with analysis context and optional sections", () => {
|
||||||
|
const payload = buildDeepAnalysisDebugPayload(baseInput());
|
||||||
|
|
||||||
|
expect(payload.trace_id).toBe("trace-1");
|
||||||
|
expect(payload.analysis_context_applied).toBe(true);
|
||||||
|
expect(payload.analysis_context).toMatchObject({
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
source: "eval_analysis_date"
|
||||||
|
});
|
||||||
|
expect(payload.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
|
||||||
|
expect(payload.address_llm_predecompose_applied).toBe(true);
|
||||||
|
expect(payload.assistant_outcome_class_v1).toBe("FULLY_ANSWERED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits optional fields when they are not provided", () => {
|
||||||
|
const input = baseInput();
|
||||||
|
input.runtimeAnalysisContext.active = false;
|
||||||
|
input.followupStateUsage = null;
|
||||||
|
input.compositionDebug.problem_unit_ids_used = [];
|
||||||
|
input.rbpLiveRouteAudit = null;
|
||||||
|
input.faLiveRouteAudit = null;
|
||||||
|
input.addressRuntimeMetaForDeep = null;
|
||||||
|
|
||||||
|
const payload = buildDeepAnalysisDebugPayload(input);
|
||||||
|
|
||||||
|
expect(payload.analysis_context).toBeNull();
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(payload, "followup_state_usage")).toBe(false);
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(payload, "problem_unit_ids_used")).toBe(false);
|
||||||
|
expect(payload.address_llm_predecompose_applied).toBe(false);
|
||||||
|
expect(payload.address_llm_predecompose_contract).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantConversationItem, buildDeepAnswerArtifacts } from "../src/services/assistantDeepResponseAssembler";
|
||||||
|
|
||||||
|
describe("assistant deep response assembler", () => {
|
||||||
|
it("strips technical tail and builds fallback answer structure when missing in composition", () => {
|
||||||
|
const artifacts = buildDeepAnswerArtifacts({
|
||||||
|
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
|
||||||
|
featureContractsV11: true,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
compositionAnswerStructureV11: null,
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalResults: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(artifacts.safeAssistantReply).toBe("Короткий ответ");
|
||||||
|
expect(artifacts.answerStructureV11?.schema_version).toBe("answer_structure_v1_1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses provided composition answer structure and creates assistant conversation item", () => {
|
||||||
|
const provided = {
|
||||||
|
schema_version: "answer_structure_v1_1",
|
||||||
|
answer_summary: "sum",
|
||||||
|
direct_answer: "direct",
|
||||||
|
mechanism_block: {
|
||||||
|
status: "grounded" as const,
|
||||||
|
mechanism_notes: [],
|
||||||
|
limitation_reason_codes: []
|
||||||
|
},
|
||||||
|
evidence_block: {
|
||||||
|
evidence_ids: [],
|
||||||
|
mechanism_notes: [],
|
||||||
|
coverage_note: "ok"
|
||||||
|
},
|
||||||
|
uncertainty_block: {
|
||||||
|
open_uncertainties: [],
|
||||||
|
limitations: []
|
||||||
|
},
|
||||||
|
next_step_block: {
|
||||||
|
recommended_actions: [],
|
||||||
|
clarification_questions: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const artifacts = buildDeepAnswerArtifacts({
|
||||||
|
safeAssistantReplyBase: "Готово",
|
||||||
|
featureContractsV11: true,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
compositionAnswerStructureV11: provided as any,
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalResults: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(artifacts.answerStructureV11).toEqual(provided);
|
||||||
|
|
||||||
|
const item = buildAssistantConversationItem({
|
||||||
|
messageId: "msg-1",
|
||||||
|
sessionId: "asst-1",
|
||||||
|
text: artifacts.safeAssistantReply,
|
||||||
|
replyType: "factual",
|
||||||
|
traceId: "trace-1",
|
||||||
|
debug: {
|
||||||
|
trace_id: "trace-1",
|
||||||
|
prompt_version: "normalizer_v2_0_2",
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
fallback_type: "none",
|
||||||
|
route_summary: null,
|
||||||
|
fragments: [],
|
||||||
|
requirements_extracted: [],
|
||||||
|
coverage_report: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
routes: [],
|
||||||
|
retrieval_status: [],
|
||||||
|
retrieval_results: [],
|
||||||
|
answer_grounding_check: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
dropped_intent_segments: [],
|
||||||
|
answer_structure_v11: null,
|
||||||
|
investigation_state_snapshot: null,
|
||||||
|
normalized: null
|
||||||
|
} as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item.message_id).toBe("msg-1");
|
||||||
|
expect(item.session_id).toBe("asst-1");
|
||||||
|
expect(item.reply_type).toBe("factual");
|
||||||
|
expect(item.text).toBe("Готово");
|
||||||
|
expect(typeof item.created_at).toBe("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantDeepTurnComposition } from "../src/services/assistantDeepTurnCompositionRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant deep turn composition runtime adapter", () => {
|
||||||
|
it("uses followup domain hint and company-anchor period signal", () => {
|
||||||
|
let capturedInput: Record<string, unknown> | null = null;
|
||||||
|
const output = buildAssistantDeepTurnComposition({
|
||||||
|
userMessage: "проверь хвосты по 60.01",
|
||||||
|
routeSummary: null,
|
||||||
|
retrievalResults: [],
|
||||||
|
requirements: [],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
followupUsage: { applied: true },
|
||||||
|
investigationState: {
|
||||||
|
schema_version: "investigation_state_v1",
|
||||||
|
session_id: "asst-1",
|
||||||
|
status: "active",
|
||||||
|
turn_index: 1,
|
||||||
|
updated_at: "2026-04-10T10:00:00.000Z",
|
||||||
|
question_id: "msg-1",
|
||||||
|
question_scope_id: null,
|
||||||
|
scope_origin: null,
|
||||||
|
focus: {
|
||||||
|
domain: "settlements_60_62",
|
||||||
|
period: null,
|
||||||
|
primary_accounts: [],
|
||||||
|
active_query_subject: null
|
||||||
|
},
|
||||||
|
narrowing_status: "unknown",
|
||||||
|
evidence_refs: [],
|
||||||
|
open_uncertainties: [],
|
||||||
|
last_answer_mode: null,
|
||||||
|
followup_context: null,
|
||||||
|
query_mode_hint: "direct_answer"
|
||||||
|
} as any,
|
||||||
|
companyAnchors: {
|
||||||
|
periods: ["2020-07"],
|
||||||
|
dates: []
|
||||||
|
},
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
featureProblemCentricAnswerV1: true,
|
||||||
|
featureLifecycleAnswerV1: true,
|
||||||
|
hasExplicitPeriodAnchor: () => false,
|
||||||
|
resolveQuestionTypeFn: () => "factual_lookup",
|
||||||
|
composeAssistantAnswerFn: ((input: Record<string, unknown>) => {
|
||||||
|
capturedInput = input;
|
||||||
|
return {
|
||||||
|
assistant_reply: "ok",
|
||||||
|
fallback_type: "none",
|
||||||
|
reply_type: "factual"
|
||||||
|
};
|
||||||
|
}) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.focusDomainHint).toBe("settlements_60_62");
|
||||||
|
expect(output.questionTypeClass).toBe("factual_lookup");
|
||||||
|
expect(output.hasPeriodInCompanyAnchors).toBe(true);
|
||||||
|
expect(output.normalizationPeriodExplicit).toBe(true);
|
||||||
|
expect(output.composition.reply_type).toBe("factual");
|
||||||
|
expect(capturedInput?.focusDomainHint).toBe("settlements_60_62");
|
||||||
|
expect(capturedInput?.normalizationPeriodExplicit).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to explicit period from normalized payload when anchors are absent", () => {
|
||||||
|
const output = buildAssistantDeepTurnComposition({
|
||||||
|
userMessage: "проверь закрытие",
|
||||||
|
routeSummary: null,
|
||||||
|
retrievalResults: [],
|
||||||
|
requirements: [],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
followupUsage: { applied: false },
|
||||||
|
investigationState: null,
|
||||||
|
companyAnchors: {
|
||||||
|
periods: [],
|
||||||
|
dates: []
|
||||||
|
},
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
featureProblemCentricAnswerV1: true,
|
||||||
|
featureLifecycleAnswerV1: true,
|
||||||
|
hasExplicitPeriodAnchor: () => true,
|
||||||
|
resolveQuestionTypeFn: () => "verification",
|
||||||
|
composeAssistantAnswerFn: (() => ({
|
||||||
|
assistant_reply: "ok",
|
||||||
|
fallback_type: "none",
|
||||||
|
reply_type: "factual"
|
||||||
|
})) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.focusDomainHint).toBeNull();
|
||||||
|
expect(output.questionTypeClass).toBe("verification");
|
||||||
|
expect(output.hasPeriodInCompanyAnchors).toBe(false);
|
||||||
|
expect(output.normalizationPeriodExplicit).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantDeepTurnRuntimeContext } from "../src/services/assistantDeepTurnContextRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant deep turn context runtime adapter", () => {
|
||||||
|
it("assembles context in deterministic order and propagates followup flag", () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
const companyAnchors = { accounts: ["60.01"] };
|
||||||
|
const temporalGuard = {
|
||||||
|
effective_primary_period: { from: "2020-07-01", to: "2020-07-31" },
|
||||||
|
primary_period_window: { from: "2020-07-01", to: "2020-07-31" }
|
||||||
|
};
|
||||||
|
const businessScope = { route_summary_resolved: { mode: "deterministic_v2", decisions: [] as any[] } } as any;
|
||||||
|
|
||||||
|
const output = buildAssistantDeepTurnRuntimeContext({
|
||||||
|
userMessage: "почему не закрыт 60.01",
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: true,
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "analysis_context"
|
||||||
|
},
|
||||||
|
followupUsage: { applied: true },
|
||||||
|
resolveCompanyAnchors: () => {
|
||||||
|
callOrder.push("anchors");
|
||||||
|
return companyAnchors;
|
||||||
|
},
|
||||||
|
resolveBusinessScopeAlignment: () => {
|
||||||
|
callOrder.push("scope_align");
|
||||||
|
return businessScope;
|
||||||
|
},
|
||||||
|
inferP0DomainFromMessage: () => {
|
||||||
|
callOrder.push("infer_domain");
|
||||||
|
return "settlements_60_62";
|
||||||
|
},
|
||||||
|
resolveTemporalGuard: (input) => {
|
||||||
|
callOrder.push("temporal");
|
||||||
|
expect(input.analysisContext).toEqual({
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "analysis_context"
|
||||||
|
});
|
||||||
|
return temporalGuard as any;
|
||||||
|
},
|
||||||
|
resolveDomainPolarityGuard: (input) => {
|
||||||
|
callOrder.push("polarity");
|
||||||
|
expect(input.focusDomainHint).toBe("settlements_60_62");
|
||||||
|
return { polarity: "supplier_payable" };
|
||||||
|
},
|
||||||
|
resolveClaimBoundAnchors: (input) => {
|
||||||
|
callOrder.push("claim");
|
||||||
|
expect(input.primaryPeriod).toEqual(temporalGuard.effective_primary_period);
|
||||||
|
return { claim_type: "prove_settlement_closure_state" } as any;
|
||||||
|
},
|
||||||
|
resolveBusinessScopeFromLiveContext: (input) => {
|
||||||
|
callOrder.push("scope_live");
|
||||||
|
expect(input.followupApplied).toBe(true);
|
||||||
|
return {
|
||||||
|
...businessScope,
|
||||||
|
live_scope_used: true
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callOrder).toEqual(["anchors", "scope_align", "infer_domain", "temporal", "polarity", "claim", "scope_live"]);
|
||||||
|
expect(output.companyAnchors).toBe(companyAnchors);
|
||||||
|
expect(output.focusDomainForGuards).toBe("settlements_60_62");
|
||||||
|
expect(output.claimAnchorAudit.claim_type).toBe("prove_settlement_closure_state");
|
||||||
|
expect(output.liveTemporalHint).toEqual({
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "analysis_context"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops unknown inferred domain and disables live temporal hint when context is inactive", () => {
|
||||||
|
const output = buildAssistantDeepTurnRuntimeContext({
|
||||||
|
userMessage: "какой-нибудь вопрос",
|
||||||
|
normalizedPayload: null as any,
|
||||||
|
routeSummary: null,
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: false,
|
||||||
|
as_of_date: null,
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: null
|
||||||
|
},
|
||||||
|
followupUsage: null,
|
||||||
|
resolveCompanyAnchors: () => ({}),
|
||||||
|
resolveBusinessScopeAlignment: () => ({ route_summary_resolved: null }),
|
||||||
|
inferP0DomainFromMessage: () => "unknown_domain",
|
||||||
|
resolveTemporalGuard: () => ({ primary_period_window: null }),
|
||||||
|
resolveDomainPolarityGuard: () => ({ polarity: "not_applicable" }),
|
||||||
|
resolveClaimBoundAnchors: () => ({ claim_type: "unknown" } as any),
|
||||||
|
resolveBusinessScopeFromLiveContext: () => ({ route_summary_resolved: null })
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.focusDomainForGuards).toBeNull();
|
||||||
|
expect(output.resolvedRouteSummary).toBeNull();
|
||||||
|
expect(output.liveTemporalHint).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { runAssistantDeepTurnGroundingRuntime } from "../src/services/assistantDeepTurnGroundingRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant deep turn grounding runtime adapter", () => {
|
||||||
|
it("runs audits, coverage-grounding pipeline and eligibility overlay in stable order", () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
const retrievalResults = [{ fragment_id: "F1" }] as any[];
|
||||||
|
const coverageEvaluation = {
|
||||||
|
requirements: [{ requirement_id: "R1" }],
|
||||||
|
coverage: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
const groundingCheckBase = {
|
||||||
|
status: "grounded_positive",
|
||||||
|
reasons: [],
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const output = runAssistantDeepTurnGroundingRuntime({
|
||||||
|
claimType: "prove_settlement_closure_state",
|
||||||
|
retrievalResults,
|
||||||
|
rbpPlanAudit: { rbp: true },
|
||||||
|
faPlanAudit: { fa: true },
|
||||||
|
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
userMessage: "check",
|
||||||
|
requirementExtraction: {
|
||||||
|
requirements: [{ requirement_id: "R1" }] as any,
|
||||||
|
byFragment: new Map([["F1", ["R1"]]])
|
||||||
|
} as any,
|
||||||
|
extractRequirements: (() => {
|
||||||
|
throw new Error("should not be called when requirementExtraction is provided");
|
||||||
|
}) as any,
|
||||||
|
evaluateCoverage: (() => {
|
||||||
|
throw new Error("should not be called directly from adapter");
|
||||||
|
}) as any,
|
||||||
|
checkGrounding: (() => {
|
||||||
|
throw new Error("should not be called directly from adapter");
|
||||||
|
}) as any,
|
||||||
|
temporalGuard: { temporal_guard_outcome: "passed" } as any,
|
||||||
|
polarityAudit: { outcome: "passed" } as any,
|
||||||
|
evidenceAudit: { admissible_evidence_count: 2 } as any,
|
||||||
|
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
|
||||||
|
targetedEvidenceHitRate: 0.5,
|
||||||
|
businessScopeResolved: ["company_specific_accounting"],
|
||||||
|
collectRbpLiveRouteAudit: (input) => {
|
||||||
|
callOrder.push("rbp_audit");
|
||||||
|
expect(input.planAudit).toEqual({ rbp: true });
|
||||||
|
return { rbp_live: 1 };
|
||||||
|
},
|
||||||
|
collectFaLiveRouteAudit: (input) => {
|
||||||
|
callOrder.push("fa_audit");
|
||||||
|
expect(input.planAudit).toEqual({ fa: true });
|
||||||
|
return { fa_live: 1 };
|
||||||
|
},
|
||||||
|
runCoverageGroundingPipelineFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("coverage_pipeline");
|
||||||
|
expect(input.retrievalResults).toBe(retrievalResults);
|
||||||
|
return {
|
||||||
|
requirementExtraction: input.requirementExtraction,
|
||||||
|
coverageEvaluation,
|
||||||
|
groundingCheckBase
|
||||||
|
};
|
||||||
|
}) as any,
|
||||||
|
applyGroundingEligibilityFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("eligibility");
|
||||||
|
expect(input.groundingCheckBase).toBe(groundingCheckBase);
|
||||||
|
return {
|
||||||
|
groundedAnswerEligibilityGuard: {
|
||||||
|
eligible: true
|
||||||
|
},
|
||||||
|
groundingCheck: groundingCheckBase
|
||||||
|
};
|
||||||
|
}) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callOrder).toEqual(["rbp_audit", "fa_audit", "coverage_pipeline", "eligibility"]);
|
||||||
|
expect(output.rbpLiveRouteAudit).toEqual({ rbp_live: 1 });
|
||||||
|
expect(output.faLiveRouteAudit).toEqual({ fa_live: 1 });
|
||||||
|
expect(output.coverageEvaluation).toBe(coverageEvaluation);
|
||||||
|
expect(output.groundedAnswerEligibilityGuard).toEqual({ eligible: true });
|
||||||
|
expect(output.groundingCheck).toBe(groundingCheckBase);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("threads default pipeline output through without custom hooks", () => {
|
||||||
|
const output = runAssistantDeepTurnGroundingRuntime({
|
||||||
|
claimType: "unknown",
|
||||||
|
retrievalResults: [],
|
||||||
|
rbpPlanAudit: null,
|
||||||
|
faPlanAudit: null,
|
||||||
|
routeSummary: null,
|
||||||
|
normalizedPayload: null as any,
|
||||||
|
userMessage: "q",
|
||||||
|
requirementExtraction: {
|
||||||
|
requirements: [],
|
||||||
|
byFragment: new Map()
|
||||||
|
} as any,
|
||||||
|
extractRequirements: () => ({
|
||||||
|
requirements: [],
|
||||||
|
byFragment: new Map()
|
||||||
|
}) as any,
|
||||||
|
evaluateCoverage: () => ({
|
||||||
|
requirements: [],
|
||||||
|
coverage: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
checkGrounding: () =>
|
||||||
|
({
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
}) as any,
|
||||||
|
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
|
||||||
|
polarityAudit: { applied: false, outcome: "not_applicable" } as any,
|
||||||
|
evidenceAudit: { admissible_evidence_count: 0 } as any,
|
||||||
|
claimAnchorAudit: null,
|
||||||
|
collectRbpLiveRouteAudit: () => null,
|
||||||
|
collectFaLiveRouteAudit: () => null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.coverageEvaluation.requirements).toEqual([]);
|
||||||
|
expect(output.groundingCheck.status).toBe("no_grounded_answer");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
applyAssistantDeepTurnGroundingEligibility,
|
||||||
|
applyAssistantDeepTurnRetrievalGuards
|
||||||
|
} from "../src/services/assistantDeepTurnGuardRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant deep turn guard runtime adapter", () => {
|
||||||
|
it("runs retrieval guards in expected order and threads outputs", () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
const seedResults = [{ fragment_id: "F1" }] as any[];
|
||||||
|
const afterPolarity = [{ fragment_id: "P1" }] as any[];
|
||||||
|
const afterTargeted = [{ fragment_id: "T1" }] as any[];
|
||||||
|
const afterGate = [{ fragment_id: "G1" }] as any[];
|
||||||
|
|
||||||
|
const output = applyAssistantDeepTurnRetrievalGuards({
|
||||||
|
retrievalResults: seedResults as any,
|
||||||
|
domainPolarityGuardInitial: { applied: true, polarity: "supplier_payable" } as any,
|
||||||
|
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
|
||||||
|
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
|
||||||
|
focusDomainForGuards: "settlements_60_62" as any,
|
||||||
|
companyAnchors: { accounts: ["60.01"] } as any,
|
||||||
|
userMessage: "check settlements",
|
||||||
|
applyDomainPolarityGuardFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("polarity");
|
||||||
|
expect(input.retrievalResults).toBe(seedResults);
|
||||||
|
return {
|
||||||
|
retrievalResults: afterPolarity,
|
||||||
|
audit: {
|
||||||
|
applied: true,
|
||||||
|
polarity: "supplier_payable",
|
||||||
|
outcome: "passed",
|
||||||
|
reason_codes: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}) as any,
|
||||||
|
applyTargetedEvidenceFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("targeted");
|
||||||
|
expect(input.retrievalResults).toBe(afterPolarity);
|
||||||
|
return {
|
||||||
|
retrievalResults: afterTargeted,
|
||||||
|
audit: {
|
||||||
|
targeted_evidence_hit_rate: 0.5,
|
||||||
|
reason_codes: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}) as any,
|
||||||
|
applyEvidenceAdmissibilityGateFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("gate");
|
||||||
|
expect(input.retrievalResults).toBe(afterTargeted);
|
||||||
|
expect(input.polarity).toBe("supplier_payable");
|
||||||
|
expect(input.userMessage).toBe("check settlements");
|
||||||
|
return {
|
||||||
|
retrievalResults: afterGate,
|
||||||
|
audit: {
|
||||||
|
admissible_evidence_count: 2,
|
||||||
|
reason_codes: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callOrder).toEqual(["polarity", "targeted", "gate"]);
|
||||||
|
expect(output.retrievalResults).toBe(afterGate);
|
||||||
|
expect(output.polarityGuardResult.retrievalResults).toBe(afterPolarity);
|
||||||
|
expect(output.targetedEvidenceResult.retrievalResults).toBe(afterTargeted);
|
||||||
|
expect(output.evidenceGateResult.retrievalResults).toBe(afterGate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("evaluates grounding eligibility and applies status overlay", () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
const groundingCheckBase = {
|
||||||
|
status: "grounded_positive",
|
||||||
|
reasons: ["base"],
|
||||||
|
route_subject_match: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = applyAssistantDeepTurnGroundingEligibility({
|
||||||
|
groundingCheckBase,
|
||||||
|
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
|
||||||
|
polarityAudit: { applied: true, outcome: "passed", polarity: "supplier_payable" } as any,
|
||||||
|
evidenceAudit: { admissible_evidence_count: 0 } as any,
|
||||||
|
claimAnchorAudit: { claim_anchor_resolution_rate: 1, missing_anchors: [], required_anchors: [] } as any,
|
||||||
|
targetedEvidenceHitRate: 0,
|
||||||
|
businessScopeResolved: ["company_specific_accounting"],
|
||||||
|
evaluateGroundedAnswerEligibilityFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("eligibility");
|
||||||
|
expect(input.targetedEvidenceHitRate).toBe(0);
|
||||||
|
return {
|
||||||
|
eligible: false,
|
||||||
|
temporal_passed: true,
|
||||||
|
eligibility_time_basis: "none",
|
||||||
|
business_scope_passed: true,
|
||||||
|
polarity_passed: true,
|
||||||
|
claim_anchors_passed: true,
|
||||||
|
claim_anchor_resolution_rate: 1,
|
||||||
|
missing_required_anchors: 0,
|
||||||
|
admissible_evidence_count: 0,
|
||||||
|
critical_contradiction: false,
|
||||||
|
outcome: "limited_or_insufficient_evidence",
|
||||||
|
grounding_mode: "limited_or_insufficient_evidence",
|
||||||
|
reason_codes: ["admissible_evidence_count_zero"]
|
||||||
|
};
|
||||||
|
}) as any,
|
||||||
|
applyEligibilityToGroundingCheckFn: ((check: Record<string, unknown>, eligibility: Record<string, unknown>) => {
|
||||||
|
callOrder.push("overlay");
|
||||||
|
expect(check.status).toBe("grounded_positive");
|
||||||
|
expect(eligibility.eligible).toBe(false);
|
||||||
|
return {
|
||||||
|
...check,
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
reasons: ["base", "not_enough_evidence"]
|
||||||
|
};
|
||||||
|
}) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callOrder).toEqual(["eligibility", "overlay"]);
|
||||||
|
expect(output.groundedAnswerEligibilityGuard.eligible).toBe(false);
|
||||||
|
expect(output.groundingCheck.status).toBe("no_grounded_answer");
|
||||||
|
expect(output.groundingCheck.reasons).toEqual(["base", "not_enough_evidence"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantDeepTurnPackagingInput, type AssistantDeepTurnInputBuilderArgs } from "../src/services/assistantDeepTurnInputBuilder";
|
||||||
|
|
||||||
|
function baseArgs(): AssistantDeepTurnInputBuilderArgs {
|
||||||
|
return {
|
||||||
|
sessionId: "asst-1",
|
||||||
|
messageId: "msg-1",
|
||||||
|
userMessage: "проверь 60.01",
|
||||||
|
normalized: {
|
||||||
|
trace_id: "trace-1",
|
||||||
|
prompt_version: "normalizer_v2_0_2",
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
normalized: {
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: "проверь 60.01",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizedQuestion: "проверь 60.01",
|
||||||
|
routeSummary: null,
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
analysisContextForContract: null,
|
||||||
|
executionPlan: [],
|
||||||
|
requirementExtractionRequirements: [],
|
||||||
|
coverageEvaluationRequirements: [],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalCalls: [],
|
||||||
|
retrievalResultsRaw: [],
|
||||||
|
retrievalResults: [],
|
||||||
|
routesForDebug: [],
|
||||||
|
resolvedExecutionState: {},
|
||||||
|
questionTypeClass: "factual_lookup",
|
||||||
|
companyAnchors: {},
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: false,
|
||||||
|
as_of_date: null,
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: null,
|
||||||
|
snapshot_mode: "auto"
|
||||||
|
},
|
||||||
|
businessScopeResolution: {},
|
||||||
|
temporalGuard: {},
|
||||||
|
polarityAudit: {},
|
||||||
|
claimAnchorAudit: {},
|
||||||
|
targetedEvidenceAudit: null,
|
||||||
|
evidenceAdmissibilityGateAudit: null,
|
||||||
|
rbpLiveRouteAudit: null,
|
||||||
|
faLiveRouteAudit: null,
|
||||||
|
groundedAnswerEligibilityGuard: {},
|
||||||
|
followupStateUsage: undefined,
|
||||||
|
composition: {
|
||||||
|
reply_type: "factual",
|
||||||
|
fallback_type: "none"
|
||||||
|
},
|
||||||
|
safeAssistantReplyBase: "ok",
|
||||||
|
featureContractsV11: true,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
investigationStateSnapshot: null,
|
||||||
|
addressRuntimeMetaForDeep: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant deep turn input builder", () => {
|
||||||
|
it("applies stable defaults for optional composition and followup fields", () => {
|
||||||
|
const built = buildAssistantDeepTurnPackagingInput(baseArgs());
|
||||||
|
|
||||||
|
expect(built.followupStateUsage).toBeNull();
|
||||||
|
expect(built.composition.answer_structure_v11).toBeNull();
|
||||||
|
expect(built.composition.problem_centric_answer_applied).toBe(false);
|
||||||
|
expect(built.composition.problem_units_used_count).toBe(0);
|
||||||
|
expect(built.composition.problem_answer_mode).toBe("stage1_policy_v11");
|
||||||
|
expect(built.composition.problem_unit_ids_used).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves explicit composition fields and normalizes unit ids array", () => {
|
||||||
|
const args = baseArgs();
|
||||||
|
args.followupStateUsage = { applied: true };
|
||||||
|
args.composition.answer_structure_v11 = {
|
||||||
|
schema_version: "answer_structure_v1_1",
|
||||||
|
answer_summary: "sum",
|
||||||
|
direct_answer: "direct",
|
||||||
|
mechanism_block: {
|
||||||
|
status: "grounded",
|
||||||
|
mechanism_notes: [],
|
||||||
|
limitation_reason_codes: []
|
||||||
|
},
|
||||||
|
evidence_block: {
|
||||||
|
evidence_ids: [],
|
||||||
|
source_refs: [],
|
||||||
|
mechanism_notes: [],
|
||||||
|
coverage_note: "ok"
|
||||||
|
},
|
||||||
|
uncertainty_block: {
|
||||||
|
open_uncertainties: [],
|
||||||
|
limitations: []
|
||||||
|
},
|
||||||
|
next_step_block: {
|
||||||
|
recommended_actions: [],
|
||||||
|
clarification_questions: []
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
args.composition.problem_centric_answer_applied = true;
|
||||||
|
args.composition.problem_units_used_count = 3;
|
||||||
|
args.composition.problem_answer_mode = "stage3_lifecycle_aware_v1";
|
||||||
|
args.composition.problem_unit_ids_used = ["pu-1", "pu-2"];
|
||||||
|
|
||||||
|
const built = buildAssistantDeepTurnPackagingInput(args);
|
||||||
|
|
||||||
|
expect(built.followupStateUsage).toEqual({ applied: true });
|
||||||
|
expect(built.composition.answer_structure_v11?.schema_version).toBe("answer_structure_v1_1");
|
||||||
|
expect(built.composition.problem_centric_answer_applied).toBe(true);
|
||||||
|
expect(built.composition.problem_units_used_count).toBe(3);
|
||||||
|
expect(built.composition.problem_answer_mode).toBe("stage3_lifecycle_aware_v1");
|
||||||
|
expect(built.composition.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { assembleAssistantDeepTurnPackaging } from "../src/services/assistantDeepTurnPackaging";
|
||||||
|
|
||||||
|
function buildRetrieval() {
|
||||||
|
return {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseInput() {
|
||||||
|
return {
|
||||||
|
sessionId: "asst-1",
|
||||||
|
messageId: "msg-1",
|
||||||
|
userMessage: "проверь хвосты по 60.01",
|
||||||
|
normalized: {
|
||||||
|
trace_id: "trace-1",
|
||||||
|
prompt_version: "normalizer_v2_0_2",
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
normalized: {
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: "проверь хвосты по 60.01",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [{ fragment_id: "F1" }],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizedQuestion: "проверь хвосты по 60.01",
|
||||||
|
routeSummary: {
|
||||||
|
mode: "deterministic_v2",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
planner: {
|
||||||
|
total_fragments: 1,
|
||||||
|
in_scope_fragments: 1,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [],
|
||||||
|
fallback: {
|
||||||
|
type: "none",
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
analysisContextForContract: {
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto" as const
|
||||||
|
},
|
||||||
|
executionPlan: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
should_execute: true,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requirementExtractionRequirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "проверить хвосты 60.01",
|
||||||
|
subject_tokens: ["account_60.01"],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
coverageEvaluationRequirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "проверить хвосты 60.01",
|
||||||
|
subject_tokens: ["account_60.01"],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
|
||||||
|
retrievalResultsRaw: [buildRetrieval()],
|
||||||
|
retrievalResults: [buildRetrieval()],
|
||||||
|
routesForDebug: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||||
|
resolvedExecutionState: { executable: 1 },
|
||||||
|
questionTypeClass: "factual_lookup",
|
||||||
|
companyAnchors: { companies: ["demo"] },
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: true,
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto" as const
|
||||||
|
},
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw: ["company_specific_accounting"],
|
||||||
|
business_scope_resolved: ["company_specific_accounting"],
|
||||||
|
company_grounding_applied: true,
|
||||||
|
scope_resolution_reason: ["resolved"]
|
||||||
|
},
|
||||||
|
temporalGuard: { temporal_guard_applied: true },
|
||||||
|
polarityAudit: { resolved_account_anchors: ["60.01"] },
|
||||||
|
claimAnchorAudit: { settlement_role: "supplier" },
|
||||||
|
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
|
||||||
|
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
|
||||||
|
rbpLiveRouteAudit: null,
|
||||||
|
faLiveRouteAudit: null,
|
||||||
|
groundedAnswerEligibilityGuard: { eligible: true },
|
||||||
|
followupStateUsage: null,
|
||||||
|
composition: {
|
||||||
|
reply_type: "factual" as const,
|
||||||
|
fallback_type: "none",
|
||||||
|
answer_structure_v11: null,
|
||||||
|
problem_centric_answer_applied: true,
|
||||||
|
problem_units_used_count: 1,
|
||||||
|
problem_answer_mode: "stage3_lifecycle_aware_v1",
|
||||||
|
problem_unit_ids_used: ["pu-1"]
|
||||||
|
},
|
||||||
|
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
|
||||||
|
featureContractsV11: true,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
investigationStateSnapshot: { status: "active" },
|
||||||
|
addressRuntimeMetaForDeep: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant deep turn packaging", () => {
|
||||||
|
it("assembles deep artifacts, debug payload and processed log in one call", () => {
|
||||||
|
const input = baseInput();
|
||||||
|
const output = assembleAssistantDeepTurnPackaging(input as any);
|
||||||
|
|
||||||
|
expect(output.deepAnswerArtifacts.safeAssistantReply).toBe("Короткий ответ");
|
||||||
|
expect(output.contractsBundleV1.outcomeClassV1).toBe("FULLY_ANSWERED");
|
||||||
|
expect(output.debug.trace_id).toBe("trace-1");
|
||||||
|
expect(output.assistantItem.message_id).toBe("msg-1");
|
||||||
|
expect(output.assistantItem.text).toBe("Короткий ответ");
|
||||||
|
expect(output.deepAnalysisLogDetails.session_id).toBe("asst-1");
|
||||||
|
expect(output.deepAnalysisLogDetails.message_id).toBe("msg-1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { runAssistantDeepTurnPackagingRuntime } from "../src/services/assistantDeepTurnPackagingRuntimeAdapter";
|
||||||
|
|
||||||
|
function buildBaseInput() {
|
||||||
|
return {
|
||||||
|
featureInvestigationStateV1: true,
|
||||||
|
sessionId: "asst-1",
|
||||||
|
questionId: "msg-user-1",
|
||||||
|
userMessage: "проверь кейс",
|
||||||
|
normalized: {
|
||||||
|
trace_id: "trace-1",
|
||||||
|
prompt_version: "p1",
|
||||||
|
schema_version: "s1",
|
||||||
|
normalized: { schema_version: "normalized_query_v2_0_2" } as any
|
||||||
|
},
|
||||||
|
normalizedQuestion: "проверь кейс",
|
||||||
|
routeSummary: null,
|
||||||
|
executionPlan: [],
|
||||||
|
requirementExtractionRequirements: [],
|
||||||
|
coverageEvaluationRequirements: [],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
retrievalCalls: [],
|
||||||
|
retrievalResultsRaw: [],
|
||||||
|
retrievalResults: [],
|
||||||
|
questionTypeClass: "factual_lookup",
|
||||||
|
companyAnchors: {},
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: false,
|
||||||
|
as_of_date: null,
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: null,
|
||||||
|
snapshot_mode: "auto" as const
|
||||||
|
},
|
||||||
|
businessScopeResolution: {},
|
||||||
|
temporalGuard: {},
|
||||||
|
polarityAudit: {},
|
||||||
|
claimAnchorAudit: {},
|
||||||
|
targetedEvidenceAudit: {},
|
||||||
|
evidenceAdmissibilityGateAudit: {},
|
||||||
|
rbpLiveRouteAudit: null,
|
||||||
|
faLiveRouteAudit: null,
|
||||||
|
groundedAnswerEligibilityGuard: {},
|
||||||
|
followupStateUsage: null,
|
||||||
|
followupApplied: false,
|
||||||
|
composition: {
|
||||||
|
assistant_reply: "raw-reply",
|
||||||
|
reply_type: "factual" as const,
|
||||||
|
fallback_type: "none"
|
||||||
|
},
|
||||||
|
featureContractsV11: true,
|
||||||
|
featureAnswerPolicyV11: true,
|
||||||
|
previousInvestigationState: null,
|
||||||
|
addressRuntimeMetaForDeep: null,
|
||||||
|
extractDroppedIntentSegments: () => [],
|
||||||
|
buildDebugRoutes: () => [],
|
||||||
|
extractExecutionState: () => null,
|
||||||
|
sanitizeReply: (value: string) => value,
|
||||||
|
persistInvestigationState: () => {},
|
||||||
|
messageIdFactory: () => "msg-fixed"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant deep turn packaging runtime adapter", () => {
|
||||||
|
it("executes pre-packaging, snapshot, persist, input-build and assembly in stable order", () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
let persistedByCallback = 0;
|
||||||
|
|
||||||
|
const output = runAssistantDeepTurnPackagingRuntime({
|
||||||
|
...buildBaseInput(),
|
||||||
|
persistInvestigationState: () => {
|
||||||
|
persistedByCallback += 1;
|
||||||
|
},
|
||||||
|
buildPrePackagingContextFn: (() => {
|
||||||
|
callOrder.push("pre");
|
||||||
|
return {
|
||||||
|
droppedIntentSegments: ["F2_dropped"],
|
||||||
|
analysisContextForContract: null,
|
||||||
|
routesForDebug: [{ fragment_id: "F1" }],
|
||||||
|
resolvedExecutionState: [{ fragment_id: "F1", execution_readiness: "ready" }],
|
||||||
|
safeAssistantReplyBase: "safe-base"
|
||||||
|
};
|
||||||
|
}) as any,
|
||||||
|
buildInvestigationStateSnapshotFn: (() => {
|
||||||
|
callOrder.push("snapshot");
|
||||||
|
return { schema_version: "investigation_state_v1" };
|
||||||
|
}) as any,
|
||||||
|
persistInvestigationStateSnapshotFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("persist");
|
||||||
|
(input.persist as Function)(input.sessionId, input.snapshot);
|
||||||
|
return true;
|
||||||
|
}) as any,
|
||||||
|
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => {
|
||||||
|
callOrder.push("input");
|
||||||
|
expect(input.messageId).toBe("msg-fixed");
|
||||||
|
expect(input.droppedIntentSegments).toEqual(["F2_dropped"]);
|
||||||
|
expect(input.safeAssistantReplyBase).toBe("safe-base");
|
||||||
|
return input;
|
||||||
|
}) as any,
|
||||||
|
assembleDeepTurnPackagingFn: (() => {
|
||||||
|
callOrder.push("assemble");
|
||||||
|
return {
|
||||||
|
deepAnswerArtifacts: {
|
||||||
|
safeAssistantReply: "assistant-safe"
|
||||||
|
},
|
||||||
|
debug: { ok: true },
|
||||||
|
assistantItem: {
|
||||||
|
message_id: "msg-fixed",
|
||||||
|
session_id: "asst-1",
|
||||||
|
role: "assistant",
|
||||||
|
text: "assistant-safe",
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-10T10:00:00.000Z",
|
||||||
|
trace_id: "trace-1",
|
||||||
|
debug: null
|
||||||
|
},
|
||||||
|
deepAnalysisLogDetails: { stage: "deep_analysis" }
|
||||||
|
};
|
||||||
|
}) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callOrder).toEqual(["pre", "snapshot", "persist", "input", "assemble"]);
|
||||||
|
expect(persistedByCallback).toBe(1);
|
||||||
|
expect(output.messageId).toBe("msg-fixed");
|
||||||
|
expect(output.safeAssistantReply).toBe("assistant-safe");
|
||||||
|
expect(output.debug).toEqual({ ok: true });
|
||||||
|
expect(output.deepAnalysisLogDetails).toEqual({ stage: "deep_analysis" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not persist investigation snapshot when feature is disabled", () => {
|
||||||
|
let persistedByCallback = 0;
|
||||||
|
|
||||||
|
const output = runAssistantDeepTurnPackagingRuntime({
|
||||||
|
...buildBaseInput(),
|
||||||
|
featureInvestigationStateV1: false,
|
||||||
|
persistInvestigationState: () => {
|
||||||
|
persistedByCallback += 1;
|
||||||
|
},
|
||||||
|
buildPrePackagingContextFn: (() => ({
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
analysisContextForContract: null,
|
||||||
|
routesForDebug: [],
|
||||||
|
resolvedExecutionState: null,
|
||||||
|
safeAssistantReplyBase: "safe-base"
|
||||||
|
})) as any,
|
||||||
|
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => input) as any,
|
||||||
|
assembleDeepTurnPackagingFn: (() => ({
|
||||||
|
deepAnswerArtifacts: {
|
||||||
|
safeAssistantReply: "assistant-safe"
|
||||||
|
},
|
||||||
|
debug: {},
|
||||||
|
assistantItem: {
|
||||||
|
message_id: "msg-fixed",
|
||||||
|
session_id: "asst-1",
|
||||||
|
role: "assistant",
|
||||||
|
text: "assistant-safe",
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-10T10:00:00.000Z",
|
||||||
|
trace_id: "trace-1",
|
||||||
|
debug: null
|
||||||
|
},
|
||||||
|
deepAnalysisLogDetails: {}
|
||||||
|
})) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.investigationStateSnapshot).toBeNull();
|
||||||
|
expect(persistedByCallback).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantDeepTurnExecutionPlan } from "../src/services/assistantDeepTurnPlanRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant deep turn plan runtime adapter", () => {
|
||||||
|
it("builds execution plan through extraction, enforcement and guard hints in stable order", () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
const byFragment = new Map<string, string[]>([["F1", ["R1"]]]);
|
||||||
|
const planInitial = [{ fragment_id: "F1", route: "store_canonical" }] as any[];
|
||||||
|
const planAfterRbp = [{ fragment_id: "F1", route: "hybrid_store_plus_live" }] as any[];
|
||||||
|
const planAfterFa = [{ fragment_id: "F1", route: "hybrid_store_plus_live", fa: true }] as any[];
|
||||||
|
const planAfterTemporal = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true }] as any[];
|
||||||
|
const planAfterPolarity = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true, polarity: true }] as any[];
|
||||||
|
|
||||||
|
const output = buildAssistantDeepTurnExecutionPlan({
|
||||||
|
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
userMessage: "check tails",
|
||||||
|
claimType: "prove_settlement_closure_state",
|
||||||
|
temporalGuard: { temporal_guard_outcome: "passed" } as any,
|
||||||
|
domainPolarityGuardInitial: { polarity: "supplier_payable" } as any,
|
||||||
|
extractRequirements: () => {
|
||||||
|
callOrder.push("extract");
|
||||||
|
return {
|
||||||
|
requirements: [{ requirement_id: "R1" }] as any[],
|
||||||
|
byFragment
|
||||||
|
};
|
||||||
|
},
|
||||||
|
toExecutionPlan: (_routeSummary, _normalizedPayload, _userMessage, requirementByFragment) => {
|
||||||
|
callOrder.push("plan");
|
||||||
|
expect(requirementByFragment).toBe(byFragment);
|
||||||
|
return planInitial as any;
|
||||||
|
},
|
||||||
|
enforceRbpLiveRoutePlan: ({ executionPlan }) => {
|
||||||
|
callOrder.push("rbp");
|
||||||
|
expect(executionPlan).toBe(planInitial);
|
||||||
|
return {
|
||||||
|
executionPlan: planAfterRbp as any,
|
||||||
|
audit: { rbp: true }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
enforceFaLiveRoutePlan: ({ executionPlan }) => {
|
||||||
|
callOrder.push("fa");
|
||||||
|
expect(executionPlan).toBe(planAfterRbp);
|
||||||
|
return {
|
||||||
|
executionPlan: planAfterFa as any,
|
||||||
|
audit: { fa: true }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
applyTemporalHintToExecutionPlan: (executionPlan) => {
|
||||||
|
callOrder.push("temporal");
|
||||||
|
expect(executionPlan).toBe(planAfterFa);
|
||||||
|
return planAfterTemporal as any;
|
||||||
|
},
|
||||||
|
applyPolarityHintToExecutionPlan: (executionPlan) => {
|
||||||
|
callOrder.push("polarity");
|
||||||
|
expect(executionPlan).toBe(planAfterTemporal);
|
||||||
|
return planAfterPolarity as any;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callOrder).toEqual(["extract", "plan", "rbp", "fa", "temporal", "polarity"]);
|
||||||
|
expect(output.requirementExtraction.byFragment).toBe(byFragment);
|
||||||
|
expect(output.executionPlan).toBe(planAfterPolarity);
|
||||||
|
expect(output.rbpRoutePlanEnforcement.audit).toEqual({ rbp: true });
|
||||||
|
expect(output.faRoutePlanEnforcement.audit).toEqual({ fa: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves empty execution plan end-to-end", () => {
|
||||||
|
const output = buildAssistantDeepTurnExecutionPlan({
|
||||||
|
routeSummary: null,
|
||||||
|
normalizedPayload: null as any,
|
||||||
|
userMessage: "noop",
|
||||||
|
claimType: "unknown",
|
||||||
|
temporalGuard: null,
|
||||||
|
domainPolarityGuardInitial: null,
|
||||||
|
extractRequirements: () => ({
|
||||||
|
requirements: [],
|
||||||
|
byFragment: new Map()
|
||||||
|
}),
|
||||||
|
toExecutionPlan: () => [],
|
||||||
|
enforceRbpLiveRoutePlan: ({ executionPlan }) => ({
|
||||||
|
executionPlan,
|
||||||
|
audit: { rbp: false }
|
||||||
|
}),
|
||||||
|
enforceFaLiveRoutePlan: ({ executionPlan }) => ({
|
||||||
|
executionPlan,
|
||||||
|
audit: { fa: false }
|
||||||
|
}),
|
||||||
|
applyTemporalHintToExecutionPlan: (executionPlan) => executionPlan,
|
||||||
|
applyPolarityHintToExecutionPlan: (executionPlan) => executionPlan
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.executionPlan).toEqual([]);
|
||||||
|
expect(output.requirementExtraction.requirements).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantDeepTurnPrePackagingContext } from "../src/services/assistantDeepTurnPrePackagingContext";
|
||||||
|
|
||||||
|
describe("assistant deep turn pre-packaging context", () => {
|
||||||
|
it("builds all pre-packaging fields with active analysis context", () => {
|
||||||
|
const output = buildAssistantDeepTurnPrePackagingContext({
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
routeSummary: null,
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: true,
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto"
|
||||||
|
},
|
||||||
|
assistantReply: "raw",
|
||||||
|
extractDroppedIntentSegments: () => ["segment_1"],
|
||||||
|
buildDebugRoutes: () => [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||||
|
extractExecutionState: () => ({ executable: 1 }),
|
||||||
|
sanitizeReply: (value, fallback) => `${value}::${fallback}`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.droppedIntentSegments).toEqual(["segment_1"]);
|
||||||
|
expect(output.analysisContextForContract).toEqual({
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto"
|
||||||
|
});
|
||||||
|
expect(output.routesForDebug).toEqual([{ fragment_id: "F1", route: "hybrid_store_plus_live" }]);
|
||||||
|
expect(output.resolvedExecutionState).toEqual({ executable: 1 });
|
||||||
|
expect(output.safeAssistantReplyBase).toContain("Нужны уточнения для надежного ответа.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null analysis context when runtime context is inactive", () => {
|
||||||
|
const output = buildAssistantDeepTurnPrePackagingContext({
|
||||||
|
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
|
||||||
|
routeSummary: null,
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: false,
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: "2020-07-01",
|
||||||
|
period_to: "2020-07-31",
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto"
|
||||||
|
},
|
||||||
|
assistantReply: "ok",
|
||||||
|
extractDroppedIntentSegments: () => [],
|
||||||
|
buildDebugRoutes: () => [],
|
||||||
|
extractExecutionState: () => null,
|
||||||
|
sanitizeReply: (value) => value
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.analysisContextForContract).toBeNull();
|
||||||
|
expect(output.routesForDebug).toEqual([]);
|
||||||
|
expect(output.safeAssistantReplyBase).toBe("ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildAssistantDeepTurnSuccessResponse } from "../src/services/assistantDeepTurnResponseBuilder";
|
||||||
|
import type { AssistantConversationItem } from "../src/types/assistant";
|
||||||
|
|
||||||
|
function buildAssistantItem(): AssistantConversationItem {
|
||||||
|
return {
|
||||||
|
message_id: "msg-1",
|
||||||
|
session_id: "asst-1",
|
||||||
|
role: "assistant",
|
||||||
|
text: "ok",
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-10T10:00:00.000Z",
|
||||||
|
trace_id: "trace-1",
|
||||||
|
debug: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant deep turn response builder", () => {
|
||||||
|
it("builds canonical assistant message response envelope", () => {
|
||||||
|
const assistantItem = buildAssistantItem();
|
||||||
|
const response = buildAssistantDeepTurnSuccessResponse({
|
||||||
|
sessionId: "asst-1",
|
||||||
|
assistantReply: "ответ",
|
||||||
|
replyType: "factual",
|
||||||
|
conversationItem: assistantItem,
|
||||||
|
debug: {
|
||||||
|
trace_id: "trace-1",
|
||||||
|
prompt_version: "normalizer_v2_0_2",
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
fallback_type: "none",
|
||||||
|
route_summary: null,
|
||||||
|
fragments: [],
|
||||||
|
requirements_extracted: [],
|
||||||
|
coverage_report: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
routes: [],
|
||||||
|
retrieval_status: [],
|
||||||
|
retrieval_results: [],
|
||||||
|
answer_grounding_check: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
dropped_intent_segments: [],
|
||||||
|
answer_structure_v11: null,
|
||||||
|
investigation_state_snapshot: null,
|
||||||
|
normalized: null
|
||||||
|
} as any,
|
||||||
|
conversation: [assistantItem]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
expect(response.session_id).toBe("asst-1");
|
||||||
|
expect(response.assistant_reply).toBe("ответ");
|
||||||
|
expect(response.reply_type).toBe("factual");
|
||||||
|
expect(response.conversation_item.message_id).toBe("msg-1");
|
||||||
|
expect(response.conversation).toEqual([assistantItem]);
|
||||||
|
expect(response.debug.trace_id).toBe("trace-1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AssistantExecutionPlanItem } from "../src/services/assistantQueryPlanning";
|
||||||
|
import { executeAssistantDeepTurnRetrievalPlan } from "../src/services/assistantDeepTurnRetrievalRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant deep turn retrieval runtime adapter", () => {
|
||||||
|
it("handles skipped, executed and failed plan items with stable call records", async () => {
|
||||||
|
const executionPlan: AssistantExecutionPlanItem[] = [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "no_route",
|
||||||
|
should_execute: false,
|
||||||
|
fragment_text: "clarify period",
|
||||||
|
no_route_reason: "insufficient_specificity",
|
||||||
|
clarification_reason: "domain_or_scope_unclear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
requirement_ids: ["R2"],
|
||||||
|
route: "store_canonical",
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: "show balances",
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F3",
|
||||||
|
requirement_ids: ["R3"],
|
||||||
|
route: "live_mcp_drilldown",
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: "tail check",
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeCalls: Array<{ fragmentId: string; route: string; rawStatus: string | null }> = [];
|
||||||
|
|
||||||
|
const output = await executeAssistantDeepTurnRetrievalPlan({
|
||||||
|
executionPlan,
|
||||||
|
liveTemporalHint: null,
|
||||||
|
executeRouteRuntime: async (route) => {
|
||||||
|
if (route === "live_mcp_drilldown") {
|
||||||
|
throw new Error("route failed");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: { route },
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "high",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mapNoRouteReason: (reason) => (reason === "insufficient_specificity" ? "Needs clarification." : "No-route decision."),
|
||||||
|
buildSkippedResult: () =>
|
||||||
|
({
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "no_route",
|
||||||
|
status: "partial"
|
||||||
|
}) as any,
|
||||||
|
normalizeRetrievalResultFn: ((fragmentId: string, _requirementIds: string[], route: string, raw: Record<string, unknown>) => {
|
||||||
|
normalizeCalls.push({
|
||||||
|
fragmentId,
|
||||||
|
route,
|
||||||
|
rawStatus: typeof raw.status === "string" ? raw.status : null
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
fragment_id: fragmentId,
|
||||||
|
route,
|
||||||
|
status: raw.status
|
||||||
|
} as any;
|
||||||
|
}) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.retrievalCalls).toEqual([
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "no_route",
|
||||||
|
status: "skipped",
|
||||||
|
query_text: "clarify period",
|
||||||
|
reason: "Needs clarification."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
requirement_ids: ["R2"],
|
||||||
|
route: "store_canonical",
|
||||||
|
status: "executed",
|
||||||
|
query_text: "show balances",
|
||||||
|
reason: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F3",
|
||||||
|
requirement_ids: ["R3"],
|
||||||
|
route: "live_mcp_drilldown",
|
||||||
|
status: "failed",
|
||||||
|
query_text: "tail check",
|
||||||
|
reason: "route failed"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(output.retrievalResultsRaw).toHaveLength(2);
|
||||||
|
expect((output.retrievalResultsRaw[0].raw_result as Record<string, unknown>).status).toBe("ok");
|
||||||
|
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).status).toBe("error");
|
||||||
|
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).limitations).toEqual(["Route executor failed."]);
|
||||||
|
expect(output.retrievalResults).toHaveLength(3);
|
||||||
|
expect(output.retrievalResults[0].status).toBe("partial");
|
||||||
|
expect(normalizeCalls).toEqual([
|
||||||
|
{ fragmentId: "F2", route: "store_canonical", rawStatus: "ok" },
|
||||||
|
{ fragmentId: "F3", route: "live_mcp_drilldown", rawStatus: "error" }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes live temporal hint into route runtime execution", async () => {
|
||||||
|
const executionPlan: AssistantExecutionPlanItem[] = [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
should_execute: true,
|
||||||
|
fragment_text: "check as of date",
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let capturedTemporalHint: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
await executeAssistantDeepTurnRetrievalPlan({
|
||||||
|
executionPlan,
|
||||||
|
liveTemporalHint: {
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "analysis_context"
|
||||||
|
},
|
||||||
|
executeRouteRuntime: async (_route, _fragmentText, options) => {
|
||||||
|
capturedTemporalHint = options.temporalHint as unknown as Record<string, unknown>;
|
||||||
|
return { status: "ok" };
|
||||||
|
},
|
||||||
|
mapNoRouteReason: () => "No-route decision.",
|
||||||
|
buildSkippedResult: (() => ({ status: "partial" })) as any,
|
||||||
|
normalizeRetrievalResultFn: (() => ({ status: "ok" })) as any
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(capturedTemporalHint).toEqual({
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "analysis_context"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
import { assembleAssistantEvidenceBundle } from "../src/services/assistantEvidenceBundleAssembler";
|
||||||
|
|
||||||
|
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
|
||||||
|
return {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant evidence bundle assembler", () => {
|
||||||
|
it("builds evidence contract and retrieval status from the same retrieval set", () => {
|
||||||
|
const assembled = assembleAssistantEvidenceBundle({
|
||||||
|
retrievalCalls: [{ route: "hybrid_store_plus_live" }, { route: "store_canonical" }],
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
evidence_id: "ev-1",
|
||||||
|
claim_ref: "requirement:R1",
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "amount",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: "signal",
|
||||||
|
confidence: "medium",
|
||||||
|
limitation: null,
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
buildRetrieval({
|
||||||
|
fragment_id: "F2",
|
||||||
|
requirement_ids: ["R2"],
|
||||||
|
route: "store_canonical",
|
||||||
|
status: "error",
|
||||||
|
result_type: "list",
|
||||||
|
errors: ["timeout"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assembled.evidenceBundleContractV1.retrieval_calls_total).toBe(2);
|
||||||
|
expect(assembled.evidenceBundleContractV1.retrieval_results_total).toBe(2);
|
||||||
|
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.ok).toBe(1);
|
||||||
|
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.error).toBe(1);
|
||||||
|
expect(assembled.evidenceBundleContractV1.evidence_total).toBe(1);
|
||||||
|
expect(assembled.evidenceBundleContractV1.source_refs_total).toBe(1);
|
||||||
|
expect(assembled.retrievalStatus).toEqual([
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
requirement_ids: ["R2"],
|
||||||
|
route: "store_canonical",
|
||||||
|
status: "error",
|
||||||
|
result_type: "list"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { RouteHintSummary } from "../src/types/normalizer";
|
||||||
|
import type { UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
import { createEmptyInvestigationState } from "../src/services/investigationState";
|
||||||
|
import {
|
||||||
|
buildAssistantInvestigationStateSnapshot,
|
||||||
|
persistAssistantInvestigationStateSnapshot
|
||||||
|
} from "../src/services/assistantInvestigationStateRuntimeAdapter";
|
||||||
|
|
||||||
|
function buildRouteSummary(): RouteHintSummary {
|
||||||
|
return {
|
||||||
|
mode: "deterministic_v2",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
planner: {
|
||||||
|
total_fragments: 1,
|
||||||
|
in_scope_fragments: 1,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
domain_relevance: "in_scope",
|
||||||
|
business_scope: "company_specific_accounting",
|
||||||
|
candidate_labels: ["anomaly_probe"],
|
||||||
|
decision_flags: {
|
||||||
|
has_multi_entity_scope: false,
|
||||||
|
asks_for_chain_explanation: false,
|
||||||
|
asks_for_ranking_or_top: false,
|
||||||
|
asks_for_period_summary: false,
|
||||||
|
asks_for_rule_check: true,
|
||||||
|
asks_for_anomaly_scan: true,
|
||||||
|
asks_for_exact_object_trace: false,
|
||||||
|
asks_for_evidence: true,
|
||||||
|
mentions_period_close_context: false
|
||||||
|
},
|
||||||
|
route: "store_feature_risk",
|
||||||
|
reason: "test-route"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
fallback: {
|
||||||
|
type: "none",
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrievalResult(): UnifiedRetrievalResult {
|
||||||
|
return {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "store_feature_risk",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant investigation state runtime adapter", () => {
|
||||||
|
it("returns null and skips persist when feature disabled", () => {
|
||||||
|
const snapshot = buildAssistantInvestigationStateSnapshot({
|
||||||
|
featureEnabled: false,
|
||||||
|
previousState: createEmptyInvestigationState("asst-1", "2026-04-10T10:00:00.000Z"),
|
||||||
|
timestamp: "2026-04-10T10:01:00.000Z",
|
||||||
|
questionId: "msg-1",
|
||||||
|
userMessage: "проверь 60.01",
|
||||||
|
routeSummary: buildRouteSummary(),
|
||||||
|
requirements: [],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
retrievalResults: [],
|
||||||
|
replyType: "factual",
|
||||||
|
followupApplied: false
|
||||||
|
});
|
||||||
|
expect(snapshot).toBeNull();
|
||||||
|
|
||||||
|
let persistCalled = false;
|
||||||
|
const persisted = persistAssistantInvestigationStateSnapshot({
|
||||||
|
featureEnabled: false,
|
||||||
|
sessionId: "asst-1",
|
||||||
|
snapshot: null,
|
||||||
|
persist: () => {
|
||||||
|
persistCalled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(persisted).toBe(false);
|
||||||
|
expect(persistCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds snapshot and persists it when feature enabled", () => {
|
||||||
|
const previous = createEmptyInvestigationState("asst-2", "2026-04-10T10:00:00.000Z");
|
||||||
|
const snapshot = buildAssistantInvestigationStateSnapshot({
|
||||||
|
featureEnabled: true,
|
||||||
|
previousState: previous,
|
||||||
|
timestamp: "2026-04-10T10:01:00.000Z",
|
||||||
|
questionId: "msg-2",
|
||||||
|
userMessage: "проверь счет 60.01 за 2020-07",
|
||||||
|
routeSummary: buildRouteSummary(),
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "проверить счет 60.01",
|
||||||
|
subject_tokens: ["account_60.01"],
|
||||||
|
status: "covered",
|
||||||
|
route: "store_feature_risk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
retrievalResults: [buildRetrievalResult()],
|
||||||
|
replyType: "factual",
|
||||||
|
followupApplied: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot).not.toBeNull();
|
||||||
|
expect(snapshot?.turn_index).toBe(1);
|
||||||
|
expect(snapshot?.question_id).toBe("msg-2");
|
||||||
|
|
||||||
|
let persistedSessionId: string | null = null;
|
||||||
|
let persistedQuestionId: string | null = null;
|
||||||
|
const persisted = persistAssistantInvestigationStateSnapshot({
|
||||||
|
featureEnabled: true,
|
||||||
|
sessionId: "asst-2",
|
||||||
|
snapshot: snapshot,
|
||||||
|
persist: (sessionId, state) => {
|
||||||
|
persistedSessionId = sessionId;
|
||||||
|
persistedQuestionId = state.question_id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(persisted).toBe(true);
|
||||||
|
expect(persistedSessionId).toBe("asst-2");
|
||||||
|
expect(persistedQuestionId).toBe("msg-2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -209,6 +209,33 @@ describe("assistant orchestration contract", () => {
|
||||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
|
||||||
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage:
|
||||||
|
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?",
|
||||||
|
effectiveAddressUserMessage:
|
||||||
|
"Показать контрагентов с вероятным несогласием между сальдо и актом сверки. Рекомендовать запросить сверку.",
|
||||||
|
followupContext: null,
|
||||||
|
llmPreDecomposeMeta: {
|
||||||
|
applied: true,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "address_query",
|
||||||
|
mode_confidence: "high",
|
||||||
|
intent: "unknown",
|
||||||
|
intent_confidence: "low"
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(true);
|
||||||
|
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(decision.livingMode).toBe("address_data");
|
||||||
|
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||||
|
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
|
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
|
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildDeepAnalysisProcessedLogDetails } from "../src/services/assistantMessageLogAssembler";
|
||||||
|
|
||||||
|
function baseInput() {
|
||||||
|
return {
|
||||||
|
sessionId: "asst-1",
|
||||||
|
messageId: "msg-1",
|
||||||
|
userMessage: "проверь 60.01",
|
||||||
|
normalizerOutput: { schema_version: "normalized_query_v2_0_2" },
|
||||||
|
executionPlan: [{ fragment_id: "F1", route: "hybrid_store_plus_live", should_execute: true }],
|
||||||
|
resolvedExecutionState: { executable: 1 },
|
||||||
|
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
|
||||||
|
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
|
||||||
|
retrievalResultsRaw: [{ status: "ok" }],
|
||||||
|
retrievalResultsNormalized: [{ status: "ok" }],
|
||||||
|
requirementsExtracted: [{ requirement_id: "R1" }],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: ["signal"],
|
||||||
|
selection_reason_summary: ["ranked"]
|
||||||
|
},
|
||||||
|
replyType: "factual",
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
questionTypeClass: "factual_lookup",
|
||||||
|
companyAnchors: { companies: ["demo"] },
|
||||||
|
runtimeAnalysisContext: {
|
||||||
|
active: true,
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
period_from: null,
|
||||||
|
period_to: null,
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "auto" as const
|
||||||
|
},
|
||||||
|
businessScopeResolution: {
|
||||||
|
business_scope_raw: ["company_specific_accounting"],
|
||||||
|
business_scope_resolved: ["company_specific_accounting"],
|
||||||
|
company_grounding_applied: true,
|
||||||
|
scope_resolution_reason: ["resolved"]
|
||||||
|
},
|
||||||
|
temporalGuard: {
|
||||||
|
raw_time_anchor: "2020-07",
|
||||||
|
raw_time_scope: "month",
|
||||||
|
resolved_time_anchor: "2020-07",
|
||||||
|
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||||
|
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
|
||||||
|
temporal_guard_input: "2020-07",
|
||||||
|
temporal_alignment_status: "aligned",
|
||||||
|
temporal_resolution_source: "analysis_context",
|
||||||
|
temporal_guard_basis: "analysis_context",
|
||||||
|
temporal_guard_applied: true,
|
||||||
|
temporal_guard_outcome: "pass"
|
||||||
|
},
|
||||||
|
polarityAudit: {
|
||||||
|
raw_numeric_tokens: ["60.01"],
|
||||||
|
classified_numeric_tokens: [{ token: "60.01" }],
|
||||||
|
rejected_as_non_accounts: [],
|
||||||
|
resolved_account_anchors: ["60.01"]
|
||||||
|
},
|
||||||
|
claimAnchorAudit: {
|
||||||
|
settlement_role: "supplier",
|
||||||
|
settlement_role_resolution_reason: ["account_60_detected"],
|
||||||
|
polarity_resolution_status: "resolved"
|
||||||
|
},
|
||||||
|
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
|
||||||
|
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
|
||||||
|
rbpLiveRouteAudit: null,
|
||||||
|
faLiveRouteAudit: null,
|
||||||
|
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
|
||||||
|
followupStateUsage: null,
|
||||||
|
compositionDebug: {
|
||||||
|
problem_centric_answer_applied: true,
|
||||||
|
problem_units_used_count: 1,
|
||||||
|
problem_answer_mode: "stage3_lifecycle_aware_v1",
|
||||||
|
problem_unit_ids_used: ["pu-1"],
|
||||||
|
fallback_type: "none"
|
||||||
|
},
|
||||||
|
outcomeClassV1: "FULLY_ANSWERED",
|
||||||
|
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
|
||||||
|
answerStructureV11: { schema_version: "answer_structure_v1_1" },
|
||||||
|
investigationStateSnapshot: { status: "active" },
|
||||||
|
assistantReply: "ok",
|
||||||
|
traceId: "trace-1"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant message log assembler", () => {
|
||||||
|
it("builds deep analysis log details and resolves full coverage status", () => {
|
||||||
|
const details = buildDeepAnalysisProcessedLogDetails(baseInput());
|
||||||
|
expect(details.session_id).toBe("asst-1");
|
||||||
|
expect(details.coverage_status).toBe("full");
|
||||||
|
expect(details.analysis_context).toMatchObject({
|
||||||
|
as_of_date: "2020-07-31"
|
||||||
|
});
|
||||||
|
expect(details.problem_unit_ids_used).toEqual(["pu-1"]);
|
||||||
|
expect(details.reply_type).toBe("factual");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks partial coverage and omits optional sections when empty", () => {
|
||||||
|
const input = baseInput();
|
||||||
|
input.coverageReport.requirements_covered = 0;
|
||||||
|
input.coverageReport.requirements_uncovered = ["R1"];
|
||||||
|
input.runtimeAnalysisContext.active = false;
|
||||||
|
input.followupStateUsage = null;
|
||||||
|
input.compositionDebug.problem_unit_ids_used = [];
|
||||||
|
|
||||||
|
const details = buildDeepAnalysisProcessedLogDetails(input);
|
||||||
|
expect(details.coverage_status).toBe("partial_or_limited");
|
||||||
|
expect(details.analysis_context).toBeNull();
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(details, "followup_state_usage")).toBe(false);
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(details, "problem_unit_ids_used")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
import type { RouteHintSummary } from "../src/types/normalizer";
|
||||||
|
import {
|
||||||
|
buildAssistantCoverageContractV1,
|
||||||
|
buildAssistantEvidenceBundleContractV1,
|
||||||
|
buildAssistantExecutionPlanContractV1,
|
||||||
|
buildAssistantQueryFrameContractV1,
|
||||||
|
classifyAssistantOutcomeClassV1
|
||||||
|
} from "../src/services/assistantOrchestrationContracts";
|
||||||
|
|
||||||
|
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
|
||||||
|
return {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: ["R1"],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
|
||||||
|
return {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: ["R1"],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
|
||||||
|
return {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [],
|
||||||
|
summary: {},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "medium",
|
||||||
|
limitations: [],
|
||||||
|
errors: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRouteSummary(): RouteHintSummary {
|
||||||
|
return {
|
||||||
|
mode: "deterministic_v2",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
planner: {
|
||||||
|
total_fragments: 2,
|
||||||
|
in_scope_fragments: 2,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [],
|
||||||
|
fallback: {
|
||||||
|
type: "none",
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant orchestration contracts v1", () => {
|
||||||
|
it("builds query frame and execution plan contracts with normalized analysis context", () => {
|
||||||
|
const queryFrame = buildAssistantQueryFrameContractV1({
|
||||||
|
userMessage: "Покажи хвосты по счету 60",
|
||||||
|
normalizedQuestion: "Покажи хвосты по счету 60",
|
||||||
|
normalized: {
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: "Покажи хвосты по счету 60",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [{ fragment_id: "F1" }, { fragment_id: "F2" }],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
routeSummary: buildRouteSummary(),
|
||||||
|
droppedIntentSegments: ["лишний сегмент"],
|
||||||
|
analysisContext: {
|
||||||
|
as_of_date: "2020-07-31",
|
||||||
|
source: "eval_analysis_date",
|
||||||
|
snapshot_mode: "unexpected_mode"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const executionPlan = buildAssistantExecutionPlanContractV1({
|
||||||
|
executionPlan: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
should_execute: true,
|
||||||
|
no_route_reason: null,
|
||||||
|
clarification_reason: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
requirement_ids: ["R2"],
|
||||||
|
route: "no_route",
|
||||||
|
should_execute: false,
|
||||||
|
no_route_reason: "insufficient_specificity",
|
||||||
|
clarification_reason: "need_period"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "req1",
|
||||||
|
subject_tokens: [],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requirement_id: "R2",
|
||||||
|
source_fragment_id: "F2",
|
||||||
|
requirement_text: "req2",
|
||||||
|
subject_tokens: [],
|
||||||
|
status: "clarification_needed",
|
||||||
|
route: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryFrame.schema_version).toBe("assistant_query_frame_v1");
|
||||||
|
expect(queryFrame.route_summary_mode).toBe("deterministic_v2");
|
||||||
|
expect(queryFrame.fragments_total).toBe(2);
|
||||||
|
expect(queryFrame.analysis_context?.as_of_date).toBe("2020-07-31");
|
||||||
|
expect(queryFrame.analysis_context?.snapshot_mode).toBe("auto");
|
||||||
|
expect(executionPlan.schema_version).toBe("assistant_execution_plan_v1");
|
||||||
|
expect(executionPlan.steps).toHaveLength(2);
|
||||||
|
expect(executionPlan.requirements_total).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("classifies fully answered and misrouted outcomes", () => {
|
||||||
|
const fullyAnswered = classifyAssistantOutcomeClassV1({
|
||||||
|
replyType: "factual_with_explanation",
|
||||||
|
coverageReport: buildCoverage({
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
}),
|
||||||
|
grounding: buildGrounding({
|
||||||
|
status: "grounded",
|
||||||
|
missing_requirements: []
|
||||||
|
}),
|
||||||
|
retrievalResults: [buildRetrieval({ status: "ok" })]
|
||||||
|
});
|
||||||
|
|
||||||
|
const misrouted = classifyAssistantOutcomeClassV1({
|
||||||
|
replyType: "route_mismatch_blocked",
|
||||||
|
coverageReport: buildCoverage(),
|
||||||
|
grounding: buildGrounding({
|
||||||
|
status: "route_mismatch_blocked"
|
||||||
|
}),
|
||||||
|
retrievalResults: [buildRetrieval({ status: "partial" })]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fullyAnswered).toBe("FULLY_ANSWERED");
|
||||||
|
expect(misrouted).toBe("MISROUTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("classifies tooling and entity-binding failures", () => {
|
||||||
|
const toolingBlocked = classifyAssistantOutcomeClassV1({
|
||||||
|
replyType: "factual",
|
||||||
|
coverageReport: buildCoverage(),
|
||||||
|
grounding: buildGrounding(),
|
||||||
|
retrievalResults: [buildRetrieval({ status: "error" }), buildRetrieval({ status: "error" })]
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityBindingFailure = classifyAssistantOutcomeClassV1({
|
||||||
|
replyType: "no_grounded_answer",
|
||||||
|
coverageReport: buildCoverage({
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: ["R1"]
|
||||||
|
}),
|
||||||
|
grounding: buildGrounding({
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: ["R1"]
|
||||||
|
}),
|
||||||
|
retrievalResults: [buildRetrieval({ status: "empty" })]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toolingBlocked).toBe("BLOCKED_BY_TOOLING");
|
||||||
|
expect(entityBindingFailure).toBe("FAILED_TO_BIND_ENTITIES");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds evidence bundle and coverage contracts", () => {
|
||||||
|
const evidenceBundle = buildAssistantEvidenceBundleContractV1({
|
||||||
|
retrievalCalls: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
status: "ok",
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
evidence_id: "ev-1",
|
||||||
|
claim_ref: "requirement:R1",
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "doc-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "amount",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: "signal",
|
||||||
|
confidence: "medium",
|
||||||
|
limitation: null,
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
limitations: ["needs_extra_period"]
|
||||||
|
}),
|
||||||
|
buildRetrieval({
|
||||||
|
status: "partial",
|
||||||
|
evidence: [],
|
||||||
|
errors: ["timeout"]
|
||||||
|
}),
|
||||||
|
buildRetrieval({
|
||||||
|
status: "error",
|
||||||
|
evidence: [],
|
||||||
|
errors: ["mcp_unavailable"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const outcomeClass = classifyAssistantOutcomeClassV1({
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
coverageReport: buildCoverage({
|
||||||
|
requirements_total: 2,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: ["R2"]
|
||||||
|
}),
|
||||||
|
grounding: buildGrounding({
|
||||||
|
status: "partial",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: ["R2"]
|
||||||
|
}),
|
||||||
|
retrievalResults: [buildRetrieval({ status: "ok" }), buildRetrieval({ status: "partial" })]
|
||||||
|
});
|
||||||
|
|
||||||
|
const coverageContract = buildAssistantCoverageContractV1({
|
||||||
|
coverageReport: buildCoverage({
|
||||||
|
requirements_total: 2,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: ["R2"]
|
||||||
|
}),
|
||||||
|
grounding: buildGrounding({
|
||||||
|
status: "partial",
|
||||||
|
missing_requirements: ["R2"]
|
||||||
|
}),
|
||||||
|
outcomeClass
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(evidenceBundle.retrieval_calls_total).toBe(3);
|
||||||
|
expect(evidenceBundle.retrieval_results_total).toBe(3);
|
||||||
|
expect(evidenceBundle.retrieval_status_breakdown.ok).toBe(1);
|
||||||
|
expect(evidenceBundle.retrieval_status_breakdown.partial).toBe(1);
|
||||||
|
expect(evidenceBundle.retrieval_status_breakdown.error).toBe(1);
|
||||||
|
expect(evidenceBundle.evidence_total).toBe(1);
|
||||||
|
expect(evidenceBundle.source_refs_total).toBe(1);
|
||||||
|
expect(evidenceBundle.error_total).toBe(2);
|
||||||
|
expect(coverageContract.outcome_class).toBe("PARTIALLY_ANSWERED");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { runAssistantCoverageGroundingPipeline } from "../src/services/assistantOrchestrationRuntimeAdapter";
|
||||||
|
|
||||||
|
describe("assistant orchestration runtime adapter", () => {
|
||||||
|
it("runs requirement -> coverage -> grounding pipeline in order", () => {
|
||||||
|
const requirementExtraction = {
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "req",
|
||||||
|
subject_tokens: ["account_60"],
|
||||||
|
status: "covered" as const,
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
byFragment: new Map<string, string[]>([["F1", ["R1"]]])
|
||||||
|
};
|
||||||
|
const coverageEvaluation = {
|
||||||
|
requirements: requirementExtraction.requirements,
|
||||||
|
coverage: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const groundingCheck = {
|
||||||
|
status: "grounded" as const,
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: ["why"],
|
||||||
|
selection_reason_summary: ["selection"]
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractRequirements = vi.fn(() => requirementExtraction);
|
||||||
|
const evaluateCoverage = vi.fn(() => coverageEvaluation);
|
||||||
|
const checkGrounding = vi.fn(() => groundingCheck);
|
||||||
|
|
||||||
|
const output = runAssistantCoverageGroundingPipeline({
|
||||||
|
routeSummary: null,
|
||||||
|
normalized: null,
|
||||||
|
userMessage: "test",
|
||||||
|
retrievalResults: [],
|
||||||
|
extractRequirements,
|
||||||
|
evaluateCoverage,
|
||||||
|
checkGrounding
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractRequirements).toHaveBeenCalledTimes(1);
|
||||||
|
expect(evaluateCoverage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(checkGrounding).toHaveBeenCalledTimes(1);
|
||||||
|
expect(output.requirementExtraction).toBe(requirementExtraction);
|
||||||
|
expect(output.coverageEvaluation).toBe(coverageEvaluation);
|
||||||
|
expect(output.groundingCheckBase).toBe(groundingCheck);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reuses precomputed requirement extraction when provided", () => {
|
||||||
|
const precomputed = {
|
||||||
|
requirements: [],
|
||||||
|
byFragment: new Map<string, string[]>()
|
||||||
|
};
|
||||||
|
const extractRequirements = vi.fn(() => {
|
||||||
|
throw new Error("extractRequirements should not be called");
|
||||||
|
});
|
||||||
|
const evaluateCoverage = vi.fn(() => ({
|
||||||
|
requirements: [],
|
||||||
|
coverage: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const checkGrounding = vi.fn(() => ({
|
||||||
|
status: "no_grounded_answer" as const,
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
const output = runAssistantCoverageGroundingPipeline({
|
||||||
|
routeSummary: null,
|
||||||
|
normalized: null,
|
||||||
|
userMessage: "test",
|
||||||
|
retrievalResults: [],
|
||||||
|
requirementExtraction: precomputed,
|
||||||
|
extractRequirements,
|
||||||
|
evaluateCoverage,
|
||||||
|
checkGrounding
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractRequirements).not.toHaveBeenCalled();
|
||||||
|
expect(evaluateCoverage).toHaveBeenCalledWith(precomputed.requirements, []);
|
||||||
|
expect(output.requirementExtraction).toBe(precomputed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildDebugRoutesFromRoute,
|
||||||
|
buildExecutionPlanFromRoute,
|
||||||
|
buildFragmentTextById
|
||||||
|
} from "../src/services/assistantQueryPlanning";
|
||||||
|
|
||||||
|
describe("assistant query planning module", () => {
|
||||||
|
it("builds fragment text map with account hints enrichment", () => {
|
||||||
|
const map = buildFragmentTextById([
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
raw_fragment_text: "проверить хвосты",
|
||||||
|
normalized_fragment_text: "",
|
||||||
|
account_hints: ["60.01", "60.02"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
raw_fragment_text: "проверить 60.01 по поставщику",
|
||||||
|
normalized_fragment_text: "",
|
||||||
|
account_hints: ["60.01"]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(map.get("F1")).toBe("проверить хвосты, по счету 60.01, 60.02");
|
||||||
|
expect(map.get("F2")).toBe("проверить 60.01 по поставщику");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds deterministic execution plan from route summary", () => {
|
||||||
|
const executionPlan = buildExecutionPlanFromRoute({
|
||||||
|
routeSummary: {
|
||||||
|
mode: "deterministic_v2",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
planner: {
|
||||||
|
total_fragments: 2,
|
||||||
|
in_scope_fragments: 2,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "no_route",
|
||||||
|
no_route_reason: "insufficient_specificity",
|
||||||
|
clarification_reason: "missing anchor",
|
||||||
|
reason: "needs clarification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fragment_id: "F2",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
reason: "route selected"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
fallback: {
|
||||||
|
type: "none",
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
userMessage: "base question",
|
||||||
|
fragmentTextById: new Map([
|
||||||
|
["F1", "уточни период"],
|
||||||
|
["F2", "проверь по 60.01"]
|
||||||
|
]),
|
||||||
|
requirementByFragment: new Map([
|
||||||
|
["F1", ["R1"]],
|
||||||
|
["F2", ["R2"]]
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(executionPlan).toHaveLength(2);
|
||||||
|
expect(executionPlan[0]).toMatchObject({
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "no_route",
|
||||||
|
should_execute: false,
|
||||||
|
no_route_reason: "insufficient_specificity",
|
||||||
|
clarification_reason: "missing anchor"
|
||||||
|
});
|
||||||
|
expect(executionPlan[1]).toMatchObject({
|
||||||
|
fragment_id: "F2",
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
should_execute: true,
|
||||||
|
no_route_reason: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds legacy debug routes via resolver", () => {
|
||||||
|
const routes = buildDebugRoutesFromRoute({
|
||||||
|
routeSummary: {
|
||||||
|
mode: "legacy_v1",
|
||||||
|
intent_class: "partner_reconciliation",
|
||||||
|
route_hint: "store_canonical",
|
||||||
|
confidence: "medium"
|
||||||
|
} as any,
|
||||||
|
resolveLegacyRouteReason: (route) => `legacy:${route}`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(routes).toHaveLength(1);
|
||||||
|
expect(routes[0]).toMatchObject({
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "store_canonical",
|
||||||
|
reason: "legacy:store_canonical",
|
||||||
|
confidence: "medium"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AssistantConversationItem, AssistantSessionState } from "../src/types/assistant";
|
||||||
|
import { commitAssistantTurnAndLog } from "../src/services/assistantTurnCommitRuntimeAdapter";
|
||||||
|
|
||||||
|
function buildAssistantItem(): AssistantConversationItem {
|
||||||
|
return {
|
||||||
|
message_id: "msg-1",
|
||||||
|
session_id: "asst-1",
|
||||||
|
role: "assistant",
|
||||||
|
text: "ok",
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-10T10:01:00.000Z",
|
||||||
|
trace_id: "trace-1",
|
||||||
|
debug: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant turn commit runtime adapter", () => {
|
||||||
|
it("appends item, persists existing session, clones conversation and logs event", () => {
|
||||||
|
const assistantItem = buildAssistantItem();
|
||||||
|
const storedSession: AssistantSessionState = {
|
||||||
|
session_id: "asst-1",
|
||||||
|
updated_at: "2026-04-10T10:01:00.000Z",
|
||||||
|
items: [assistantItem],
|
||||||
|
investigation_state: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const calls = {
|
||||||
|
append: 0,
|
||||||
|
persist: 0,
|
||||||
|
log: 0
|
||||||
|
};
|
||||||
|
let loggedPayload: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
const result = commitAssistantTurnAndLog({
|
||||||
|
sessionId: "asst-1",
|
||||||
|
assistantItem,
|
||||||
|
eventType: "assistant_message",
|
||||||
|
logDetails: { some: "details" },
|
||||||
|
appendItem: () => {
|
||||||
|
calls.append += 1;
|
||||||
|
},
|
||||||
|
getSession: () => storedSession,
|
||||||
|
persistSession: () => {
|
||||||
|
calls.persist += 1;
|
||||||
|
},
|
||||||
|
cloneConversation: (items) => items.map((item) => ({ ...item, debug: item.debug ? { ...item.debug } : null })),
|
||||||
|
logEvent: (payload) => {
|
||||||
|
calls.log += 1;
|
||||||
|
loggedPayload = payload as unknown as Record<string, unknown>;
|
||||||
|
},
|
||||||
|
nowIso: () => "2026-04-10T10:02:00.000Z"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(calls.append).toBe(1);
|
||||||
|
expect(calls.persist).toBe(1);
|
||||||
|
expect(calls.log).toBe(1);
|
||||||
|
expect(result.currentSession?.session_id).toBe("asst-1");
|
||||||
|
expect(result.conversation).toEqual([assistantItem]);
|
||||||
|
expect(result.conversation).not.toBe(storedSession.items);
|
||||||
|
expect(loggedPayload?.["sessionId"]).toBe("asst-1");
|
||||||
|
expect(loggedPayload?.["eventType"]).toBe("assistant_message");
|
||||||
|
expect(loggedPayload?.["message"]).toBe("assistant_message_processed");
|
||||||
|
expect(loggedPayload?.["timestamp"]).toBe("2026-04-10T10:02:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips persist when session is missing and still logs with empty conversation", () => {
|
||||||
|
const assistantItem = buildAssistantItem();
|
||||||
|
let persistCalled = false;
|
||||||
|
let logCalled = false;
|
||||||
|
|
||||||
|
const result = commitAssistantTurnAndLog({
|
||||||
|
sessionId: "asst-missing",
|
||||||
|
assistantItem,
|
||||||
|
eventType: "assistant_message",
|
||||||
|
logDetails: { x: 1 },
|
||||||
|
appendItem: () => {},
|
||||||
|
getSession: () => null,
|
||||||
|
persistSession: () => {
|
||||||
|
persistCalled = true;
|
||||||
|
},
|
||||||
|
cloneConversation: (items) => items.map((item) => ({ ...item })),
|
||||||
|
logEvent: () => {
|
||||||
|
logCalled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(persistCalled).toBe(false);
|
||||||
|
expect(logCalled).toBe(true);
|
||||||
|
expect(result.currentSession).toBeNull();
|
||||||
|
expect(result.conversation).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -11,7 +11,8 @@ const FLAG_KEYS = [
|
||||||
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
|
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
|
||||||
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1",
|
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1",
|
||||||
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
|
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
|
||||||
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1"
|
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
|
||||||
|
"FEATURE_ASSISTANT_ADDRESS_QUERY_V1"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
|
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
|
||||||
|
|
@ -623,6 +624,7 @@ describe("wave10 settlement corrective regression", () => {
|
||||||
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = "1";
|
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = "1";
|
||||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = "1";
|
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = "1";
|
||||||
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = "1";
|
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = "1";
|
||||||
|
process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = "0";
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { createApp } = await import("../src/server");
|
const { createApp } = await import("../src/server");
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,9 @@ describe("questionTypeResolver", () => {
|
||||||
expect(resolveQuestionType("Почему не сходится 62.01/62.02?"))
|
expect(resolveQuestionType("Почему не сходится 62.01/62.02?"))
|
||||||
.toBe("why_breaks");
|
.toBe("why_breaks");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps generic non-why questions as unknown", () => {
|
||||||
|
expect(resolveQuestionType("Какие реализации стоит проверить заранее, чтобы не испортить отчетность за месяц?"))
|
||||||
|
.toBe("unknown");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue