ARCH: добавить pilot executor MCP discovery
This commit is contained in:
parent
9d5ae69953
commit
4181bb9c9b
|
|
@ -759,6 +759,35 @@ 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.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -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,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,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");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue