ARCH: добавить двусторонний MCP discovery для нетто-потока

This commit is contained in:
2026-04-20 19:11:22 +03:00
parent 52da709671
commit 99a568241d
39 changed files with 7979 additions and 67 deletions
@@ -86,6 +86,12 @@ export interface AssistantOrganizationAuthority {
organizationClarificationSelectionFromScope: string | null;
}
export interface AssistantOrganizationClarificationContinuation {
explicitSelection: string | null;
selection: string | null;
hasContinuation: boolean;
}
function fallbackToNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
@@ -1077,3 +1083,50 @@ export function resolveAssistantOrganizationAuthority(
organizationClarificationSelectionFromScope
};
}
export function resolveOrganizationClarificationContinuation(input: {
rawMessages: unknown[];
organizationClarificationCandidates?: unknown[];
organizationClarificationSelectionFromScope?: unknown;
lastOrganizationClarificationDebug?: Record<string, unknown> | null;
resolveOrganizationSelectionFromMessage?: (message: string, candidates: unknown[]) => string | null;
toNonEmptyString?: (value: unknown) => string | null;
normalizeOrganizationScopeValue?: (value: unknown) => string | null;
}): AssistantOrganizationClarificationContinuation {
const toNonEmptyString = input.toNonEmptyString ?? fallbackToNonEmptyString;
const normalizeOrganizationScopeValue =
input.normalizeOrganizationScopeValue ?? normalizeOrganizationScopeDefault;
const candidates = Array.isArray(input.organizationClarificationCandidates)
? input.organizationClarificationCandidates
: [];
const resolveOrganizationSelectionFromMessage = input.resolveOrganizationSelectionFromMessage;
const messages = Array.isArray(input.rawMessages) ? input.rawMessages : [];
let explicitSelection: string | null = null;
if (typeof resolveOrganizationSelectionFromMessage === "function") {
for (const message of messages) {
const normalizedMessage = toNonEmptyString(message);
if (!normalizedMessage) {
continue;
}
explicitSelection = resolveOrganizationSelectionFromMessage(normalizedMessage, candidates);
if (explicitSelection) {
break;
}
}
}
const normalizedScopeSelection = normalizeOrganizationScopeValue(input.organizationClarificationSelectionFromScope);
const selection =
explicitSelection ??
(normalizedScopeSelection &&
candidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === normalizedScopeSelection)
? normalizedScopeSelection
: null);
return {
explicitSelection,
selection,
hasContinuation: Boolean(input.lastOrganizationClarificationDebug && selection)
};
}
@@ -92,11 +92,15 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.pilot_scope === "counterparty_value_flow_query_movements_v1" ||
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" ||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1"
);
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
}
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
@@ -189,6 +193,43 @@ 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 sideDateRange(first: string | null, latest: string | null): string {
if (first && latest) {
return ` первая дата ${first}, последняя ${latest}`;
}
return " даты движения не выделены";
}
function derivedBidirectionalValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_bidirectional_value_flow;
if (!flow) {
return null;
}
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const incoming = flow.incoming_customer_revenue;
const outgoing = flow.outgoing_supplier_payout;
const netLabel =
flow.net_direction === "net_incoming"
? "нетто в нашу сторону"
: flow.net_direction === "net_outgoing"
? "нетто исходящий"
: "нетто нулевое";
const limitCaveat = flow.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут хотя бы по одной стороне; полный запрошенный период может быть покрыт не полностью."
: "";
return [
`По найденным строкам 1С${counterparty}${period}: получили ${incoming.total_amount_human_ru} по входящим движениям, заплатили ${outgoing.total_amount_human_ru} по исходящим платежам/списаниям.`,
`Расчетное ${netLabel}: ${flow.net_amount_human_ru}`,
`Входящие строки с суммой: ${incoming.rows_with_amount} из ${incoming.rows_matched};${sideDateRange(incoming.first_movement_date, incoming.latest_movement_date)}.`,
`Исходящие строки с суммой: ${outgoing.rows_with_amount} из ${outgoing.rows_matched};${sideDateRange(outgoing.first_movement_date, outgoing.latest_movement_date)}.`,
`${limitCaveat} Это расчет по найденным строкам 1С, а не подтверждение полного сальдо вне проверенного окна.`
]
.join(" ")
.replace(/\s+/g, " ")
.trim();
}
export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract {
@@ -205,7 +246,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
@@ -54,10 +54,33 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
}
export interface AssistantMcpDiscoveryValueFlowSideSummary {
rows_matched: number;
rows_with_amount: number;
total_amount: number;
total_amount_human_ru: string;
first_movement_date: string | null;
latest_movement_date: string | null;
coverage_limited_by_probe_limit: boolean;
}
export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
counterparty: string | null;
period_scope: string | null;
incoming_customer_revenue: AssistantMcpDiscoveryValueFlowSideSummary;
outgoing_supplier_payout: AssistantMcpDiscoveryValueFlowSideSummary;
net_amount: number;
net_amount_human_ru: string;
net_direction: "net_incoming" | "net_outgoing" | "balanced";
coverage_limited_by_probe_limit: boolean;
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows";
}
export type AssistantMcpDiscoveryPilotScope =
| "counterparty_lifecycle_query_documents_v1"
| "counterparty_value_flow_query_movements_v1"
| "counterparty_supplier_payout_query_movements_v1";
| "counterparty_supplier_payout_query_movements_v1"
| "counterparty_bidirectional_value_flow_query_movements_v1";
export interface AssistantMcpDiscoveryPilotExecutionContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
@@ -73,6 +96,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
source_rows_summary: string | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
query_limitations: string[];
reason_codes: string[];
}
@@ -205,10 +229,12 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
interface ValueFlowPilotProfile {
scope: Extract<
AssistantMcpDiscoveryPilotScope,
"counterparty_value_flow_query_movements_v1" | "counterparty_supplier_payout_query_movements_v1"
| "counterparty_value_flow_query_movements_v1"
| "counterparty_supplier_payout_query_movements_v1"
| "counterparty_bidirectional_value_flow_query_movements_v1"
>;
recipe_intent: Extract<AddressIntent, "customer_revenue_and_payments" | "supplier_payouts_profile">;
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"];
recipe_intent: Extract<AddressIntent, "customer_revenue_and_payments" | "supplier_payouts_profile"> | null;
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"] | "bidirectional_net_value_flow";
}
function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): ValueFlowPilotProfile {
@@ -216,6 +242,18 @@ function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): V
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${action} ${unsupported}`;
if (
combined.includes("net_value_flow") ||
combined.includes("bidirectional") ||
combined.includes("netting") ||
combined.includes("net")
) {
return {
scope: "counterparty_bidirectional_value_flow_query_movements_v1",
recipe_intent: null,
direction: "bidirectional_net_value_flow"
};
}
if (
combined.includes("payout") ||
combined.includes("outflow") ||
@@ -435,6 +473,91 @@ function deriveValueFlow(
};
}
function deriveValueFlowSideSummary(
result: AddressMcpQueryExecutorResult | null,
probeLimit: number
): AssistantMcpDiscoveryValueFlowSideSummary {
if (!result || result.error || result.matched_rows <= 0) {
return {
rows_matched: 0,
rows_with_amount: 0,
total_amount: 0,
total_amount_human_ru: formatAmountHumanRu(0),
first_movement_date: null,
latest_movement_date: null,
coverage_limited_by_probe_limit: false
};
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
const amount = rowAmountValue(row);
if (amount !== null) {
totalAmount += amount;
rowsWithAmount += 1;
}
}
const dates = result.rows
.map((row) => rowDateValue(row))
.filter((value): value is string => Boolean(value))
.sort();
return {
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
total_amount: totalAmount,
total_amount_human_ru: formatAmountHumanRu(totalAmount),
first_movement_date: dates[0] ?? null,
latest_movement_date: dates[dates.length - 1] ?? null,
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit
};
}
function deriveBidirectionalValueFlow(input: {
incomingResult: AddressMcpQueryExecutorResult | null;
outgoingResult: AddressMcpQueryExecutorResult | null;
counterparty: string | null;
periodScope: string | null;
probeLimit: number;
}): AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null {
const incoming = deriveValueFlowSideSummary(input.incomingResult, input.probeLimit);
const outgoing = deriveValueFlowSideSummary(input.outgoingResult, input.probeLimit);
if (incoming.rows_with_amount <= 0 && outgoing.rows_with_amount <= 0) {
return null;
}
const netAmount = incoming.total_amount - outgoing.total_amount;
return {
counterparty: input.counterparty,
period_scope: input.periodScope,
incoming_customer_revenue: incoming,
outgoing_supplier_payout: outgoing,
net_amount: netAmount,
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
net_direction: netAmount > 0 ? "net_incoming" : netAmount < 0 ? "net_outgoing" : "balanced",
coverage_limited_by_probe_limit:
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows"
};
}
function summarizeBidirectionalValueFlowRows(input: {
incomingResult: AddressMcpQueryExecutorResult | null;
outgoingResult: AddressMcpQueryExecutorResult | null;
}): string | null {
const incoming = input.incomingResult;
const outgoing = input.outgoingResult;
if (!incoming && !outgoing) {
return null;
}
const incomingSummary = incoming?.error
? "incoming value-flow query failed"
: `${incoming?.fetched_rows ?? 0} incoming value-flow rows fetched, ${incoming?.matched_rows ?? 0} matched`;
const outgoingSummary = outgoing?.error
? "outgoing supplier-payout query failed"
: `${outgoing?.fetched_rows ?? 0} outgoing supplier-payout rows fetched, ${outgoing?.matched_rows ?? 0} matched`;
return `${incomingSummary}; ${outgoingSummary}`;
}
function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
@@ -464,7 +587,25 @@ function buildValueFlowConfirmedFacts(
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
: "1C value-flow rows were found for the requested counterparty scope"
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildBidirectionalValueFlowConfirmedFacts(
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
if (!derived) {
return [];
}
const hasIncoming = derived.incoming_customer_revenue.rows_matched > 0;
const hasOutgoing = derived.outgoing_supplier_payout.rows_matched > 0;
if (derived.counterparty) {
return [
`1C bidirectional value-flow rows were checked for counterparty ${derived.counterparty}: incoming=${hasIncoming ? "found" : "not_found"}, outgoing=${hasOutgoing ? "found" : "not_found"}`
];
}
return [
`1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=${hasIncoming ? "found" : "not_found"}, outgoing=${hasOutgoing ? "found" : "not_found"}`
];
}
@@ -485,6 +626,15 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
}
function buildBidirectionalValueFlowInferredFacts(
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
if (!derived) {
return [];
}
return ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"];
}
function buildLifecycleUnknownFacts(): string[] {
return ["Legal registration date is not proven by this MCP discovery pilot"];
}
@@ -514,6 +664,24 @@ function buildValueFlowUnknownFacts(
return unknownFacts;
}
function buildBidirectionalValueFlowUnknownFacts(
periodScope: string | null,
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
const unknownFacts: string[] = [];
if (derived?.coverage_limited_by_probe_limit) {
unknownFacts.push(
"Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached"
);
}
unknownFacts.push(
periodScope
? "Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time bidirectional value-flow is not proven without an explicit checked period"
);
return unknownFacts;
}
function buildEmptyEvidence(
planner: AssistantMcpDiscoveryPlannerContract,
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
@@ -557,6 +725,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes
};
@@ -579,6 +748,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes
};
@@ -608,6 +778,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes
};
@@ -620,7 +791,117 @@ export async function executeAssistantMcpDiscoveryPilot(
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildValueFlowFilters(planner);
const valueFlowProfile = valueFlowPilotProfile(planner);
const selection = selectAddressRecipe(valueFlowProfile.recipe_intent, filters);
if (valueFlowProfile.direction === "bidirectional_net_value_flow") {
let incomingResult: AddressMcpQueryExecutorResult | null = null;
let outgoingResult: AddressMcpQueryExecutorResult | null = null;
const incomingSelection = selectAddressRecipe("customer_revenue_and_payments", filters);
const outgoingSelection = selectAddressRecipe("supplier_payouts_profile", filters);
if (!incomingSelection.selected_recipe || !outgoingSelection.selected_recipe) {
pushReason(reasonCodes, "pilot_bidirectional_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Bidirectional value-flow recipes are not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: valueFlowProfile.scope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Bidirectional value-flow recipes are not available"],
reason_codes: reasonCodes
};
}
pushReason(reasonCodes, "pilot_bidirectional_value_flow_recipes_selected");
const incomingRecipePlan = buildAddressRecipePlan(incomingSelection.selected_recipe, filters);
const outgoingRecipePlan = buildAddressRecipePlan(outgoingSelection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_movements") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(
skippedProbeResult(step, "pilot_bidirectional_value_flow_uses_two_query_movements_and_derives_net")
);
continue;
}
incomingResult = await deps.executeAddressMcpQuery({
query: incomingRecipePlan.query,
limit: incomingRecipePlan.limit,
account_scope: incomingRecipePlan.account_scope
});
outgoingResult = await deps.executeAddressMcpQuery({
query: outgoingRecipePlan.query,
limit: outgoingRecipePlan.limit,
account_scope: outgoingRecipePlan.account_scope
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, incomingResult));
probeResults.push(queryResultToProbeResult(step.primitive_id, outgoingResult));
if (incomingResult.error) {
pushUnique(queryLimitations, incomingResult.error);
pushReason(reasonCodes, "pilot_bidirectional_incoming_query_movements_mcp_error");
}
if (outgoingResult.error) {
pushUnique(queryLimitations, outgoingResult.error);
pushReason(reasonCodes, "pilot_bidirectional_outgoing_query_movements_mcp_error");
}
if (!incomingResult.error || !outgoingResult.error) {
pushReason(reasonCodes, "pilot_bidirectional_query_movements_mcp_executed");
}
}
const sourceRowsSummary = summarizeBidirectionalValueFlowRows({ incomingResult, outgoingResult });
const derivedBidirectionalValueFlow = deriveBidirectionalValueFlow({
incomingResult,
outgoingResult,
counterparty,
periodScope: dateScope,
probeLimit: planner.discovery_plan.execution_budget.max_rows_per_probe
});
if (derivedBidirectionalValueFlow) {
pushReason(reasonCodes, "pilot_derived_bidirectional_value_flow_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildBidirectionalValueFlowConfirmedFacts(derivedBidirectionalValueFlow),
inferredFacts: buildBidirectionalValueFlowInferredFacts(derivedBidirectionalValueFlow),
unknownFacts: buildBidirectionalValueFlowUnknownFacts(dateScope, derivedBidirectionalValueFlow),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: valueFlowProfile.scope,
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: derivedBidirectionalValueFlow,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
const recipeIntent = valueFlowProfile.recipe_intent;
const selection = recipeIntent ? selectAddressRecipe(recipeIntent, filters) : { selected_recipe: null };
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
@@ -638,6 +919,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Value-flow recipe is not available"],
reason_codes: reasonCodes
};
@@ -707,6 +989,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: sourceRowsSummary,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
@@ -732,6 +1015,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes
};
@@ -789,6 +1073,7 @@ export async function executeAssistantMcpDiscoveryPilot(
source_rows_summary: sourceRowsSummary,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
@@ -101,7 +101,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const axes: string[] = [];
addScopeAxes(axes, meaning);
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value"])) {
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@@ -112,6 +112,22 @@ function localizeLine(value: string): string {
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
}
const bidirectionalMatch = value.match(
/^1C bidirectional value-flow rows were checked for counterparty\s+(.+): incoming=(found|not_found), outgoing=(found|not_found)$/i
);
if (bidirectionalMatch) {
const incoming = bidirectionalMatch[2] === "found" ? "входящие строки найдены" : "входящие строки не найдены";
const outgoing = bidirectionalMatch[3] === "found" ? "исходящие строки найдены" : "исходящие строки не найдены";
return `В 1С проверены входящие и исходящие денежные строки по контрагенту ${bidirectionalMatch[1]}: ${incoming}, ${outgoing}.`;
}
const bidirectionalScopeMatch = value.match(
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
);
if (bidirectionalScopeMatch) {
const incoming = bidirectionalScopeMatch[1] === "found" ? "входящие строки найдены" : "входящие строки не найдены";
const outgoing = bidirectionalScopeMatch[2] === "found" ? "исходящие строки найдены" : "исходящие строки не найдены";
return `В 1С проверены входящие и исходящие денежные строки по запрошенному контрагентскому контуру: ${incoming}, ${outgoing}.`;
}
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
}
@@ -121,12 +137,18 @@ function localizeLine(value: string): string {
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 (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена.";
}
if (/^Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached$/i.test(value)) {
return "Полное покрытие запрошенного периода не подтверждено: проверка достигла лимита найденных строк.";
}
if (/^Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached$/i.test(value)) {
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено: хотя бы одна сторона проверки достигла лимита найденных строк.";
}
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
}
@@ -139,6 +161,12 @@ function localizeLine(value: string): string {
if (/^Full all-time supplier-payout amount is not proven without an explicit checked period$/i.test(value)) {
return "Полный объем исходящих платежей за все время без явно проверенного периода не подтвержден.";
}
if (/^Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный двусторонний денежный поток вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
}
return value;
}
@@ -150,6 +150,12 @@ function hasPayoutSignal(text: string): boolean {
);
}
function hasBidirectionalValueFlowSignal(text: string): boolean {
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 semanticNeedFor(input: {
domain: string | null;
action: string | null;
@@ -161,7 +167,7 @@ function semanticNeedFor(input: {
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value|net|netting|balance|cashflow)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
if (/(?:document|documents|list_documents)/iu.test(combined)) {
@@ -201,8 +207,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
const payoutSignal = valueFlowSignal && hasPayoutSignal(rawText);
const bidirectionalValueFlowSignal = !lifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const valueFlowSignal = !lifecycleSignal && (hasValueFlowSignal(rawText) || bidirectionalValueFlowSignal);
const payoutSignal = valueFlowSignal && !bidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
@@ -225,12 +232,29 @@ export function buildAssistantMcpDiscoveryTurnInput(
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? (payoutSignal ? "payout" : "turnover") : rawAction,
asked_action_family: lifecycleSignal
? "activity_duration"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "net_value_flow"
: payoutSignal
? "payout"
: "turnover"
: rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family:
unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? (payoutSignal ? "counterparty_payouts_or_outflow" : "counterparty_value_or_turnover") : null),
unsupported ??
(lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "counterparty_bidirectional_value_flow_or_netting"
: payoutSignal
? "counterparty_payouts_or_outflow"
: "counterparty_value_or_turnover"
: null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
};
@@ -284,6 +308,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}
if (bidirectionalValueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_bidirectional_value_flow_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
@@ -1,6 +1,7 @@
// @ts-nocheck
import {
resolveAssistantOrganizationAuthority
resolveAssistantOrganizationAuthority,
resolveOrganizationClarificationContinuation
} from "./assistantContinuityPolicy";
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
@@ -409,17 +410,22 @@ export function createAssistantRoutePolicy(deps) {
continuitySnapshot.lastGroundedAddressDebug;
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems);
const organizationClarificationCandidates = organizationAuthority.organizationClarificationCandidates;
const organizationClarificationSelectionFromScope = organizationAuthority.organizationClarificationSelectionFromScope;
const explicitOrganizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ??
null;
const organizationClarificationSelection = explicitOrganizationClarificationSelection ??
(organizationClarificationSelectionFromScope &&
organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope)
? organizationClarificationSelectionFromScope
: null);
const organizationClarificationContinuation = resolveOrganizationClarificationContinuation({
rawMessages: [
rawUserMessage,
repairedRawUserMessage,
effectiveAddressUserMessage,
repairedEffectiveAddressUserMessage
],
organizationClarificationCandidates,
organizationClarificationSelectionFromScope: organizationAuthority.organizationClarificationSelectionFromScope,
lastOrganizationClarificationDebug,
resolveOrganizationSelectionFromMessage,
toNonEmptyString,
normalizeOrganizationScopeValue
});
const explicitOrganizationClarificationSelection = organizationClarificationContinuation.explicitSelection;
const organizationClarificationSelection = organizationClarificationContinuation.selection;
const metaSignals = resolveMetaSignalSet({
rawUserMessage,
repairedRawUserMessage,
@@ -10,6 +10,7 @@ import {
readAddressDebugFilters,
readAddressDebugItem,
readAddressDebugTemporalScope,
resolveOrganizationClarificationContinuation,
resolveNavigationSessionContextState,
resolveAddressDebugCarryoverFilters,
resolveAddressDebugAnchorContext,
@@ -414,17 +415,18 @@ export function createAssistantTransitionPolicy(deps) {
const organizationClarificationCandidates = Array.isArray(organizationAuthority.organizationClarificationCandidates)
? organizationAuthority.organizationClarificationCandidates
: [];
const explicitOrganizationClarificationSelection =
deps.resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(deps.toNonEmptyString(alternateMessage)
? deps.resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null);
const organizationClarificationSelection =
explicitOrganizationClarificationSelection ??
deps.normalizeOrganizationScopeValue(organizationAuthority.organizationClarificationSelectionFromScope);
const hasOrganizationClarificationContinuation = Boolean(
lastOrganizationClarificationDebug && organizationClarificationSelection
);
const organizationClarificationContinuation = resolveOrganizationClarificationContinuation({
rawMessages: [userMessage, alternateMessage],
organizationClarificationCandidates,
organizationClarificationSelectionFromScope: organizationAuthority.organizationClarificationSelectionFromScope,
lastOrganizationClarificationDebug,
resolveOrganizationSelectionFromMessage: deps.resolveOrganizationSelectionFromMessage,
toNonEmptyString: deps.toNonEmptyString,
normalizeOrganizationScopeValue: deps.normalizeOrganizationScopeValue
});
const explicitOrganizationClarificationSelection = organizationClarificationContinuation.explicitSelection;
const organizationClarificationSelection = organizationClarificationContinuation.selection;
const hasOrganizationClarificationContinuation = organizationClarificationContinuation.hasContinuation;
const carryoverSourceDebug =
previousAddressDebug ??
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);