ARCH: добавить value-flow pilot MCP discovery

This commit is contained in:
2026-04-20 18:15:26 +03:00
parent e85d456576
commit 49fd08652c
21 changed files with 1100 additions and 66 deletions
@@ -40,11 +40,27 @@ export interface AssistantMcpDiscoveryDerivedActivityPeriod {
inference_basis: "first_and_latest_confirmed_1c_activity_rows";
}
export interface AssistantMcpDiscoveryDerivedValueFlow {
counterparty: string | null;
period_scope: string | null;
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;
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
}
export type AssistantMcpDiscoveryPilotScope =
| "counterparty_lifecycle_query_documents_v1"
| "counterparty_value_flow_query_movements_v1";
export interface AssistantMcpDiscoveryPilotExecutionContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryPilotExecutor";
pilot_status: AssistantMcpDiscoveryPilotStatus;
pilot_scope: "counterparty_lifecycle_query_documents_v1";
pilot_scope: AssistantMcpDiscoveryPilotScope;
dry_run: AssistantMcpDiscoveryRuntimeDryRunContract;
mcp_execution_performed: boolean;
executed_primitives: string[];
@@ -53,6 +69,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
evidence: AssistantMcpDiscoveryEvidenceContract;
source_rows_summary: string | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
query_limitations: string[];
reason_codes: string[];
}
@@ -141,6 +158,20 @@ function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): A
};
}
function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
@@ -152,6 +183,22 @@ function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
);
}
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported}`;
return (
planner.proposed_primitives.includes("query_movements") &&
(combined.includes("turnover") ||
combined.includes("revenue") ||
combined.includes("payment") ||
combined.includes("payout") ||
combined.includes("value"))
);
}
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: step.primitive_id,
@@ -175,7 +222,7 @@ function queryResultToProbeResult(
};
}
function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
function summarizeLifecycleRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
@@ -185,6 +232,16 @@ function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
}
function summarizeValueFlowRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP value-flow rows fetched";
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function rowDateValue(row: Record<string, unknown>): string | null {
const candidates = [
row["Период"],
@@ -204,6 +261,38 @@ function rowDateValue(row: Record<string, unknown>): string | null {
return null;
}
function rowAmountValue(row: Record<string, unknown>): number | null {
const candidates = [
row["Сумма"],
row["РЎСѓРјРјР°"],
row["СуммаДокумента"],
row["СуммаДокумента"],
row["Amount"],
row["amount"],
row["Total"],
row["total"]
];
for (const candidate of candidates) {
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 monthDiff(firstIsoDate: string, latestIsoDate: string): number {
const first = new Date(`${firstIsoDate}T00:00:00.000Z`);
const latest = new Date(`${latestIsoDate}T00:00:00.000Z`);
@@ -259,7 +348,54 @@ function deriveActivityPeriod(
};
}
function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
function formatAmountHumanRu(amount: number): string {
const formatted = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 2,
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2
})
.format(amount)
.replace(/\u00a0/g, " ");
return `${formatted} руб.`;
}
function deriveValueFlow(
result: AddressMcpQueryExecutorResult | null,
counterparty: string | null,
periodScope: string | null
): AssistantMcpDiscoveryDerivedValueFlow | null {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
const amount = rowAmountValue(row);
if (amount !== null) {
totalAmount += amount;
rowsWithAmount += 1;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const dates = result.rows
.map((row) => rowDateValue(row))
.filter((value): value is string => Boolean(value))
.sort();
return {
counterparty,
period_scope: periodScope,
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,
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
@@ -270,17 +406,43 @@ function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty
];
}
function buildInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
function buildValueFlowConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildLifecycleInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
}
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
}
function buildUnknownFacts(): string[] {
function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueFlow | null): string[] {
if (!derived) {
return [];
}
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
}
function buildLifecycleUnknownFacts(): string[] {
return ["Legal registration date is not proven by this MCP discovery pilot"];
}
function buildValueFlowUnknownFacts(periodScope: string | null): string[] {
return [
periodScope
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time turnover is not proven without an explicit checked period"
];
}
function buildEmptyEvidence(
planner: AssistantMcpDiscoveryPlannerContract,
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
@@ -323,6 +485,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes
};
@@ -344,12 +507,16 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes
};
}
if (!isLifecyclePilotEligible(planner)) {
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@@ -369,13 +536,99 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes
};
}
let queryResult: AddressMcpQueryExecutorResult | null = null;
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
if (valueFlowPilotEligible) {
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildValueFlowFilters(planner);
const selection = selectAddressRecipe("customer_revenue_and_payments", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_value_flow_query_movements_v1",
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,
query_limitations: ["Value-flow recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = buildAddressRecipePlan(selection.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_value_flow_uses_query_movements_and_derives_aggregate"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
} else {
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
unknownFacts: buildValueFlowUnknownFacts(dateScope),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "counterparty_value_flow_query_movements_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_activity_period: null,
derived_value_flow: derivedValueFlow,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildLifecycleFilters(planner);
const selection = selectAddressRecipe("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) {
@@ -394,6 +647,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes
};
@@ -421,7 +675,7 @@ export async function executeAssistantMcpDiscoveryPilot(
}
}
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
const sourceRowsSummary = queryResult ? summarizeLifecycleRows(queryResult) : null;
const derivedActivityPeriod = deriveActivityPeriod(queryResult);
if (derivedActivityPeriod) {
pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows");
@@ -429,9 +683,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(),
confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
unknownFacts: buildLifecycleUnknownFacts(),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
@@ -450,6 +704,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};