ARCH: добавить помесячный MCP discovery для нетто-потока
This commit is contained in:
@@ -40,10 +40,21 @@ export interface AssistantMcpDiscoveryDerivedActivityPeriod {
|
||||
inference_basis: "first_and_latest_confirmed_1c_activity_rows";
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryAggregationAxis = "month";
|
||||
export type AssistantMcpDiscoveryNetDirection = "net_incoming" | "net_outgoing" | "balanced";
|
||||
|
||||
export interface AssistantMcpDiscoveryValueFlowMonthBucket {
|
||||
month_bucket: string;
|
||||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
total_amount_human_ru: string;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
|
||||
counterparty: string | null;
|
||||
period_scope: string | null;
|
||||
aggregation_axis: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
@@ -51,6 +62,7 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
first_movement_date: string | null;
|
||||
latest_movement_date: string | null;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
monthly_breakdown: AssistantMcpDiscoveryValueFlowMonthBucket[];
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
@@ -64,15 +76,30 @@ export interface AssistantMcpDiscoveryValueFlowSideSummary {
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket {
|
||||
month_bucket: string;
|
||||
incoming_total_amount: number;
|
||||
incoming_total_amount_human_ru: string;
|
||||
incoming_rows_with_amount: number;
|
||||
outgoing_total_amount: number;
|
||||
outgoing_total_amount_human_ru: string;
|
||||
outgoing_rows_with_amount: number;
|
||||
net_amount: number;
|
||||
net_amount_human_ru: string;
|
||||
net_direction: AssistantMcpDiscoveryNetDirection;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
|
||||
counterparty: string | null;
|
||||
period_scope: string | null;
|
||||
aggregation_axis: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
incoming_customer_revenue: AssistantMcpDiscoveryValueFlowSideSummary;
|
||||
outgoing_supplier_payout: AssistantMcpDiscoveryValueFlowSideSummary;
|
||||
net_amount: number;
|
||||
net_amount_human_ru: string;
|
||||
net_direction: "net_incoming" | "net_outgoing" | "balanced";
|
||||
net_direction: AssistantMcpDiscoveryNetDirection;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
monthly_breakdown: AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket[];
|
||||
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
@@ -138,6 +165,13 @@ function pushUnique(target: string[], value: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function aggregationAxisForPlanner(
|
||||
planner: AssistantMcpDiscoveryPlannerContract
|
||||
): AssistantMcpDiscoveryAggregationAxis | null {
|
||||
const axis = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_aggregation_axis)?.toLowerCase();
|
||||
return axis === "month" ? "month" : null;
|
||||
}
|
||||
|
||||
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
for (const candidate of candidates) {
|
||||
@@ -367,6 +401,21 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||
return match ? `${match[1]}-${match[2]}` : null;
|
||||
}
|
||||
|
||||
function netDirectionFromAmount(amount: number): AssistantMcpDiscoveryNetDirection {
|
||||
if (amount > 0) {
|
||||
return "net_incoming";
|
||||
}
|
||||
if (amount < 0) {
|
||||
return "net_outgoing";
|
||||
}
|
||||
return "balanced";
|
||||
}
|
||||
|
||||
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`);
|
||||
@@ -432,12 +481,88 @@ function formatAmountHumanRu(amount: number): string {
|
||||
return `${formatted} руб.`;
|
||||
}
|
||||
|
||||
function deriveValueFlowMonthBreakdown(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||
): AssistantMcpDiscoveryValueFlowMonthBucket[] {
|
||||
if (!result || result.error || aggregationAxis !== "month") {
|
||||
return [];
|
||||
}
|
||||
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||
for (const row of result.rows) {
|
||||
const isoDate = rowDateValue(row);
|
||||
const monthBucket = monthBucketFromIsoDate(isoDate);
|
||||
const amount = rowAmountValue(row);
|
||||
if (!monthBucket || amount === null) {
|
||||
continue;
|
||||
}
|
||||
const current = buckets.get(monthBucket) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||
current.rows_with_amount += 1;
|
||||
current.total_amount += amount;
|
||||
buckets.set(monthBucket, current);
|
||||
}
|
||||
return Array.from(buckets.entries())
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([monthBucket, bucket]) => ({
|
||||
month_bucket: monthBucket,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||
}));
|
||||
}
|
||||
|
||||
function deriveBidirectionalValueFlowMonthBreakdown(input: {
|
||||
incomingResult: AddressMcpQueryExecutorResult | null;
|
||||
outgoingResult: AddressMcpQueryExecutorResult | null;
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
}): AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket[] {
|
||||
if (input.aggregationAxis !== "month") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const incomingBuckets = deriveValueFlowMonthBreakdown(input.incomingResult, "month");
|
||||
const outgoingBuckets = deriveValueFlowMonthBreakdown(input.outgoingResult, "month");
|
||||
const allMonthBuckets = new Set<string>();
|
||||
for (const bucket of incomingBuckets) {
|
||||
allMonthBuckets.add(bucket.month_bucket);
|
||||
}
|
||||
for (const bucket of outgoingBuckets) {
|
||||
allMonthBuckets.add(bucket.month_bucket);
|
||||
}
|
||||
|
||||
const incomingByMonth = new Map(incomingBuckets.map((bucket) => [bucket.month_bucket, bucket]));
|
||||
const outgoingByMonth = new Map(outgoingBuckets.map((bucket) => [bucket.month_bucket, bucket]));
|
||||
|
||||
return Array.from(allMonthBuckets)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.map((monthBucket) => {
|
||||
const incoming = incomingByMonth.get(monthBucket);
|
||||
const outgoing = outgoingByMonth.get(monthBucket);
|
||||
const incomingAmount = incoming?.total_amount ?? 0;
|
||||
const outgoingAmount = outgoing?.total_amount ?? 0;
|
||||
const netAmount = incomingAmount - outgoingAmount;
|
||||
return {
|
||||
month_bucket: monthBucket,
|
||||
incoming_total_amount: incomingAmount,
|
||||
incoming_total_amount_human_ru: formatAmountHumanRu(incomingAmount),
|
||||
incoming_rows_with_amount: incoming?.rows_with_amount ?? 0,
|
||||
outgoing_total_amount: outgoingAmount,
|
||||
outgoing_total_amount_human_ru: formatAmountHumanRu(outgoingAmount),
|
||||
outgoing_rows_with_amount: outgoing?.rows_with_amount ?? 0,
|
||||
net_amount: netAmount,
|
||||
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
|
||||
net_direction: netDirectionFromAmount(netAmount)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function deriveValueFlow(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
counterparty: string | null,
|
||||
periodScope: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||
probeLimit: number
|
||||
probeLimit: number,
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return null;
|
||||
@@ -462,6 +587,7 @@ function deriveValueFlow(
|
||||
value_flow_direction: direction,
|
||||
counterparty,
|
||||
period_scope: periodScope,
|
||||
aggregation_axis: aggregationAxis,
|
||||
rows_matched: result.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
total_amount: totalAmount,
|
||||
@@ -469,6 +595,7 @@ function deriveValueFlow(
|
||||
first_movement_date: dates[0] ?? null,
|
||||
latest_movement_date: dates[dates.length - 1] ?? null,
|
||||
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit,
|
||||
monthly_breakdown: deriveValueFlowMonthBreakdown(result, aggregationAxis),
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
@@ -519,6 +646,7 @@ function deriveBidirectionalValueFlow(input: {
|
||||
counterparty: string | null;
|
||||
periodScope: string | null;
|
||||
probeLimit: number;
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
}): AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null {
|
||||
const incoming = deriveValueFlowSideSummary(input.incomingResult, input.probeLimit);
|
||||
const outgoing = deriveValueFlowSideSummary(input.outgoingResult, input.probeLimit);
|
||||
@@ -529,13 +657,19 @@ function deriveBidirectionalValueFlow(input: {
|
||||
return {
|
||||
counterparty: input.counterparty,
|
||||
period_scope: input.periodScope,
|
||||
aggregation_axis: input.aggregationAxis,
|
||||
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",
|
||||
net_direction: netDirectionFromAmount(netAmount),
|
||||
coverage_limited_by_probe_limit:
|
||||
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
|
||||
monthly_breakdown: deriveBidirectionalValueFlowMonthBreakdown({
|
||||
incomingResult: input.incomingResult,
|
||||
outgoingResult: input.outgoingResult,
|
||||
aggregationAxis: input.aggregationAxis
|
||||
}),
|
||||
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
@@ -620,10 +754,16 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
||||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
const facts: string[] = [];
|
||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"];
|
||||
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
||||
} else {
|
||||
facts.push("Counterparty value-flow total was calculated from confirmed 1C movement rows");
|
||||
}
|
||||
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
|
||||
if (derived.aggregation_axis === "month" && derived.monthly_breakdown.length > 0) {
|
||||
facts.push("Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows");
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowInferredFacts(
|
||||
@@ -632,7 +772,11 @@ function buildBidirectionalValueFlowInferredFacts(
|
||||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
return ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"];
|
||||
const facts = ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"];
|
||||
if (derived.aggregation_axis === "month" && derived.monthly_breakdown.length > 0) {
|
||||
facts.push("Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows");
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
function buildLifecycleUnknownFacts(): string[] {
|
||||
@@ -786,6 +930,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
||||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||
|
||||
if (valueFlowPilotEligible) {
|
||||
let queryResult: AddressMcpQueryExecutorResult | null = null;
|
||||
@@ -864,10 +1009,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
outgoingResult,
|
||||
counterparty,
|
||||
periodScope: dateScope,
|
||||
probeLimit: planner.discovery_plan.execution_budget.max_rows_per_probe
|
||||
probeLimit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
aggregationAxis
|
||||
});
|
||||
if (derivedBidirectionalValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_bidirectional_value_flow_from_confirmed_rows");
|
||||
if (aggregationAxis === "month" && derivedBidirectionalValueFlow.monthly_breakdown.length > 0) {
|
||||
pushReason(reasonCodes, "pilot_derived_bidirectional_monthly_breakdown_from_confirmed_rows");
|
||||
}
|
||||
}
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
@@ -959,10 +1108,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
counterparty,
|
||||
dateScope,
|
||||
valueFlowProfile.direction,
|
||||
planner.discovery_plan.execution_budget.max_rows_per_probe
|
||||
planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
aggregationAxis
|
||||
);
|
||||
if (derivedValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
|
||||
if (aggregationAxis === "month" && derivedValueFlow.monthly_breakdown.length > 0) {
|
||||
pushReason(reasonCodes, "pilot_derived_value_flow_monthly_breakdown_from_confirmed_rows");
|
||||
}
|
||||
}
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
|
||||
Reference in New Issue
Block a user