NODEDC_1C/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntim...

429 lines
17 KiB
TypeScript

import {
buildAssistantMcpDiscoveryAnswerDraft,
type AssistantMcpDiscoveryAnswerDraftContract
} from "./assistantMcpDiscoveryAnswerAdapter";
import {
executeAssistantMcpDiscoveryPilot,
type AssistantMcpDiscoveryPilotExecutionContract,
type AssistantMcpDiscoveryPilotExecutorDeps
} from "./assistantMcpDiscoveryPilotExecutor";
import {
planAssistantMcpDiscovery,
type AssistantMcpDiscoveryChainId,
type AssistantMcpDiscoveryMetadataSurfaceRef,
type AssistantMcpDiscoveryPlannerContract
} from "./assistantMcpDiscoveryPlanner";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
"assistant_mcp_discovery_runtime_bridge_v1" as const;
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
"assistant_mcp_discovery_loop_state_v1" as const;
export const ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION =
"assistant_mcp_route_candidate_v1" as const;
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
| "answer_draft_ready"
| "checked_sources_only"
| "needs_clarification"
| "blocked"
| "unsupported";
export type AssistantMcpDiscoveryLoopStatus =
| "awaiting_clarification"
| "ready_for_next_hop"
| "blocked";
export type AssistantMcpRouteCandidateStatus =
| "ready_for_reviewed_execution"
| "needs_user_scope"
| "needs_route_enablement"
| "blocked";
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
semanticDataNeed?: string | null;
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
metadataSurface?: AssistantMcpDiscoveryMetadataSurfaceRef | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
}
export interface AssistantMcpDiscoveryLoopStateContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
loop_status: AssistantMcpDiscoveryLoopStatus;
selected_chain_id: AssistantMcpDiscoveryChainId;
catalog_chain_template_matches: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_matches"];
catalog_chain_template_alignment: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"];
pilot_scope: AssistantMcpDiscoveryPilotExecutionContract["pilot_scope"];
asked_domain_family: string | null;
asked_action_family: string | null;
unsupported_but_understood_family: string | null;
ranking_need: string | null;
pending_axes: string[];
provided_axes: string[];
explicit_entity_candidates: string[];
metadata_scope_hint: string | null;
subject_resolution_optional: boolean;
explicit_organization_scope: string | null;
explicit_date_scope: string | null;
}
export interface AssistantMcpRouteCandidateContract {
schema_version: typeof ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
candidate_status: AssistantMcpRouteCandidateStatus;
selected_chain_id: AssistantMcpDiscoveryChainId;
selected_chain_summary: string;
nearest_catalog_chain_template: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["top_chain_template_match"];
catalog_alignment_status: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["alignment_status"];
business_fact_family: string | null;
action_family: string | null;
proof_expectation: string | null;
required_axes: string[];
provided_axes: string[];
missing_axes: string[];
executable_now: boolean;
enablement_reason: string | null;
recommended_next_action: string;
forbidden_overclaim_flags: string[];
}
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;
loop_state: AssistantMcpDiscoveryLoopStateContract;
route_candidate: AssistantMcpRouteCandidateContract;
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";
}
function loopStatusFor(
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
): AssistantMcpDiscoveryLoopStatus {
if (bridgeStatus === "needs_clarification") {
return "awaiting_clarification";
}
if (bridgeStatus === "blocked" || bridgeStatus === "unsupported") {
return "blocked";
}
return "ready_for_next_hop";
}
function routeCandidateStatusFor(
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
): AssistantMcpRouteCandidateStatus {
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
return "blocked";
}
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
return "needs_user_scope";
}
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
return "needs_route_enablement";
}
if (missingProofFamily) {
return "needs_route_enablement";
}
return "ready_for_reviewed_execution";
}
function flattenAxes(
pilot: AssistantMcpDiscoveryPilotExecutionContract,
source: "provided_axes" | "missing_axis_options"
): string[] {
const result: string[] = [];
for (const step of pilot.dry_run.execution_steps) {
if (source === "provided_axes") {
for (const axis of step.provided_axes) {
if (axis && !result.includes(axis)) {
result.push(axis);
}
}
continue;
}
for (const option of step.missing_axis_options) {
for (const axis of option) {
if (axis && !result.includes(axis)) {
result.push(axis);
}
}
}
}
return result;
}
function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] {
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
return uniqueStrings(values);
}
function firstNonEmpty(values: Array<string | null | undefined>): string | null {
for (const value of values) {
const text = String(value ?? "").trim();
if (text) {
return text;
}
}
return null;
}
type AssistantMcpDiscoveryBusinessOverviewMissingProofFamily = NonNullable<
AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]
>["missing_proof_families"][number];
function routeCandidateProofFamiliesFor(actionFamily: string | null, proofExpectation: string | null): string[] {
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
const result: string[] = [];
const add = (family: string) => {
if (!result.includes(family)) {
result.push(family);
}
};
if (!combined || combined === "broad_evaluation bounded_inference") {
return result;
}
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
add("inventory_reserve_liquidation_quality");
}
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
add("debt_due_date_aging_quality");
}
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
add("vendor_risk_procurement_quality");
}
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
add("accounting_profit_margin");
}
return result;
}
function routeCandidateMissingProofFamily(
planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null {
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
return null;
}
const wantedFamilies = routeCandidateProofFamiliesFor(
planner.data_need_graph?.action_family ?? null,
planner.data_need_graph?.proof_expectation ?? null
);
if (wantedFamilies.length <= 0) {
return null;
}
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
}
function routeCandidateEnablementReason(
status: AssistantMcpRouteCandidateStatus,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
missingAxes: string[],
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
): string | null {
if (status === "ready_for_reviewed_execution") {
return null;
}
if (status === "needs_user_scope") {
return missingAxes.length > 0
? `Missing scope axes: ${missingAxes.join(", ")}`
: "Selected chain needs user clarification before MCP execution";
}
if (missingProofFamily) {
return [
`Missing reviewed proof family: ${missingProofFamily.family}`,
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
missingProofFamily.current_supported_evidence
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
: null,
`must_not_claim=${missingProofFamily.must_not_claim}`
]
.filter((item): item is string => Boolean(item))
.join("; ");
}
return firstNonEmpty([
...pilot.query_limitations,
...pilot.evidence.unknown_facts,
"Selected chain is not safely executable by the reviewed MCP runtime yet"
]);
}
function routeCandidateNextAction(status: AssistantMcpRouteCandidateStatus): string {
if (status === "ready_for_reviewed_execution") {
return "Execute through the reviewed runtime bridge and truth gate.";
}
if (status === "needs_user_scope") {
return "Ask the user for the missing scope axes before MCP execution.";
}
if (status === "needs_route_enablement") {
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
}
return "Do not execute until the blocking reason is resolved.";
}
function buildRouteCandidate(
planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
): AssistantMcpRouteCandidateContract {
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
const providedAxes = flattenAxes(pilot, "provided_axes");
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
return {
schema_version: ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
candidate_status: candidateStatus,
selected_chain_id: planner.selected_chain_id,
selected_chain_summary: planner.selected_chain_summary,
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
action_family: planner.data_need_graph?.action_family ?? null,
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
required_axes: [...planner.required_axes],
provided_axes: providedAxes,
missing_axes: missingAxes,
executable_now: candidateStatus === "ready_for_reviewed_execution",
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
recommended_next_action: routeCandidateNextAction(candidateStatus),
forbidden_overclaim_flags: uniqueStrings([
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
])
};
}
function buildLoopState(
planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
): AssistantMcpDiscoveryLoopStateContract {
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
return {
schema_version: ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: loopStatusFor(bridgeStatus),
selected_chain_id: planner.selected_chain_id,
catalog_chain_template_matches: [...planner.catalog_chain_template_matches],
catalog_chain_template_alignment: planner.catalog_chain_template_alignment,
pilot_scope: pilot.pilot_scope,
asked_domain_family: planner.discovery_plan.turn_meaning_ref?.asked_domain_family ?? null,
asked_action_family: planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? null,
unsupported_but_understood_family:
planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family ?? null,
ranking_need: planner.data_need_graph?.ranking_need ?? planner.discovery_plan.turn_meaning_ref?.seeded_ranking_need ?? null,
pending_axes: plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"),
provided_axes: flattenAxes(pilot, "provided_axes"),
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
metadata_scope_hint:
planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint ??
planner.data_need_graph?.metadata_scope_hint ??
null,
subject_resolution_optional:
planner.discovery_plan.turn_meaning_ref?.subject_resolution_optional === true ||
planner.data_need_graph?.subject_resolution_optional === true,
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
explicit_date_scope: planner.discovery_plan.turn_meaning_ref?.explicit_date_scope ?? null
};
}
export async function runAssistantMcpDiscoveryRuntimeBridge(
input: AssistantMcpDiscoveryRuntimeBridgeInput
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
const planner = planAssistantMcpDiscovery({
semanticDataNeed: input.semanticDataNeed,
dataNeedGraph: input.dataNeedGraph,
metadataSurface: input.metadataSurface,
turnMeaning: input.turnMeaning
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
const loopState = buildLoopState(planner, pilot, bridgeStatus);
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
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");
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
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,
loop_state: loopState,
route_candidate: routeCandidate,
user_facing_response_allowed: bridgeStatus !== "blocked",
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
requires_user_clarification: bridgeStatus === "needs_clarification",
reason_codes: reasonCodes
};
}