Planner Autonomy: связать inventory templates с exact runtime
This commit is contained in:
+13
-1
@@ -296,7 +296,13 @@ function headlineFor(mode, pilot) {
|
||||
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") {
|
||||
@@ -430,6 +436,9 @@ function nextStepFor(mode, pilot) {
|
||||
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) {
|
||||
@@ -486,8 +495,11 @@ function buildMustNotClaim(pilot) {
|
||||
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.");
|
||||
|
||||
+376
-17
@@ -107,6 +107,26 @@ function dateScopeToFilters(dateScope) {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
function asOfDateFromDateScope(dateScope) {
|
||||
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) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
@@ -133,6 +153,34 @@ function buildValueFlowFilters(planner) {
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function buildInventoryExactFilters(planner) {
|
||||
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 = {
|
||||
...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) {
|
||||
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
|
||||
}
|
||||
@@ -340,6 +388,12 @@ function isValueFlowPilotEligible(planner) {
|
||||
combined.includes("payout") ||
|
||||
combined.includes("value")));
|
||||
}
|
||||
function isInventoryPilotEligible(planner) {
|
||||
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) {
|
||||
if (planner.selected_chain_id === "metadata_inspection" ||
|
||||
planner.selected_chain_id === "metadata_lane_clarification" ||
|
||||
@@ -468,6 +522,32 @@ function valueFlowPilotProfile(planner) {
|
||||
direction: "incoming_customer_revenue"
|
||||
};
|
||||
}
|
||||
function inventoryIntentForPlanner(planner) {
|
||||
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) {
|
||||
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, limitation) {
|
||||
return {
|
||||
primitive_id: step.primitive_id,
|
||||
@@ -677,6 +757,15 @@ function summarizeValueFlowRows(result) {
|
||||
}
|
||||
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
|
||||
}
|
||||
function summarizeInventoryRows(result) {
|
||||
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) {
|
||||
if (result.error) {
|
||||
return null;
|
||||
@@ -1267,6 +1356,49 @@ function rowAmountValue(row) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function rowNumberValue(row, keys) {
|
||||
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, keys) {
|
||||
for (const key of keys) {
|
||||
const text = toNonEmptyString(row[key]);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function rowInventoryItemValue(row) {
|
||||
return rowTextValue(row, ["Номенклатура", "Item", "item", "Товар", "Product", "product"]);
|
||||
}
|
||||
function rowWarehouseValue(row) {
|
||||
return rowTextValue(row, ["Склад", "Warehouse", "warehouse"]);
|
||||
}
|
||||
function rowDocumentValue(row) {
|
||||
return rowTextValue(row, ["Регистратор", "Registrator", "registrator", "Документ", "Document", "document"]);
|
||||
}
|
||||
function rowQuantityValue(row) {
|
||||
return rowNumberValue(row, ["Количество", "Quantity", "quantity", "Qty", "qty", "Остаток", "Balance", "balance"]);
|
||||
}
|
||||
function rowCounterpartyValue(row) {
|
||||
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
|
||||
for (const candidate of candidates) {
|
||||
@@ -1663,6 +1795,109 @@ function buildBidirectionalValueFlowConfirmedFacts(derived) {
|
||||
`1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=${hasIncoming ? "found" : "not_found"}, outgoing=${hasOutgoing ? "found" : "not_found"}`
|
||||
];
|
||||
}
|
||||
function inventoryLabelRu(intent) {
|
||||
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) {
|
||||
const parts = [];
|
||||
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) {
|
||||
const item = rowInventoryItemValue(row);
|
||||
const quantity = rowQuantityValue(row);
|
||||
const warehouse = rowWarehouseValue(row);
|
||||
const counterparty = rowCounterpartyValue(row);
|
||||
const document = rowDocumentValue(row);
|
||||
const parts = [];
|
||||
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, planner, intent) {
|
||||
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) => Boolean(value));
|
||||
const sampleSuffix = samples.length > 0 ? ` Примеры строк: ${samples.join("; ")}.` : "";
|
||||
return [`В 1С найдены подтвержденные строки ${inventoryLabelRu(intent)}${scope}: ${result.matched_rows}.${sampleSuffix}`];
|
||||
}
|
||||
function buildInventoryInferredFacts(result, intent) {
|
||||
if (result.error || result.fetched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (result.matched_rows <= 0) {
|
||||
return [
|
||||
`По ${inventoryLabelRu(intent)} удалось проверить только ограниченный срез 1С; подтвержденных строк этим поиском не найдено.`
|
||||
];
|
||||
}
|
||||
return [
|
||||
`Вывод по ${inventoryLabelRu(intent)} ограничен найденными строками 1С и указанными датой, организацией, позицией или поставщиком.`
|
||||
];
|
||||
}
|
||||
function buildInventoryUnknownFacts(result, intent, dateScope) {
|
||||
const facts = [
|
||||
dateScope
|
||||
? `Полный товарный контур вне проверенного среза ${dateScope} не подтвержден.`
|
||||
: "Полный товарный контур без явного проверенного периода или даты не подтвержден."
|
||||
];
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
facts.unshift(`Подтвержденный факт по ${inventoryLabelRu(intent)} в проверенных строках 1С не найден.`);
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
function buildLifecycleInferredFacts(result) {
|
||||
if (result.error || result.fetched_rows <= 0) {
|
||||
return [];
|
||||
@@ -1831,15 +2066,6 @@ function pilotScopeForPlanner(planner) {
|
||||
return "entity_resolution_search_v1";
|
||||
}
|
||||
}
|
||||
function isLivePilotChainSupported(chainId) {
|
||||
if (chainId === "inventory_stock_snapshot" ||
|
||||
chainId === "inventory_supplier_overlap" ||
|
||||
chainId === "inventory_purchase_provenance" ||
|
||||
chainId === "inventory_sale_trace") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const runtimeDeps = {
|
||||
...DEFAULT_DEPS,
|
||||
@@ -1906,14 +2132,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
|
||||
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
|
||||
const entityResolutionPilotEligible = isEntityResolutionPilotEligible(planner);
|
||||
const livePilotChainSupported = isLivePilotChainSupported(planner.selected_chain_id);
|
||||
if (!livePilotChainSupported ||
|
||||
(!metadataPilotEligible &&
|
||||
!documentPilotEligible &&
|
||||
!movementPilotEligible &&
|
||||
!lifecyclePilotEligible &&
|
||||
!valueFlowPilotEligible &&
|
||||
!entityResolutionPilotEligible)) {
|
||||
const inventoryPilotEligible = isInventoryPilotEligible(planner);
|
||||
if (!metadataPilotEligible &&
|
||||
!documentPilotEligible &&
|
||||
!movementPilotEligible &&
|
||||
!lifecyclePilotEligible &&
|
||||
!valueFlowPilotEligible &&
|
||||
!entityResolutionPilotEligible &&
|
||||
!inventoryPilotEligible) {
|
||||
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
|
||||
for (const step of dryRun.execution_steps) {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
@@ -1946,6 +2172,139 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const organizationScope = organizationScopeForPlanner(planner);
|
||||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||
const rankingNeed = rankingNeedForPlanner(planner);
|
||||
if (inventoryPilotEligible) {
|
||||
let queryResult = 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: exports.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 = (0, addressRecipeCatalog_1.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: exports.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: exports.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 = (0, addressRecipeCatalog_1.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 = (0, assistantMcpDiscoveryPolicy_1.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: exports.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 = null;
|
||||
const metadataScope = metadataScopeForPlanner(planner);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -293,7 +293,11 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only");
|
||||
});
|
||||
|
||||
it("keeps inventory catalog templates unsupported until exact runtime evidence is bridged", async () => {
|
||||
it("bridges inventory stock catalog templates through exact runtime evidence", async () => {
|
||||
const deps = buildDeps([
|
||||
{ Period: "2021-09-30T00:00:00", Item: "Widget A", Quantity: 10, Warehouse: "Main" },
|
||||
{ Period: "2021-09-30T00:00:00", Item: "Widget B", Quantity: 4, Warehouse: "Reserve" }
|
||||
]);
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
@@ -322,7 +326,7 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
explicit_organization_scope: "OOO Alternative Plus",
|
||||
explicit_date_scope: "2021-09-30"
|
||||
},
|
||||
deps: buildDeps([])
|
||||
deps
|
||||
});
|
||||
const userFacing = [
|
||||
result.answer_draft.headline,
|
||||
@@ -333,12 +337,14 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
result.answer_draft.next_step_line ?? ""
|
||||
].join("\n");
|
||||
|
||||
expect(result.bridge_status).toBe("unsupported");
|
||||
expect(result.answer_draft.answer_mode).toBe("checked_sources_only");
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(result.pilot.pilot_scope).toBe("inventory_route_template_v1");
|
||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||
expect(result.pilot.mcp_execution_performed).toBe(true);
|
||||
expect(result.pilot.executed_primitives).toEqual(["query_movements"]);
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledWith(expect.objectContaining({ account_scope: ["41.01"] }));
|
||||
expect(result.loop_state).toMatchObject({
|
||||
loop_status: "blocked",
|
||||
loop_status: "ready_for_next_hop",
|
||||
selected_chain_id: "inventory_stock_snapshot",
|
||||
pilot_scope: "inventory_route_template_v1",
|
||||
asked_domain_family: "inventory_stock",
|
||||
@@ -346,21 +352,126 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
explicit_organization_scope: "OOO Alternative Plus",
|
||||
explicit_date_scope: "2021-09-30"
|
||||
});
|
||||
expect(result.business_fact_answer_allowed).toBe(false);
|
||||
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.reason_codes).toContain("pilot_inventory_exact_recipe_selected");
|
||||
expect(result.reason_codes).toContain("pilot_inventory_exact_mcp_executed");
|
||||
expect(result.reason_codes).not.toContain("pilot_scope_unsupported_for_live_execution");
|
||||
expect(result.answer_draft.must_not_claim).toEqual(
|
||||
expect.arrayContaining([
|
||||
"Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.",
|
||||
"Do not expose inventory_route_template_v1 or MCP primitive names in the user answer."
|
||||
"Do not expose inventory_route_template_v1 or MCP primitive names in the user answer.",
|
||||
"Do not claim full inventory coverage outside the checked rows, date, organization, item, or supplier scope."
|
||||
])
|
||||
);
|
||||
expect(userFacing).toContain("Инвентарный route-template");
|
||||
expect(result.answer_draft.must_not_claim).not.toContain(
|
||||
"Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence."
|
||||
);
|
||||
expect(userFacing).toContain("exact inventory runtime");
|
||||
expect(userFacing).toContain("Widget A");
|
||||
expect(userFacing).not.toContain("inventory_route_template_v1");
|
||||
expect(userFacing).not.toContain("query_movements");
|
||||
expect(userFacing).not.toContain("primitive");
|
||||
expect(userFacing).not.toContain("MCP discovery pilot");
|
||||
});
|
||||
|
||||
it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
|
||||
const deps = buildDeps([
|
||||
{
|
||||
Period: "2021-08-10T00:00:00",
|
||||
Item: "Widget A",
|
||||
Counterparty: "Supplier One",
|
||||
Registrator: "Purchase 0001"
|
||||
}
|
||||
]);
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: ["Widget A"],
|
||||
business_fact_family: "inventory_purchase_provenance",
|
||||
action_family: "purchase_provenance",
|
||||
aggregation_need: null,
|
||||
time_scope_need: null,
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"resolve_entity_reference",
|
||||
"fetch_scoped_documents",
|
||||
"drilldown_related_objects",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unproven_supplier_attribution"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_inventory_purchase_provenance"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "inventory_stock",
|
||||
asked_action_family: "purchase_provenance",
|
||||
explicit_entity_candidates: ["Widget A"]
|
||||
},
|
||||
deps
|
||||
});
|
||||
const userFacing = [
|
||||
result.answer_draft.headline,
|
||||
...result.answer_draft.confirmed_lines,
|
||||
...result.answer_draft.inference_lines,
|
||||
...result.answer_draft.unknown_lines,
|
||||
...result.answer_draft.limitation_lines,
|
||||
result.answer_draft.next_step_line ?? ""
|
||||
].join("\n");
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.planner.selected_chain_id).toBe("inventory_purchase_provenance");
|
||||
expect(result.pilot.executed_primitives).toEqual(["query_documents"]);
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledWith(expect.objectContaining({ account_scope: ["41.01"] }));
|
||||
expect(userFacing).toContain("Widget A");
|
||||
expect(userFacing).toContain("Supplier One");
|
||||
expect(userFacing).not.toContain("inventory_route_template_v1");
|
||||
expect(userFacing).not.toContain("query_documents");
|
||||
expect(userFacing).not.toContain("primitive");
|
||||
});
|
||||
|
||||
it("keeps selected-item inventory templates in clarification when the item anchor is missing", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "inventory_sale_trace",
|
||||
action_family: "sale_trace",
|
||||
aggregation_need: null,
|
||||
time_scope_need: null,
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "clarification_required",
|
||||
clarification_gaps: ["item"],
|
||||
decomposition_candidates: [
|
||||
"resolve_entity_reference",
|
||||
"fetch_scoped_documents",
|
||||
"drilldown_related_objects",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unproven_buyer_or_sale_trace"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_inventory_sale_trace"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "inventory_stock",
|
||||
asked_action_family: "sale_trace"
|
||||
},
|
||||
deps: buildDeps([])
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("needs_clarification");
|
||||
expect(result.requires_user_clarification).toBe(true);
|
||||
expect(result.pilot.pilot_scope).toBe("inventory_route_template_v1");
|
||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
|
||||
expect(result.loop_state.pending_axes).toContain("item");
|
||||
});
|
||||
|
||||
it("keeps document evidence executable when the planner expands primitives from fact-axis search", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
|
||||
Reference in New Issue
Block a user