ARCH: подключить catalog primitive search к data-need planner
This commit is contained in:
parent
70325dacb6
commit
8b6cd4c329
|
|
@ -1,6 +1,7 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = void 0;
|
||||
exports.searchAssistantMcpCatalogPrimitivesByDecompositionCandidates = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates;
|
||||
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
|
||||
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
|
||||
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
|
||||
|
|
@ -11,6 +12,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "search_business_entity",
|
||||
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
|
||||
decomposition_hints: ["search_business_entity"],
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "document", "account"],
|
||||
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
|
||||
|
|
@ -21,6 +23,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "inspect_1c_metadata",
|
||||
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
|
||||
decomposition_hints: ["inspect_metadata_surface"],
|
||||
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
|
||||
optional_axes: ["business_entity", "account", "counterparty"],
|
||||
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
|
||||
|
|
@ -31,6 +34,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "resolve_entity_reference",
|
||||
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
|
||||
decomposition_hints: ["resolve_entity_reference"],
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "inn", "document"],
|
||||
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
|
||||
|
|
@ -41,6 +45,12 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "query_movements",
|
||||
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
|
||||
decomposition_hints: [
|
||||
"collect_scoped_movements",
|
||||
"collect_incoming_movements",
|
||||
"collect_outgoing_movements",
|
||||
"fetch_scoped_movements"
|
||||
],
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
|
|
@ -51,6 +61,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "query_documents",
|
||||
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
|
||||
decomposition_hints: ["fetch_scoped_documents", "fetch_supporting_documents"],
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
|
|
@ -61,6 +72,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "aggregate_by_axis",
|
||||
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
|
||||
decomposition_hints: ["aggregate_checked_amounts", "aggregate_ranked_axis_values", "aggregate_by_month"],
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
|
|
@ -71,6 +83,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "drilldown_related_objects",
|
||||
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
|
||||
decomposition_hints: ["drilldown_related_objects"],
|
||||
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
|
||||
optional_axes: ["period", "account", "amount"],
|
||||
output_fact_kinds: ["related_objects", "relationship_edges"],
|
||||
|
|
@ -81,6 +94,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "probe_coverage",
|
||||
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
|
||||
decomposition_hints: ["probe_coverage"],
|
||||
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
|
||||
optional_axes: ["period", "organization", "counterparty", "document", "account"],
|
||||
output_fact_kinds: ["coverage_status", "known_gaps"],
|
||||
|
|
@ -91,6 +105,7 @@ const PRIMITIVE_CONTRACTS = [
|
|||
{
|
||||
primitive_id: "explain_evidence_basis",
|
||||
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
|
||||
decomposition_hints: ["explain_evidence_basis"],
|
||||
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
|
||||
optional_axes: ["coverage_target", "domain_family"],
|
||||
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
|
||||
|
|
@ -123,6 +138,33 @@ function hasAnyAxisGroup(axisSet, groups) {
|
|||
function missingAxisGroups(axisSet, groups) {
|
||||
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
function normalizeDecompositionStep(value) {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates(input) {
|
||||
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
|
||||
const result = [];
|
||||
for (const candidate of input.decomposition_candidates) {
|
||||
const normalizedCandidate = normalizeDecompositionStep(candidate);
|
||||
if (!normalizedCandidate) {
|
||||
continue;
|
||||
}
|
||||
for (const contract of PRIMITIVE_CONTRACTS) {
|
||||
if (contract.primitive_id === "aggregate_by_axis" &&
|
||||
normalizedCandidate === "aggregate_by_month" &&
|
||||
!allowAggregateByAxis) {
|
||||
continue;
|
||||
}
|
||||
if (!contract.decomposition_hints.includes(normalizedCandidate)) {
|
||||
continue;
|
||||
}
|
||||
if (!result.includes(contract.primitive_id)) {
|
||||
result.push(contract.primitive_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function buildAssistantMcpCatalogIndex() {
|
||||
const reasonCodes = [];
|
||||
const missingContracts = assistantMcpDiscoveryPolicy_1.ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ function headlineFor(mode, pilot) {
|
|||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||
}
|
||||
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
|
||||
return "По данным 1С можно построить ограниченный рейтинг по контрагентам на подтвержденных строках денежных движений.";
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
|
|
@ -323,15 +323,15 @@ function headlineFor(mode, pilot) {
|
|||
}
|
||||
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
return `Могу сравнить входящий и исходящий денежный поток, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
return `Могу посчитать рейтинг по денежному потоку между контрагентами, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать общий денежный поток в проверяемом окне, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
return `Могу посчитать общий денежный поток в проверяемом окне, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
|
|
@ -370,7 +370,7 @@ function nextStepFor(mode, pilot) {
|
|||
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
|
||||
}
|
||||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||
return clarificationNextStepLine(pilot, "рейтингу контрагентов по денежному потоку");
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "денежному потоку");
|
||||
|
|
@ -547,7 +547,7 @@ function derivedRankedValueFlowInferenceLine(pilot) {
|
|||
}
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||
return `Рейтинг по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||
}
|
||||
function derivedRankedValueFlowConfirmedLine(pilot) {
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
|
|
@ -570,7 +570,7 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
|||
.join("; ");
|
||||
const trail = tail ? ` Следом: ${tail}.` : "";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; ranking может быть неполным."
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,37 @@ function includesAny(text, tokens) {
|
|||
function isYearDateScope(meaning) {
|
||||
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
|
||||
}
|
||||
function primitivesFromGraphDecomposition(input) {
|
||||
const decompositionCandidates = input.dataNeedGraph?.decomposition_candidates ?? [];
|
||||
if (decompositionCandidates.length <= 0) {
|
||||
return { primitives: input.fallbackPrimitives, reasonCodes: [] };
|
||||
}
|
||||
const searchedPrimitives = (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogPrimitivesByDecompositionCandidates)({
|
||||
decomposition_candidates: decompositionCandidates,
|
||||
allow_aggregate_by_axis: input.allowAggregateByAxis
|
||||
});
|
||||
if (searchedPrimitives.length <= 0) {
|
||||
return {
|
||||
primitives: input.fallbackPrimitives,
|
||||
reasonCodes: ["planner_fell_back_to_recipe_primitives_after_empty_catalog_search"]
|
||||
};
|
||||
}
|
||||
const mergedPrimitives = [...searchedPrimitives];
|
||||
for (const primitive of input.fallbackPrimitives) {
|
||||
if (!mergedPrimitives.includes(primitive)) {
|
||||
mergedPrimitives.push(primitive);
|
||||
}
|
||||
}
|
||||
return {
|
||||
primitives: mergedPrimitives,
|
||||
reasonCodes: mergedPrimitives.length === searchedPrimitives.length
|
||||
? ["planner_selected_catalog_primitives_from_decomposition_candidates"]
|
||||
: [
|
||||
"planner_selected_catalog_primitives_from_decomposition_candidates",
|
||||
"planner_completed_catalog_searched_chain_with_recipe_primitives"
|
||||
]
|
||||
};
|
||||
}
|
||||
function budgetOverrideFor(input, recipe) {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
|
|
@ -110,6 +141,11 @@ function recipeFor(input) {
|
|||
}
|
||||
if (graphFactFamily === "value_flow") {
|
||||
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "probe_coverage"],
|
||||
allowAggregateByAxis: false
|
||||
});
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||
|
|
@ -119,12 +155,18 @@ function recipeFor(input) {
|
|||
semanticDataNeed: "bidirectional value-flow comparison evidence",
|
||||
chainId: "value_flow_comparison",
|
||||
chainSummary: "Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
|
||||
primitives: ["query_movements", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
|
||||
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
allowAggregateByAxis: true
|
||||
});
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -132,14 +174,20 @@ function recipeFor(input) {
|
|||
semanticDataNeed: "ranked value-flow evidence",
|
||||
chainId: "value_flow_ranking",
|
||||
chainSummary: "Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
|
||||
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: dataNeedGraph.ranking_need === "bottom_asc"
|
||||
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
|
||||
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (openScopeTotalWithoutSubject) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
allowAggregateByAxis: true
|
||||
});
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
|
|
@ -151,13 +199,19 @@ function recipeFor(input) {
|
|||
semanticDataNeed: "organization-scoped value-flow evidence",
|
||||
chainId: "value_flow",
|
||||
chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
|
||||
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
|
||||
: "planner_selected_open_scope_value_flow_total_from_data_need_graph"
|
||||
: "planner_selected_open_scope_value_flow_total_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
allowAggregateByAxis: true
|
||||
});
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -168,14 +222,19 @@ function recipeFor(input) {
|
|||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
chainId: "value_flow",
|
||||
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||
? "planner_selected_monthly_value_flow_from_data_need_graph"
|
||||
: "planner_selected_value_flow_from_data_need_graph"
|
||||
: "planner_selected_value_flow_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (graphFactFamily === "activity_lifecycle") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"]
|
||||
});
|
||||
pushUnique(axes, "document_date");
|
||||
pushUnique(axes, "coverage_target");
|
||||
pushUnique(axes, "evidence_basis");
|
||||
|
|
@ -183,56 +242,77 @@ function recipeFor(input) {
|
|||
semanticDataNeed: "counterparty lifecycle evidence",
|
||||
chainId: "lifecycle",
|
||||
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_lifecycle_from_data_need_graph"
|
||||
reason: "planner_selected_lifecycle_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (graphFactFamily === "schema_surface") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["inspect_1c_metadata"]
|
||||
});
|
||||
pushUnique(axes, "metadata_scope");
|
||||
return {
|
||||
semanticDataNeed: "1C metadata evidence",
|
||||
chainId: "metadata_inspection",
|
||||
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_metadata_from_data_need_graph"
|
||||
reason: "planner_selected_metadata_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (graphFactFamily === "movement_evidence") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"]
|
||||
});
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "movement evidence",
|
||||
chainId: "movement_evidence",
|
||||
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_movement_from_data_need_graph"
|
||||
reason: "planner_selected_movement_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (graphFactFamily === "document_evidence") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"]
|
||||
});
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
chainId: "document_evidence",
|
||||
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_document_from_data_need_graph"
|
||||
reason: "planner_selected_document_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"]
|
||||
});
|
||||
pushUnique(axes, "business_entity");
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "entity discovery evidence",
|
||||
chainId: "entity_resolution",
|
||||
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
|
||||
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: graphAction === "search_business_entity"
|
||||
? "planner_selected_entity_resolution_from_data_need_graph"
|
||||
: "planner_selected_entity_resolution_recipe"
|
||||
: "planner_selected_entity_resolution_recipe",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
||||
|
|
@ -347,6 +427,9 @@ function planAssistantMcpDiscovery(input) {
|
|||
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||
const reasonCodes = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
for (const reason of recipe.extraReasons ?? []) {
|
||||
pushReason(reasonCodes, reason);
|
||||
}
|
||||
if (dataNeedGraph) {
|
||||
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type AssistantMcpCatalogPlanReviewStatus = "catalog_compatible" | "needs_
|
|||
export interface AssistantMcpCatalogPrimitiveContract {
|
||||
primitive_id: AssistantMcpDiscoveryPrimitive;
|
||||
purpose: string;
|
||||
decomposition_hints: string[];
|
||||
required_axes_any_of: string[][];
|
||||
optional_axes: string[];
|
||||
output_fact_kinds: string[];
|
||||
|
|
@ -43,6 +44,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "search_business_entity",
|
||||
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
|
||||
decomposition_hints: ["search_business_entity"],
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "document", "account"],
|
||||
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
|
||||
|
|
@ -53,6 +55,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "inspect_1c_metadata",
|
||||
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
|
||||
decomposition_hints: ["inspect_metadata_surface"],
|
||||
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
|
||||
optional_axes: ["business_entity", "account", "counterparty"],
|
||||
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
|
||||
|
|
@ -63,6 +66,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "resolve_entity_reference",
|
||||
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
|
||||
decomposition_hints: ["resolve_entity_reference"],
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "inn", "document"],
|
||||
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
|
||||
|
|
@ -73,6 +77,12 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "query_movements",
|
||||
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
|
||||
decomposition_hints: [
|
||||
"collect_scoped_movements",
|
||||
"collect_incoming_movements",
|
||||
"collect_outgoing_movements",
|
||||
"fetch_scoped_movements"
|
||||
],
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
|
|
@ -83,6 +93,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "query_documents",
|
||||
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
|
||||
decomposition_hints: ["fetch_scoped_documents", "fetch_supporting_documents"],
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
|
|
@ -93,6 +104,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "aggregate_by_axis",
|
||||
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
|
||||
decomposition_hints: ["aggregate_checked_amounts", "aggregate_ranked_axis_values", "aggregate_by_month"],
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
|
|
@ -103,6 +115,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "drilldown_related_objects",
|
||||
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
|
||||
decomposition_hints: ["drilldown_related_objects"],
|
||||
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
|
||||
optional_axes: ["period", "account", "amount"],
|
||||
output_fact_kinds: ["related_objects", "relationship_edges"],
|
||||
|
|
@ -113,6 +126,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "probe_coverage",
|
||||
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
|
||||
decomposition_hints: ["probe_coverage"],
|
||||
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
|
||||
optional_axes: ["period", "organization", "counterparty", "document", "account"],
|
||||
output_fact_kinds: ["coverage_status", "known_gaps"],
|
||||
|
|
@ -123,6 +137,7 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
|
|||
{
|
||||
primitive_id: "explain_evidence_basis",
|
||||
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
|
||||
decomposition_hints: ["explain_evidence_basis"],
|
||||
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
|
||||
optional_axes: ["coverage_target", "domain_family"],
|
||||
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
|
||||
|
|
@ -164,6 +179,47 @@ function missingAxisGroups(axisSet: Set<string>, groups: string[][]): string[][]
|
|||
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
|
||||
function normalizeDecompositionStep(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export interface AssistantMcpCatalogPrimitiveSearchInput {
|
||||
decomposition_candidates: string[];
|
||||
allow_aggregate_by_axis?: boolean;
|
||||
}
|
||||
|
||||
export function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates(
|
||||
input: AssistantMcpCatalogPrimitiveSearchInput
|
||||
): AssistantMcpDiscoveryPrimitive[] {
|
||||
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
|
||||
const result: AssistantMcpDiscoveryPrimitive[] = [];
|
||||
|
||||
for (const candidate of input.decomposition_candidates) {
|
||||
const normalizedCandidate = normalizeDecompositionStep(candidate);
|
||||
if (!normalizedCandidate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const contract of PRIMITIVE_CONTRACTS) {
|
||||
if (
|
||||
contract.primitive_id === "aggregate_by_axis" &&
|
||||
normalizedCandidate === "aggregate_by_month" &&
|
||||
!allowAggregateByAxis
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!contract.decomposition_hints.includes(normalizedCandidate)) {
|
||||
continue;
|
||||
}
|
||||
if (!result.includes(contract.primitive_id)) {
|
||||
result.push(contract.primitive_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildAssistantMcpCatalogIndex(): AssistantMcpCatalogIndexContract {
|
||||
const reasonCodes: string[] = [];
|
||||
const missingContracts = ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||
}
|
||||
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
|
||||
return "По данным 1С можно построить ограниченный рейтинг по контрагентам на подтвержденных строках денежных движений.";
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
|
|
@ -409,15 +409,15 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
}
|
||||
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
return `Могу сравнить входящий и исходящий денежный поток, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
return `Могу посчитать рейтинг по денежному потоку между контрагентами, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать общий денежный поток в проверяемом окне, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
return `Могу посчитать общий денежный поток в проверяемом окне, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
|
|
@ -461,7 +461,7 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
|
||||
}
|
||||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||
return clarificationNextStepLine(pilot, "рейтингу контрагентов по денежному потоку");
|
||||
}
|
||||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "денежному потоку");
|
||||
|
|
@ -655,7 +655,7 @@ function derivedRankedValueFlowInferenceLine(pilot: AssistantMcpDiscoveryPilotEx
|
|||
}
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||
return `Рейтинг по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||
}
|
||||
|
||||
function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
|
|
@ -680,7 +680,7 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
|||
.join("; ");
|
||||
const trail = tail ? ` Следом: ${tail}.` : "";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; ranking может быть неполным."
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
type AssistantMcpDiscoveryTurnMeaningRef
|
||||
} from "./assistantMcpDiscoveryPolicy";
|
||||
import {
|
||||
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
|
||||
type AssistantMcpCatalogPlanReview
|
||||
} from "./assistantMcpCatalogIndex";
|
||||
|
|
@ -53,6 +54,7 @@ interface PlannerRecipe {
|
|||
primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
axes: string[];
|
||||
reason: string;
|
||||
extraReasons?: string[];
|
||||
}
|
||||
|
||||
interface PlannerBudgetOverride {
|
||||
|
|
@ -133,6 +135,46 @@ function isYearDateScope(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | u
|
|||
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
|
||||
}
|
||||
|
||||
function primitivesFromGraphDecomposition(input: {
|
||||
dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||
fallbackPrimitives: AssistantMcpDiscoveryPrimitive[];
|
||||
allowAggregateByAxis?: boolean;
|
||||
}): { primitives: AssistantMcpDiscoveryPrimitive[]; reasonCodes: string[] } {
|
||||
const decompositionCandidates = input.dataNeedGraph?.decomposition_candidates ?? [];
|
||||
if (decompositionCandidates.length <= 0) {
|
||||
return { primitives: input.fallbackPrimitives, reasonCodes: [] };
|
||||
}
|
||||
|
||||
const searchedPrimitives = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates({
|
||||
decomposition_candidates: decompositionCandidates,
|
||||
allow_aggregate_by_axis: input.allowAggregateByAxis
|
||||
});
|
||||
if (searchedPrimitives.length <= 0) {
|
||||
return {
|
||||
primitives: input.fallbackPrimitives,
|
||||
reasonCodes: ["planner_fell_back_to_recipe_primitives_after_empty_catalog_search"]
|
||||
};
|
||||
}
|
||||
|
||||
const mergedPrimitives = [...searchedPrimitives];
|
||||
for (const primitive of input.fallbackPrimitives) {
|
||||
if (!mergedPrimitives.includes(primitive)) {
|
||||
mergedPrimitives.push(primitive);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primitives: mergedPrimitives,
|
||||
reasonCodes:
|
||||
mergedPrimitives.length === searchedPrimitives.length
|
||||
? ["planner_selected_catalog_primitives_from_decomposition_candidates"]
|
||||
: [
|
||||
"planner_selected_catalog_primitives_from_decomposition_candidates",
|
||||
"planner_completed_catalog_searched_chain_with_recipe_primitives"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: PlannerRecipe): PlannerBudgetOverride {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
|
|
@ -184,6 +226,11 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
|
||||
if (graphFactFamily === "value_flow") {
|
||||
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "probe_coverage"],
|
||||
allowAggregateByAxis: false
|
||||
});
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||
|
|
@ -194,12 +241,18 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
chainId: "value_flow_comparison",
|
||||
chainSummary:
|
||||
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
|
||||
primitives: ["query_movements", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
|
||||
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
allowAggregateByAxis: true
|
||||
});
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -208,15 +261,21 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
chainId: "value_flow_ranking",
|
||||
chainSummary:
|
||||
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
|
||||
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason:
|
||||
dataNeedGraph.ranking_need === "bottom_asc"
|
||||
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
|
||||
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
if (openScopeTotalWithoutSubject) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
allowAggregateByAxis: true
|
||||
});
|
||||
pushUnique(axes, "organization");
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
|
|
@ -229,14 +288,20 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
chainId: "value_flow",
|
||||
chainSummary:
|
||||
"Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
|
||||
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason:
|
||||
requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
|
||||
: "planner_selected_open_scope_value_flow_total_from_data_need_graph"
|
||||
: "planner_selected_open_scope_value_flow_total_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
allowAggregateByAxis: true
|
||||
});
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -247,16 +312,21 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
chainId: "value_flow",
|
||||
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason:
|
||||
requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||
? "planner_selected_monthly_value_flow_from_data_need_graph"
|
||||
: "planner_selected_value_flow_from_data_need_graph"
|
||||
: "planner_selected_value_flow_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "activity_lifecycle") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"]
|
||||
});
|
||||
pushUnique(axes, "document_date");
|
||||
pushUnique(axes, "coverage_target");
|
||||
pushUnique(axes, "evidence_basis");
|
||||
|
|
@ -264,61 +334,82 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
semanticDataNeed: "counterparty lifecycle evidence",
|
||||
chainId: "lifecycle",
|
||||
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_lifecycle_from_data_need_graph"
|
||||
reason: "planner_selected_lifecycle_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "schema_surface") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["inspect_1c_metadata"]
|
||||
});
|
||||
pushUnique(axes, "metadata_scope");
|
||||
return {
|
||||
semanticDataNeed: "1C metadata evidence",
|
||||
chainId: "metadata_inspection",
|
||||
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_metadata_from_data_need_graph"
|
||||
reason: "planner_selected_metadata_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "movement_evidence") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"]
|
||||
});
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "movement evidence",
|
||||
chainId: "movement_evidence",
|
||||
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_movement_from_data_need_graph"
|
||||
reason: "planner_selected_movement_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "document_evidence") {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"]
|
||||
});
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
chainId: "document_evidence",
|
||||
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason: "planner_selected_document_from_data_need_graph"
|
||||
reason: "planner_selected_document_from_data_need_graph",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
|
||||
const primitiveSelection = primitivesFromGraphDecomposition({
|
||||
dataNeedGraph,
|
||||
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"]
|
||||
});
|
||||
pushUnique(axes, "business_entity");
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "entity discovery evidence",
|
||||
chainId: "entity_resolution",
|
||||
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
|
||||
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||
primitives: primitiveSelection.primitives,
|
||||
axes,
|
||||
reason:
|
||||
graphAction === "search_business_entity"
|
||||
? "planner_selected_entity_resolution_from_data_need_graph"
|
||||
: "planner_selected_entity_resolution_recipe"
|
||||
: "planner_selected_entity_resolution_recipe",
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -448,6 +539,9 @@ export function planAssistantMcpDiscovery(
|
|||
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
for (const reason of recipe.extraReasons ?? []) {
|
||||
pushReason(reasonCodes, reason);
|
||||
}
|
||||
if (dataNeedGraph) {
|
||||
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } fr
|
|||
import {
|
||||
buildAssistantMcpCatalogIndex,
|
||||
getAssistantMcpCatalogPrimitive,
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
|
||||
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates
|
||||
} from "../src/services/assistantMcpCatalogIndex";
|
||||
|
||||
describe("assistant MCP catalog index", () => {
|
||||
|
|
@ -16,11 +17,44 @@ describe("assistant MCP catalog index", () => {
|
|||
for (const entry of index.primitives) {
|
||||
expect(entry.safe_for_model_planning).toBe(true);
|
||||
expect(entry.runtime_must_execute).toBe(true);
|
||||
expect(entry.decomposition_hints.length).toBeGreaterThan(0);
|
||||
expect(entry.required_axes_any_of.length).toBeGreaterThan(0);
|
||||
expect(entry.output_fact_kinds.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("can search reviewed primitives from data-need decomposition candidates", () => {
|
||||
const primitives = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates({
|
||||
decomposition_candidates: [
|
||||
"resolve_entity_reference",
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"probe_coverage"
|
||||
]
|
||||
});
|
||||
|
||||
expect(primitives).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"query_movements",
|
||||
"aggregate_by_axis",
|
||||
"probe_coverage"
|
||||
]);
|
||||
});
|
||||
|
||||
it("can suppress aggregate_by_axis for decomposition shapes that derive comparison without an aggregate primitive", () => {
|
||||
const primitives = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates({
|
||||
decomposition_candidates: [
|
||||
"collect_incoming_movements",
|
||||
"collect_outgoing_movements",
|
||||
"aggregate_by_month",
|
||||
"probe_coverage"
|
||||
],
|
||||
allow_aggregate_by_axis: false
|
||||
});
|
||||
|
||||
expect(primitives).toEqual(["query_movements", "probe_coverage"]);
|
||||
});
|
||||
|
||||
it("marks a counterparty turnover discovery plan as catalog-compatible when required axes exist", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "counterparty turnover evidence",
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("needs_clarification");
|
||||
expect(draft.headline).toContain("ranking");
|
||||
expect(draft.headline).toContain("рейтинг");
|
||||
expect(draft.next_step_line).toContain("организацию");
|
||||
expect(draft.next_step_line).not.toContain("Уточните контрагента");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
||||
expect(result.reason_codes).toContain("planner_consumed_data_need_graph_v1");
|
||||
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_decomposition_candidates");
|
||||
});
|
||||
|
||||
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
|
||||
|
|
@ -147,6 +148,7 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
|
||||
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
|
||||
expect(result.reason_codes).toContain("planner_selected_movement_from_data_need_graph");
|
||||
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_decomposition_candidates");
|
||||
});
|
||||
|
||||
it("can select value-flow chain from data need graph even when turn meaning family is still under-specified", () => {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
expect(result.requires_user_clarification).toBe(true);
|
||||
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
|
||||
expect(result.answer_draft.headline).toContain("ranking");
|
||||
expect(result.answer_draft.headline).toContain("рейтинг");
|
||||
expect(result.answer_draft.next_step_line).toContain("организацию");
|
||||
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue