Planner Autonomy: связать inventory templates с exact runtime

This commit is contained in:
2026-05-01 13:31:41 +03:00
parent 3f7caae668
commit a4db26a76e
7 changed files with 975 additions and 57 deletions
@@ -391,7 +391,13 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
}
if (isInventoryTemplatePilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По exact inventory runtime в 1С найдены подтвержденные строки; ответ ограничен проверенным складским/товарным срезом.";
}
if (isInventoryTemplatePilot(pilot) && mode === "checked_sources_only") {
if (pilot.mcp_execution_performed) {
return "Exact inventory runtime был проверен, но подтвержденный складской/товарный факт в найденных строках не получен.";
}
return "Инвентарный route-template уже выбран, но live-исполнение этого generic MCP контура еще не подключено; складской/товарный факт не подтвержден.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
@@ -532,6 +538,9 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "checked_sources_only" && isInventoryTemplatePilot(pilot)) {
if (pilot.mcp_execution_performed) {
return "Можно уточнить дату, организацию, склад, поставщика или позицию и повторить exact inventory проверку.";
}
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
@@ -589,8 +598,11 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
}
if (isInventoryTemplatePilot(pilot)) {
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
if (!pilot.mcp_execution_performed) {
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
}
claims.push("Do not expose inventory_route_template_v1 or MCP primitive names in the user answer.");
claims.push("Do not claim full inventory coverage outside the checked rows, date, organization, item, or supplier scope.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
@@ -337,6 +337,27 @@ function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "p
return {};
}
function asOfDateFromDateScope(dateScope: string | null): string | null {
if (!dateScope) {
return null;
}
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (dateMatch) {
return `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
}
const monthMatch = dateScope.match(/^(\d{4})-(\d{2})$/);
if (monthMatch) {
const year = Number(monthMatch[1]);
const month = Number(monthMatch[2]);
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`;
}
}
const yearMatch = dateScope.match(/^(\d{4})$/);
return yearMatch ? `${yearMatch[1]}-12-31` : null;
}
function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
@@ -365,6 +386,37 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A
};
}
function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const subject = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
const asOfDate = asOfDateFromDateScope(dateScope);
const filters: AddressFilterSet = {
...dateScopeToFilters(dateScope),
...(asOfDate ? { as_of_date: asOfDate } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
if (
planner.selected_chain_id === "inventory_purchase_provenance" ||
planner.selected_chain_id === "inventory_sale_trace"
) {
return {
...filters,
...(subject ? { item: subject } : {})
};
}
if (planner.selected_chain_id === "inventory_supplier_overlap") {
return {
...filters,
...(subject ? { counterparty: subject } : {})
};
}
return filters;
}
function organizationScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
@@ -611,6 +663,15 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
);
}
function isInventoryPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
return (
planner.selected_chain_id === "inventory_stock_snapshot" ||
planner.selected_chain_id === "inventory_supplier_overlap" ||
planner.selected_chain_id === "inventory_purchase_provenance" ||
planner.selected_chain_id === "inventory_sale_trace"
);
}
function isMetadataPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
if (
planner.selected_chain_id === "metadata_inspection" ||
@@ -766,6 +827,36 @@ function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): V
};
}
function inventoryIntentForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AddressIntent | null {
switch (planner.selected_chain_id) {
case "inventory_stock_snapshot":
return "inventory_on_hand_as_of_date";
case "inventory_supplier_overlap":
return "inventory_supplier_stock_overlap_as_of_date";
case "inventory_purchase_provenance":
return "inventory_purchase_provenance_for_item";
case "inventory_sale_trace":
return "inventory_sale_trace_for_item";
default:
return null;
}
}
function inventoryExecutablePrimitiveForPlanner(
planner: AssistantMcpDiscoveryPlannerContract
): AssistantMcpDiscoveryRuntimeStepContract["primitive_id"] | null {
switch (planner.selected_chain_id) {
case "inventory_stock_snapshot":
case "inventory_supplier_overlap":
return "query_movements";
case "inventory_purchase_provenance":
case "inventory_sale_trace":
return "query_documents";
default:
return null;
}
}
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: step.primitive_id,
@@ -1032,6 +1123,16 @@ function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryR
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function summarizeInventoryRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP inventory exact rows fetched";
}
return `${result.fetched_rows} MCP inventory exact rows fetched, ${result.matched_rows} matched inventory scope`;
}
function summarizeMetadataRows(result: AddressMcpMetadataRowsResult): string | null {
if (result.error) {
return null;
@@ -1741,6 +1842,55 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
return null;
}
function rowNumberValue(row: Record<string, unknown>, keys: string[]): number | null {
for (const key of keys) {
const candidate = row[key];
if (typeof candidate === "number" && Number.isFinite(candidate)) {
return candidate;
}
const text = toNonEmptyString(candidate);
if (!text) {
continue;
}
const normalized = text
.replace(/\s+/g, "")
.replace(/\u00a0/g, "")
.replace(",", ".")
.replace(/[^\d.-]/g, "");
const parsed = Number(normalized);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function rowTextValue(row: Record<string, unknown>, keys: string[]): string | null {
for (const key of keys) {
const text = toNonEmptyString(row[key]);
if (text) {
return text;
}
}
return null;
}
function rowInventoryItemValue(row: Record<string, unknown>): string | null {
return rowTextValue(row, ["Номенклатура", "Item", "item", "Товар", "Product", "product"]);
}
function rowWarehouseValue(row: Record<string, unknown>): string | null {
return rowTextValue(row, ["Склад", "Warehouse", "warehouse"]);
}
function rowDocumentValue(row: Record<string, unknown>): string | null {
return rowTextValue(row, ["Регистратор", "Registrator", "registrator", "Документ", "Document", "document"]);
}
function rowQuantityValue(row: Record<string, unknown>): number | null {
return rowNumberValue(row, ["Количество", "Quantity", "quantity", "Qty", "qty", "Остаток", "Balance", "balance"]);
}
function rowCounterpartyValue(row: Record<string, unknown>): string | null {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
@@ -2219,6 +2369,130 @@ function buildBidirectionalValueFlowConfirmedFacts(
];
}
function inventoryLabelRu(intent: AddressIntent): string {
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
return "связи поставщиков с товарным остатком";
}
if (intent === "inventory_purchase_provenance_for_item") {
return "закупочной истории позиции";
}
if (intent === "inventory_sale_trace_for_item") {
return "продаж по позиции";
}
return "складского среза";
}
function inventoryScopeSuffixRu(input: {
intent: AddressIntent;
item: string | null;
counterparty: string | null;
organization: string | null;
asOfDate: string | null;
dateScope: string | null;
}): string {
const parts: string[] = [];
if (input.organization) {
parts.push(`по организации ${input.organization}`);
}
if (input.item) {
parts.push(`по позиции ${input.item}`);
}
if (input.intent === "inventory_supplier_stock_overlap_as_of_date" && input.counterparty) {
parts.push(`по поставщику/контрагенту ${input.counterparty}`);
}
if (input.asOfDate) {
parts.push(`на ${input.asOfDate}`);
} else if (input.dateScope) {
parts.push(`за ${input.dateScope}`);
}
return parts.length > 0 ? ` ${parts.join(", ")}` : "";
}
function inventoryRowSample(row: Record<string, unknown>): string | null {
const item = rowInventoryItemValue(row);
const quantity = rowQuantityValue(row);
const warehouse = rowWarehouseValue(row);
const counterparty = rowCounterpartyValue(row);
const document = rowDocumentValue(row);
const parts: string[] = [];
if (item) {
parts.push(item);
}
if (quantity !== null) {
parts.push(`${quantity} шт.`);
}
if (warehouse) {
parts.push(`склад ${warehouse}`);
}
if (counterparty) {
parts.push(`контрагент ${counterparty}`);
}
if (document) {
parts.push(`документ ${document}`);
}
return parts.length > 0 ? parts.join(", ") : null;
}
function buildInventoryConfirmedFacts(
result: AddressMcpQueryExecutorResult,
planner: AssistantMcpDiscoveryPlannerContract,
intent: AddressIntent
): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const item =
intent === "inventory_purchase_provenance_for_item" || intent === "inventory_sale_trace_for_item"
? firstEntityCandidate(planner)
: null;
const counterparty = intent === "inventory_supplier_stock_overlap_as_of_date" ? firstEntityCandidate(planner) : null;
const scope = inventoryScopeSuffixRu({
intent,
item,
counterparty,
organization: organizationScopeForPlanner(planner),
asOfDate: asOfDateFromDateScope(dateScope),
dateScope
});
const samples = result.rows
.slice(0, 3)
.map((row) => inventoryRowSample(row))
.filter((value): value is string => Boolean(value));
const sampleSuffix = samples.length > 0 ? ` Примеры строк: ${samples.join("; ")}.` : "";
return [`В 1С найдены подтвержденные строки ${inventoryLabelRu(intent)}${scope}: ${result.matched_rows}.${sampleSuffix}`];
}
function buildInventoryInferredFacts(result: AddressMcpQueryExecutorResult, intent: AddressIntent): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
}
if (result.matched_rows <= 0) {
return [
`По ${inventoryLabelRu(intent)} удалось проверить только ограниченный срез 1С; подтвержденных строк этим поиском не найдено.`
];
}
return [
`Вывод по ${inventoryLabelRu(intent)} ограничен найденными строками 1С и указанными датой, организацией, позицией или поставщиком.`
];
}
function buildInventoryUnknownFacts(
result: AddressMcpQueryExecutorResult | null,
intent: AddressIntent,
dateScope: string | null
): string[] {
const facts = [
dateScope
? `Полный товарный контур вне проверенного среза ${dateScope} не подтвержден.`
: "Полный товарный контур без явного проверенного периода или даты не подтвержден."
];
if (!result || result.error || result.matched_rows <= 0) {
facts.unshift(`Подтвержденный факт по ${inventoryLabelRu(intent)} в проверенных строках 1С не найден.`);
}
return facts;
}
function buildLifecycleInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
@@ -2441,18 +2715,6 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As
}
}
function isLivePilotChainSupported(chainId: AssistantMcpDiscoveryChainId): boolean {
if (
chainId === "inventory_stock_snapshot" ||
chainId === "inventory_supplier_overlap" ||
chainId === "inventory_purchase_provenance" ||
chainId === "inventory_sale_trace"
) {
return false;
}
return true;
}
export async function executeAssistantMcpDiscoveryPilot(
planner: AssistantMcpDiscoveryPlannerContract,
deps: AssistantMcpDiscoveryPilotExecutorDeps = DEFAULT_DEPS
@@ -2525,16 +2787,16 @@ export async function executeAssistantMcpDiscoveryPilot(
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
const entityResolutionPilotEligible = isEntityResolutionPilotEligible(planner);
const livePilotChainSupported = isLivePilotChainSupported(planner.selected_chain_id);
const inventoryPilotEligible = isInventoryPilotEligible(planner);
if (
!livePilotChainSupported ||
(!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible)
!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible &&
!inventoryPilotEligible
) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
@@ -2570,6 +2832,152 @@ export async function executeAssistantMcpDiscoveryPilot(
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (inventoryPilotEligible) {
let queryResult: AddressMcpQueryExecutorResult | null = null;
const inventoryIntent = inventoryIntentForPlanner(planner);
const executablePrimitive = inventoryExecutablePrimitiveForPlanner(planner);
if (!inventoryIntent || !executablePrimitive) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_not_mapped");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Inventory exact recipe is not mapped");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Inventory exact recipe is not mapped"],
reason_codes: reasonCodes
};
}
const filters = buildInventoryExactFilters(planner);
const selection = selectAddressRecipe(inventoryIntent, filters);
if (selection.missing_required_filters.length > 0) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_needs_required_filters");
const evidence = buildEmptyEvidence(
planner,
dryRun,
probeResults,
`Inventory exact recipe needs required filters: ${selection.missing_required_filters.join(", ")}`
);
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: [`Inventory exact recipe needs required filters: ${selection.missing_required_filters.join(", ")}`],
reason_codes: reasonCodes
};
}
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Inventory exact recipe is not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Inventory exact recipe is not available"],
reason_codes: reasonCodes
};
}
pushReason(reasonCodes, "pilot_inventory_exact_recipe_selected");
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== executablePrimitive) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, `pilot_inventory_exact_bridge_executes_${executablePrimitive}`));
continue;
}
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_inventory_exact_mcp_error");
} else {
pushReason(reasonCodes, "pilot_inventory_exact_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeInventoryRows(queryResult) : null;
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildInventoryConfirmedFacts(queryResult, planner, inventoryIntent) : [],
inferredFacts: queryResult ? buildInventoryInferredFacts(queryResult, inventoryIntent) : [],
unknownFacts: buildInventoryUnknownFacts(
queryResult,
inventoryIntent,
toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope)
),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
if (metadataPilotEligible) {
let metadataResult: AddressMcpMetadataRowsResult | null = null;
const metadataScope = metadataScopeForPlanner(planner);