ARCH: добавить entry point MCP discovery
This commit is contained in:
parent
95f3fdafbb
commit
76db8ef012
|
|
@ -867,6 +867,30 @@ 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 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.
|
- `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.
|
||||||
|
|
||||||
## Execution Rule
|
## Execution Rule
|
||||||
|
|
||||||
Do not implement this plan as:
|
Do not implement this plan as:
|
||||||
|
|
|
||||||
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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,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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue