ARCH: добавить двусторонний MCP discovery для нетто-потока
This commit is contained in:
@@ -54,10 +54,33 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryValueFlowSideSummary {
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
total_amount_human_ru: string;
|
||||
first_movement_date: string | null;
|
||||
latest_movement_date: string | null;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
|
||||
counterparty: string | null;
|
||||
period_scope: string | null;
|
||||
incoming_customer_revenue: AssistantMcpDiscoveryValueFlowSideSummary;
|
||||
outgoing_supplier_payout: AssistantMcpDiscoveryValueFlowSideSummary;
|
||||
net_amount: number;
|
||||
net_amount_human_ru: string;
|
||||
net_direction: "net_incoming" | "net_outgoing" | "balanced";
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryPilotScope =
|
||||
| "counterparty_lifecycle_query_documents_v1"
|
||||
| "counterparty_value_flow_query_movements_v1"
|
||||
| "counterparty_supplier_payout_query_movements_v1";
|
||||
| "counterparty_supplier_payout_query_movements_v1"
|
||||
| "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
|
||||
export interface AssistantMcpDiscoveryPilotExecutionContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
|
||||
@@ -73,6 +96,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
|
||||
source_rows_summary: string | null;
|
||||
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
|
||||
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
|
||||
query_limitations: string[];
|
||||
reason_codes: string[];
|
||||
}
|
||||
@@ -205,10 +229,12 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
|
||||
interface ValueFlowPilotProfile {
|
||||
scope: Extract<
|
||||
AssistantMcpDiscoveryPilotScope,
|
||||
"counterparty_value_flow_query_movements_v1" | "counterparty_supplier_payout_query_movements_v1"
|
||||
| "counterparty_value_flow_query_movements_v1"
|
||||
| "counterparty_supplier_payout_query_movements_v1"
|
||||
| "counterparty_bidirectional_value_flow_query_movements_v1"
|
||||
>;
|
||||
recipe_intent: Extract<AddressIntent, "customer_revenue_and_payments" | "supplier_payouts_profile">;
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"];
|
||||
recipe_intent: Extract<AddressIntent, "customer_revenue_and_payments" | "supplier_payouts_profile"> | null;
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"] | "bidirectional_net_value_flow";
|
||||
}
|
||||
|
||||
function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): ValueFlowPilotProfile {
|
||||
@@ -216,6 +242,18 @@ function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): V
|
||||
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
|
||||
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
|
||||
const combined = `${action} ${unsupported}`;
|
||||
if (
|
||||
combined.includes("net_value_flow") ||
|
||||
combined.includes("bidirectional") ||
|
||||
combined.includes("netting") ||
|
||||
combined.includes("net")
|
||||
) {
|
||||
return {
|
||||
scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
recipe_intent: null,
|
||||
direction: "bidirectional_net_value_flow"
|
||||
};
|
||||
}
|
||||
if (
|
||||
combined.includes("payout") ||
|
||||
combined.includes("outflow") ||
|
||||
@@ -435,6 +473,91 @@ function deriveValueFlow(
|
||||
};
|
||||
}
|
||||
|
||||
function deriveValueFlowSideSummary(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
probeLimit: number
|
||||
): AssistantMcpDiscoveryValueFlowSideSummary {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return {
|
||||
rows_matched: 0,
|
||||
rows_with_amount: 0,
|
||||
total_amount: 0,
|
||||
total_amount_human_ru: formatAmountHumanRu(0),
|
||||
first_movement_date: null,
|
||||
latest_movement_date: null,
|
||||
coverage_limited_by_probe_limit: false
|
||||
};
|
||||
}
|
||||
|
||||
let totalAmount = 0;
|
||||
let rowsWithAmount = 0;
|
||||
for (const row of result.rows) {
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount !== null) {
|
||||
totalAmount += amount;
|
||||
rowsWithAmount += 1;
|
||||
}
|
||||
}
|
||||
const dates = result.rows
|
||||
.map((row) => rowDateValue(row))
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.sort();
|
||||
return {
|
||||
rows_matched: result.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
total_amount: totalAmount,
|
||||
total_amount_human_ru: formatAmountHumanRu(totalAmount),
|
||||
first_movement_date: dates[0] ?? null,
|
||||
latest_movement_date: dates[dates.length - 1] ?? null,
|
||||
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit
|
||||
};
|
||||
}
|
||||
|
||||
function deriveBidirectionalValueFlow(input: {
|
||||
incomingResult: AddressMcpQueryExecutorResult | null;
|
||||
outgoingResult: AddressMcpQueryExecutorResult | null;
|
||||
counterparty: string | null;
|
||||
periodScope: string | null;
|
||||
probeLimit: number;
|
||||
}): AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null {
|
||||
const incoming = deriveValueFlowSideSummary(input.incomingResult, input.probeLimit);
|
||||
const outgoing = deriveValueFlowSideSummary(input.outgoingResult, input.probeLimit);
|
||||
if (incoming.rows_with_amount <= 0 && outgoing.rows_with_amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
const netAmount = incoming.total_amount - outgoing.total_amount;
|
||||
return {
|
||||
counterparty: input.counterparty,
|
||||
period_scope: input.periodScope,
|
||||
incoming_customer_revenue: incoming,
|
||||
outgoing_supplier_payout: outgoing,
|
||||
net_amount: netAmount,
|
||||
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
|
||||
net_direction: netAmount > 0 ? "net_incoming" : netAmount < 0 ? "net_outgoing" : "balanced",
|
||||
coverage_limited_by_probe_limit:
|
||||
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
|
||||
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeBidirectionalValueFlowRows(input: {
|
||||
incomingResult: AddressMcpQueryExecutorResult | null;
|
||||
outgoingResult: AddressMcpQueryExecutorResult | null;
|
||||
}): string | null {
|
||||
const incoming = input.incomingResult;
|
||||
const outgoing = input.outgoingResult;
|
||||
if (!incoming && !outgoing) {
|
||||
return null;
|
||||
}
|
||||
const incomingSummary = incoming?.error
|
||||
? "incoming value-flow query failed"
|
||||
: `${incoming?.fetched_rows ?? 0} incoming value-flow rows fetched, ${incoming?.matched_rows ?? 0} matched`;
|
||||
const outgoingSummary = outgoing?.error
|
||||
? "outgoing supplier-payout query failed"
|
||||
: `${outgoing?.fetched_rows ?? 0} outgoing supplier-payout rows fetched, ${outgoing?.matched_rows ?? 0} matched`;
|
||||
return `${incomingSummary}; ${outgoingSummary}`;
|
||||
}
|
||||
|
||||
function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
return [];
|
||||
@@ -464,7 +587,25 @@ function buildValueFlowConfirmedFacts(
|
||||
return [
|
||||
counterparty
|
||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||
: "1C value-flow rows were found for the requested counterparty scope"
|
||||
: "1C value-flow rows were found for the requested counterparty scope"
|
||||
];
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowConfirmedFacts(
|
||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||
): string[] {
|
||||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
const hasIncoming = derived.incoming_customer_revenue.rows_matched > 0;
|
||||
const hasOutgoing = derived.outgoing_supplier_payout.rows_matched > 0;
|
||||
if (derived.counterparty) {
|
||||
return [
|
||||
`1C bidirectional value-flow rows were checked for counterparty ${derived.counterparty}: incoming=${hasIncoming ? "found" : "not_found"}, outgoing=${hasOutgoing ? "found" : "not_found"}`
|
||||
];
|
||||
}
|
||||
return [
|
||||
`1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=${hasIncoming ? "found" : "not_found"}, outgoing=${hasOutgoing ? "found" : "not_found"}`
|
||||
];
|
||||
}
|
||||
|
||||
@@ -485,6 +626,15 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
||||
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowInferredFacts(
|
||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||
): string[] {
|
||||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
return ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"];
|
||||
}
|
||||
|
||||
function buildLifecycleUnknownFacts(): string[] {
|
||||
return ["Legal registration date is not proven by this MCP discovery pilot"];
|
||||
}
|
||||
@@ -514,6 +664,24 @@ function buildValueFlowUnknownFacts(
|
||||
return unknownFacts;
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowUnknownFacts(
|
||||
periodScope: string | null,
|
||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||
): string[] {
|
||||
const unknownFacts: string[] = [];
|
||||
if (derived?.coverage_limited_by_probe_limit) {
|
||||
unknownFacts.push(
|
||||
"Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached"
|
||||
);
|
||||
}
|
||||
unknownFacts.push(
|
||||
periodScope
|
||||
? "Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time bidirectional value-flow is not proven without an explicit checked period"
|
||||
);
|
||||
return unknownFacts;
|
||||
}
|
||||
|
||||
function buildEmptyEvidence(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
|
||||
@@ -557,6 +725,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot was blocked before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
@@ -579,6 +748,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot needs more scope before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
@@ -608,6 +778,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
@@ -620,7 +791,117 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
let queryResult: AddressMcpQueryExecutorResult | null = null;
|
||||
const filters = buildValueFlowFilters(planner);
|
||||
const valueFlowProfile = valueFlowPilotProfile(planner);
|
||||
const selection = selectAddressRecipe(valueFlowProfile.recipe_intent, filters);
|
||||
if (valueFlowProfile.direction === "bidirectional_net_value_flow") {
|
||||
let incomingResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let outgoingResult: AddressMcpQueryExecutorResult | null = null;
|
||||
const incomingSelection = selectAddressRecipe("customer_revenue_and_payments", filters);
|
||||
const outgoingSelection = selectAddressRecipe("supplier_payouts_profile", filters);
|
||||
if (!incomingSelection.selected_recipe || !outgoingSelection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_value_flow_recipe_not_available");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Bidirectional value-flow recipes are not available");
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: ["Bidirectional value-flow recipes are not available"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
pushReason(reasonCodes, "pilot_bidirectional_value_flow_recipes_selected");
|
||||
const incomingRecipePlan = buildAddressRecipePlan(incomingSelection.selected_recipe, filters);
|
||||
const outgoingRecipePlan = buildAddressRecipePlan(outgoingSelection.selected_recipe, filters);
|
||||
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_movements") {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(
|
||||
skippedProbeResult(step, "pilot_bidirectional_value_flow_uses_two_query_movements_and_derives_net")
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
incomingResult = await deps.executeAddressMcpQuery({
|
||||
query: incomingRecipePlan.query,
|
||||
limit: incomingRecipePlan.limit,
|
||||
account_scope: incomingRecipePlan.account_scope
|
||||
});
|
||||
outgoingResult = await deps.executeAddressMcpQuery({
|
||||
query: outgoingRecipePlan.query,
|
||||
limit: outgoingRecipePlan.limit,
|
||||
account_scope: outgoingRecipePlan.account_scope
|
||||
});
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, incomingResult));
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, outgoingResult));
|
||||
if (incomingResult.error) {
|
||||
pushUnique(queryLimitations, incomingResult.error);
|
||||
pushReason(reasonCodes, "pilot_bidirectional_incoming_query_movements_mcp_error");
|
||||
}
|
||||
if (outgoingResult.error) {
|
||||
pushUnique(queryLimitations, outgoingResult.error);
|
||||
pushReason(reasonCodes, "pilot_bidirectional_outgoing_query_movements_mcp_error");
|
||||
}
|
||||
if (!incomingResult.error || !outgoingResult.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_query_movements_mcp_executed");
|
||||
}
|
||||
}
|
||||
|
||||
const sourceRowsSummary = summarizeBidirectionalValueFlowRows({ incomingResult, outgoingResult });
|
||||
const derivedBidirectionalValueFlow = deriveBidirectionalValueFlow({
|
||||
incomingResult,
|
||||
outgoingResult,
|
||||
counterparty,
|
||||
periodScope: dateScope,
|
||||
probeLimit: planner.discovery_plan.execution_budget.max_rows_per_probe
|
||||
});
|
||||
if (derivedBidirectionalValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_bidirectional_value_flow_from_confirmed_rows");
|
||||
}
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: buildBidirectionalValueFlowConfirmedFacts(derivedBidirectionalValueFlow),
|
||||
inferredFacts: buildBidirectionalValueFlowInferredFacts(derivedBidirectionalValueFlow),
|
||||
unknownFacts: buildBidirectionalValueFlowUnknownFacts(dateScope, derivedBidirectionalValueFlow),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
});
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "executed",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: executedPrimitives.length > 0,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: derivedBidirectionalValueFlow,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
const recipeIntent = valueFlowProfile.recipe_intent;
|
||||
const selection = recipeIntent ? selectAddressRecipe(recipeIntent, filters) : { selected_recipe: null };
|
||||
if (!selection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
|
||||
@@ -638,6 +919,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: ["Value-flow recipe is not available"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
@@ -707,6 +989,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: derivedValueFlow,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
@@ -732,6 +1015,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: ["Lifecycle recipe is not available"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
@@ -789,6 +1073,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
derived_activity_period: derivedActivityPeriod,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user