ARCH: добавить помесячный MCP discovery для нетто-потока
This commit is contained in:
parent
99a568241d
commit
4baa54fe81
|
|
@ -1261,6 +1261,49 @@ Module progress:
|
|||
|
||||
- Big Block 5 MCP Semantic Data Agent: `99%`.
|
||||
|
||||
## Progress Update - 2026-04-21 MCP Discovery Monthly Bidirectional Multi-Axis Aggregation
|
||||
|
||||
The nineteenth implementation slice of Big Block 5 closes the first explicit multi-axis aggregation gap for guarded MCP discovery.
|
||||
|
||||
Before this slice the assistant could answer:
|
||||
|
||||
- incoming value-flow total;
|
||||
- outgoing supplier-payout total;
|
||||
- composed bidirectional net total.
|
||||
|
||||
But it could not keep a user request like "по месяцам" as part of the machine-readable turn meaning, plan shape, and final guarded answer.
|
||||
|
||||
New behavior:
|
||||
|
||||
- monthly wording such as `по месяцам`, `помесячно`, `ежемесячно`, `monthly`, and `month by month` is captured as `asked_aggregation_axis=month`;
|
||||
- the discovery planner keeps the monthly request as an explicit `calendar_month` axis on top of the existing guarded value-flow recipe;
|
||||
- runtime derives `monthly_breakdown` for single-direction value-flow and for composed bidirectional net value-flow from the same confirmed 1C rows;
|
||||
- the guarded answer draft now adds month-by-month business lines instead of flattening a monthly question back into a single total;
|
||||
- the response-candidate layer keeps those monthly lines user-facing and localizes the monthly inference basis without leaking planner/runtime/pilot mechanics.
|
||||
|
||||
Replay result:
|
||||
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun12` passed `8/8` after adding the new monthly step and exposed one presentation defect: duplicated punctuation in monthly amount lines;
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun13` passed `8/8` after punctuation cleanup;
|
||||
- final status: `accepted`;
|
||||
- the new step `step_07_counterparty_bidirectional_monthly_net_flow_uses_guarded_discovery` answered through guarded MCP discovery with monthly lines from `янв 2020` through `дек 2020`;
|
||||
- user-facing text keeps the same honesty boundary as the total net answer: outgoing coverage still hits the `100`-row probe limit, so the monthly shape is a found-row calculation rather than a fully proven all-period saldo;
|
||||
- user-facing monthly output contains no `query_documents`, `query_movements`, `runtime_`, `planner_`, `catalog_`, `primitive`, or `pilot_` leak.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed `90/90`;
|
||||
- `npm run build` passed;
|
||||
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun13 --timeout-seconds 180` passed `8/8`, final status `accepted`.
|
||||
|
||||
Known remaining boundary:
|
||||
|
||||
- this still does not make Qwen3 an unrestricted self-navigating 1C agent across arbitrary registers and schemas; follow-up drilldown over the discovered rows and broader self-navigation remain future work outside this first completed guarded multi-axis slice.
|
||||
|
||||
Module progress:
|
||||
|
||||
- Big Block 5 MCP Semantic Data Agent: `100%`.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -173,7 +173,41 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_off_domain_living_chat_not_hijacked",
|
||||
"step_id": "step_07_counterparty_bidirectional_monthly_net_flow_uses_guarded_discovery",
|
||||
"title": "Unsupported-but-understood counterparty monthly net cash-flow question keeps monthly structure from discovery",
|
||||
"question": "какое помесячное нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили по месяцам?",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)1с|найден|строк|проверен",
|
||||
"(?i)получил|входящ|поступ",
|
||||
"(?i)заплат|исходящ|списан|плат[её]ж",
|
||||
"(?i)помесяч|по месяцам|янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек",
|
||||
"(?i)нетто|сальдо|разниц",
|
||||
"(?i)сумм|руб",
|
||||
"(?i)2020|период",
|
||||
"(?i)не подтвержд|проверенн|найденн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)primitive",
|
||||
"(?i)pilot_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"mcp_discovery_bidirectional_value_flow",
|
||||
"counterparty_monthly_net_cash_flow",
|
||||
"multi_axis_aggregation",
|
||||
"unsupported_current_turn_meaning_boundary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_off_domain_living_chat_not_hijacked",
|
||||
"title": "Off-domain living chat remains human and is not hijacked by discovery carryover",
|
||||
"question": "а чем капибара отличается от утки?",
|
||||
"required_answer_patterns_any": [
|
||||
|
|
|
|||
|
|
@ -65,6 +65,17 @@ function isValueFlowPilot(pilot) {
|
|||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1");
|
||||
}
|
||||
function headlineFor(mode, pilot) {
|
||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
return "По данным 1С найдены строки денежных движений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
|
|
@ -118,6 +129,38 @@ function buildMustNotClaim(pilot) {
|
|||
}
|
||||
return claims;
|
||||
}
|
||||
const RU_MONTH_LABELS_SHORT = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек"
|
||||
];
|
||||
function monthLabelRu(monthBucket) {
|
||||
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return monthBucket;
|
||||
}
|
||||
const monthIndex = Number(match[2]) - 1;
|
||||
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
|
||||
return `${label} ${match[1]}`;
|
||||
}
|
||||
function netLabelRu(netDirection) {
|
||||
if (netDirection === "net_incoming") {
|
||||
return "нетто в нашу сторону";
|
||||
}
|
||||
if (netDirection === "net_outgoing") {
|
||||
return "нетто исходящее";
|
||||
}
|
||||
return "нетто нулевое";
|
||||
}
|
||||
function derivedActivityInferenceLine(pilot) {
|
||||
const period = pilot.derived_activity_period;
|
||||
if (!period) {
|
||||
|
|
@ -151,6 +194,19 @@ function derivedValueFlowConfirmedLine(pilot) {
|
|||
: "";
|
||||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||||
}
|
||||
function derivedValueFlowMonthlyLines(pilot) {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map((bucket) => {
|
||||
const monthLabel = monthLabelRu(bucket.month_bucket);
|
||||
if (flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
}
|
||||
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
});
|
||||
}
|
||||
function sideDateRange(first, latest) {
|
||||
if (first && latest) {
|
||||
return ` первая дата ${first}, последняя ${latest}`;
|
||||
|
|
@ -185,6 +241,13 @@ function derivedBidirectionalValueFlowConfirmedLine(pilot) {
|
|||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function derivedBidirectionalValueFlowMonthlyLines(pilot) {
|
||||
const flow = pilot.derived_bidirectional_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map((bucket) => `Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`);
|
||||
}
|
||||
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
const mode = modeFor(pilot);
|
||||
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
|
||||
|
|
@ -200,8 +263,14 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
||||
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||
: derivedValueFlowMonthlyLines(pilot);
|
||||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ function pushUnique(target, value) {
|
|||
target.push(text);
|
||||
}
|
||||
}
|
||||
function aggregationAxisForPlanner(planner) {
|
||||
const axis = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_aggregation_axis)?.toLowerCase();
|
||||
return axis === "month" ? "month" : null;
|
||||
}
|
||||
function firstEntityCandidate(planner) {
|
||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
for (const candidate of candidates) {
|
||||
|
|
@ -231,6 +235,19 @@ function rowAmountValue(row) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function monthBucketFromIsoDate(isoDate) {
|
||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||
return match ? `${match[1]}-${match[2]}` : null;
|
||||
}
|
||||
function netDirectionFromAmount(amount) {
|
||||
if (amount > 0) {
|
||||
return "net_incoming";
|
||||
}
|
||||
if (amount < 0) {
|
||||
return "net_outgoing";
|
||||
}
|
||||
return "balanced";
|
||||
}
|
||||
function monthDiff(firstIsoDate, latestIsoDate) {
|
||||
const first = new Date(`${firstIsoDate}T00:00:00.000Z`);
|
||||
const latest = new Date(`${latestIsoDate}T00:00:00.000Z`);
|
||||
|
|
@ -290,7 +307,70 @@ function formatAmountHumanRu(amount) {
|
|||
.replace(/\u00a0/g, " ");
|
||||
return `${formatted} руб.`;
|
||||
}
|
||||
function deriveValueFlow(result, counterparty, periodScope, direction, probeLimit) {
|
||||
function deriveValueFlowMonthBreakdown(result, aggregationAxis) {
|
||||
if (!result || result.error || aggregationAxis !== "month") {
|
||||
return [];
|
||||
}
|
||||
const buckets = new Map();
|
||||
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) {
|
||||
if (input.aggregationAxis !== "month") {
|
||||
return [];
|
||||
}
|
||||
const incomingBuckets = deriveValueFlowMonthBreakdown(input.incomingResult, "month");
|
||||
const outgoingBuckets = deriveValueFlowMonthBreakdown(input.outgoingResult, "month");
|
||||
const allMonthBuckets = new Set();
|
||||
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, counterparty, periodScope, direction, probeLimit, aggregationAxis) {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -314,6 +394,7 @@ function deriveValueFlow(result, counterparty, periodScope, direction, probeLimi
|
|||
value_flow_direction: direction,
|
||||
counterparty,
|
||||
period_scope: periodScope,
|
||||
aggregation_axis: aggregationAxis,
|
||||
rows_matched: result.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
total_amount: totalAmount,
|
||||
|
|
@ -321,6 +402,7 @@ function deriveValueFlow(result, counterparty, periodScope, direction, probeLimi
|
|||
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"
|
||||
};
|
||||
}
|
||||
|
|
@ -369,12 +451,18 @@ 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"
|
||||
};
|
||||
}
|
||||
|
|
@ -444,16 +532,27 @@ function buildValueFlowInferredFacts(derived) {
|
|||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
const facts = [];
|
||||
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");
|
||||
}
|
||||
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
|
||||
else {
|
||||
facts.push("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(derived) {
|
||||
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() {
|
||||
return ["Legal registration date is not proven by this MCP discovery pilot"];
|
||||
|
|
@ -574,6 +673,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
}
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
||||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||
if (valueFlowPilotEligible) {
|
||||
let queryResult = null;
|
||||
const filters = buildValueFlowFilters(planner);
|
||||
|
|
@ -645,10 +745,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
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 = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
|
|
@ -729,9 +833,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
}
|
||||
}
|
||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, planner.discovery_plan.execution_budget.max_rows_per_probe);
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, 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 = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ function pushUnique(target, value) {
|
|||
function hasEntity(meaning) {
|
||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
function aggregationAxis(meaning) {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
function addScopeAxes(axes, meaning) {
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "counterparty");
|
||||
|
|
@ -59,16 +62,22 @@ function recipeFor(input) {
|
|||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "month") {
|
||||
pushUnique(axes, "calendar_month");
|
||||
}
|
||||
return {
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_value_flow_recipe"
|
||||
reason: requestedAggregationAxis === "month"
|
||||
? "planner_selected_monthly_value_flow_recipe"
|
||||
: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
if (includesAny(combined, ["document", "documents"])) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ function normalizeTurnMeaning(value) {
|
|||
const result = {};
|
||||
const domain = toNonEmptyString(value.asked_domain_family);
|
||||
const action = toNonEmptyString(value.asked_action_family);
|
||||
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
|
||||
const organization = toNonEmptyString(value.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||
|
|
@ -84,6 +85,9 @@ function normalizeTurnMeaning(value) {
|
|||
if (action) {
|
||||
result.asked_action_family = action;
|
||||
}
|
||||
if (aggregationAxis) {
|
||||
result.asked_aggregation_axis = aggregationAxis;
|
||||
}
|
||||
if (entities.length > 0) {
|
||||
result.explicit_entity_candidates = entities;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,12 +100,18 @@ function localizeLine(value) {
|
|||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Помесячная раскладка денежного потока сгруппирована только по подтвержденным строкам движений 1С.";
|
||||
}
|
||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||
}
|
||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
||||
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
||||
}
|
||||
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Юридическая дата регистрации этим поиском не подтверждена.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ function hasPayoutSignal(text) {
|
|||
function hasBidirectionalValueFlowSignal(text) {
|
||||
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
||||
}
|
||||
function hasMonthlyAggregationSignal(text) {
|
||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
|
||||
}
|
||||
function semanticNeedFor(input) {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
|
||||
|
|
@ -142,8 +145,10 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const bidirectionalValueFlowSignal = !lifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||
const valueFlowSignal = !lifecycleSignal && (hasValueFlowSignal(rawText) || bidirectionalValueFlowSignal);
|
||||
const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||
const monthlyAggregationSignal = valueFlowSignal && hasMonthlyAggregationSignal(rawText);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const semanticDataNeed = semanticNeedFor({
|
||||
|
|
@ -170,6 +175,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "payout"
|
||||
: "turnover"
|
||||
: rawAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
explicit_organization_scope: explicitOrganizationScope,
|
||||
explicit_date_scope: collectDateScope(predecomposeContract),
|
||||
|
|
@ -192,6 +198,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (toNonEmptyString(turnMeaning.asked_action_family)) {
|
||||
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.asked_aggregation_axis)) {
|
||||
cleanTurnMeaning.asked_aggregation_axis = turnMeaning.asked_aggregation_axis;
|
||||
}
|
||||
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
|
||||
}
|
||||
|
|
@ -236,6 +245,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (bidirectionalValueFlowSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
}
|
||||
if (monthlyAggregationSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,18 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b
|
|||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const askedMonthlyBreakdown =
|
||||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
return "По данным 1С найдены строки денежных движений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||||
}
|
||||
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
|
||||
}
|
||||
|
|
@ -154,6 +166,41 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
return claims;
|
||||
}
|
||||
|
||||
const RU_MONTH_LABELS_SHORT = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек"
|
||||
] as const;
|
||||
|
||||
function monthLabelRu(monthBucket: string): string {
|
||||
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return monthBucket;
|
||||
}
|
||||
const monthIndex = Number(match[2]) - 1;
|
||||
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
|
||||
return `${label} ${match[1]}`;
|
||||
}
|
||||
|
||||
function netLabelRu(netDirection: "net_incoming" | "net_outgoing" | "balanced"): string {
|
||||
if (netDirection === "net_incoming") {
|
||||
return "нетто в нашу сторону";
|
||||
}
|
||||
if (netDirection === "net_outgoing") {
|
||||
return "нетто исходящее";
|
||||
}
|
||||
return "нетто нулевое";
|
||||
}
|
||||
|
||||
function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const period = pilot.derived_activity_period;
|
||||
if (!period) {
|
||||
|
|
@ -193,6 +240,20 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
|
|||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||||
}
|
||||
|
||||
function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map((bucket) => {
|
||||
const monthLabel = monthLabelRu(bucket.month_bucket);
|
||||
if (flow.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
}
|
||||
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||||
});
|
||||
}
|
||||
|
||||
function sideDateRange(first: string | null, latest: string | null): string {
|
||||
if (first && latest) {
|
||||
return ` первая дата ${first}, последняя ${latest}`;
|
||||
|
|
@ -230,6 +291,17 @@ function derivedBidirectionalValueFlowConfirmedLine(pilot: AssistantMcpDiscovery
|
|||
.trim();
|
||||
}
|
||||
|
||||
function derivedBidirectionalValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
const flow = pilot.derived_bidirectional_value_flow;
|
||||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return flow.monthly_breakdown.map(
|
||||
(bucket) =>
|
||||
`Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`
|
||||
);
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): AssistantMcpDiscoveryAnswerDraftContract {
|
||||
|
|
@ -247,8 +319,15 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
||||
const monthlyConfirmedLines =
|
||||
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||
: derivedValueFlowMonthlyLines(pilot);
|
||||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
|
|||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
|
||||
function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
|
||||
if (hasEntity(meaning)) {
|
||||
pushUnique(axes, "counterparty");
|
||||
|
|
@ -99,17 +103,23 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes: string[] = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
|
||||
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "month") {
|
||||
pushUnique(axes, "calendar_month");
|
||||
}
|
||||
return {
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_value_flow_recipe"
|
||||
reason: requestedAggregationAxis === "month"
|
||||
? "planner_selected_monthly_value_flow_recipe"
|
||||
: "planner_selected_value_flow_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounde
|
|||
export interface AssistantMcpDiscoveryTurnMeaningRef {
|
||||
asked_domain_family?: string | null;
|
||||
asked_action_family?: string | null;
|
||||
asked_aggregation_axis?: string | null;
|
||||
explicit_entity_candidates?: string[];
|
||||
explicit_organization_scope?: string | null;
|
||||
explicit_date_scope?: string | null;
|
||||
|
|
@ -164,6 +165,7 @@ function normalizeTurnMeaning(
|
|||
const result: AssistantMcpDiscoveryTurnMeaningRef = {};
|
||||
const domain = toNonEmptyString(value.asked_domain_family);
|
||||
const action = toNonEmptyString(value.asked_action_family);
|
||||
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
|
||||
const organization = toNonEmptyString(value.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||
|
|
@ -174,6 +176,9 @@ function normalizeTurnMeaning(
|
|||
if (action) {
|
||||
result.asked_action_family = action;
|
||||
}
|
||||
if (aggregationAxis) {
|
||||
result.asked_aggregation_axis = aggregationAxis;
|
||||
}
|
||||
if (entities.length > 0) {
|
||||
result.explicit_entity_candidates = entities;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,12 +134,18 @@ function localizeLine(value: string): string {
|
|||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Помесячная раскладка денежного потока сгруппирована только по подтвержденным строкам движений 1С.";
|
||||
}
|
||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||
}
|
||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
|
||||
}
|
||||
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
|
||||
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
|
||||
}
|
||||
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Юридическая дата регистрации этим поиском не подтверждена.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function hasMonthlyAggregationSignal(text: string): boolean {
|
||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function semanticNeedFor(input: {
|
||||
domain: string | null;
|
||||
action: string | null;
|
||||
|
|
@ -210,9 +216,11 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const bidirectionalValueFlowSignal = !lifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||
const valueFlowSignal = !lifecycleSignal && (hasValueFlowSignal(rawText) || bidirectionalValueFlowSignal);
|
||||
const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||
const monthlyAggregationSignal = valueFlowSignal && hasMonthlyAggregationSignal(rawText);
|
||||
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const semanticDataNeed = semanticNeedFor({
|
||||
|
|
@ -241,6 +249,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "payout"
|
||||
: "turnover"
|
||||
: rawAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
explicit_organization_scope: explicitOrganizationScope,
|
||||
explicit_date_scope: collectDateScope(predecomposeContract),
|
||||
|
|
@ -265,6 +274,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (toNonEmptyString(turnMeaning.asked_action_family)) {
|
||||
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.asked_aggregation_axis)) {
|
||||
cleanTurnMeaning.asked_aggregation_axis = turnMeaning.asked_aggregation_axis;
|
||||
}
|
||||
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
|
||||
}
|
||||
|
|
@ -311,6 +323,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (bidirectionalValueFlowSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
}
|
||||
if (monthlyAggregationSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,48 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||
});
|
||||
|
||||
it("renders monthly bidirectional breakdown lines when the turn explicitly asked by month", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
asked_aggregation_axis: "month",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" }
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const confirmedText = draft.confirmed_lines.join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("помесяч");
|
||||
expect(confirmedText).toContain("Помесячно: янв 2020");
|
||||
expect(confirmedText).toContain("получили 10 000 руб.");
|
||||
expect(confirmedText).toContain("заплатили 4 000 руб.");
|
||||
expect(confirmedText).toContain("Помесячно: фев 2020");
|
||||
expect(confirmedText).toContain("нетто в нашу сторону 1 500,50 руб.");
|
||||
expect(draft.reason_codes).toContain("answer_contains_monthly_breakdown");
|
||||
});
|
||||
|
||||
it("does not leak primitive names or query text into user-facing lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -260,6 +260,65 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("derives monthly bidirectional value-flow breakdown when the turn explicitly asks by month", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
asked_aggregation_axis: "month",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_bidirectional_value_flow?.aggregation_axis).toBe("month");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown).toMatchObject([
|
||||
{
|
||||
month_bucket: "2020-01",
|
||||
incoming_total_amount: 10000,
|
||||
incoming_rows_with_amount: 1,
|
||||
outgoing_total_amount: 4000,
|
||||
outgoing_rows_with_amount: 1,
|
||||
net_amount: 6000,
|
||||
net_direction: "net_incoming"
|
||||
},
|
||||
{
|
||||
month_bucket: "2020-02",
|
||||
incoming_total_amount: 2500.5,
|
||||
incoming_rows_with_amount: 1,
|
||||
outgoing_total_amount: 1000,
|
||||
outgoing_rows_with_amount: 1,
|
||||
net_amount: 1500.5,
|
||||
net_direction: "net_incoming"
|
||||
}
|
||||
]);
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[0]?.incoming_total_amount_human_ru).toContain("10 000");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[0]?.net_amount_human_ru).toContain("6 000");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[1]?.incoming_total_amount_human_ru).toContain("2 500,50");
|
||||
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[1]?.net_amount_human_ru).toContain("1 500,50");
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_derived_bidirectional_monthly_breakdown_from_confirmed_rows");
|
||||
});
|
||||
|
||||
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,35 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
|
||||
it("keeps requested monthly aggregation as an explicit planning axis for value-flow discovery", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
asked_aggregation_axis: "month",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.proposed_primitives).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"query_movements",
|
||||
"aggregate_by_axis",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.required_axes).toEqual([
|
||||
"counterparty",
|
||||
"period",
|
||||
"aggregate_axis",
|
||||
"amount",
|
||||
"coverage_target",
|
||||
"calendar_month"
|
||||
]);
|
||||
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_recipe");
|
||||
});
|
||||
|
||||
it("builds a document discovery plan without falling back to movement primitives", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,37 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("keeps monthly breakdown lines user-facing and localizes monthly inference basis", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.",
|
||||
confirmed_lines: [
|
||||
"1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found",
|
||||
"Помесячно: янв 2020 — получили 10 000 руб., заплатили 4 000 руб., нетто в нашу сторону 6 000 руб."
|
||||
],
|
||||
inference_lines: [
|
||||
"Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows"
|
||||
],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("Помесячно: янв 2020");
|
||||
expect(candidate.reply_text).toContain("Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.");
|
||||
expect(candidate.reply_text).not.toContain("Counterparty monthly net value-flow breakdown");
|
||||
});
|
||||
|
||||
it("returns not applicable when discovery was skipped for an exact supported route", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
|
|
|
|||
|
|
@ -131,6 +131,26 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
|
||||
});
|
||||
|
||||
it("captures monthly aggregation as part of bidirectional value-flow meaning", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "какое нетто по деньгам с Группа СВК за 2020 год по месяцам: сколько получили и сколько заплатили помесячно?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "Группа СВК" },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
asked_aggregation_axis: "month",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_monthly_aggregation_signal_detected");
|
||||
});
|
||||
|
||||
it("does not activate discovery for supported exact current-turn intent", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue