ARCH: добавить supplier payout pilot MCP discovery
This commit is contained in:
parent
49fd08652c
commit
52da709671
|
|
@ -1154,6 +1154,64 @@ Module progress:
|
|||
|
||||
- Big Block 5 MCP Semantic Data Agent: `97%`.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Discovery Supplier Payout Pilot And Address-Lane Arbitration
|
||||
|
||||
The seventeenth implementation slice of Big Block 5 closes the main boundary left by the first value-flow pilot: outgoing supplier payments.
|
||||
|
||||
New behavior:
|
||||
|
||||
- raw payout/outflow wording such as `мы заплатили`, `заплатили`, `перечислили`, `списание`, `расход`, `поставщик`, `supplier`, `payout`, and `outflow` is recognized as counterparty value-flow discovery;
|
||||
- the turn input adapter now marks these turns as `asked_action_family=payout` and `unsupported_but_understood_family=counterparty_payouts_or_outflow`;
|
||||
- the pilot chooses `supplier_payouts_profile` instead of `customer_revenue_and_payments`;
|
||||
- `derived_value_flow` now includes `value_flow_direction`, so answer composition can distinguish incoming customer revenue from outgoing supplier payout;
|
||||
- supplier payout evidence gets a separate pilot scope: `counterparty_supplier_payout_query_movements_v1`;
|
||||
- the answer says `исходящие платежи/списания`, not generic turnover, and bounds the result to found rows and the checked period.
|
||||
- if the probe row limit is reached, `derived_value_flow.coverage_limited_by_probe_limit=true` and the answer explicitly says that full requested-period coverage is not proven.
|
||||
|
||||
The live replay exposed one more architecture gap before the final fix:
|
||||
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun7` failed on `сколько мы заплатили Группа СВК за 2020 год?`;
|
||||
- MCP discovery had already produced the correct `counterparty_supplier_payout_query_movements_v1` candidate;
|
||||
- the visible answer still came from stale `list_documents_by_counterparty` carryover and showed incoming bank receipt documents instead of outgoing payments.
|
||||
|
||||
The response policy gate was therefore extended into the address lane.
|
||||
|
||||
It may now replace an address-lane answer only when:
|
||||
|
||||
- the MCP discovery entry point is `bridge_executed`;
|
||||
- discovery was attempted;
|
||||
- `turn_input.should_run_discovery=true`;
|
||||
- the current reply source is `address_query_runtime_v1`, `address_lane`, or `address_exact`;
|
||||
- the response candidate passes the same guarded text checks as living-chat and deep gates.
|
||||
|
||||
This keeps exact supported routes safe because ordinary exact turns have discovery `skipped_not_applicable` or `should_run_discovery=false`.
|
||||
|
||||
Replay result:
|
||||
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun8` passed 6/6, final status `accepted`;
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun9` passed 6/6 after adding the explicit row-limit coverage warning;
|
||||
- the supplier payout step now answers through `mcp_discovery_response_candidate_guarded`;
|
||||
- the answer reports `19 568 878,06 руб.` across `100 из 100` found outgoing payment rows for `Группа СВК` in `2020`;
|
||||
- first found movement date: `2020-01-09`;
|
||||
- latest found movement date: `2020-03-16`;
|
||||
- because the probe reached the row limit, the answer now says that full requested-period coverage is not confirmed;
|
||||
- the answer explicitly says that full outgoing payments outside the checked period are not confirmed;
|
||||
- lifecycle, incoming value-flow, supplier payout, and off-domain steps did not leak `query_documents`, `query_movements`, `runtime_`, `planner_`, `catalog_`, `primitive`, or `pilot_` in user-facing text.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.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` passed 49/49;
|
||||
- `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_rerun9 --timeout-seconds 180` passed 6/6, final status `accepted`.
|
||||
|
||||
Known remaining boundary:
|
||||
|
||||
- this still does not implement arbitrary cross-register self-navigation by Qwen3. The assistant has two guarded value-flow pilots now: incoming customer revenue and outgoing supplier payouts. Bidirectional netting, multi-axis aggregation, follow-up drilldown over the discovered rows, and richer coverage proofs remain future discovery-layer work.
|
||||
|
||||
Module progress:
|
||||
|
||||
- Big Block 5 MCP Semantic Data Agent: `98%`.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -111,7 +111,37 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_off_domain_living_chat_not_hijacked",
|
||||
"step_id": "step_05_counterparty_supplier_payout_uses_guarded_discovery",
|
||||
"title": "Unsupported-but-understood counterparty payout question uses guarded supplier discovery answer",
|
||||
"question": "сколько мы заплатили Группа СВК за 2020 год?",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)1с|найден|строк|подтвержд",
|
||||
"(?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_supplier_payout",
|
||||
"counterparty_outgoing_payments",
|
||||
"unsupported_current_turn_meaning_boundary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_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": [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddress
|
|||
const assistantCapabilityBindingResponseGuard_1 = require("./assistantCapabilityBindingResponseGuard");
|
||||
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
||||
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
|
||||
const assistantMcpDiscoveryResponsePolicy_1 = require("./assistantMcpDiscoveryResponsePolicy");
|
||||
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
||||
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
||||
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
||||
|
|
@ -221,14 +222,29 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
|||
...debugWithMcpDiscovery,
|
||||
capability_binding_response_guard: guardedResponse.audit
|
||||
};
|
||||
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
|
||||
currentReply: guardedResponse.assistantReply,
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
addressRuntimeMeta: debugWithResponseGuard
|
||||
});
|
||||
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
? mcpDiscoveryResponsePolicy.reply_text
|
||||
: guardedResponse.assistantReply;
|
||||
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
|
||||
const finalDebug = {
|
||||
...debugWithResponseGuard,
|
||||
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
|
||||
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
|
||||
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
|
||||
};
|
||||
const finalization = finalizeAddressTurnSafe({
|
||||
sessionId: input.sessionId,
|
||||
userMessage: input.userMessage,
|
||||
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
||||
assistantReply: guardedResponse.assistantReply,
|
||||
replyType: guardedResponse.replyType,
|
||||
assistantReply: finalAssistantReply,
|
||||
replyType: finalReplyType,
|
||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||
debug: debugWithResponseGuard,
|
||||
debug: finalDebug,
|
||||
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
||||
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
||||
appendItem: input.appendItem,
|
||||
|
|
@ -240,6 +256,6 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
|||
});
|
||||
return {
|
||||
response: finalization.response,
|
||||
debug: debugWithResponseGuard
|
||||
debug: finalDebug
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,15 @@ function modeFor(pilot) {
|
|||
}
|
||||
return "checked_sources_only";
|
||||
}
|
||||
function isValueFlowPilot(pilot) {
|
||||
return (pilot.pilot_scope === "counterparty_value_flow_query_movements_v1" ||
|
||||
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1");
|
||||
}
|
||||
function headlineFor(mode, pilot) {
|
||||
if (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 (mode === "confirmed_with_bounded_inference") {
|
||||
|
|
@ -98,7 +105,7 @@ function buildMustNotClaim(pilot) {
|
|||
claims.push("Do not claim legal registration age unless a legal registration source is confirmed.");
|
||||
claims.push("Do not present inferred activity duration as a formally confirmed legal fact.");
|
||||
}
|
||||
if (pilot.pilot_scope === "counterparty_value_flow_query_movements_v1") {
|
||||
if (isValueFlowPilot(pilot)) {
|
||||
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
||||
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||
}
|
||||
|
|
@ -125,10 +132,20 @@ function derivedValueFlowConfirmedLine(pilot) {
|
|||
}
|
||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
|
||||
const totalLabel = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "сумма исходящих платежей/списаний составляет"
|
||||
: "сумма составляет";
|
||||
const caveat = flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
|
||||
const dates = flow.first_movement_date && flow.latest_movement_date
|
||||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
||||
: "";
|
||||
return `По найденным строкам денежных движений в 1С${counterparty}${period} сумма составляет ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates} Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.`;
|
||||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью."
|
||||
: "";
|
||||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||||
}
|
||||
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
const mode = modeFor(pilot);
|
||||
|
|
|
|||
|
|
@ -115,6 +115,27 @@ function isValueFlowPilotEligible(planner) {
|
|||
combined.includes("payout") ||
|
||||
combined.includes("value")));
|
||||
}
|
||||
function valueFlowPilotProfile(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
|
||||
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
|
||||
const combined = `${action} ${unsupported}`;
|
||||
if (combined.includes("payout") ||
|
||||
combined.includes("outflow") ||
|
||||
combined.includes("supplier") ||
|
||||
combined.includes("paid")) {
|
||||
return {
|
||||
scope: "counterparty_supplier_payout_query_movements_v1",
|
||||
recipe_intent: "supplier_payouts_profile",
|
||||
direction: "outgoing_supplier_payout"
|
||||
};
|
||||
}
|
||||
return {
|
||||
scope: "counterparty_value_flow_query_movements_v1",
|
||||
recipe_intent: "customer_revenue_and_payments",
|
||||
direction: "incoming_customer_revenue"
|
||||
};
|
||||
}
|
||||
function skippedProbeResult(step, limitation) {
|
||||
return {
|
||||
primitive_id: step.primitive_id,
|
||||
|
|
@ -259,7 +280,7 @@ function formatAmountHumanRu(amount) {
|
|||
.replace(/\u00a0/g, " ");
|
||||
return `${formatted} руб.`;
|
||||
}
|
||||
function deriveValueFlow(result, counterparty, periodScope) {
|
||||
function deriveValueFlow(result, counterparty, periodScope, direction, probeLimit) {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -280,6 +301,7 @@ function deriveValueFlow(result, counterparty, periodScope) {
|
|||
.filter((value) => Boolean(value))
|
||||
.sort();
|
||||
return {
|
||||
value_flow_direction: direction,
|
||||
counterparty,
|
||||
period_scope: periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
|
|
@ -288,6 +310,7 @@ function deriveValueFlow(result, counterparty, periodScope) {
|
|||
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,
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
|
|
@ -301,10 +324,17 @@ function buildLifecycleConfirmedFacts(result, counterparty) {
|
|||
: "1C activity rows were found for the requested counterparty scope"
|
||||
];
|
||||
}
|
||||
function buildValueFlowConfirmedFacts(result, counterparty) {
|
||||
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (direction === "outgoing_supplier_payout") {
|
||||
return [
|
||||
counterparty
|
||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||
: "1C supplier-payout rows were found for the requested counterparty scope"
|
||||
];
|
||||
}
|
||||
return [
|
||||
counterparty
|
||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||
|
|
@ -321,17 +351,29 @@ function buildValueFlowInferredFacts(derived) {
|
|||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"];
|
||||
}
|
||||
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
|
||||
}
|
||||
function buildLifecycleUnknownFacts() {
|
||||
return ["Legal registration date is not proven by this MCP discovery pilot"];
|
||||
}
|
||||
function buildValueFlowUnknownFacts(periodScope) {
|
||||
return [
|
||||
periodScope
|
||||
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time turnover is not proven without an explicit checked period"
|
||||
];
|
||||
function buildValueFlowUnknownFacts(periodScope, direction, derived) {
|
||||
const unknownFacts = [];
|
||||
if (derived?.coverage_limited_by_probe_limit) {
|
||||
unknownFacts.push("Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached");
|
||||
}
|
||||
if (direction === "outgoing_supplier_payout") {
|
||||
unknownFacts.push(periodScope
|
||||
? "Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time supplier-payout amount is not proven without an explicit checked period");
|
||||
return unknownFacts;
|
||||
}
|
||||
unknownFacts.push(periodScope
|
||||
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time turnover is not proven without an explicit checked period");
|
||||
return unknownFacts;
|
||||
}
|
||||
function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
|
||||
return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
|
|
@ -423,7 +465,8 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
if (valueFlowPilotEligible) {
|
||||
let queryResult = null;
|
||||
const filters = buildValueFlowFilters(planner);
|
||||
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("customer_revenue_and_payments", filters);
|
||||
const valueFlowProfile = valueFlowPilotProfile(planner);
|
||||
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)(valueFlowProfile.recipe_intent, filters);
|
||||
if (!selection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
|
||||
|
|
@ -431,7 +474,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
|
|
@ -445,6 +488,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
pushReason(reasonCodes, valueFlowProfile.direction === "outgoing_supplier_payout"
|
||||
? "pilot_supplier_payout_recipe_selected"
|
||||
: "pilot_customer_revenue_recipe_selected");
|
||||
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_movements") {
|
||||
|
|
@ -468,16 +514,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
}
|
||||
}
|
||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, planner.discovery_plan.execution_budget.max_rows_per_probe);
|
||||
if (derivedValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
|
||||
}
|
||||
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
|
||||
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty, valueFlowProfile.direction) : [],
|
||||
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
|
||||
unknownFacts: buildValueFlowUnknownFacts(dateScope),
|
||||
unknownFacts: buildValueFlowUnknownFacts(dateScope, valueFlowProfile.direction, derivedValueFlow),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
|
|
@ -486,7 +532,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "executed",
|
||||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: executedPrimitives.length > 0,
|
||||
executed_primitives: executedPrimitives,
|
||||
|
|
|
|||
|
|
@ -75,21 +75,40 @@ function localizeLine(value) {
|
|||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
||||
}
|
||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
|
||||
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from 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 (/^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 (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full all-time supplier-payout amount is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function section(title, lines) {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,14 @@ function isDiscoveryReadyDeepCandidate(input, entryPoint) {
|
|||
turnInput?.should_run_discovery === true &&
|
||||
(source === "deep_analysis" || source === "partial_coverage" || source === "normalizer_v2_0_2"));
|
||||
}
|
||||
function isDiscoveryReadyAddressCandidate(input, entryPoint) {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
|
||||
return (entryPoint?.entry_status === "bridge_executed" &&
|
||||
entryPoint.discovery_attempted === true &&
|
||||
turnInput?.should_run_discovery === true &&
|
||||
(source === "address_lane" || source === "address_exact" || source === "address_query_runtime_v1"));
|
||||
}
|
||||
function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||
const currentReply = String(input.currentReply ?? "");
|
||||
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||
|
|
@ -90,6 +98,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
|
||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
}
|
||||
|
|
@ -102,6 +111,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
if (!discoveryReadyDeepCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
|
||||
}
|
||||
if (!discoveryReadyAddressCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
}
|
||||
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||
}
|
||||
|
|
@ -115,7 +127,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
|
||||
}
|
||||
const canApply = Boolean(entryPoint) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@ function hasLifecycleSignal(text) {
|
|||
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
|
||||
}
|
||||
function hasValueFlowSignal(text) {
|
||||
return /(?:оборот|выручк|оплат|плат[её]ж|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|value[-\s]?flow|turnover|revenue|payment|payout|cash\s+flow)/iu.test(text);
|
||||
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow)/iu.test(text);
|
||||
}
|
||||
function hasPayoutSignal(text) {
|
||||
return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(text);
|
||||
}
|
||||
function semanticNeedFor(input) {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
|
|
@ -134,6 +137,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
|
||||
const lifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
|
||||
const payoutSignal = valueFlowSignal && hasPayoutSignal(rawText);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
|
|
@ -153,11 +157,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
|
||||
const turnMeaning = {
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? (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 ? "counterparty_value_or_turnover" : null),
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? (payoutSignal ? "counterparty_payouts_or_outflow" : "counterparty_value_or_turnover") : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
|
||||
};
|
||||
const cleanTurnMeaning = {};
|
||||
|
|
@ -205,6 +209,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (valueFlowSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
|
||||
}
|
||||
if (payoutSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
|
||||
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
||||
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
|
||||
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
|
||||
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
||||
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
||||
|
|
@ -280,14 +281,29 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
|||
...debugWithMcpDiscovery,
|
||||
capability_binding_response_guard: guardedResponse.audit
|
||||
};
|
||||
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: guardedResponse.assistantReply,
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
addressRuntimeMeta: debugWithResponseGuard
|
||||
});
|
||||
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
? mcpDiscoveryResponsePolicy.reply_text
|
||||
: guardedResponse.assistantReply;
|
||||
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
|
||||
const finalDebug = {
|
||||
...debugWithResponseGuard,
|
||||
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
|
||||
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
|
||||
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
|
||||
};
|
||||
const finalization = finalizeAddressTurnSafe({
|
||||
sessionId: input.sessionId,
|
||||
userMessage: input.userMessage,
|
||||
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
||||
assistantReply: guardedResponse.assistantReply,
|
||||
replyType: guardedResponse.replyType,
|
||||
assistantReply: finalAssistantReply,
|
||||
replyType: finalReplyType,
|
||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||
debug: debugWithResponseGuard,
|
||||
debug: finalDebug,
|
||||
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
||||
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
||||
appendItem: input.appendItem,
|
||||
|
|
@ -300,6 +316,6 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
|||
|
||||
return {
|
||||
response: finalization.response as ResponseType,
|
||||
debug: debugWithResponseGuard
|
||||
debug: finalDebug
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,18 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
|
|||
return "checked_sources_only";
|
||||
}
|
||||
|
||||
function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
pilot.pilot_scope === "counterparty_value_flow_query_movements_v1" ||
|
||||
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
|
||||
);
|
||||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
if (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 (mode === "confirmed_with_bounded_inference") {
|
||||
|
|
@ -130,7 +140,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
claims.push("Do not claim legal registration age unless a legal registration source is confirmed.");
|
||||
claims.push("Do not present inferred activity duration as a formally confirmed legal fact.");
|
||||
}
|
||||
if (pilot.pilot_scope === "counterparty_value_flow_query_movements_v1") {
|
||||
if (isValueFlowPilot(pilot)) {
|
||||
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
||||
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||
}
|
||||
|
|
@ -159,11 +169,24 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
|
|||
}
|
||||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||||
const movementLabel =
|
||||
flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
|
||||
const totalLabel =
|
||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "сумма исходящих платежей/списаний составляет"
|
||||
: "сумма составляет";
|
||||
const caveat =
|
||||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
||||
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
|
||||
const dates =
|
||||
flow.first_movement_date && flow.latest_movement_date
|
||||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
||||
: "";
|
||||
return `По найденным строкам денежных движений в 1С${counterparty}${period} сумма составляет ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates} Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.`;
|
||||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью."
|
||||
: "";
|
||||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
type AssistantMcpDiscoveryProbeResult
|
||||
} from "./assistantMcpDiscoveryPolicy";
|
||||
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
|
||||
import type { AddressFilterSet } from "../types/addressQuery";
|
||||
import type { AddressFilterSet, AddressIntent } from "../types/addressQuery";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_pilot_executor_v1" as const;
|
||||
|
|
@ -41,6 +41,7 @@ export interface AssistantMcpDiscoveryDerivedActivityPeriod {
|
|||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
|
||||
counterparty: string | null;
|
||||
period_scope: string | null;
|
||||
rows_matched: number;
|
||||
|
|
@ -49,12 +50,14 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
|
|||
total_amount_human_ru: string;
|
||||
first_movement_date: string | null;
|
||||
latest_movement_date: string | null;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryPilotScope =
|
||||
| "counterparty_lifecycle_query_documents_v1"
|
||||
| "counterparty_value_flow_query_movements_v1";
|
||||
| "counterparty_value_flow_query_movements_v1"
|
||||
| "counterparty_supplier_payout_query_movements_v1";
|
||||
|
||||
export interface AssistantMcpDiscoveryPilotExecutionContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
|
||||
|
|
@ -199,6 +202,39 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
|
|||
);
|
||||
}
|
||||
|
||||
interface ValueFlowPilotProfile {
|
||||
scope: Extract<
|
||||
AssistantMcpDiscoveryPilotScope,
|
||||
"counterparty_value_flow_query_movements_v1" | "counterparty_supplier_payout_query_movements_v1"
|
||||
>;
|
||||
recipe_intent: Extract<AddressIntent, "customer_revenue_and_payments" | "supplier_payouts_profile">;
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"];
|
||||
}
|
||||
|
||||
function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): ValueFlowPilotProfile {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
|
||||
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
|
||||
const combined = `${action} ${unsupported}`;
|
||||
if (
|
||||
combined.includes("payout") ||
|
||||
combined.includes("outflow") ||
|
||||
combined.includes("supplier") ||
|
||||
combined.includes("paid")
|
||||
) {
|
||||
return {
|
||||
scope: "counterparty_supplier_payout_query_movements_v1",
|
||||
recipe_intent: "supplier_payouts_profile",
|
||||
direction: "outgoing_supplier_payout"
|
||||
};
|
||||
}
|
||||
return {
|
||||
scope: "counterparty_value_flow_query_movements_v1",
|
||||
recipe_intent: "customer_revenue_and_payments",
|
||||
direction: "incoming_customer_revenue"
|
||||
};
|
||||
}
|
||||
|
||||
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
|
||||
return {
|
||||
primitive_id: step.primitive_id,
|
||||
|
|
@ -361,7 +397,9 @@ function formatAmountHumanRu(amount: number): string {
|
|||
function deriveValueFlow(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
counterparty: string | null,
|
||||
periodScope: string | null
|
||||
periodScope: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||
probeLimit: number
|
||||
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return null;
|
||||
|
|
@ -383,6 +421,7 @@ function deriveValueFlow(
|
|||
.filter((value): value is string => Boolean(value))
|
||||
.sort();
|
||||
return {
|
||||
value_flow_direction: direction,
|
||||
counterparty,
|
||||
period_scope: periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
|
|
@ -391,6 +430,7 @@ function deriveValueFlow(
|
|||
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,
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
|
|
@ -406,10 +446,21 @@ function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, cou
|
|||
];
|
||||
}
|
||||
|
||||
function buildValueFlowConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
|
||||
function buildValueFlowConfirmedFacts(
|
||||
result: AddressMcpQueryExecutorResult,
|
||||
counterparty: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
||||
): string[] {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (direction === "outgoing_supplier_payout") {
|
||||
return [
|
||||
counterparty
|
||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||
: "1C supplier-payout rows were found for the requested counterparty scope"
|
||||
];
|
||||
}
|
||||
return [
|
||||
counterparty
|
||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||
|
|
@ -428,6 +479,9 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
|||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||
return ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"];
|
||||
}
|
||||
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
|
||||
}
|
||||
|
||||
|
|
@ -435,12 +489,29 @@ function buildLifecycleUnknownFacts(): string[] {
|
|||
return ["Legal registration date is not proven by this MCP discovery pilot"];
|
||||
}
|
||||
|
||||
function buildValueFlowUnknownFacts(periodScope: string | null): string[] {
|
||||
return [
|
||||
function buildValueFlowUnknownFacts(
|
||||
periodScope: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||
derived: AssistantMcpDiscoveryDerivedValueFlow | null
|
||||
): string[] {
|
||||
const unknownFacts: string[] = [];
|
||||
if (derived?.coverage_limited_by_probe_limit) {
|
||||
unknownFacts.push("Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached");
|
||||
}
|
||||
if (direction === "outgoing_supplier_payout") {
|
||||
unknownFacts.push(
|
||||
periodScope
|
||||
? "Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time supplier-payout amount is not proven without an explicit checked period"
|
||||
);
|
||||
return unknownFacts;
|
||||
}
|
||||
unknownFacts.push(
|
||||
periodScope
|
||||
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time turnover is not proven without an explicit checked period"
|
||||
];
|
||||
);
|
||||
return unknownFacts;
|
||||
}
|
||||
|
||||
function buildEmptyEvidence(
|
||||
|
|
@ -548,7 +619,8 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
if (valueFlowPilotEligible) {
|
||||
let queryResult: AddressMcpQueryExecutorResult | null = null;
|
||||
const filters = buildValueFlowFilters(planner);
|
||||
const selection = selectAddressRecipe("customer_revenue_and_payments", filters);
|
||||
const valueFlowProfile = valueFlowPilotProfile(planner);
|
||||
const selection = selectAddressRecipe(valueFlowProfile.recipe_intent, filters);
|
||||
if (!selection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
|
||||
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
|
||||
|
|
@ -556,7 +628,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "unsupported",
|
||||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: false,
|
||||
executed_primitives: executedPrimitives,
|
||||
|
|
@ -570,6 +642,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
valueFlowProfile.direction === "outgoing_supplier_payout"
|
||||
? "pilot_supplier_payout_recipe_selected"
|
||||
: "pilot_customer_revenue_recipe_selected"
|
||||
);
|
||||
|
||||
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
|
|
@ -594,16 +672,22 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
}
|
||||
|
||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
|
||||
const derivedValueFlow = deriveValueFlow(
|
||||
queryResult,
|
||||
counterparty,
|
||||
dateScope,
|
||||
valueFlowProfile.direction,
|
||||
planner.discovery_plan.execution_budget.max_rows_per_probe
|
||||
);
|
||||
if (derivedValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
|
||||
}
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
|
||||
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty, valueFlowProfile.direction) : [],
|
||||
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
|
||||
unknownFacts: buildValueFlowUnknownFacts(dateScope),
|
||||
unknownFacts: buildValueFlowUnknownFacts(dateScope, valueFlowProfile.direction, derivedValueFlow),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
|
|
@ -613,7 +697,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "executed",
|
||||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: executedPrimitives.length > 0,
|
||||
executed_primitives: executedPrimitives,
|
||||
|
|
|
|||
|
|
@ -105,21 +105,40 @@ function localizeLine(value: string): string {
|
|||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
||||
}
|
||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
|
||||
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from 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 (/^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 (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full all-time supplier-payout amount is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,20 @@ function isDiscoveryReadyDeepCandidate(
|
|||
);
|
||||
}
|
||||
|
||||
function isDiscoveryReadyAddressCandidate(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
|
||||
return (
|
||||
entryPoint?.entry_status === "bridge_executed" &&
|
||||
entryPoint.discovery_attempted === true &&
|
||||
turnInput?.should_run_discovery === true &&
|
||||
(source === "address_lane" || source === "address_exact" || source === "address_query_runtime_v1")
|
||||
);
|
||||
}
|
||||
|
||||
export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||
): AssistantMcpDiscoveryResponsePolicyResult {
|
||||
|
|
@ -149,6 +163,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
|
||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
||||
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
|
|
@ -162,6 +177,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
if (!discoveryReadyDeepCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
|
||||
}
|
||||
if (!discoveryReadyAddressCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
}
|
||||
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||
}
|
||||
|
|
@ -177,7 +195,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
|
||||
const canApply =
|
||||
Boolean(entryPoint) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
|
|
|
|||
|
|
@ -139,7 +139,13 @@ function hasLifecycleSignal(text: string): boolean {
|
|||
}
|
||||
|
||||
function hasValueFlowSignal(text: string): boolean {
|
||||
return /(?:оборот|выручк|оплат|плат[её]ж|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|value[-\s]?flow|turnover|revenue|payment|payout|cash\s+flow)/iu.test(
|
||||
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow)/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function hasPayoutSignal(text: string): boolean {
|
||||
return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
|
@ -196,6 +202,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
|
||||
const lifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
|
||||
const payoutSignal = valueFlowSignal && hasPayoutSignal(rawText);
|
||||
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
|
|
@ -218,11 +225,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
|
||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? (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 ? "counterparty_value_or_turnover" : null),
|
||||
unsupported_but_understood_family:
|
||||
unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? (payoutSignal ? "counterparty_payouts_or_outflow" : "counterparty_value_or_turnover") : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
|
||||
};
|
||||
|
||||
|
|
@ -273,6 +281,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (valueFlowSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
|
||||
}
|
||||
if (payoutSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,80 @@ describe("assistant address lane response runtime adapter", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("can replace a stale address exact answer with a guarded MCP discovery candidate", () => {
|
||||
const finalizeAddressTurn = vi.fn((input) => ({
|
||||
response: {
|
||||
ok: true,
|
||||
assistant_reply: input.assistantReply,
|
||||
reply_type: input.replyType,
|
||||
debug: input.debug
|
||||
}
|
||||
}));
|
||||
|
||||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||||
sessionId: "asst-mcp-address",
|
||||
userMessage: "сколько мы заплатили СВК за 2020?",
|
||||
effectiveAddressUserMessage: "сколько мы заплатили СВК за 2020?",
|
||||
addressLane: {
|
||||
handled: true,
|
||||
reply_text: "stale documents answer",
|
||||
reply_type: "factual",
|
||||
debug: {}
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
mcpDiscoveryRuntimeEntryPoint: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: { adapter_status: "ready", should_run_discovery: true },
|
||||
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: "Discovery payout answer",
|
||||
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
||||
inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"],
|
||||
unknown_lines: ["Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
},
|
||||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||||
}
|
||||
},
|
||||
knownOrganizations: [],
|
||||
activeOrganization: null,
|
||||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||||
buildAddressDebugPayload: () => ({}),
|
||||
buildAddressFollowupOffer: () => null,
|
||||
mergeKnownOrganizations: (items) => items,
|
||||
toNonEmptyString: () => null,
|
||||
appendItem: () => {},
|
||||
getSession: () => ({ session_id: "asst-mcp-address", updated_at: "", items: [], investigation_state: null } as any),
|
||||
persistSession: () => {},
|
||||
cloneConversation: (items) => items,
|
||||
logEvent: () => {},
|
||||
messageIdFactory: () => "msg-mcp-address",
|
||||
finalizeAddressTurn
|
||||
});
|
||||
|
||||
expect(finalizeAddressTurn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
assistantReply: expect.stringContaining("Discovery payout answer"),
|
||||
replyType: "partial_coverage",
|
||||
debug: expect.objectContaining({
|
||||
mcp_discovery_response_applied: true
|
||||
})
|
||||
})
|
||||
);
|
||||
expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний");
|
||||
});
|
||||
|
||||
it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => {
|
||||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||||
sessionId: "asst-2",
|
||||
|
|
|
|||
|
|
@ -110,6 +110,35 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.limitation_lines.join("\n")).not.toContain("pilot_");
|
||||
});
|
||||
|
||||
it("turns supplier payout evidence into a bounded outgoing payment answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([
|
||||
{ Period: "2020-03-15T00:00:00", Amount: 4100, Counterparty: "SVK" },
|
||||
{ Period: "2020-04-20T00:00:00", Amount: "900,25", 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("исходящих платежей/списаний");
|
||||
expect(confirmedText).toContain("5 000,25 руб.");
|
||||
expect(draft.inference_lines.join("\n")).toContain("supplier-payout total");
|
||||
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
||||
});
|
||||
|
||||
it("does not leak primitive names or query text into user-facing lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
rows_matched: 2,
|
||||
rows_with_amount: 2,
|
||||
total_amount: 3750.5,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
first_movement_date: "2020-01-15",
|
||||
latest_movement_date: "2020-02-20",
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||
|
|
@ -119,6 +120,69 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(call?.limit).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("executes supplier payout query_movements and derives a guarded outgoing payment sum", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const deps = buildDeps([
|
||||
{ Period: "2020-03-15T00:00:00", Amount: 4100, Counterparty: "SVK" },
|
||||
{ Period: "2020-04-20T00:00:00", Amount: "900,25", Counterparty: "SVK" }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
|
||||
expect(result.derived_value_flow).toMatchObject({
|
||||
value_flow_direction: "outgoing_supplier_payout",
|
||||
counterparty: "SVK",
|
||||
period_scope: "2020",
|
||||
rows_matched: 2,
|
||||
rows_with_amount: 2,
|
||||
total_amount: 5000.25,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
first_movement_date: "2020-03-15",
|
||||
latest_movement_date: "2020-04-20"
|
||||
});
|
||||
expect(result.evidence.confirmed_facts[0]).toContain("supplier-payout rows");
|
||||
expect(result.evidence.inferred_facts[0]).toContain("supplier-payout total");
|
||||
expect(result.evidence.unknown_facts).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
||||
expect(result.reason_codes).toContain("pilot_supplier_payout_recipe_selected");
|
||||
|
||||
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
|
||||
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
|
||||
});
|
||||
|
||||
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const rows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, buildDeps(rows));
|
||||
|
||||
expect(result.derived_value_flow?.coverage_limited_by_probe_limit).toBe(true);
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,42 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("localizes supplier-payout evidence without leaking pilot mechanics", () => {
|
||||
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 supplier-payout rows were found for counterparty SVK",
|
||||
"По найденным строкам исходящих платежей/списаний в 1С по контрагенту SVK за период 2020 сумма исходящих платежей/списаний составляет 5 000 руб."
|
||||
],
|
||||
inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"],
|
||||
unknown_lines: [
|
||||
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached",
|
||||
"Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"
|
||||
],
|
||||
limitation_lines: ["pilot_value_flow_uses_query_movements_and_derives_aggregate"],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.candidate_status).toBe("ready_for_guarded_use");
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки исходящих платежей/списаний по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("5 000 руб.");
|
||||
expect(candidate.reply_text).toContain("Полное покрытие запрошенного периода не подтверждено");
|
||||
expect(candidate.reply_text).toContain("Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).not.toContain("pilot_");
|
||||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -109,6 +109,45 @@ describe("assistant MCP discovery response policy", () => {
|
|||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
|
||||
});
|
||||
|
||||
it("applies a guarded candidate for discovery-ready address lane answers", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "stale exact route answer",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
addressRuntimeMeta: {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
|
||||
expect(result.reply_text).toContain("Confirmed fact");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
});
|
||||
|
||||
it("keeps address lane answers when discovery was not requested for the current turn", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "supported exact route answer",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
addressRuntimeMeta: {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "not_applicable",
|
||||
should_run_discovery: false
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.reply_text).toBe("supported exact route answer");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
});
|
||||
|
||||
it("keeps the current reply when the candidate has no grounded text", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "route is not wired",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,28 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps payout wording as outgoing supplier-payout discovery", () => {
|
||||
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.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_payout_signal_detected");
|
||||
});
|
||||
|
||||
it("does not activate discovery for supported exact current-turn intent", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue