ARCH: добавить value-flow pilot MCP discovery
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user