NODEDC_1C/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanne...

256 lines
8.4 KiB
TypeScript

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;
}
interface PlannerBudgetOverride {
maxProbeCount?: number;
}
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 aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
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 isYearDateScope(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
}
function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: PlannerRecipe): PlannerBudgetOverride {
const meaning = input.turnMeaning ?? null;
const requestedAggregationAxis = aggregationAxis(meaning);
const isValueFlowRecipe =
recipe.semanticDataNeed === "counterparty value-flow evidence" &&
recipe.primitives.includes("query_movements");
if (!isValueFlowRecipe) {
return {};
}
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
return {
maxProbeCount: 30
};
}
return {};
}
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[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "counterparty value-flow evidence",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_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 (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 (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 budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const reasonCodes: string[] = [];
pushReason(reasonCodes, recipe.reason);
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed,
turnMeaning: input.turnMeaning,
proposedPrimitives: recipe.primitives,
requiredAxes: recipe.axes,
maxProbeCount: budgetOverride.maxProbeCount
});
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
};
}