Compare commits
10 Commits
a75da40178
...
de4e064885
| Author | SHA1 | Date |
|---|---|---|
|
|
de4e064885 | |
|
|
75e665e836 | |
|
|
76db8ef012 | |
|
|
95f3fdafbb | |
|
|
9d6e7066e0 | |
|
|
b4865f36b1 | |
|
|
4181bb9c9b | |
|
|
9d5ae69953 | |
|
|
98988fa635 | |
|
|
1752d1babf |
|
|
@ -20,6 +20,13 @@
|
|||
- `Необходимость жирного ручного прогона` must reflect how strongly the current change still needs a broad manual reality-check beyond unit tests, narrow replay, and build verification.
|
||||
- The percentages must be honest, architecture-aware, and useful for deciding whether the current pass is safe enough to trust without additional human validation.
|
||||
|
||||
## development_stage_progress_rule
|
||||
- After every completed development stage, always report `Прогресс модуля: X%`.
|
||||
- The percentage must describe progress inside the current large module or plan block, not the whole project.
|
||||
- If the stage belongs to a named large block, include that block name next to the percentage.
|
||||
- Use an integer `0%` to `100%` scale and keep the estimate architecture-aware, based on implemented runtime wiring, tests, replay coverage, and remaining integration risk.
|
||||
- Do not inflate progress because unit tests are green; semantic replay and real runtime wiring still count as unfinished work when they are pending.
|
||||
|
||||
## graphify
|
||||
|
||||
This project has a graphify knowledge graph at graphify-out/.
|
||||
|
|
|
|||
|
|
@ -682,6 +682,245 @@ Validation:
|
|||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts` passed 6/6;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Catalog Index Seed
|
||||
|
||||
The second implementation slice of Big Block 5 added the first machine-readable catalog brain for guarded MCP discovery:
|
||||
|
||||
- `assistantMcpCatalogIndex.ts`
|
||||
- `assistantMcpCatalogIndex.test.ts`
|
||||
|
||||
The catalog does not execute 1C/MCP calls yet.
|
||||
|
||||
It records what each reviewed discovery primitive is allowed to mean:
|
||||
|
||||
- business purpose;
|
||||
- required grounding axes;
|
||||
- optional axes;
|
||||
- output fact kinds;
|
||||
- minimum evidence floor.
|
||||
|
||||
This gives future planner/executor wiring a deterministic policy surface:
|
||||
|
||||
- every discovery primitive must have a catalog contract;
|
||||
- a discovery plan can be reviewed against catalog-required axes before execution;
|
||||
- missing axes degrade to `needs_more_axes` instead of a blind query;
|
||||
- blocked discovery plans remain blocked at catalog review level.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts` passed 11/11;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Planner Seed
|
||||
|
||||
The third implementation slice of Big Block 5 added a standalone planner over the discovery policy and catalog index:
|
||||
|
||||
- `assistantMcpDiscoveryPlanner.ts`
|
||||
- `assistantMcpDiscoveryPlanner.test.ts`
|
||||
|
||||
The planner still does not execute live MCP calls.
|
||||
|
||||
It converts current-turn meaning into a guarded discovery plan and immediately reviews it against catalog constraints:
|
||||
|
||||
- value-flow questions choose entity resolution, movement query, aggregation, and coverage probe;
|
||||
- document questions choose entity resolution, document query, and coverage probe;
|
||||
- lifecycle/activity-duration questions choose document evidence plus explicit evidence-basis explanation;
|
||||
- metadata questions stay in metadata inspection;
|
||||
- unclassified turns remain in clarification state rather than executing a blind query.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts` passed 17/17;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Runtime Dry-Run Adapter
|
||||
|
||||
The fourth implementation slice of Big Block 5 added a dry-run runtime adapter:
|
||||
|
||||
- `assistantMcpDiscoveryRuntimeAdapter.ts`
|
||||
- `assistantMcpDiscoveryRuntimeAdapter.test.ts`
|
||||
|
||||
This adapter still does not execute live MCP calls.
|
||||
|
||||
It turns planner output into an execution package that future live wiring can consume safely:
|
||||
|
||||
- ordered execution steps;
|
||||
- primitive purpose and expected fact kinds;
|
||||
- provided and missing grounding axes;
|
||||
- evidence floor and stop condition per primitive;
|
||||
- execution budget;
|
||||
- mandatory evidence gate inputs;
|
||||
- user-facing fallback for missing scope or policy blocks.
|
||||
|
||||
The contract explicitly records `mcp_execution_performed=false`, so this block cannot accidentally query 1C.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts` passed 21/21;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Pilot Executor
|
||||
|
||||
The fifth implementation slice of Big Block 5 added the first isolated execution pilot:
|
||||
|
||||
- `assistantMcpDiscoveryPilotExecutor.ts`
|
||||
- `assistantMcpDiscoveryPilotExecutor.test.ts`
|
||||
|
||||
This pilot is still not wired into the hot user-facing assistant runtime.
|
||||
|
||||
It can execute only one intentionally narrow primitive path:
|
||||
|
||||
- `counterparty_lifecycle_query_documents_v1`
|
||||
- lifecycle/activity-duration turn meaning;
|
||||
- `query_documents` through injected MCP dependencies;
|
||||
- all other planned primitives remain skipped in the pilot until dedicated executors exist.
|
||||
|
||||
The pilot preserves the safety rules from the previous layers:
|
||||
|
||||
- it will not execute when dry-run status needs clarification;
|
||||
- it will not execute unsupported ready plans;
|
||||
- MCP errors become limitations, not facts;
|
||||
- legal registration age remains an unknown fact;
|
||||
- business activity duration may only be treated as a bounded inference from confirmed 1C activity rows.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts` passed 25/25;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Answer Adapter
|
||||
|
||||
The sixth implementation slice of Big Block 5 added a human-safe answer draft adapter:
|
||||
|
||||
- `assistantMcpDiscoveryAnswerAdapter.ts`
|
||||
- `assistantMcpDiscoveryAnswerAdapter.test.ts`
|
||||
|
||||
This adapter is still not wired into the hot assistant runtime.
|
||||
|
||||
It converts pilot evidence into an answer draft that can later be consumed by the final answer layer:
|
||||
|
||||
- confirmed lines;
|
||||
- bounded inference lines;
|
||||
- unknown fact boundaries;
|
||||
- user-facing limitations;
|
||||
- next-step guidance;
|
||||
- `must_not_claim` constraints.
|
||||
|
||||
The adapter explicitly blocks internal mechanics from user-facing lines:
|
||||
|
||||
- MCP primitive names;
|
||||
- query text;
|
||||
- debug/reason mechanics;
|
||||
- raw runtime/planner/catalog codes.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts` passed 29/29;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Runtime Bridge
|
||||
|
||||
The seventh implementation slice of Big Block 5 added a runtime bridge contract:
|
||||
|
||||
- `assistantMcpDiscoveryRuntimeBridge.ts`
|
||||
- `assistantMcpDiscoveryRuntimeBridge.test.ts`
|
||||
|
||||
This bridge is still not wired into the hot assistant answer path.
|
||||
|
||||
It composes the already separated discovery layers into one orchestration-facing contract:
|
||||
|
||||
- planner;
|
||||
- isolated pilot executor;
|
||||
- human-safe answer draft;
|
||||
- bridge status;
|
||||
- explicit `hot_runtime_wired=false`;
|
||||
- business-fact answer authorization flags;
|
||||
- clarification and unsupported-state flags.
|
||||
|
||||
This gives future runtime integration a single object to attach to debug/evidence flow without letting discovery silently become a user-facing answer path.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 33/33;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Turn Input Adapter
|
||||
|
||||
The eighth implementation slice of Big Block 5 added the adapter between current-turn meaning/predecompose contracts and MCP discovery input:
|
||||
|
||||
- `assistantMcpDiscoveryTurnInputAdapter.ts`
|
||||
- `assistantMcpDiscoveryTurnInputAdapter.test.ts`
|
||||
|
||||
This adapter still does not wire discovery into the hot answer path.
|
||||
|
||||
It solves the next runtime integration seam:
|
||||
|
||||
- maps `assistant_turn_meaning_v1` into `AssistantMcpDiscoveryTurnMeaningRef`;
|
||||
- extracts counterparty, organization, and date scope from predecompose contracts;
|
||||
- bootstraps lifecycle/activity-duration questions from raw user wording;
|
||||
- treats unsupported-but-understood meaning as discovery-eligible instead of stale-replay fallback;
|
||||
- avoids serializing structured entity candidates as `[object Object]`;
|
||||
- keeps supported exact routes out of MCP discovery.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 37/37;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Runtime Entry Point
|
||||
|
||||
The ninth implementation slice of Big Block 5 added a runtime entry point:
|
||||
|
||||
- `assistantMcpDiscoveryRuntimeEntryPoint.ts`
|
||||
- `assistantMcpDiscoveryRuntimeEntryPoint.test.ts`
|
||||
|
||||
This entry point is still not wired into the hot assistant answer path.
|
||||
|
||||
It gives runtime integration a single safe call boundary:
|
||||
|
||||
- builds discovery turn input from assistant turn meaning, predecompose contract, and raw wording;
|
||||
- skips supported exact turns before any discovery execution;
|
||||
- executes the runtime bridge only for discovery-eligible turns;
|
||||
- keeps `hot_runtime_wired=false`;
|
||||
- exposes `discovery_attempted`, `entry_status`, `turn_input`, and optional `bridge`.
|
||||
|
||||
This is the first complete non-hot pipeline from current-turn context to guarded MCP discovery result.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 40/40;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Debug Attachment
|
||||
|
||||
The tenth implementation slice of Big Block 5 added a debug/evidence attachment layer:
|
||||
|
||||
- `assistantMcpDiscoveryDebugAttachment.ts`
|
||||
- `assistantMcpDiscoveryDebugAttachment.test.ts`
|
||||
|
||||
It is now attached to:
|
||||
|
||||
- address-lane response runtime debug;
|
||||
- deep-analysis debug payloads.
|
||||
|
||||
This still does not execute MCP discovery automatically and still does not change the user-facing answer.
|
||||
|
||||
It gives runtime and replay review a stable observable surface:
|
||||
|
||||
- `assistant_mcp_discovery_entry_point_v1`;
|
||||
- `mcp_discovery_entry_status`;
|
||||
- `mcp_discovery_attempted`;
|
||||
- `mcp_discovery_hot_runtime_wired=false`;
|
||||
- `mcp_discovery_bridge_status`;
|
||||
- `mcp_discovery_answer_mode`;
|
||||
- business-fact/user-facing authorization flags;
|
||||
- clarification flag.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryDebugAttachment.test.ts assistantDebugPayloadAssembler.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts` passed 13/13;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponse
|
|||
const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter");
|
||||
const assistantCapabilityBindingResponseGuard_1 = require("./assistantCapabilityBindingResponseGuard");
|
||||
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
||||
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
|
||||
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
||||
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
||||
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
||||
|
|
@ -208,13 +209,16 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
|||
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
||||
});
|
||||
const debugWithMcpDiscovery = (0, assistantMcpDiscoveryDebugAttachment_1.attachAssistantMcpDiscoveryDebug)(debugWithCapabilityBinding, {
|
||||
addressRuntimeMeta: input.llmPreDecomposeMeta
|
||||
});
|
||||
const guardedResponse = (0, assistantCapabilityBindingResponseGuard_1.applyAssistantCapabilityBindingResponseGuard)({
|
||||
assistantReply: safeAddressReply,
|
||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
||||
capabilityBinding: debugWithCapabilityBinding.assistant_capability_binding_v1
|
||||
capabilityBinding: debugWithMcpDiscovery.assistant_capability_binding_v1
|
||||
});
|
||||
const debugWithResponseGuard = {
|
||||
...debugWithCapabilityBinding,
|
||||
...debugWithMcpDiscovery,
|
||||
capability_binding_response_guard: guardedResponse.audit
|
||||
};
|
||||
const finalization = finalizeAddressTurnSafe({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ exports.buildAssistantBackendErrorDebugPayload = buildAssistantBackendErrorDebug
|
|||
exports.buildAddressRuntimeDebugPayload = buildAddressRuntimeDebugPayload;
|
||||
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
|
||||
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
||||
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
|
||||
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
||||
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
||||
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
||||
|
|
@ -240,6 +241,7 @@ function buildDeepAnalysisDebugPayload(input) {
|
|||
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
|
||||
address_semantic_extraction_contract: input.addressRuntimeMetaForDeep?.semanticExtractionContract ?? null,
|
||||
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
|
||||
assistant_mcp_discovery_entry_point_v1: input.addressRuntimeMetaForDeep?.mcpDiscoveryRuntimeEntryPoint ?? null,
|
||||
assistant_outcome_class_v1: input.outcomeClassV1,
|
||||
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
|
||||
answer_contract_stage4_v1: answerContractStage4Audit,
|
||||
|
|
@ -263,10 +265,13 @@ function buildDeepAnalysisDebugPayload(input) {
|
|||
coverageReport: input.coverageReport,
|
||||
replyType: "deep_analysis"
|
||||
});
|
||||
return (0, assistantCapabilityRuntimeBindingAdapter_1.attachAssistantCapabilityRuntimeBinding)(debugWithStateTransition, {
|
||||
const debugWithCapabilityBinding = (0, assistantCapabilityRuntimeBindingAdapter_1.attachAssistantCapabilityRuntimeBinding)(debugWithStateTransition, {
|
||||
addressRuntimeMeta: input.addressRuntimeMetaForDeep,
|
||||
groundingStatus: input.groundingCheck.status,
|
||||
coverageReport: input.coverageReport,
|
||||
replyType: "deep_analysis"
|
||||
});
|
||||
return (0, assistantMcpDiscoveryDebugAttachment_1.attachAssistantMcpDiscoveryDebug)(debugWithCapabilityBinding, {
|
||||
addressRuntimeMeta: input.addressRuntimeMetaForDeep
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
|
||||
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
|
||||
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1";
|
||||
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = "assistant_mcp_catalog_plan_review_v1";
|
||||
const PRIMITIVE_CONTRACTS = [
|
||||
{
|
||||
primitive_id: "search_business_entity",
|
||||
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "document", "account"],
|
||||
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
|
||||
evidence_floor: "rows_received",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "inspect_1c_metadata",
|
||||
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
|
||||
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
|
||||
optional_axes: ["business_entity", "account", "counterparty"],
|
||||
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "resolve_entity_reference",
|
||||
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "inn", "document"],
|
||||
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "query_movements",
|
||||
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "query_documents",
|
||||
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "aggregate_by_axis",
|
||||
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "drilldown_related_objects",
|
||||
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
|
||||
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
|
||||
optional_axes: ["period", "account", "amount"],
|
||||
output_fact_kinds: ["related_objects", "relationship_edges"],
|
||||
evidence_floor: "rows_received",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "probe_coverage",
|
||||
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
|
||||
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
|
||||
optional_axes: ["period", "organization", "counterparty", "document", "account"],
|
||||
output_fact_kinds: ["coverage_status", "known_gaps"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "explain_evidence_basis",
|
||||
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
|
||||
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
|
||||
optional_axes: ["coverage_target", "domain_family"],
|
||||
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
}
|
||||
];
|
||||
const PRIMITIVE_CONTRACT_MAP = new Map(PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract]));
|
||||
function toStringSet(values) {
|
||||
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
|
||||
}
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function hasAnyAxisGroup(axisSet, groups) {
|
||||
return groups.some((group) => group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
function missingAxisGroups(axisSet, groups) {
|
||||
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
function buildAssistantMcpCatalogIndex() {
|
||||
const reasonCodes = [];
|
||||
const missingContracts = assistantMcpDiscoveryPolicy_1.ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
|
||||
if (missingContracts.length > 0) {
|
||||
pushReason(reasonCodes, "catalog_missing_discovery_primitive_contract");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpCatalogIndex",
|
||||
primitives: PRIMITIVE_CONTRACTS,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
function getAssistantMcpCatalogPrimitive(primitive) {
|
||||
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
|
||||
if (!contract) {
|
||||
throw new Error(`Missing MCP catalog primitive contract: ${primitive}`);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
function reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan) {
|
||||
const reasonCodes = [];
|
||||
const axisSet = toStringSet(plan.required_axes);
|
||||
const reviewedPrimitives = [];
|
||||
const missingAxesByPrimitive = {};
|
||||
const unknownPrimitives = [];
|
||||
const evidenceFloors = {};
|
||||
for (const primitive of plan.allowed_primitives) {
|
||||
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
|
||||
if (!contract) {
|
||||
unknownPrimitives.push(primitive);
|
||||
continue;
|
||||
}
|
||||
reviewedPrimitives.push(primitive);
|
||||
evidenceFloors[primitive] = contract.evidence_floor;
|
||||
if (!hasAnyAxisGroup(axisSet, contract.required_axes_any_of)) {
|
||||
missingAxesByPrimitive[primitive] = missingAxisGroups(axisSet, contract.required_axes_any_of);
|
||||
}
|
||||
}
|
||||
if (unknownPrimitives.length > 0) {
|
||||
pushReason(reasonCodes, "catalog_unknown_primitive_in_discovery_plan");
|
||||
}
|
||||
if (Object.keys(missingAxesByPrimitive).length > 0) {
|
||||
pushReason(reasonCodes, "catalog_required_axes_missing_for_primitive");
|
||||
}
|
||||
if (plan.plan_status !== "allowed") {
|
||||
pushReason(reasonCodes, "catalog_review_received_non_allowed_plan");
|
||||
}
|
||||
let reviewStatus = "catalog_compatible";
|
||||
if (unknownPrimitives.length > 0 || plan.plan_status === "blocked") {
|
||||
reviewStatus = "catalog_blocked";
|
||||
}
|
||||
else if (plan.plan_status !== "allowed" || Object.keys(missingAxesByPrimitive).length > 0) {
|
||||
reviewStatus = "needs_more_axes";
|
||||
}
|
||||
if (reviewStatus === "catalog_compatible") {
|
||||
pushReason(reasonCodes, "catalog_plan_compatible");
|
||||
}
|
||||
else if (reviewStatus === "catalog_blocked") {
|
||||
pushReason(reasonCodes, "catalog_plan_blocked");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "catalog_plan_needs_more_axes");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpCatalogIndex",
|
||||
review_status: reviewStatus,
|
||||
reviewed_primitives: reviewedPrimitives,
|
||||
missing_axes_by_primitive: missingAxesByPrimitive,
|
||||
unknown_primitives: unknownPrimitives,
|
||||
evidence_floors: evidenceFloors,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function uniqueStrings(values) {
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function isInternalMechanicsLine(value) {
|
||||
const text = value.toLowerCase();
|
||||
return (text.includes("primitive") ||
|
||||
text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("resolve_entity_reference") ||
|
||||
text.includes("probe_coverage") ||
|
||||
text.includes("explain_evidence_basis") ||
|
||||
text.includes("pilot_only_executes") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_"));
|
||||
}
|
||||
function userFacingLimitations(values) {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
function modeFor(pilot) {
|
||||
if (pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.evidence.answer_permission === "confirmed_answer") {
|
||||
return "confirmed_with_bounded_inference";
|
||||
}
|
||||
if (pilot.evidence.answer_permission === "bounded_inference") {
|
||||
return "bounded_inference_only";
|
||||
}
|
||||
return "checked_sources_only";
|
||||
}
|
||||
function headlineFor(mode) {
|
||||
if (mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||||
}
|
||||
if (mode === "bounded_inference_only") {
|
||||
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
if (mode === "blocked") {
|
||||
return "Поиск в 1С заблокирован runtime-политикой до выполнения.";
|
||||
}
|
||||
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
|
||||
}
|
||||
function nextStepFor(mode, pilot) {
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
|
||||
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
|
||||
}
|
||||
if (mode === "blocked") {
|
||||
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function buildMustNotClaim(pilot) {
|
||||
const claims = [
|
||||
"Do not claim legal registration age unless a legal registration source is confirmed.",
|
||||
"Do not present inferred activity duration as a formally confirmed legal fact.",
|
||||
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
|
||||
"Do not claim rows were checked when mcp_execution_performed=false."
|
||||
];
|
||||
if (pilot.evidence.confirmed_facts.length === 0) {
|
||||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
const mode = modeFor(pilot);
|
||||
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
|
||||
pushReason(reasonCodes, `answer_mode_${mode}`);
|
||||
if (pilot.evidence.unknown_facts.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
|
||||
}
|
||||
if (pilot.evidence.inferred_facts.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_bounded_inference");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
answer_mode: mode,
|
||||
headline: headlineFor(mode),
|
||||
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
|
||||
inference_lines: uniqueStrings(pilot.evidence.inferred_facts),
|
||||
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
next_step_line: nextStepFor(mode, pilot),
|
||||
internal_mechanics_allowed: false,
|
||||
must_not_claim: buildMustNotClaim(pilot),
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
};
|
||||
}
|
||||
53
llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js
vendored
Normal file
53
llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildAssistantMcpDiscoveryDebugAttachmentFields = buildAssistantMcpDiscoveryDebugAttachmentFields;
|
||||
exports.attachAssistantMcpDiscoveryDebug = attachAssistantMcpDiscoveryDebug;
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
function isMcpDiscoveryEntryPointContract(value) {
|
||||
const record = toRecordObject(value);
|
||||
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
|
||||
}
|
||||
function resolveEntryPoint(input) {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
}
|
||||
const runtimeMetaEntryPoint = input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
|
||||
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
|
||||
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
|
||||
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
|
||||
}
|
||||
function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
||||
const entryPoint = resolveEntryPoint(input);
|
||||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
return {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
||||
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
|
||||
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
|
||||
mcp_discovery_hot_runtime_wired: false,
|
||||
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
|
||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
mcp_discovery_requires_clarification: bridge?.requires_user_clarification === true
|
||||
};
|
||||
}
|
||||
function attachAssistantMcpDiscoveryDebug(debugPayload, input) {
|
||||
return {
|
||||
...debugPayload,
|
||||
...buildAssistantMcpDiscoveryDebugAttachmentFields(input)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = void 0;
|
||||
exports.executeAssistantMcpDiscoveryPilot = executeAssistantMcpDiscoveryPilot;
|
||||
const addressMcpClient_1 = require("./addressMcpClient");
|
||||
const assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter");
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
|
||||
const DEFAULT_DEPS = {
|
||||
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery
|
||||
};
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function pushUnique(target, value) {
|
||||
const text = value.trim();
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
function firstEntityCandidate(planner) {
|
||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
for (const candidate of candidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function dateScopeToFilters(dateScope) {
|
||||
if (!dateScope) {
|
||||
return {};
|
||||
}
|
||||
const yearMatch = dateScope.match(/^(\d{4})$/);
|
||||
if (yearMatch) {
|
||||
return {
|
||||
period_from: `${yearMatch[1]}-01-01`,
|
||||
period_to: `${yearMatch[1]}-12-31`
|
||||
};
|
||||
}
|
||||
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (dateMatch) {
|
||||
const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
|
||||
return {
|
||||
period_from: date,
|
||||
period_to: date
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
function buildLifecycleFilters(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
|
||||
return {
|
||||
...dateScopeToFilters(dateScope),
|
||||
...(counterparty ? { counterparty } : {}),
|
||||
...(organization ? { organization } : {}),
|
||||
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function isLifecyclePilotEligible(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
|
||||
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
|
||||
const combined = `${domain} ${action}`;
|
||||
return (planner.proposed_primitives.includes("query_documents") &&
|
||||
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age")));
|
||||
}
|
||||
function skippedProbeResult(step, limitation) {
|
||||
return {
|
||||
primitive_id: step.primitive_id,
|
||||
status: "skipped",
|
||||
rows_received: 0,
|
||||
rows_matched: 0,
|
||||
limitation
|
||||
};
|
||||
}
|
||||
function queryResultToProbeResult(primitiveId, result) {
|
||||
return {
|
||||
primitive_id: primitiveId,
|
||||
status: result.error ? "error" : "ok",
|
||||
rows_received: result.fetched_rows,
|
||||
rows_matched: result.matched_rows,
|
||||
limitation: result.error
|
||||
};
|
||||
}
|
||||
function summarizeRows(result) {
|
||||
if (result.error) {
|
||||
return null;
|
||||
}
|
||||
if (result.fetched_rows <= 0) {
|
||||
return "0 MCP document rows fetched";
|
||||
}
|
||||
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
|
||||
}
|
||||
function buildConfirmedFacts(result, counterparty) {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
counterparty
|
||||
? `1C activity rows were found for counterparty ${counterparty}`
|
||||
: "1C activity rows were found for the requested counterparty scope"
|
||||
];
|
||||
}
|
||||
function buildInferredFacts(result) {
|
||||
if (result.error || result.fetched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
|
||||
}
|
||||
function buildUnknownFacts() {
|
||||
return ["Legal registration date is not proven by this MCP discovery pilot"];
|
||||
}
|
||||
function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
|
||||
return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
unknownFacts: [reason],
|
||||
queryLimitations: [reason],
|
||||
recommendedNextProbe: dryRun.user_facing_fallback
|
||||
});
|
||||
}
|
||||
async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const dryRun = (0, assistantMcpDiscoveryRuntimeAdapter_1.buildAssistantMcpDiscoveryRuntimeDryRun)(planner);
|
||||
const reasonCodes = [...dryRun.reason_codes];
|
||||
const executedPrimitives = [];
|
||||
const skippedPrimitives = [];
|
||||
const probeResults = [];
|
||||
const queryLimitations = [];
|
||||
if (dryRun.adapter_status === "blocked") {
|
||||
pushReason(reasonCodes, "pilot_blocked_before_mcp_execution");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot was blocked before execution");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "blocked",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["MCP discovery pilot was blocked before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
if (dryRun.adapter_status !== "dry_run_ready") {
|
||||
pushReason(reasonCodes, "pilot_needs_clarification_before_mcp_execution");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot needs more scope before execution");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "skipped_needs_clarification",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["MCP discovery pilot needs more scope before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
if (!isLifecyclePilotEligible(planner)) {
|
||||
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
|
||||
for (const step of dryRun.execution_steps) {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_scope_unsupported_for_live_execution"));
|
||||
}
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot scope is not implemented yet");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
let queryResult = null;
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const filters = buildLifecycleFilters(planner);
|
||||
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", filters);
|
||||
if (!selection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_lifecycle_recipe_not_available");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Lifecycle recipe is not available");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["Lifecycle recipe is not available"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_documents") {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents"));
|
||||
continue;
|
||||
}
|
||||
queryResult = await deps.executeAddressMcpQuery({
|
||||
query: recipePlan.query,
|
||||
limit: recipePlan.limit,
|
||||
account_scope: recipePlan.account_scope
|
||||
});
|
||||
executedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
|
||||
if (queryResult.error) {
|
||||
pushUnique(queryLimitations, queryResult.error);
|
||||
pushReason(reasonCodes, "pilot_query_documents_mcp_error");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
|
||||
}
|
||||
}
|
||||
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
|
||||
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
|
||||
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
|
||||
unknownFacts: buildUnknownFacts(),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
});
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "executed",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: executedPrimitives.length > 0,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = void 0;
|
||||
exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery;
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1";
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
function lower(value) {
|
||||
return String(value ?? "").trim().toLowerCase();
|
||||
}
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function pushUnique(target, value) {
|
||||
const text = value.trim();
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
function hasEntity(meaning) {
|
||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
function addScopeAxes(axes, meaning) {
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "counterparty");
|
||||
}
|
||||
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
||||
pushUnique(axes, "organization");
|
||||
}
|
||||
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
||||
pushUnique(axes, "period");
|
||||
}
|
||||
}
|
||||
function includesAny(text, tokens) {
|
||||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
function recipeFor(input) {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
const action = lower(meaning?.asked_action_family);
|
||||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes = [];
|
||||
addScopeAxes(axes, meaning);
|
||||
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value"])) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_document_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
|
||||
pushUnique(axes, "document_date");
|
||||
pushUnique(axes, "coverage_target");
|
||||
pushUnique(axes, "evidence_basis");
|
||||
return {
|
||||
semanticDataNeed: "counterparty lifecycle evidence",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||
axes,
|
||||
reason: "planner_selected_lifecycle_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["metadata", "schema", "catalog"])) {
|
||||
pushUnique(axes, "metadata_scope");
|
||||
return {
|
||||
semanticDataNeed: "1C metadata evidence",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_metadata_recipe"
|
||||
};
|
||||
}
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "business_entity");
|
||||
return {
|
||||
semanticDataNeed: "entity discovery evidence",
|
||||
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_entity_resolution_recipe"
|
||||
};
|
||||
}
|
||||
return {
|
||||
semanticDataNeed: "unclassified 1C discovery need",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_clarification_recipe"
|
||||
};
|
||||
}
|
||||
function statusFrom(plan, review) {
|
||||
if (plan.plan_status === "blocked" || review.review_status === "catalog_blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (plan.plan_status !== "allowed" || review.review_status !== "catalog_compatible") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
return "ready_for_execution";
|
||||
}
|
||||
function planAssistantMcpDiscovery(input) {
|
||||
const recipe = recipeFor(input);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const reasonCodes = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
const plan = (0, assistantMcpDiscoveryPolicy_1.buildAssistantMcpDiscoveryPlan)({
|
||||
semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes
|
||||
});
|
||||
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
if (plannerStatus === "ready_for_execution") {
|
||||
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
|
||||
}
|
||||
else if (plannerStatus === "blocked") {
|
||||
pushReason(reasonCodes, "planner_blocked_by_policy_or_catalog");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||
planner_status: plannerStatus,
|
||||
semantic_data_need: semanticDataNeed,
|
||||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
catalog_review: review,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
129
llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeAdapter.js
vendored
Normal file
129
llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryRuntimeDryRun = buildAssistantMcpDiscoveryRuntimeDryRun;
|
||||
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_dry_run_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function uniqueStrings(values) {
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
const text = value.trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function stepStatusFor(input) {
|
||||
if (input.adapterStatus === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (input.missingAxisOptions.length > 0) {
|
||||
return "missing_axes";
|
||||
}
|
||||
return "ready";
|
||||
}
|
||||
function stopConditionFor(evidenceFloor) {
|
||||
if (evidenceFloor === "rows_matched") {
|
||||
return "stop_after_allowed_probe_returns_matched_rows_or_reports_no_match";
|
||||
}
|
||||
if (evidenceFloor === "rows_received") {
|
||||
return "stop_after_allowed_probe_returns_rows_or_reports_empty_source";
|
||||
}
|
||||
if (evidenceFloor === "source_summary") {
|
||||
return "stop_after_allowed_probe_returns_source_summary_or_limitation";
|
||||
}
|
||||
return "stop_without_fact_claim";
|
||||
}
|
||||
function adapterStatusFor(planner) {
|
||||
if (planner.planner_status === "blocked" || planner.catalog_review.review_status === "catalog_blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (planner.planner_status !== "ready_for_execution" || planner.catalog_review.review_status !== "catalog_compatible") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
return "dry_run_ready";
|
||||
}
|
||||
function fallbackFor(status) {
|
||||
if (status === "dry_run_ready") {
|
||||
return null;
|
||||
}
|
||||
if (status === "blocked") {
|
||||
return "explain_that_runtime_policy_blocked_mcp_discovery";
|
||||
}
|
||||
return "ask_for_missing_scope_before_mcp_discovery";
|
||||
}
|
||||
function buildAssistantMcpDiscoveryRuntimeDryRun(planner) {
|
||||
const adapterStatus = adapterStatusFor(planner);
|
||||
const reasonCodes = uniqueStrings([
|
||||
...planner.reason_codes,
|
||||
...planner.discovery_plan.reason_codes,
|
||||
...planner.catalog_review.reason_codes
|
||||
]);
|
||||
const providedAxes = uniqueStrings(planner.required_axes);
|
||||
const executionSteps = planner.discovery_plan.allowed_primitives.map((primitiveId, index) => {
|
||||
const catalog = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogPrimitive)(primitiveId);
|
||||
const missingAxisOptions = planner.catalog_review.missing_axes_by_primitive[primitiveId] ?? [];
|
||||
return {
|
||||
sequence: index + 1,
|
||||
primitive_id: primitiveId,
|
||||
step_status: stepStatusFor({ adapterStatus, missingAxisOptions }),
|
||||
purpose: catalog.purpose,
|
||||
provided_axes: providedAxes,
|
||||
required_axis_options: catalog.required_axes_any_of,
|
||||
missing_axis_options: missingAxisOptions,
|
||||
optional_axes: catalog.optional_axes,
|
||||
expected_fact_kinds: catalog.output_fact_kinds,
|
||||
evidence_floor: catalog.evidence_floor,
|
||||
runtime_must_execute: true,
|
||||
dry_run_only: true,
|
||||
stop_condition: stopConditionFor(catalog.evidence_floor)
|
||||
};
|
||||
});
|
||||
if (adapterStatus === "dry_run_ready") {
|
||||
pushReason(reasonCodes, "runtime_dry_run_ready_without_mcp_execution");
|
||||
}
|
||||
else if (adapterStatus === "blocked") {
|
||||
pushReason(reasonCodes, "runtime_dry_run_blocked_before_mcp_execution");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "runtime_dry_run_needs_clarification_before_mcp_execution");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeAdapter",
|
||||
adapter_status: adapterStatus,
|
||||
planner_status: planner.planner_status,
|
||||
mcp_execution_performed: false,
|
||||
execution_steps: executionSteps,
|
||||
execution_budget: planner.discovery_plan.execution_budget,
|
||||
evidence_gate: {
|
||||
required: true,
|
||||
expected_inputs: [
|
||||
"probe_results",
|
||||
"confirmed_facts",
|
||||
"inferred_facts",
|
||||
"unknown_facts",
|
||||
"source_rows_summary",
|
||||
"query_limitations"
|
||||
],
|
||||
answer_may_use_raw_model_claims: false
|
||||
},
|
||||
user_facing_fallback: fallbackFor(adapterStatus),
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function uniqueStrings(values) {
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function bridgeStatusFor(pilot, draft) {
|
||||
if (draft.answer_mode === "blocked" || pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (draft.answer_mode === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.pilot_status === "unsupported") {
|
||||
return "unsupported";
|
||||
}
|
||||
if (draft.answer_mode === "checked_sources_only") {
|
||||
return "checked_sources_only";
|
||||
}
|
||||
return "answer_draft_ready";
|
||||
}
|
||||
function businessFactAnswerAllowed(draft) {
|
||||
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
|
||||
}
|
||||
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
|
||||
semanticDataNeed: input.semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning
|
||||
});
|
||||
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
|
||||
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
bridge_status: bridgeStatus,
|
||||
hot_runtime_wired: false,
|
||||
planner,
|
||||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
81
llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeEntryPoint.js
vendored
Normal file
81
llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeEntryPoint.js
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION = void 0;
|
||||
exports.runAssistantMcpDiscoveryRuntimeEntryPoint = runAssistantMcpDiscoveryRuntimeEntryPoint;
|
||||
const assistantMcpDiscoveryRuntimeBridge_1 = require("./assistantMcpDiscoveryRuntimeBridge");
|
||||
const assistantMcpDiscoveryTurnInputAdapter_1 = require("./assistantMcpDiscoveryTurnInputAdapter");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_entry_point_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function uniqueStrings(values) {
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function skippedContract(input) {
|
||||
const reasonCodes = uniqueStrings(input.turnInput.reason_codes);
|
||||
pushReason(reasonCodes, input.reason);
|
||||
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: input.status,
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: false,
|
||||
turn_input: input.turnInput,
|
||||
bridge: null,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) {
|
||||
const turnInput = (0, assistantMcpDiscoveryTurnInputAdapter_1.buildAssistantMcpDiscoveryTurnInput)(input);
|
||||
if (!turnInput.should_run_discovery) {
|
||||
return skippedContract({
|
||||
status: "skipped_not_applicable",
|
||||
turnInput,
|
||||
reason: "runtime_entry_point_skipped_supported_exact_turn"
|
||||
});
|
||||
}
|
||||
if (!turnInput.turn_meaning_ref) {
|
||||
return skippedContract({
|
||||
status: "skipped_needs_more_context",
|
||||
turnInput,
|
||||
reason: "runtime_entry_point_skipped_missing_discovery_turn_meaning"
|
||||
});
|
||||
}
|
||||
const bridge = await (0, assistantMcpDiscoveryRuntimeBridge_1.runAssistantMcpDiscoveryRuntimeBridge)({
|
||||
semanticDataNeed: turnInput.semantic_data_need,
|
||||
turnMeaning: turnInput.turn_meaning_ref,
|
||||
deps: input.deps
|
||||
});
|
||||
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
|
||||
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
|
||||
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: turnInput,
|
||||
bridge,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
215
llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js
vendored
Normal file
215
llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
|
||||
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function pushUnique(target, value) {
|
||||
const text = toNonEmptyString(value);
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
function compactLower(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function candidateValue(value) {
|
||||
const direct = toNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
return direct;
|
||||
}
|
||||
const record = toRecordObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return (toNonEmptyString(record.value) ??
|
||||
toNonEmptyString(record.name) ??
|
||||
toNonEmptyString(record.ref) ??
|
||||
toNonEmptyString(record.text));
|
||||
}
|
||||
function collectEntityCandidates(value) {
|
||||
const result = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
pushUnique(result, candidateValue(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pushUnique(result, candidateValue(value));
|
||||
return result;
|
||||
}
|
||||
function collectPredecomposeEntities(predecompose) {
|
||||
const entities = toRecordObject(predecompose?.entities);
|
||||
return {
|
||||
counterparty: toNonEmptyString(entities?.counterparty),
|
||||
organization: toNonEmptyString(entities?.organization)
|
||||
};
|
||||
}
|
||||
function collectDateScope(predecompose) {
|
||||
const period = toRecordObject(predecompose?.period);
|
||||
const asOfDate = toNonEmptyString(period?.as_of_date);
|
||||
const periodFrom = toNonEmptyString(period?.period_from);
|
||||
const periodTo = toNonEmptyString(period?.period_to);
|
||||
if (asOfDate) {
|
||||
return asOfDate;
|
||||
}
|
||||
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
|
||||
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
|
||||
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
|
||||
return yearFrom[1];
|
||||
}
|
||||
if (periodFrom && periodTo) {
|
||||
return `${periodFrom}..${periodTo}`;
|
||||
}
|
||||
return periodFrom ?? periodTo ?? null;
|
||||
}
|
||||
function hasLifecycleSignal(text) {
|
||||
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
|
||||
}
|
||||
function semanticNeedFor(input) {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
|
||||
return "counterparty lifecycle evidence";
|
||||
}
|
||||
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
|
||||
return "counterparty value-flow evidence";
|
||||
}
|
||||
if (/(?:document|documents|list_documents)/iu.test(combined)) {
|
||||
return "document evidence";
|
||||
}
|
||||
if (/(?:metadata|schema|catalog)/iu.test(combined)) {
|
||||
return "1C metadata evidence";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function shouldRunDiscovery(input) {
|
||||
if (input.lifecycleSignal || input.unsupported) {
|
||||
return true;
|
||||
}
|
||||
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
|
||||
const predecomposeContract = toRecordObject(input.predecomposeContract);
|
||||
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
|
||||
const reasonCodes = [];
|
||||
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
|
||||
const lifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const semanticDataNeed = semanticNeedFor({
|
||||
domain: rawDomain,
|
||||
action: rawAction,
|
||||
unsupported,
|
||||
lifecycleSignal
|
||||
});
|
||||
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||
pushUnique(entityCandidates, predecomposeEntities.counterparty);
|
||||
const turnMeaning = {
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
explicit_organization_scope: predecomposeEntities.organization,
|
||||
explicit_date_scope: collectDateScope(predecomposeContract),
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
|
||||
};
|
||||
const cleanTurnMeaning = {};
|
||||
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
|
||||
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.asked_action_family)) {
|
||||
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
|
||||
}
|
||||
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
|
||||
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
|
||||
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
|
||||
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
|
||||
}
|
||||
if (turnMeaning.stale_replay_forbidden) {
|
||||
cleanTurnMeaning.stale_replay_forbidden = true;
|
||||
}
|
||||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported,
|
||||
lifecycleSignal,
|
||||
semanticDataNeed,
|
||||
explicitIntentCandidate
|
||||
});
|
||||
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
|
||||
const sourceSignal = assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
if (predecomposeEntities.counterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (entityCandidates.length > 0) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
|
||||
}
|
||||
if (!runDiscovery) {
|
||||
pushReason(reasonCodes, "mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
}
|
||||
if (runDiscovery && !hasTurnMeaning) {
|
||||
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
|
||||
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
||||
should_run_discovery: runDiscovery,
|
||||
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
||||
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
||||
source_signal: sourceSignal,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
|
||||
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
|
||||
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
||||
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
|
||||
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
||||
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
||||
|
|
@ -267,13 +268,16 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
|||
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
||||
});
|
||||
const debugWithMcpDiscovery = attachAssistantMcpDiscoveryDebug(debugWithCapabilityBinding, {
|
||||
addressRuntimeMeta: input.llmPreDecomposeMeta
|
||||
});
|
||||
const guardedResponse = applyAssistantCapabilityBindingResponseGuard({
|
||||
assistantReply: safeAddressReply,
|
||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
||||
capabilityBinding: debugWithCapabilityBinding.assistant_capability_binding_v1
|
||||
capabilityBinding: debugWithMcpDiscovery.assistant_capability_binding_v1
|
||||
});
|
||||
const debugWithResponseGuard = {
|
||||
...debugWithCapabilityBinding,
|
||||
...debugWithMcpDiscovery,
|
||||
capability_binding_response_guard: guardedResponse.audit
|
||||
};
|
||||
const finalization = finalizeAddressTurnSafe({
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type {
|
|||
TemporalGuardAudit
|
||||
} from "./assistantRuntimeGuards";
|
||||
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
||||
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
|
||||
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
||||
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
||||
|
|
@ -333,6 +334,7 @@ export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInp
|
|||
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
|
||||
address_semantic_extraction_contract: input.addressRuntimeMetaForDeep?.semanticExtractionContract ?? null,
|
||||
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
|
||||
assistant_mcp_discovery_entry_point_v1: input.addressRuntimeMetaForDeep?.mcpDiscoveryRuntimeEntryPoint ?? null,
|
||||
assistant_outcome_class_v1: input.outcomeClassV1,
|
||||
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
|
||||
answer_contract_stage4_v1: answerContractStage4Audit,
|
||||
|
|
@ -356,10 +358,13 @@ export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInp
|
|||
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
||||
replyType: "deep_analysis"
|
||||
});
|
||||
return attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
|
||||
const debugWithCapabilityBinding = attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
|
||||
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined,
|
||||
groundingStatus: input.groundingCheck.status,
|
||||
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
||||
replyType: "deep_analysis"
|
||||
});
|
||||
return attachAssistantMcpDiscoveryDebug(debugWithCapabilityBinding, {
|
||||
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined
|
||||
}) as unknown as AssistantDebugPayload;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,251 @@
|
|||
import {
|
||||
ASSISTANT_MCP_DISCOVERY_PRIMITIVES,
|
||||
type AssistantMcpDiscoveryPlanContract,
|
||||
type AssistantMcpDiscoveryPrimitive
|
||||
} from "./assistantMcpDiscoveryPolicy";
|
||||
|
||||
export const ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1" as const;
|
||||
export const ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = "assistant_mcp_catalog_plan_review_v1" as const;
|
||||
|
||||
export type AssistantMcpCatalogEvidenceFloor = "none" | "rows_received" | "rows_matched" | "source_summary";
|
||||
export type AssistantMcpCatalogPlanReviewStatus = "catalog_compatible" | "needs_more_axes" | "catalog_blocked";
|
||||
|
||||
export interface AssistantMcpCatalogPrimitiveContract {
|
||||
primitive_id: AssistantMcpDiscoveryPrimitive;
|
||||
purpose: string;
|
||||
required_axes_any_of: string[][];
|
||||
optional_axes: string[];
|
||||
output_fact_kinds: string[];
|
||||
evidence_floor: AssistantMcpCatalogEvidenceFloor;
|
||||
safe_for_model_planning: true;
|
||||
runtime_must_execute: true;
|
||||
}
|
||||
|
||||
export interface AssistantMcpCatalogIndexContract {
|
||||
schema_version: typeof ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpCatalogIndex";
|
||||
primitives: AssistantMcpCatalogPrimitiveContract[];
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface AssistantMcpCatalogPlanReview {
|
||||
schema_version: typeof ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpCatalogIndex";
|
||||
review_status: AssistantMcpCatalogPlanReviewStatus;
|
||||
reviewed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
missing_axes_by_primitive: Record<string, string[][]>;
|
||||
unknown_primitives: string[];
|
||||
evidence_floors: Record<string, AssistantMcpCatalogEvidenceFloor>;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
||||
{
|
||||
primitive_id: "search_business_entity",
|
||||
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "document", "account"],
|
||||
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
|
||||
evidence_floor: "rows_received",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "inspect_1c_metadata",
|
||||
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
|
||||
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
|
||||
optional_axes: ["business_entity", "account", "counterparty"],
|
||||
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "resolve_entity_reference",
|
||||
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "inn", "document"],
|
||||
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "query_movements",
|
||||
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "query_documents",
|
||||
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "aggregate_by_axis",
|
||||
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "drilldown_related_objects",
|
||||
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
|
||||
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
|
||||
optional_axes: ["period", "account", "amount"],
|
||||
output_fact_kinds: ["related_objects", "relationship_edges"],
|
||||
evidence_floor: "rows_received",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "probe_coverage",
|
||||
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
|
||||
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
|
||||
optional_axes: ["period", "organization", "counterparty", "document", "account"],
|
||||
output_fact_kinds: ["coverage_status", "known_gaps"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "explain_evidence_basis",
|
||||
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
|
||||
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
|
||||
optional_axes: ["coverage_target", "domain_family"],
|
||||
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
}
|
||||
];
|
||||
|
||||
const PRIMITIVE_CONTRACT_MAP = new Map<AssistantMcpDiscoveryPrimitive, AssistantMcpCatalogPrimitiveContract>(
|
||||
PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract])
|
||||
);
|
||||
|
||||
function toStringSet(values: string[]): Set<string> {
|
||||
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function hasAnyAxisGroup(axisSet: Set<string>, groups: string[][]): boolean {
|
||||
return groups.some((group) => group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
|
||||
function missingAxisGroups(axisSet: Set<string>, groups: string[][]): string[][] {
|
||||
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
|
||||
export function buildAssistantMcpCatalogIndex(): AssistantMcpCatalogIndexContract {
|
||||
const reasonCodes: string[] = [];
|
||||
const missingContracts = ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
|
||||
if (missingContracts.length > 0) {
|
||||
pushReason(reasonCodes, "catalog_missing_discovery_primitive_contract");
|
||||
} else {
|
||||
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
|
||||
}
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpCatalogIndex",
|
||||
primitives: PRIMITIVE_CONTRACTS,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
export function getAssistantMcpCatalogPrimitive(
|
||||
primitive: AssistantMcpDiscoveryPrimitive
|
||||
): AssistantMcpCatalogPrimitiveContract {
|
||||
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
|
||||
if (!contract) {
|
||||
throw new Error(`Missing MCP catalog primitive contract: ${primitive}`);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
export function reviewAssistantMcpDiscoveryPlanAgainstCatalog(
|
||||
plan: AssistantMcpDiscoveryPlanContract
|
||||
): AssistantMcpCatalogPlanReview {
|
||||
const reasonCodes: string[] = [];
|
||||
const axisSet = toStringSet(plan.required_axes);
|
||||
const reviewedPrimitives: AssistantMcpDiscoveryPrimitive[] = [];
|
||||
const missingAxesByPrimitive: Record<string, string[][]> = {};
|
||||
const unknownPrimitives: string[] = [];
|
||||
const evidenceFloors: Record<string, AssistantMcpCatalogEvidenceFloor> = {};
|
||||
|
||||
for (const primitive of plan.allowed_primitives) {
|
||||
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
|
||||
if (!contract) {
|
||||
unknownPrimitives.push(primitive);
|
||||
continue;
|
||||
}
|
||||
reviewedPrimitives.push(primitive);
|
||||
evidenceFloors[primitive] = contract.evidence_floor;
|
||||
if (!hasAnyAxisGroup(axisSet, contract.required_axes_any_of)) {
|
||||
missingAxesByPrimitive[primitive] = missingAxisGroups(axisSet, contract.required_axes_any_of);
|
||||
}
|
||||
}
|
||||
|
||||
if (unknownPrimitives.length > 0) {
|
||||
pushReason(reasonCodes, "catalog_unknown_primitive_in_discovery_plan");
|
||||
}
|
||||
if (Object.keys(missingAxesByPrimitive).length > 0) {
|
||||
pushReason(reasonCodes, "catalog_required_axes_missing_for_primitive");
|
||||
}
|
||||
if (plan.plan_status !== "allowed") {
|
||||
pushReason(reasonCodes, "catalog_review_received_non_allowed_plan");
|
||||
}
|
||||
|
||||
let reviewStatus: AssistantMcpCatalogPlanReviewStatus = "catalog_compatible";
|
||||
if (unknownPrimitives.length > 0 || plan.plan_status === "blocked") {
|
||||
reviewStatus = "catalog_blocked";
|
||||
} else if (plan.plan_status !== "allowed" || Object.keys(missingAxesByPrimitive).length > 0) {
|
||||
reviewStatus = "needs_more_axes";
|
||||
}
|
||||
|
||||
if (reviewStatus === "catalog_compatible") {
|
||||
pushReason(reasonCodes, "catalog_plan_compatible");
|
||||
} else if (reviewStatus === "catalog_blocked") {
|
||||
pushReason(reasonCodes, "catalog_plan_blocked");
|
||||
} else {
|
||||
pushReason(reasonCodes, "catalog_plan_needs_more_axes");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpCatalogIndex",
|
||||
review_status: reviewStatus,
|
||||
reviewed_primitives: reviewedPrimitives,
|
||||
missing_axes_by_primitive: missingAxesByPrimitive,
|
||||
unknown_primitives: unknownPrimitives,
|
||||
evidence_floors: evidenceFloors,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_answer_draft_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryAnswerMode =
|
||||
| "confirmed_with_bounded_inference"
|
||||
| "bounded_inference_only"
|
||||
| "checked_sources_only"
|
||||
| "needs_clarification"
|
||||
| "blocked";
|
||||
|
||||
export interface AssistantMcpDiscoveryAnswerDraftContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter";
|
||||
answer_mode: AssistantMcpDiscoveryAnswerMode;
|
||||
headline: string;
|
||||
confirmed_lines: string[];
|
||||
inference_lines: string[];
|
||||
unknown_lines: string[];
|
||||
limitation_lines: string[];
|
||||
next_step_line: string | null;
|
||||
internal_mechanics_allowed: false;
|
||||
must_not_claim: string[];
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isInternalMechanicsLine(value: string): boolean {
|
||||
const text = value.toLowerCase();
|
||||
return (
|
||||
text.includes("primitive") ||
|
||||
text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("resolve_entity_reference") ||
|
||||
text.includes("probe_coverage") ||
|
||||
text.includes("explain_evidence_basis") ||
|
||||
text.includes("pilot_only_executes") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_")
|
||||
);
|
||||
}
|
||||
|
||||
function userFacingLimitations(values: string[]): string[] {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
|
||||
function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantMcpDiscoveryAnswerMode {
|
||||
if (pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.evidence.answer_permission === "confirmed_answer") {
|
||||
return "confirmed_with_bounded_inference";
|
||||
}
|
||||
if (pilot.evidence.answer_permission === "bounded_inference") {
|
||||
return "bounded_inference_only";
|
||||
}
|
||||
return "checked_sources_only";
|
||||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode): string {
|
||||
if (mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||||
}
|
||||
if (mode === "bounded_inference_only") {
|
||||
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
if (mode === "blocked") {
|
||||
return "Поиск в 1С заблокирован runtime-политикой до выполнения.";
|
||||
}
|
||||
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
|
||||
}
|
||||
|
||||
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
|
||||
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
|
||||
}
|
||||
if (mode === "blocked") {
|
||||
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
const claims = [
|
||||
"Do not claim legal registration age unless a legal registration source is confirmed.",
|
||||
"Do not present inferred activity duration as a formally confirmed legal fact.",
|
||||
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
|
||||
"Do not claim rows were checked when mcp_execution_performed=false."
|
||||
];
|
||||
if (pilot.evidence.confirmed_facts.length === 0) {
|
||||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): AssistantMcpDiscoveryAnswerDraftContract {
|
||||
const mode = modeFor(pilot);
|
||||
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
|
||||
pushReason(reasonCodes, `answer_mode_${mode}`);
|
||||
if (pilot.evidence.unknown_facts.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
|
||||
}
|
||||
if (pilot.evidence.inferred_facts.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_bounded_inference");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
answer_mode: mode,
|
||||
headline: headlineFor(mode),
|
||||
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
|
||||
inference_lines: uniqueStrings(pilot.evidence.inferred_facts),
|
||||
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
next_step_line: nextStepFor(mode, pilot),
|
||||
internal_mechanics_allowed: false,
|
||||
must_not_claim: buildMustNotClaim(pilot),
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
|
||||
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
||||
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
|
||||
mcp_discovery_entry_status: string | null;
|
||||
mcp_discovery_attempted: boolean;
|
||||
mcp_discovery_hot_runtime_wired: false;
|
||||
mcp_discovery_bridge_status: string | null;
|
||||
mcp_discovery_answer_mode: string | null;
|
||||
mcp_discovery_business_fact_answer_allowed: boolean;
|
||||
mcp_discovery_user_facing_response_allowed: boolean;
|
||||
mcp_discovery_requires_clarification: boolean;
|
||||
}
|
||||
|
||||
export interface AttachAssistantMcpDiscoveryDebugInput {
|
||||
entryPoint?: unknown;
|
||||
addressRuntimeMeta?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
function toRecordObject(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||
const record = toRecordObject(value);
|
||||
return (
|
||||
record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
}
|
||||
const runtimeMetaEntryPoint =
|
||||
input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
|
||||
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
|
||||
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
|
||||
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
||||
input: AttachAssistantMcpDiscoveryDebugInput
|
||||
): AssistantMcpDiscoveryDebugAttachmentFields {
|
||||
const entryPoint = resolveEntryPoint(input);
|
||||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
|
||||
return {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
||||
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
|
||||
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
|
||||
mcp_discovery_hot_runtime_wired: false,
|
||||
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
|
||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
mcp_discovery_requires_clarification: bridge?.requires_user_clarification === true
|
||||
};
|
||||
}
|
||||
|
||||
export function attachAssistantMcpDiscoveryDebug<T extends Record<string, unknown>>(
|
||||
debugPayload: T,
|
||||
input: AttachAssistantMcpDiscoveryDebugInput
|
||||
): T & AssistantMcpDiscoveryDebugAttachmentFields {
|
||||
return {
|
||||
...debugPayload,
|
||||
...buildAssistantMcpDiscoveryDebugAttachmentFields(input)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
import {
|
||||
executeAddressMcpQuery,
|
||||
type AddressMcpMetadataRowsResult
|
||||
} from "./addressMcpClient";
|
||||
import {
|
||||
buildAssistantMcpDiscoveryRuntimeDryRun,
|
||||
type AssistantMcpDiscoveryRuntimeDryRunContract,
|
||||
type AssistantMcpDiscoveryRuntimeStepContract
|
||||
} from "./assistantMcpDiscoveryRuntimeAdapter";
|
||||
import type { AssistantMcpDiscoveryPlannerContract } from "./assistantMcpDiscoveryPlanner";
|
||||
import {
|
||||
resolveAssistantMcpDiscoveryEvidence,
|
||||
type AssistantMcpDiscoveryEvidenceContract,
|
||||
type AssistantMcpDiscoveryProbeResult
|
||||
} from "./assistantMcpDiscoveryPolicy";
|
||||
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
|
||||
import type { AddressFilterSet } from "../types/addressQuery";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_pilot_executor_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryPilotStatus =
|
||||
| "executed"
|
||||
| "skipped_needs_clarification"
|
||||
| "blocked"
|
||||
| "unsupported";
|
||||
|
||||
export interface AssistantMcpDiscoveryPilotExecutorDeps {
|
||||
executeAddressMcpQuery: typeof executeAddressMcpQuery;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryPilotExecutionContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor";
|
||||
pilot_status: AssistantMcpDiscoveryPilotStatus;
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1";
|
||||
dry_run: AssistantMcpDiscoveryRuntimeDryRunContract;
|
||||
mcp_execution_performed: boolean;
|
||||
executed_primitives: string[];
|
||||
skipped_primitives: string[];
|
||||
probe_results: AssistantMcpDiscoveryProbeResult[];
|
||||
evidence: AssistantMcpDiscoveryEvidenceContract;
|
||||
source_rows_summary: string | null;
|
||||
query_limitations: string[];
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
type AddressMcpQueryExecutorResult = Awaited<ReturnType<typeof executeAddressMcpQuery>>;
|
||||
|
||||
const DEFAULT_DEPS: AssistantMcpDiscoveryPilotExecutorDeps = {
|
||||
executeAddressMcpQuery
|
||||
};
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function pushUnique(target: string[], value: string): void {
|
||||
const text = value.trim();
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
for (const candidate of candidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "period_from" | "period_to"> {
|
||||
if (!dateScope) {
|
||||
return {};
|
||||
}
|
||||
const yearMatch = dateScope.match(/^(\d{4})$/);
|
||||
if (yearMatch) {
|
||||
return {
|
||||
period_from: `${yearMatch[1]}-01-01`,
|
||||
period_to: `${yearMatch[1]}-12-31`
|
||||
};
|
||||
}
|
||||
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (dateMatch) {
|
||||
const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
|
||||
return {
|
||||
period_from: date,
|
||||
period_to: date
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
|
||||
return {
|
||||
...dateScopeToFilters(dateScope),
|
||||
...(counterparty ? { counterparty } : {}),
|
||||
...(organization ? { organization } : {}),
|
||||
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
|
||||
function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
|
||||
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
|
||||
const combined = `${domain} ${action}`;
|
||||
return (
|
||||
planner.proposed_primitives.includes("query_documents") &&
|
||||
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age"))
|
||||
);
|
||||
}
|
||||
|
||||
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
|
||||
return {
|
||||
primitive_id: step.primitive_id,
|
||||
status: "skipped",
|
||||
rows_received: 0,
|
||||
rows_matched: 0,
|
||||
limitation
|
||||
};
|
||||
}
|
||||
|
||||
function queryResultToProbeResult(
|
||||
primitiveId: string,
|
||||
result: AddressMcpQueryExecutorResult
|
||||
): AssistantMcpDiscoveryProbeResult {
|
||||
return {
|
||||
primitive_id: primitiveId,
|
||||
status: result.error ? "error" : "ok",
|
||||
rows_received: result.fetched_rows,
|
||||
rows_matched: result.matched_rows,
|
||||
limitation: result.error
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
|
||||
if (result.error) {
|
||||
return null;
|
||||
}
|
||||
if (result.fetched_rows <= 0) {
|
||||
return "0 MCP document rows fetched";
|
||||
}
|
||||
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
|
||||
}
|
||||
|
||||
function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
counterparty
|
||||
? `1C activity rows were found for counterparty ${counterparty}`
|
||||
: "1C activity rows were found for the requested counterparty scope"
|
||||
];
|
||||
}
|
||||
|
||||
function buildInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
|
||||
if (result.error || result.fetched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
|
||||
}
|
||||
|
||||
function buildUnknownFacts(): string[] {
|
||||
return ["Legal registration date is not proven by this MCP discovery pilot"];
|
||||
}
|
||||
|
||||
function buildEmptyEvidence(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
|
||||
probeResults: AssistantMcpDiscoveryProbeResult[],
|
||||
reason: string
|
||||
): AssistantMcpDiscoveryEvidenceContract {
|
||||
return resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
unknownFacts: [reason],
|
||||
queryLimitations: [reason],
|
||||
recommendedNextProbe: dryRun.user_facing_fallback
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeAssistantMcpDiscoveryPilot(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
deps: AssistantMcpDiscoveryPilotExecutorDeps = DEFAULT_DEPS
|
||||
): Promise<AssistantMcpDiscoveryPilotExecutionContract> {
|
||||
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
|
||||
const reasonCodes = [...dryRun.reason_codes];
|
||||
const executedPrimitives: string[] = [];
|
||||
const skippedPrimitives: string[] = [];
|
||||
const probeResults: AssistantMcpDiscoveryProbeResult[] = [];
|
||||
const queryLimitations: string[] = [];
|
||||
|
||||
if (dryRun.adapter_status === "blocked") {
|
||||
pushReason(reasonCodes, "pilot_blocked_before_mcp_execution");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot was blocked before execution");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "blocked",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["MCP discovery pilot was blocked before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (dryRun.adapter_status !== "dry_run_ready") {
|
||||
pushReason(reasonCodes, "pilot_needs_clarification_before_mcp_execution");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot needs more scope before execution");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "skipped_needs_clarification",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["MCP discovery pilot needs more scope before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (!isLifecyclePilotEligible(planner)) {
|
||||
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
|
||||
for (const step of dryRun.execution_steps) {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_scope_unsupported_for_live_execution"));
|
||||
}
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot scope is not implemented yet");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
let queryResult: AddressMcpQueryExecutorResult | null = null;
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const filters = buildLifecycleFilters(planner);
|
||||
const selection = selectAddressRecipe("counterparty_activity_lifecycle", filters);
|
||||
if (!selection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_lifecycle_recipe_not_available");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Lifecycle recipe is not available");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
query_limitations: ["Lifecycle recipe is not available"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_documents") {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents"));
|
||||
continue;
|
||||
}
|
||||
queryResult = await deps.executeAddressMcpQuery({
|
||||
query: recipePlan.query,
|
||||
limit: recipePlan.limit,
|
||||
account_scope: recipePlan.account_scope
|
||||
});
|
||||
executedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
|
||||
if (queryResult.error) {
|
||||
pushUnique(queryLimitations, queryResult.error);
|
||||
pushReason(reasonCodes, "pilot_query_documents_mcp_error");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
|
||||
}
|
||||
}
|
||||
|
||||
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
|
||||
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
|
||||
unknownFacts: buildUnknownFacts(),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
});
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "executed",
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1",
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: executedPrimitives.length > 0,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryPilotMetadataResult = AddressMcpMetadataRowsResult;
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
import {
|
||||
buildAssistantMcpDiscoveryPlan,
|
||||
type AssistantMcpDiscoveryPlanContract,
|
||||
type AssistantMcpDiscoveryPrimitive,
|
||||
type AssistantMcpDiscoveryTurnMeaningRef
|
||||
} from "./assistantMcpDiscoveryPolicy";
|
||||
import {
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
|
||||
type AssistantMcpCatalogPlanReview
|
||||
} from "./assistantMcpCatalogIndex";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryPlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
|
||||
|
||||
export interface AssistantMcpDiscoveryPlannerInput {
|
||||
semanticDataNeed?: string | null;
|
||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryPlannerContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryPlanner";
|
||||
planner_status: AssistantMcpDiscoveryPlannerStatus;
|
||||
semantic_data_need: string | null;
|
||||
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
required_axes: string[];
|
||||
discovery_plan: AssistantMcpDiscoveryPlanContract;
|
||||
catalog_review: AssistantMcpCatalogPlanReview;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
interface PlannerRecipe {
|
||||
semanticDataNeed: string;
|
||||
primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
axes: string[];
|
||||
reason: string;
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function lower(value: unknown): string {
|
||||
return String(value ?? "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function pushUnique(target: string[], value: string): void {
|
||||
const text = value.trim();
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
|
||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "counterparty");
|
||||
}
|
||||
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
||||
pushUnique(axes, "organization");
|
||||
}
|
||||
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
||||
pushUnique(axes, "period");
|
||||
}
|
||||
}
|
||||
|
||||
function includesAny(text: string, tokens: string[]): boolean {
|
||||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
|
||||
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
const action = lower(meaning?.asked_action_family);
|
||||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes: string[] = [];
|
||||
addScopeAxes(axes, meaning);
|
||||
|
||||
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value"])) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_document_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
|
||||
pushUnique(axes, "document_date");
|
||||
pushUnique(axes, "coverage_target");
|
||||
pushUnique(axes, "evidence_basis");
|
||||
return {
|
||||
semanticDataNeed: "counterparty lifecycle evidence",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||
axes,
|
||||
reason: "planner_selected_lifecycle_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["metadata", "schema", "catalog"])) {
|
||||
pushUnique(axes, "metadata_scope");
|
||||
return {
|
||||
semanticDataNeed: "1C metadata evidence",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_metadata_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "business_entity");
|
||||
return {
|
||||
semanticDataNeed: "entity discovery evidence",
|
||||
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_entity_resolution_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
semanticDataNeed: "unclassified 1C discovery need",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_clarification_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
function statusFrom(
|
||||
plan: AssistantMcpDiscoveryPlanContract,
|
||||
review: AssistantMcpCatalogPlanReview
|
||||
): AssistantMcpDiscoveryPlannerStatus {
|
||||
if (plan.plan_status === "blocked" || review.review_status === "catalog_blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (plan.plan_status !== "allowed" || review.review_status !== "catalog_compatible") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
return "ready_for_execution";
|
||||
}
|
||||
|
||||
export function planAssistantMcpDiscovery(
|
||||
input: AssistantMcpDiscoveryPlannerInput
|
||||
): AssistantMcpDiscoveryPlannerContract {
|
||||
const recipe = recipeFor(input);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes
|
||||
});
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
|
||||
if (plannerStatus === "ready_for_execution") {
|
||||
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
|
||||
} else if (plannerStatus === "blocked") {
|
||||
pushReason(reasonCodes, "planner_blocked_by_policy_or_catalog");
|
||||
} else {
|
||||
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||
planner_status: plannerStatus,
|
||||
semantic_data_need: semanticDataNeed,
|
||||
proposed_primitives: recipe.primitives,
|
||||
required_axes: recipe.axes,
|
||||
discovery_plan: plan,
|
||||
catalog_review: review,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
import {
|
||||
getAssistantMcpCatalogPrimitive,
|
||||
type AssistantMcpCatalogEvidenceFloor
|
||||
} from "./assistantMcpCatalogIndex";
|
||||
import {
|
||||
type AssistantMcpDiscoveryPlannerContract,
|
||||
type AssistantMcpDiscoveryPlannerStatus
|
||||
} from "./assistantMcpDiscoveryPlanner";
|
||||
import type { AssistantMcpDiscoveryPrimitive } from "./assistantMcpDiscoveryPolicy";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_runtime_dry_run_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeAdapterStatus =
|
||||
| "dry_run_ready"
|
||||
| "needs_clarification"
|
||||
| "blocked";
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeStepStatus = "ready" | "missing_axes" | "blocked";
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeStepContract {
|
||||
sequence: number;
|
||||
primitive_id: AssistantMcpDiscoveryPrimitive;
|
||||
step_status: AssistantMcpDiscoveryRuntimeStepStatus;
|
||||
purpose: string;
|
||||
provided_axes: string[];
|
||||
required_axis_options: string[][];
|
||||
missing_axis_options: string[][];
|
||||
optional_axes: string[];
|
||||
expected_fact_kinds: string[];
|
||||
evidence_floor: AssistantMcpCatalogEvidenceFloor;
|
||||
runtime_must_execute: true;
|
||||
dry_run_only: true;
|
||||
stop_condition: string;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryEvidenceGateRequirement {
|
||||
required: true;
|
||||
expected_inputs: string[];
|
||||
answer_may_use_raw_model_claims: false;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeDryRunContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeAdapter";
|
||||
adapter_status: AssistantMcpDiscoveryRuntimeAdapterStatus;
|
||||
planner_status: AssistantMcpDiscoveryPlannerStatus;
|
||||
mcp_execution_performed: false;
|
||||
execution_steps: AssistantMcpDiscoveryRuntimeStepContract[];
|
||||
execution_budget: {
|
||||
max_probe_count: number;
|
||||
max_rows_per_probe: number;
|
||||
};
|
||||
evidence_gate: AssistantMcpDiscoveryEvidenceGateRequirement;
|
||||
user_facing_fallback: string | null;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const text = value.trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stepStatusFor(input: {
|
||||
adapterStatus: AssistantMcpDiscoveryRuntimeAdapterStatus;
|
||||
missingAxisOptions: string[][];
|
||||
}): AssistantMcpDiscoveryRuntimeStepStatus {
|
||||
if (input.adapterStatus === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (input.missingAxisOptions.length > 0) {
|
||||
return "missing_axes";
|
||||
}
|
||||
return "ready";
|
||||
}
|
||||
|
||||
function stopConditionFor(evidenceFloor: AssistantMcpCatalogEvidenceFloor): string {
|
||||
if (evidenceFloor === "rows_matched") {
|
||||
return "stop_after_allowed_probe_returns_matched_rows_or_reports_no_match";
|
||||
}
|
||||
if (evidenceFloor === "rows_received") {
|
||||
return "stop_after_allowed_probe_returns_rows_or_reports_empty_source";
|
||||
}
|
||||
if (evidenceFloor === "source_summary") {
|
||||
return "stop_after_allowed_probe_returns_source_summary_or_limitation";
|
||||
}
|
||||
return "stop_without_fact_claim";
|
||||
}
|
||||
|
||||
function adapterStatusFor(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryRuntimeAdapterStatus {
|
||||
if (planner.planner_status === "blocked" || planner.catalog_review.review_status === "catalog_blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (planner.planner_status !== "ready_for_execution" || planner.catalog_review.review_status !== "catalog_compatible") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
return "dry_run_ready";
|
||||
}
|
||||
|
||||
function fallbackFor(status: AssistantMcpDiscoveryRuntimeAdapterStatus): string | null {
|
||||
if (status === "dry_run_ready") {
|
||||
return null;
|
||||
}
|
||||
if (status === "blocked") {
|
||||
return "explain_that_runtime_policy_blocked_mcp_discovery";
|
||||
}
|
||||
return "ask_for_missing_scope_before_mcp_discovery";
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryRuntimeDryRun(
|
||||
planner: AssistantMcpDiscoveryPlannerContract
|
||||
): AssistantMcpDiscoveryRuntimeDryRunContract {
|
||||
const adapterStatus = adapterStatusFor(planner);
|
||||
const reasonCodes = uniqueStrings([
|
||||
...planner.reason_codes,
|
||||
...planner.discovery_plan.reason_codes,
|
||||
...planner.catalog_review.reason_codes
|
||||
]);
|
||||
|
||||
const providedAxes = uniqueStrings(planner.required_axes);
|
||||
const executionSteps = planner.discovery_plan.allowed_primitives.map((primitiveId, index) => {
|
||||
const catalog = getAssistantMcpCatalogPrimitive(primitiveId);
|
||||
const missingAxisOptions = planner.catalog_review.missing_axes_by_primitive[primitiveId] ?? [];
|
||||
return {
|
||||
sequence: index + 1,
|
||||
primitive_id: primitiveId,
|
||||
step_status: stepStatusFor({ adapterStatus, missingAxisOptions }),
|
||||
purpose: catalog.purpose,
|
||||
provided_axes: providedAxes,
|
||||
required_axis_options: catalog.required_axes_any_of,
|
||||
missing_axis_options: missingAxisOptions,
|
||||
optional_axes: catalog.optional_axes,
|
||||
expected_fact_kinds: catalog.output_fact_kinds,
|
||||
evidence_floor: catalog.evidence_floor,
|
||||
runtime_must_execute: true,
|
||||
dry_run_only: true,
|
||||
stop_condition: stopConditionFor(catalog.evidence_floor)
|
||||
} satisfies AssistantMcpDiscoveryRuntimeStepContract;
|
||||
});
|
||||
|
||||
if (adapterStatus === "dry_run_ready") {
|
||||
pushReason(reasonCodes, "runtime_dry_run_ready_without_mcp_execution");
|
||||
} else if (adapterStatus === "blocked") {
|
||||
pushReason(reasonCodes, "runtime_dry_run_blocked_before_mcp_execution");
|
||||
} else {
|
||||
pushReason(reasonCodes, "runtime_dry_run_needs_clarification_before_mcp_execution");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeAdapter",
|
||||
adapter_status: adapterStatus,
|
||||
planner_status: planner.planner_status,
|
||||
mcp_execution_performed: false,
|
||||
execution_steps: executionSteps,
|
||||
execution_budget: planner.discovery_plan.execution_budget,
|
||||
evidence_gate: {
|
||||
required: true,
|
||||
expected_inputs: [
|
||||
"probe_results",
|
||||
"confirmed_facts",
|
||||
"inferred_facts",
|
||||
"unknown_facts",
|
||||
"source_rows_summary",
|
||||
"query_limitations"
|
||||
],
|
||||
answer_may_use_raw_model_claims: false
|
||||
},
|
||||
user_facing_fallback: fallbackFor(adapterStatus),
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import {
|
||||
buildAssistantMcpDiscoveryAnswerDraft,
|
||||
type AssistantMcpDiscoveryAnswerDraftContract
|
||||
} from "./assistantMcpDiscoveryAnswerAdapter";
|
||||
import {
|
||||
executeAssistantMcpDiscoveryPilot,
|
||||
type AssistantMcpDiscoveryPilotExecutionContract,
|
||||
type AssistantMcpDiscoveryPilotExecutorDeps
|
||||
} from "./assistantMcpDiscoveryPilotExecutor";
|
||||
import {
|
||||
planAssistantMcpDiscovery,
|
||||
type AssistantMcpDiscoveryPlannerContract
|
||||
} from "./assistantMcpDiscoveryPlanner";
|
||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||
| "answer_draft_ready"
|
||||
| "checked_sources_only"
|
||||
| "needs_clarification"
|
||||
| "blocked"
|
||||
| "unsupported";
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||
semanticDataNeed?: string | null;
|
||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
bridge_status: AssistantMcpDiscoveryRuntimeBridgeStatus;
|
||||
hot_runtime_wired: false;
|
||||
planner: AssistantMcpDiscoveryPlannerContract;
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||
user_facing_response_allowed: boolean;
|
||||
business_fact_answer_allowed: boolean;
|
||||
requires_user_clarification: boolean;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function bridgeStatusFor(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
draft: AssistantMcpDiscoveryAnswerDraftContract
|
||||
): AssistantMcpDiscoveryRuntimeBridgeStatus {
|
||||
if (draft.answer_mode === "blocked" || pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (draft.answer_mode === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_clarification";
|
||||
}
|
||||
if (pilot.pilot_status === "unsupported") {
|
||||
return "unsupported";
|
||||
}
|
||||
if (draft.answer_mode === "checked_sources_only") {
|
||||
return "checked_sources_only";
|
||||
}
|
||||
return "answer_draft_ready";
|
||||
}
|
||||
|
||||
function businessFactAnswerAllowed(draft: AssistantMcpDiscoveryAnswerDraftContract): boolean {
|
||||
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
|
||||
}
|
||||
|
||||
export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||
input: AssistantMcpDiscoveryRuntimeBridgeInput
|
||||
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
semanticDataNeed: input.semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
|
||||
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
bridge_status: bridgeStatus,
|
||||
hot_runtime_wired: false,
|
||||
planner,
|
||||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
runAssistantMcpDiscoveryRuntimeBridge,
|
||||
type AssistantMcpDiscoveryRuntimeBridgeContract
|
||||
} from "./assistantMcpDiscoveryRuntimeBridge";
|
||||
import {
|
||||
buildAssistantMcpDiscoveryTurnInput,
|
||||
type AssistantMcpDiscoveryTurnInputContract,
|
||||
type BuildAssistantMcpDiscoveryTurnInputAdapterInput
|
||||
} from "./assistantMcpDiscoveryTurnInputAdapter";
|
||||
import type { AssistantMcpDiscoveryPilotExecutorDeps } from "./assistantMcpDiscoveryPilotExecutor";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_runtime_entry_point_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeEntryPointStatus =
|
||||
| "bridge_executed"
|
||||
| "skipped_not_applicable"
|
||||
| "skipped_needs_more_context";
|
||||
|
||||
export interface RunAssistantMcpDiscoveryRuntimeEntryPointInput
|
||||
extends BuildAssistantMcpDiscoveryTurnInputAdapterInput {
|
||||
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
entry_status: AssistantMcpDiscoveryRuntimeEntryPointStatus;
|
||||
hot_runtime_wired: false;
|
||||
discovery_attempted: boolean;
|
||||
turn_input: AssistantMcpDiscoveryTurnInputContract;
|
||||
bridge: AssistantMcpDiscoveryRuntimeBridgeContract | null;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !result.includes(text)) {
|
||||
result.push(text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function skippedContract(input: {
|
||||
status: Exclude<AssistantMcpDiscoveryRuntimeEntryPointStatus, "bridge_executed">;
|
||||
turnInput: AssistantMcpDiscoveryTurnInputContract;
|
||||
reason: string;
|
||||
}): AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||
const reasonCodes = uniqueStrings(input.turnInput.reason_codes);
|
||||
pushReason(reasonCodes, input.reason);
|
||||
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: input.status,
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: false,
|
||||
turn_input: input.turnInput,
|
||||
bridge: null,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
|
||||
input: RunAssistantMcpDiscoveryRuntimeEntryPointInput
|
||||
): Promise<AssistantMcpDiscoveryRuntimeEntryPointContract> {
|
||||
const turnInput = buildAssistantMcpDiscoveryTurnInput(input);
|
||||
if (!turnInput.should_run_discovery) {
|
||||
return skippedContract({
|
||||
status: "skipped_not_applicable",
|
||||
turnInput,
|
||||
reason: "runtime_entry_point_skipped_supported_exact_turn"
|
||||
});
|
||||
}
|
||||
if (!turnInput.turn_meaning_ref) {
|
||||
return skippedContract({
|
||||
status: "skipped_needs_more_context",
|
||||
turnInput,
|
||||
reason: "runtime_entry_point_skipped_missing_discovery_turn_meaning"
|
||||
});
|
||||
}
|
||||
|
||||
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
semanticDataNeed: turnInput.semantic_data_need,
|
||||
turnMeaning: turnInput.turn_meaning_ref,
|
||||
deps: input.deps
|
||||
});
|
||||
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
|
||||
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
|
||||
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: turnInput,
|
||||
bridge,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_turn_input_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryTurnInputStatus = "ready" | "needs_more_context" | "not_applicable";
|
||||
export type AssistantMcpDiscoveryTurnInputSource =
|
||||
| "assistant_turn_meaning"
|
||||
| "predecompose_contract"
|
||||
| "raw_text"
|
||||
| "none";
|
||||
|
||||
export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
|
||||
assistantTurnMeaning?: Record<string, unknown> | null;
|
||||
predecomposeContract?: Record<string, unknown> | null;
|
||||
userMessage?: string | null;
|
||||
effectiveMessage?: string | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryTurnInputContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryTurnInputAdapter";
|
||||
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
|
||||
should_run_discovery: boolean;
|
||||
semantic_data_need: string | null;
|
||||
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
source_signal: AssistantMcpDiscoveryTurnInputSource;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
function toRecordObject(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
|
||||
function pushReason(target: string[], value: string): void {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function pushUnique(target: string[], value: unknown): void {
|
||||
const text = toNonEmptyString(value);
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
function compactLower(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function candidateValue(value: unknown): string | null {
|
||||
const direct = toNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
return direct;
|
||||
}
|
||||
const record = toRecordObject(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
toNonEmptyString(record.value) ??
|
||||
toNonEmptyString(record.name) ??
|
||||
toNonEmptyString(record.ref) ??
|
||||
toNonEmptyString(record.text)
|
||||
);
|
||||
}
|
||||
|
||||
function collectEntityCandidates(value: unknown): string[] {
|
||||
const result: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
pushUnique(result, candidateValue(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pushUnique(result, candidateValue(value));
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectPredecomposeEntities(predecompose: Record<string, unknown> | null): {
|
||||
counterparty: string | null;
|
||||
organization: string | null;
|
||||
} {
|
||||
const entities = toRecordObject(predecompose?.entities);
|
||||
return {
|
||||
counterparty: toNonEmptyString(entities?.counterparty),
|
||||
organization: toNonEmptyString(entities?.organization)
|
||||
};
|
||||
}
|
||||
|
||||
function collectDateScope(predecompose: Record<string, unknown> | null): string | null {
|
||||
const period = toRecordObject(predecompose?.period);
|
||||
const asOfDate = toNonEmptyString(period?.as_of_date);
|
||||
const periodFrom = toNonEmptyString(period?.period_from);
|
||||
const periodTo = toNonEmptyString(period?.period_to);
|
||||
if (asOfDate) {
|
||||
return asOfDate;
|
||||
}
|
||||
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
|
||||
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
|
||||
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
|
||||
return yearFrom[1];
|
||||
}
|
||||
if (periodFrom && periodTo) {
|
||||
return `${periodFrom}..${periodTo}`;
|
||||
}
|
||||
return periodFrom ?? periodTo ?? null;
|
||||
}
|
||||
|
||||
function hasLifecycleSignal(text: string): boolean {
|
||||
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function semanticNeedFor(input: {
|
||||
domain: string | null;
|
||||
action: string | null;
|
||||
unsupported: string | null;
|
||||
lifecycleSignal: boolean;
|
||||
}): string | null {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
|
||||
return "counterparty lifecycle evidence";
|
||||
}
|
||||
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
|
||||
return "counterparty value-flow evidence";
|
||||
}
|
||||
if (/(?:document|documents|list_documents)/iu.test(combined)) {
|
||||
return "document evidence";
|
||||
}
|
||||
if (/(?:metadata|schema|catalog)/iu.test(combined)) {
|
||||
return "1C metadata evidence";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRunDiscovery(input: {
|
||||
unsupported: string | null;
|
||||
lifecycleSignal: boolean;
|
||||
semanticDataNeed: string | null;
|
||||
explicitIntentCandidate: string | null;
|
||||
}): boolean {
|
||||
if (input.lifecycleSignal || input.unsupported) {
|
||||
return true;
|
||||
}
|
||||
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryTurnInput(
|
||||
input: BuildAssistantMcpDiscoveryTurnInputAdapterInput
|
||||
): AssistantMcpDiscoveryTurnInputContract {
|
||||
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
|
||||
const predecomposeContract = toRecordObject(input.predecomposeContract);
|
||||
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
|
||||
const reasonCodes: string[] = [];
|
||||
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
|
||||
const lifecycleSignal = hasLifecycleSignal(rawText);
|
||||
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const semanticDataNeed = semanticNeedFor({
|
||||
domain: rawDomain,
|
||||
action: rawAction,
|
||||
unsupported,
|
||||
lifecycleSignal
|
||||
});
|
||||
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||
pushUnique(entityCandidates, predecomposeEntities.counterparty);
|
||||
|
||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
explicit_organization_scope: predecomposeEntities.organization,
|
||||
explicit_date_scope: collectDateScope(predecomposeContract),
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
|
||||
};
|
||||
|
||||
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
|
||||
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
|
||||
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.asked_action_family)) {
|
||||
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
|
||||
}
|
||||
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
|
||||
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
|
||||
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
|
||||
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
|
||||
}
|
||||
if (turnMeaning.stale_replay_forbidden) {
|
||||
cleanTurnMeaning.stale_replay_forbidden = true;
|
||||
}
|
||||
|
||||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported,
|
||||
lifecycleSignal,
|
||||
semanticDataNeed,
|
||||
explicitIntentCandidate
|
||||
});
|
||||
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
|
||||
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
if (predecomposeEntities.counterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (entityCandidates.length > 0) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
|
||||
}
|
||||
if (!runDiscovery) {
|
||||
pushReason(reasonCodes, "mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
}
|
||||
if (runDiscovery && !hasTurnMeaning) {
|
||||
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
|
||||
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
||||
should_run_discovery: runDiscovery,
|
||||
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
||||
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
||||
source_signal: sourceSignal,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@ export interface AssistantAddressRuntimeMetaForDeep {
|
|||
predecomposeContract?: Record<string, unknown> | null;
|
||||
semanticExtractionContract?: Record<string, unknown> | null;
|
||||
orchestrationContract?: Record<string, unknown> | null;
|
||||
mcpDiscoveryRuntimeEntryPoint?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AssistantRequirement {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,72 @@ describe("assistant address lane response runtime adapter", () => {
|
|||
capability_binding_response_guard: expect.objectContaining({
|
||||
schema_version: "assistant_capability_binding_response_guard_v1",
|
||||
applied: false
|
||||
})
|
||||
}),
|
||||
assistant_mcp_discovery_entry_point_v1: null,
|
||||
mcp_discovery_attempted: false,
|
||||
mcp_discovery_hot_runtime_wired: false
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("attaches MCP discovery summary from predecompose runtime meta without changing the reply", () => {
|
||||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||||
sessionId: "asst-mcp",
|
||||
userMessage: "raw",
|
||||
effectiveAddressUserMessage: "raw",
|
||||
addressLane: {
|
||||
handled: true,
|
||||
reply_text: "answer",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
mcpDiscoveryRuntimeEntryPoint: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: { adapter_status: "ready" },
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: { answer_mode: "confirmed_with_bounded_inference" }
|
||||
},
|
||||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||||
}
|
||||
},
|
||||
knownOrganizations: [],
|
||||
activeOrganization: null,
|
||||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||||
buildAddressDebugPayload: () => ({}),
|
||||
buildAddressFollowupOffer: () => null,
|
||||
mergeKnownOrganizations: (items) => items,
|
||||
toNonEmptyString: () => null,
|
||||
appendItem: () => {},
|
||||
getSession: () => ({ session_id: "asst-mcp", updated_at: "", items: [], investigation_state: null } as any),
|
||||
persistSession: () => {},
|
||||
cloneConversation: (items) => items,
|
||||
logEvent: () => {},
|
||||
messageIdFactory: () => "msg-mcp",
|
||||
finalizeAddressTurn: () => ({
|
||||
response: {
|
||||
ok: true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
expect(runtime.response).toEqual({ ok: true });
|
||||
expect(runtime.debug).toEqual(
|
||||
expect.objectContaining({
|
||||
mcp_discovery_entry_status: "bridge_executed",
|
||||
mcp_discovery_attempted: true,
|
||||
mcp_discovery_bridge_status: "answer_draft_ready",
|
||||
mcp_discovery_answer_mode: "confirmed_with_bounded_inference",
|
||||
mcp_discovery_business_fact_answer_allowed: true,
|
||||
mcp_discovery_hot_runtime_wired: false
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@ describe("assistant debug payload assembler", () => {
|
|||
binding_action: "observe_only"
|
||||
})
|
||||
);
|
||||
expect(payload.assistant_mcp_discovery_entry_point_v1).toBeNull();
|
||||
expect(payload.mcp_discovery_attempted).toBe(false);
|
||||
expect(payload.mcp_discovery_hot_runtime_wired).toBe(false);
|
||||
});
|
||||
|
||||
it("omits optional fields when they are not provided", () => {
|
||||
|
|
@ -188,4 +191,35 @@ describe("assistant debug payload assembler", () => {
|
|||
expect(payload.answer_contract_stage4_v1?.legacy_blocks_present).toContain("Что сломано");
|
||||
expect(payload.answer_contract_stage4_v1?.legacy_blocks_present).toContain("Ограничения");
|
||||
});
|
||||
|
||||
it("attaches MCP discovery entry point summary when runtime meta provides it", () => {
|
||||
const input = baseInput();
|
||||
input.addressRuntimeMetaForDeep = {
|
||||
...input.addressRuntimeMetaForDeep,
|
||||
mcpDiscoveryRuntimeEntryPoint: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: { adapter_status: "ready" },
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: { answer_mode: "confirmed_with_bounded_inference" }
|
||||
},
|
||||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||||
}
|
||||
};
|
||||
|
||||
const payload = buildDeepAnalysisDebugPayload(input);
|
||||
|
||||
expect(payload.mcp_discovery_entry_status).toBe("bridge_executed");
|
||||
expect(payload.mcp_discovery_attempted).toBe(true);
|
||||
expect(payload.mcp_discovery_bridge_status).toBe("answer_draft_ready");
|
||||
expect(payload.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(payload.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } from "../src/services/assistantMcpDiscoveryPolicy";
|
||||
import {
|
||||
buildAssistantMcpCatalogIndex,
|
||||
getAssistantMcpCatalogPrimitive,
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog
|
||||
} from "../src/services/assistantMcpCatalogIndex";
|
||||
|
||||
describe("assistant MCP catalog index", () => {
|
||||
it("declares a catalog contract for every reviewed discovery primitive", () => {
|
||||
const index = buildAssistantMcpCatalogIndex();
|
||||
const primitiveIds = index.primitives.map((entry) => entry.primitive_id);
|
||||
|
||||
expect(index.reason_codes).toContain("catalog_covers_all_discovery_primitives");
|
||||
expect(primitiveIds).toEqual([...ASSISTANT_MCP_DISCOVERY_PRIMITIVES]);
|
||||
for (const entry of index.primitives) {
|
||||
expect(entry.safe_for_model_planning).toBe(true);
|
||||
expect(entry.runtime_must_execute).toBe(true);
|
||||
expect(entry.required_axes_any_of.length).toBeGreaterThan(0);
|
||||
expect(entry.output_fact_kinds.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("marks a counterparty turnover discovery plan as catalog-compatible when required axes exist", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "counterparty turnover evidence",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
proposedPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis"],
|
||||
requiredAxes: ["counterparty", "period", "aggregate_axis", "amount"]
|
||||
});
|
||||
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
||||
expect(review.review_status).toBe("catalog_compatible");
|
||||
expect(review.reason_codes).toContain("catalog_plan_compatible");
|
||||
expect(review.missing_axes_by_primitive).toEqual({});
|
||||
expect(review.evidence_floors.query_movements).toBe("rows_matched");
|
||||
});
|
||||
|
||||
it("asks for more axes before document discovery can run safely", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "document evidence",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_documents",
|
||||
asked_action_family: "list_documents"
|
||||
},
|
||||
proposedPrimitives: ["query_documents"],
|
||||
requiredAxes: ["amount"]
|
||||
});
|
||||
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
||||
expect(review.review_status).toBe("needs_more_axes");
|
||||
expect(review.reason_codes).toContain("catalog_required_axes_missing_for_primitive");
|
||||
expect(review.missing_axes_by_primitive.query_documents).toEqual([
|
||||
["document"],
|
||||
["counterparty"],
|
||||
["contract"],
|
||||
["period", "organization"]
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves source-summary evidence floors for metadata and coverage primitives", () => {
|
||||
expect(getAssistantMcpCatalogPrimitive("inspect_1c_metadata").evidence_floor).toBe("source_summary");
|
||||
expect(getAssistantMcpCatalogPrimitive("probe_coverage").evidence_floor).toBe("source_summary");
|
||||
expect(getAssistantMcpCatalogPrimitive("resolve_entity_reference").evidence_floor).toBe("rows_matched");
|
||||
});
|
||||
|
||||
it("turns a non-allowed discovery plan into a catalog-level blocked review", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "raw model sql",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
proposedPrimitives: ["raw_sql"],
|
||||
requiredAxes: ["counterparty"]
|
||||
});
|
||||
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
||||
expect(plan.plan_status).toBe("blocked");
|
||||
expect(review.review_status).toBe("catalog_blocked");
|
||||
expect(review.reason_codes).toContain("catalog_review_received_non_allowed_plan");
|
||||
expect(review.reason_codes).toContain("catalog_plan_blocked");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildAssistantMcpDiscoveryAnswerDraft } from "../src/services/assistantMcpDiscoveryAnswerAdapter";
|
||||
import { executeAssistantMcpDiscoveryPilot } from "../src/services/assistantMcpDiscoveryPilotExecutor";
|
||||
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
|
||||
|
||||
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpQuery: vi.fn(async () => ({
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery answer adapter", () => {
|
||||
it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.internal_mechanics_allowed).toBe(false);
|
||||
expect(draft.headline).toContain("подтвержденная активность");
|
||||
expect(draft.confirmed_lines[0]).toContain("SVK");
|
||||
expect(draft.inference_lines[0]).toContain("may be inferred");
|
||||
expect(draft.unknown_lines).toContain("Legal registration date is not proven by this MCP discovery pilot");
|
||||
expect(draft.must_not_claim).toContain("Do not present inferred activity duration as a formally confirmed legal fact.");
|
||||
expect(draft.reason_codes).toContain("answer_contains_unknown_fact_boundary");
|
||||
});
|
||||
|
||||
it("uses checked-sources mode when MCP failed and avoids confirmed facts", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([], "MCP fetch failed: timeout"));
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
||||
expect(draft.confirmed_lines).toEqual([]);
|
||||
expect(draft.limitation_lines).toContain("MCP fetch failed: timeout");
|
||||
expect(draft.next_step_line).toContain("MCP");
|
||||
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
});
|
||||
|
||||
it("asks for clarification when discovery did not execute due to missing scope", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("needs_clarification");
|
||||
expect(draft.headline).toBe("Нужно уточнить контекст перед поиском в 1С.");
|
||||
expect(draft.next_step_line).toContain("Уточните контрагента");
|
||||
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
|
||||
});
|
||||
|
||||
it("does not leak primitive names or query text into user-facing lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const userFacing = [
|
||||
draft.headline,
|
||||
...draft.confirmed_lines,
|
||||
...draft.inference_lines,
|
||||
...draft.unknown_lines,
|
||||
...draft.limitation_lines,
|
||||
draft.next_step_line ?? ""
|
||||
].join("\n");
|
||||
|
||||
expect(userFacing).not.toContain("query_documents");
|
||||
expect(userFacing).not.toContain("SELECT");
|
||||
expect(userFacing).not.toContain("ВЫБРАТЬ");
|
||||
expect(userFacing).not.toContain("primitive");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { attachAssistantMcpDiscoveryDebug } from "../src/services/assistantMcpDiscoveryDebugAttachment";
|
||||
|
||||
function entryPointContract(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: { adapter_status: "ready" },
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
},
|
||||
reason_codes: ["runtime_entry_point_bridge_executed"],
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery debug attachment", () => {
|
||||
it("attaches a validated entry point contract and exposes summary flags", () => {
|
||||
const debug = attachAssistantMcpDiscoveryDebug(
|
||||
{ trace_id: "trace-1" },
|
||||
{ addressRuntimeMeta: { mcpDiscoveryRuntimeEntryPoint: entryPointContract() } }
|
||||
);
|
||||
|
||||
expect(debug.assistant_mcp_discovery_entry_point_v1?.schema_version).toBe(
|
||||
"assistant_mcp_discovery_runtime_entry_point_v1"
|
||||
);
|
||||
expect(debug.mcp_discovery_entry_status).toBe("bridge_executed");
|
||||
expect(debug.mcp_discovery_attempted).toBe(true);
|
||||
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
|
||||
expect(debug.mcp_discovery_bridge_status).toBe("answer_draft_ready");
|
||||
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
||||
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
|
||||
expect(debug.mcp_discovery_requires_clarification).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps safe null flags when no validated entry point exists", () => {
|
||||
const debug = attachAssistantMcpDiscoveryDebug(
|
||||
{ trace_id: "trace-1" },
|
||||
{ addressRuntimeMeta: { mcpDiscoveryRuntimeEntryPoint: { schema_version: "wrong" } } }
|
||||
);
|
||||
|
||||
expect(debug.assistant_mcp_discovery_entry_point_v1).toBeNull();
|
||||
expect(debug.mcp_discovery_entry_status).toBeNull();
|
||||
expect(debug.mcp_discovery_attempted).toBe(false);
|
||||
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
|
||||
expect(debug.mcp_discovery_bridge_status).toBeNull();
|
||||
expect(debug.mcp_discovery_answer_mode).toBeNull();
|
||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
|
||||
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);
|
||||
expect(debug.mcp_discovery_requires_clarification).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
|
||||
import { executeAssistantMcpDiscoveryPilot } from "../src/services/assistantMcpDiscoveryPilotExecutor";
|
||||
|
||||
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpQuery: vi.fn(async () => ({
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery pilot executor", () => {
|
||||
it("executes only the lifecycle query_documents primitive through injected MCP deps", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" },
|
||||
{ Период: "2023-12-20T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY", Контрагент: "SVK" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.mcp_execution_performed).toBe(true);
|
||||
expect(result.executed_primitives).toEqual(["query_documents"]);
|
||||
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "probe_coverage", "explain_evidence_basis"]);
|
||||
expect(result.evidence.evidence_status).toBe("confirmed");
|
||||
expect(result.evidence.confirmed_facts[0]).toContain("SVK");
|
||||
expect(result.evidence.inferred_facts[0]).toContain("may be inferred");
|
||||
expect(result.evidence.unknown_facts).toContain("Legal registration date is not proven by this MCP discovery pilot");
|
||||
expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched lifecycle scope");
|
||||
expect(result.reason_codes).toContain("pilot_query_documents_mcp_executed");
|
||||
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
|
||||
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
|
||||
expect(String(call?.query ?? "")).toContain("Документ.ПоступлениеНаРасчетныйСчет");
|
||||
expect(call?.limit).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not execute MCP when dry-run still needs clarification", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("skipped_needs_clarification");
|
||||
expect(result.mcp_execution_performed).toBe(false);
|
||||
expect(result.evidence.evidence_status).toBe("insufficient");
|
||||
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("unsupported");
|
||||
expect(result.mcp_execution_performed).toBe(false);
|
||||
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
|
||||
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
|
||||
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("records MCP errors as limitations without converting them into facts", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([], "MCP fetch failed: timeout");
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.mcp_execution_performed).toBe(true);
|
||||
expect(result.evidence.evidence_status).toBe("insufficient");
|
||||
expect(result.evidence.confirmed_facts).toEqual([]);
|
||||
expect(result.query_limitations).toContain("MCP fetch failed: timeout");
|
||||
expect(result.reason_codes).toContain("pilot_query_documents_mcp_error");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
|
||||
|
||||
describe("assistant MCP discovery planner", () => {
|
||||
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.proposed_primitives).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"query_movements",
|
||||
"aggregate_by_axis",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("needs_clarification");
|
||||
expect(result.catalog_review.review_status).toBe("needs_more_axes");
|
||||
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["period", "counterparty"]);
|
||||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
|
||||
it("builds a document discovery plan without falling back to movement primitives", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
|
||||
expect(result.proposed_primitives).not.toContain("query_movements");
|
||||
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
|
||||
});
|
||||
|
||||
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.proposed_primitives).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"query_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
]);
|
||||
expect(result.required_axes).toEqual(["counterparty", "document_date", "coverage_target", "evidence_basis"]);
|
||||
});
|
||||
|
||||
it("uses metadata-only planning when the user asks about available schema surface", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_catalog"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
|
||||
expect(result.required_axes).toEqual(["metadata_scope"]);
|
||||
expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary");
|
||||
});
|
||||
|
||||
it("does not mark an unclassified turn as executable without turn meaning context", () => {
|
||||
const result = planAssistantMcpDiscovery({});
|
||||
|
||||
expect(result.planner_status).toBe("needs_clarification");
|
||||
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
|
||||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
|
||||
import { buildAssistantMcpDiscoveryRuntimeDryRun } from "../src/services/assistantMcpDiscoveryRuntimeAdapter";
|
||||
|
||||
describe("assistant MCP discovery runtime adapter", () => {
|
||||
it("turns a catalog-compatible value-flow plan into an execution-ready dry-run package", () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
|
||||
|
||||
expect(dryRun.adapter_status).toBe("dry_run_ready");
|
||||
expect(dryRun.mcp_execution_performed).toBe(false);
|
||||
expect(dryRun.user_facing_fallback).toBeNull();
|
||||
expect(dryRun.execution_steps.map((step) => step.primitive_id)).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"query_movements",
|
||||
"aggregate_by_axis",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(dryRun.execution_steps.every((step) => step.step_status === "ready")).toBe(true);
|
||||
expect(dryRun.execution_steps.every((step) => step.dry_run_only)).toBe(true);
|
||||
expect(dryRun.evidence_gate).toEqual({
|
||||
required: true,
|
||||
expected_inputs: [
|
||||
"probe_results",
|
||||
"confirmed_facts",
|
||||
"inferred_facts",
|
||||
"unknown_facts",
|
||||
"source_rows_summary",
|
||||
"query_limitations"
|
||||
],
|
||||
answer_may_use_raw_model_claims: false
|
||||
});
|
||||
expect(dryRun.reason_codes).toContain("runtime_dry_run_ready_without_mcp_execution");
|
||||
});
|
||||
|
||||
it("keeps execution in clarification state when catalog axes are missing", () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
|
||||
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
|
||||
const movementStep = dryRun.execution_steps.find((step) => step.primitive_id === "query_movements");
|
||||
|
||||
expect(dryRun.adapter_status).toBe("needs_clarification");
|
||||
expect(dryRun.mcp_execution_performed).toBe(false);
|
||||
expect(dryRun.user_facing_fallback).toBe("ask_for_missing_scope_before_mcp_discovery");
|
||||
expect(movementStep?.step_status).toBe("missing_axes");
|
||||
expect(movementStep?.missing_axis_options).toContainEqual(["period", "counterparty"]);
|
||||
expect(dryRun.reason_codes).toContain("runtime_dry_run_needs_clarification_before_mcp_execution");
|
||||
});
|
||||
|
||||
it("keeps blocked planner output blocked before any MCP execution", () => {
|
||||
const planner = planAssistantMcpDiscovery({});
|
||||
planner.planner_status = "blocked";
|
||||
planner.discovery_plan.plan_status = "blocked";
|
||||
planner.catalog_review.review_status = "catalog_blocked";
|
||||
|
||||
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
|
||||
|
||||
expect(dryRun.adapter_status).toBe("blocked");
|
||||
expect(dryRun.mcp_execution_performed).toBe(false);
|
||||
expect(dryRun.user_facing_fallback).toBe("explain_that_runtime_policy_blocked_mcp_discovery");
|
||||
expect(dryRun.execution_steps.every((step) => step.step_status === "blocked")).toBe(true);
|
||||
expect(dryRun.reason_codes).toContain("runtime_dry_run_blocked_before_mcp_execution");
|
||||
});
|
||||
|
||||
it("uses source-summary stop conditions for metadata-only dry runs", () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "inspect_catalog"
|
||||
}
|
||||
});
|
||||
|
||||
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
|
||||
|
||||
expect(dryRun.adapter_status).toBe("dry_run_ready");
|
||||
expect(dryRun.execution_steps).toHaveLength(1);
|
||||
expect(dryRun.execution_steps[0]).toMatchObject({
|
||||
primitive_id: "inspect_1c_metadata",
|
||||
evidence_floor: "source_summary",
|
||||
stop_condition: "stop_after_allowed_probe_returns_source_summary_or_limitation"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAssistantMcpDiscoveryRuntimeBridge } from "../src/services/assistantMcpDiscoveryRuntimeBridge";
|
||||
|
||||
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpQuery: vi.fn(async () => ({
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery runtime bridge", () => {
|
||||
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
|
||||
});
|
||||
|
||||
expect(result.schema_version).toBe("assistant_mcp_discovery_runtime_bridge_v1");
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.hot_runtime_wired).toBe(false);
|
||||
expect(result.pilot.mcp_execution_performed).toBe(true);
|
||||
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.user_facing_response_allowed).toBe(true);
|
||||
expect(result.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
});
|
||||
|
||||
it("keeps missing scope as clarification and does not authorize a business fact answer", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("needs_clarification");
|
||||
expect(result.requires_user_clarification).toBe(true);
|
||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||
expect(result.business_fact_answer_allowed).toBe(false);
|
||||
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
|
||||
});
|
||||
|
||||
it("keeps unsupported ready plans outside the hot answer path", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "document",
|
||||
asked_action_family: "documents",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("unsupported");
|
||||
expect(result.hot_runtime_wired).toBe(false);
|
||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||
expect(result.business_fact_answer_allowed).toBe(false);
|
||||
expect(result.reason_codes).toContain("runtime_bridge_status_unsupported");
|
||||
});
|
||||
|
||||
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
|
||||
});
|
||||
const userFacing = [
|
||||
result.answer_draft.headline,
|
||||
...result.answer_draft.confirmed_lines,
|
||||
...result.answer_draft.inference_lines,
|
||||
...result.answer_draft.unknown_lines,
|
||||
...result.answer_draft.limitation_lines,
|
||||
result.answer_draft.next_step_line ?? ""
|
||||
].join("\n");
|
||||
|
||||
expect(userFacing).not.toContain("query_documents");
|
||||
expect(userFacing).not.toContain("runtime_bridge");
|
||||
expect(userFacing).not.toContain("primitive");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAssistantMcpDiscoveryRuntimeEntryPoint } from "../src/services/assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
|
||||
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||
return {
|
||||
executeAddressMcpQuery: vi.fn(async () => ({
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery runtime entry point", () => {
|
||||
it("runs the bridge for discovery-eligible lifecycle turn context", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
userMessage: "Сколько лет мы работаем с Группа СВК?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "Группа СВК" },
|
||||
period: {}
|
||||
},
|
||||
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.hot_runtime_wired).toBe(false);
|
||||
expect(result.discovery_attempted).toBe(true);
|
||||
expect(result.turn_input.semantic_data_need).toBe("counterparty lifecycle evidence");
|
||||
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(result.reason_codes).toContain("runtime_entry_point_bridge_executed");
|
||||
});
|
||||
|
||||
it("skips supported exact turns before any discovery execution", async () => {
|
||||
const deps = buildDeps([]);
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_intent_candidate: "list_documents_by_counterparty",
|
||||
explicit_entity_candidates: [{ value: "SVK" }]
|
||||
},
|
||||
deps
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("skipped_not_applicable");
|
||||
expect(result.discovery_attempted).toBe(false);
|
||||
expect(result.bridge).toBeNull();
|
||||
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
|
||||
expect(result.reason_codes).toContain("runtime_entry_point_skipped_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("passes unsupported-but-understood value turns into bridge with normalized entity scope", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "counterparty_value_or_turnover",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||||
explicit_entity_candidates: [{ value: "SVK" }]
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "Группа СВК" },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
},
|
||||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
expect(result.entry_status).toBe("bridge_executed");
|
||||
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
|
||||
expect(result.bridge?.bridge_status).toBe("unsupported");
|
||||
expect(result.bridge?.hot_runtime_wired).toBe(false);
|
||||
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildAssistantMcpDiscoveryTurnInput } from "../src/services/assistantMcpDiscoveryTurnInputAdapter";
|
||||
|
||||
describe("assistant MCP discovery turn input adapter", () => {
|
||||
it("maps unsupported assistant turn meaning into a discovery-ready value-flow input", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "counterparty_value_or_turnover",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||||
stale_replay_forbidden: true,
|
||||
explicit_entity_candidates: [{ type: "counterparty", value: "SVK", source: "current_turn_loose_entity_tail" }]
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "Группа СВК", organization: "Альтернатива" },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
|
||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
|
||||
expect(result.turn_meaning_ref?.stale_replay_forbidden).toBe(true);
|
||||
});
|
||||
|
||||
it("bootstraps lifecycle discovery from raw user wording and predecompose scope", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "Сколько лет мы работаем с Группа СВК?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "Группа СВК" },
|
||||
period: { period_from: null, period_to: null, as_of_date: null }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.source_signal).toBe("predecompose_contract");
|
||||
expect(result.semantic_data_need).toBe("counterparty lifecycle evidence");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_lifecycle",
|
||||
asked_action_family: "activity_duration",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
unsupported_but_understood_family: "counterparty_lifecycle",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("does not activate discovery for supported exact current-turn intent", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_intent_candidate: "list_documents_by_counterparty",
|
||||
explicit_entity_candidates: [{ value: "SVK" }],
|
||||
stale_replay_forbidden: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("not_applicable");
|
||||
expect(result.should_run_discovery).toBe(false);
|
||||
expect(result.turn_meaning_ref).toBeNull();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("never serializes object candidates as [object Object]", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "counterparty_value_or_turnover",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||||
explicit_entity_candidates: [{ type: "counterparty", value: "SVK" }]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK"]);
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).not.toContain("[object Object]");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue