ARCH: добавить value-flow pilot MCP discovery

This commit is contained in:
dctouch 2026-04-20 18:15:26 +03:00
parent e85d456576
commit 49fd08652c
21 changed files with 1100 additions and 66 deletions

View File

@ -1091,6 +1091,69 @@ Validation:
- `npm run build` passed; - `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_rerun3 --timeout-seconds 180` passed 4/4, final status `accepted`. - `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_rerun3 --timeout-seconds 180` passed 4/4, final status `accepted`.
## Progress Update - 2026-04-20 MCP Discovery Value-Flow Pilot And Deep Response Gate
The sixteenth implementation slice of Big Block 5 extends guarded MCP discovery from lifecycle evidence to the first bounded value-flow evidence path:
- `assistantMcpDiscoveryTurnInputAdapter.ts`
- `assistantMcpDiscoveryPilotExecutor.ts`
- `assistantMcpDiscoveryAnswerAdapter.ts`
- `assistantMcpDiscoveryResponseCandidate.ts`
- `assistantMcpDiscoveryResponsePolicy.ts`
- `assistantDeepTurnResponseRuntimeAdapter.ts`
- `address_truth_harness_phase19_mcp_discovery_response_gate.json`
The new path is deliberately narrow.
It does not turn the assistant into a free-form autonomous 1C agent yet. It adds a guarded pilot for counterparty value-flow questions when the exact route does not own the current turn:
- raw Russian/English value-flow signals such as `денежный поток`, `оборот`, `выручка`, `оплата`, `turnover`, and `revenue`;
- Cyrillic-safe signal detection instead of JavaScript `\w` assumptions;
- organization-shaped targets can be reinterpreted as counterparty candidates for this contour when no explicit counterparty was extracted;
- the planner may choose value-flow primitives, but the pilot executes only the guarded `query_movements` branch through the existing `customer_revenue_and_payments` recipe;
- the pilot derives `derived_value_flow` from confirmed movement rows: counterparty, period scope, matched rows, rows with amount, total amount, first movement date, latest movement date, and `inference_basis=sum_of_confirmed_1c_value_flow_rows`;
- the user-facing answer says what was found, what was calculated, and what is not proven outside the checked period;
- internal terms such as primitive names, query ids, runtime/planner/catalog mechanics, and `pilot_` reason codes are filtered from the final answer.
The deep response adapter now preserves the MCP discovery entry point in deep runtime meta and can replace a bad deep/partial answer with a guarded discovery candidate only when:
- the entry point is `bridge_executed`;
- discovery was attempted;
- `turn_input.should_run_discovery=true`;
- the current reply source is `deep_analysis`, `partial_coverage`, or `normalizer_v2_0_2`;
- the response candidate passes the same guarded text checks as the living-chat gate.
Live replay finding:
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun4` failed after adding the value-flow question because the visible answer still came from the deep partial path and drifted into unrelated store/asset/amortization wording;
- after preserving the discovery entry point for deep packaging and allowing the guarded deep candidate, `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun5` passed 5/5;
- after the punctuation cleanup, `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun6` also passed 5/5, final status `accepted`.
The value-flow replay answer for `какой денежный поток был у Группа СВК за 2020 год?` now states:
- found 1C money-movement rows for `Группа СВК`;
- period: `2020`;
- calculated sum: `47 628 853,03 руб.`;
- rows with amount: `44 из 44`;
- first movement date: `2020-01-09`;
- latest movement date: `2020-12-30`;
- full turnover outside the checked period is not confirmed by this search.
Validation:
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 37/37;
- `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_rerun6 --timeout-seconds 180` passed 5/5, final status `accepted`;
- step-level human answer review confirmed no `query_documents`, `query_movements`, `runtime_`, `planner_`, `catalog_`, `primitive`, or `pilot_` leak in lifecycle/value-flow user-facing answers.
Known remaining boundary:
- this slice covers incoming customer value-flow through `customer_revenue_and_payments`; supplier payouts, bidirectional money flow, arbitrary cross-register discovery, and multi-axis aggregation are still future MCP semantic discovery work.
Module progress:
- Big Block 5 MCP Semantic Data Agent: `97%`.
## Execution Rule ## Execution Rule
Do not implement this plan as: Do not implement this plan as:

View File

@ -82,7 +82,36 @@
] ]
}, },
{ {
"step_id": "step_04_off_domain_living_chat_not_hijacked", "step_id": "step_04_counterparty_value_flow_uses_guarded_discovery",
"title": "Unsupported-but-understood counterparty value-flow question uses guarded discovery answer",
"question": "какой денежный поток был у Группа СВК за 2020 год?",
"required_answer_patterns_all": [
"(?i)свк",
"(?i)1с|найден|строк|подтвержд",
"(?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_value_flow",
"counterparty_turnover",
"unsupported_current_turn_meaning_boundary"
]
},
{
"step_id": "step_05_off_domain_living_chat_not_hijacked",
"title": "Off-domain living chat remains human and is not hijacked by discovery carryover", "title": "Off-domain living chat remains human and is not hijacked by discovery carryover",
"question": "а чем капибара отличается от утки?", "question": "а чем капибара отличается от утки?",
"required_answer_patterns_any": [ "required_answer_patterns_any": [

View File

@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantDeepTurnResponseRuntime = runAssistantDeepTurnResponseRuntime; exports.runAssistantDeepTurnResponseRuntime = runAssistantDeepTurnResponseRuntime;
const assistantDeepTurnPackagingRuntimeAdapter_1 = require("./assistantDeepTurnPackagingRuntimeAdapter"); const assistantDeepTurnPackagingRuntimeAdapter_1 = require("./assistantDeepTurnPackagingRuntimeAdapter");
const assistantDeepTurnFinalizeRuntimeAdapter_1 = require("./assistantDeepTurnFinalizeRuntimeAdapter"); const assistantDeepTurnFinalizeRuntimeAdapter_1 = require("./assistantDeepTurnFinalizeRuntimeAdapter");
const assistantMcpDiscoveryResponsePolicy_1 = require("./assistantMcpDiscoveryResponsePolicy");
function toNullableString(value) { function toNullableString(value) {
if (typeof value !== "string") { if (typeof value !== "string") {
return null; return null;
@ -68,12 +69,15 @@ function normalizeAddressRuntimeMetaForDeep(value) {
toolGateDecision: toNullableString(source.toolGateDecision), toolGateDecision: toNullableString(source.toolGateDecision),
toolGateReason: toNullableString(source.toolGateReason), toolGateReason: toNullableString(source.toolGateReason),
predecomposeContract: source.predecomposeContract ?? null, predecomposeContract: source.predecomposeContract ?? null,
orchestrationContract: source.orchestrationContract ?? null semanticExtractionContract: source.semanticExtractionContract ?? null,
orchestrationContract: source.orchestrationContract ?? null,
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
}; };
} }
function runAssistantDeepTurnResponseRuntime(input) { function runAssistantDeepTurnResponseRuntime(input) {
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime; const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn; const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
const addressRuntimeMetaForDeep = normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep);
const packagingRuntime = runPackagingRuntimeSafe({ const packagingRuntime = runPackagingRuntimeSafe({
featureInvestigationStateV1: input.featureInvestigationStateV1, featureInvestigationStateV1: input.featureInvestigationStateV1,
sessionId: input.sessionId, sessionId: input.sessionId,
@ -108,7 +112,7 @@ function runAssistantDeepTurnResponseRuntime(input) {
featureContractsV11: input.featureContractsV11, featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11, featureAnswerPolicyV11: input.featureAnswerPolicyV11,
previousInvestigationState: input.previousInvestigationState ?? null, previousInvestigationState: input.previousInvestigationState ?? null,
addressRuntimeMetaForDeep: normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep), addressRuntimeMetaForDeep,
extractDroppedIntentSegments: input.extractDroppedIntentSegments, extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes, buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState, extractExecutionState: input.extractExecutionState,
@ -116,12 +120,35 @@ function runAssistantDeepTurnResponseRuntime(input) {
persistInvestigationState: input.persistInvestigationState, persistInvestigationState: input.persistInvestigationState,
messageIdFactory: input.messageIdFactory messageIdFactory: input.messageIdFactory
}); });
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
currentReply: packagingRuntime.safeAssistantReply,
currentReplySource: "deep_analysis",
addressRuntimeMeta: addressRuntimeMetaForDeep
});
const assistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: packagingRuntime.safeAssistantReply;
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
const debug = {
...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
};
const assistantItem = mcpDiscoveryResponsePolicy.applied
? {
...packagingRuntime.assistantItem,
text: assistantReply,
reply_type: replyType,
debug
}
: packagingRuntime.assistantItem;
const finalization = runFinalizeDeepTurnSafe({ const finalization = runFinalizeDeepTurnSafe({
sessionId: input.sessionId, sessionId: input.sessionId,
assistantReply: packagingRuntime.safeAssistantReply, assistantReply,
replyType: input.composition.reply_type, replyType,
assistantItem: packagingRuntime.assistantItem, assistantItem,
debug: packagingRuntime.debug, debug,
deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails, deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails,
appendItem: input.appendItem, appendItem: input.appendItem,
getSession: input.getSession, getSession: input.getSession,
@ -131,6 +158,6 @@ function runAssistantDeepTurnResponseRuntime(input) {
}); });
return { return {
response: finalization.response, response: finalization.response,
debug: packagingRuntime.debug debug
}; };
} }

View File

@ -36,6 +36,7 @@ function isInternalMechanicsLine(value) {
text.includes("probe_coverage") || text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") || text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") || text.includes("pilot_only_executes") ||
text.includes("pilot_") ||
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_")); text.includes("catalog_"));
@ -58,7 +59,10 @@ function modeFor(pilot) {
} }
return "checked_sources_only"; return "checked_sources_only";
} }
function headlineFor(mode) { function headlineFor(mode, pilot) {
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
if (mode === "confirmed_with_bounded_inference") { if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк."; return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
} }
@ -87,11 +91,17 @@ function nextStepFor(mode, pilot) {
} }
function buildMustNotClaim(pilot) { function buildMustNotClaim(pilot) {
const claims = [ const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.", "Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false." "Do not claim rows were checked when mcp_execution_performed=false."
]; ];
if (pilot.pilot_scope === "counterparty_lifecycle_query_documents_v1") {
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") {
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.");
}
if (pilot.evidence.confirmed_facts.length === 0) { if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty."); claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
} }
@ -108,6 +118,18 @@ function derivedActivityInferenceLine(pilot) {
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации." "Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
].join(" "); ].join(" ");
} }
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
return null;
}
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
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С, а не подтверждение полного оборота вне проверенного окна.`;
}
function buildAssistantMcpDiscoveryAnswerDraft(pilot) { function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const mode = modeFor(pilot); const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes]; const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
@ -122,12 +144,16 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const inferenceLines = derivedInferenceLine const inferenceLines = derivedInferenceLine
? [derivedInferenceLine] ? [derivedInferenceLine]
: pilot.evidence.inferred_facts; : pilot.evidence.inferred_facts;
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
return { return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter", policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode, answer_mode: mode,
headline: headlineFor(mode), headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts), confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines), inference_lines: uniqueStrings(inferenceLines),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts), unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]), limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),

View File

@ -81,6 +81,19 @@ function buildLifecycleFilters(planner) {
sort: "period_asc" sort: "period_asc"
}; };
} }
function buildValueFlowFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner) { function isLifecyclePilotEligible(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
@ -89,6 +102,19 @@ function isLifecyclePilotEligible(planner) {
return (planner.proposed_primitives.includes("query_documents") && return (planner.proposed_primitives.includes("query_documents") &&
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age"))); (combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age")));
} }
function isValueFlowPilotEligible(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported}`;
return (planner.proposed_primitives.includes("query_movements") &&
(combined.includes("turnover") ||
combined.includes("revenue") ||
combined.includes("payment") ||
combined.includes("payout") ||
combined.includes("value")));
}
function skippedProbeResult(step, limitation) { function skippedProbeResult(step, limitation) {
return { return {
primitive_id: step.primitive_id, primitive_id: step.primitive_id,
@ -107,7 +133,7 @@ function queryResultToProbeResult(primitiveId, result) {
limitation: result.error limitation: result.error
}; };
} }
function summarizeRows(result) { function summarizeLifecycleRows(result) {
if (result.error) { if (result.error) {
return null; return null;
} }
@ -116,6 +142,15 @@ function summarizeRows(result) {
} }
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`; return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
} }
function summarizeValueFlowRows(result) {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP value-flow rows fetched";
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function rowDateValue(row) { function rowDateValue(row) {
const candidates = [ const candidates = [
row["Период"], row["Период"],
@ -134,6 +169,37 @@ function rowDateValue(row) {
} }
return null; return null;
} }
function rowAmountValue(row) {
const candidates = [
row["Сумма"],
row["РЎСѓРјРјР°"],
row["СуммаДокумента"],
row["СуммаДокумента"],
row["Amount"],
row["amount"],
row["Total"],
row["total"]
];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate)) {
return candidate;
}
const text = toNonEmptyString(candidate);
if (!text) {
continue;
}
const normalized = text
.replace(/\s+/g, "")
.replace(/\u00a0/g, "")
.replace(",", ".")
.replace(/[^\d.-]/g, "");
const parsed = Number(normalized);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function monthDiff(firstIsoDate, latestIsoDate) { function monthDiff(firstIsoDate, latestIsoDate) {
const first = new Date(`${firstIsoDate}T00:00:00.000Z`); const first = new Date(`${firstIsoDate}T00:00:00.000Z`);
const latest = new Date(`${latestIsoDate}T00:00:00.000Z`); const latest = new Date(`${latestIsoDate}T00:00:00.000Z`);
@ -184,7 +250,48 @@ function deriveActivityPeriod(result) {
inference_basis: "first_and_latest_confirmed_1c_activity_rows" inference_basis: "first_and_latest_confirmed_1c_activity_rows"
}; };
} }
function buildConfirmedFacts(result, counterparty) { function formatAmountHumanRu(amount) {
const formatted = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 2,
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2
})
.format(amount)
.replace(/\u00a0/g, " ");
return `${formatted} руб.`;
}
function deriveValueFlow(result, counterparty, periodScope) {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
const amount = rowAmountValue(row);
if (amount !== null) {
totalAmount += amount;
rowsWithAmount += 1;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const dates = result.rows
.map((row) => rowDateValue(row))
.filter((value) => Boolean(value))
.sort();
return {
counterparty,
period_scope: periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
total_amount: totalAmount,
total_amount_human_ru: formatAmountHumanRu(totalAmount),
first_movement_date: dates[0] ?? null,
latest_movement_date: dates[dates.length - 1] ?? null,
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function buildLifecycleConfirmedFacts(result, counterparty) {
if (result.error || result.matched_rows <= 0) { if (result.error || result.matched_rows <= 0) {
return []; return [];
} }
@ -194,15 +301,38 @@ function buildConfirmedFacts(result, counterparty) {
: "1C activity rows were found for the requested counterparty scope" : "1C activity rows were found for the requested counterparty scope"
]; ];
} }
function buildInferredFacts(result) { function buildValueFlowConfirmedFacts(result, counterparty) {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildLifecycleInferredFacts(result) {
if (result.error || result.fetched_rows <= 0) { if (result.error || result.fetched_rows <= 0) {
return []; return [];
} }
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"]; return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
} }
function buildUnknownFacts() { function buildValueFlowInferredFacts(derived) {
if (!derived) {
return [];
}
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"]; 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 buildEmptyEvidence(planner, dryRun, probeResults, reason) { function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({ return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan, plan: planner.discovery_plan,
@ -235,6 +365,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot was blocked before execution"], query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
@ -255,11 +386,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"], query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
if (!isLifecyclePilotEligible(planner)) { const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) { for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id); skippedPrimitives.push(step.primitive_id);
@ -279,12 +413,94 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"], query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
let queryResult = null;
const counterparty = firstEntityCandidate(planner); const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
if (valueFlowPilotEligible) {
let queryResult = null;
const filters = buildValueFlowFilters(planner);
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("customer_revenue_and_payments", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Value-flow recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_movements") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_value_flow_uses_query_movements_and_derives_aggregate"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
unknownFacts: buildValueFlowUnknownFacts(dateScope),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
let queryResult = null;
const filters = buildLifecycleFilters(planner); const filters = buildLifecycleFilters(planner);
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", filters); const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) { if (!selection.selected_recipe) {
@ -303,6 +519,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Lifecycle recipe is not available"], query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
@ -329,7 +546,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
pushReason(reasonCodes, "pilot_query_documents_mcp_executed"); pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
} }
} }
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null; const sourceRowsSummary = queryResult ? summarizeLifecycleRows(queryResult) : null;
const derivedActivityPeriod = deriveActivityPeriod(queryResult); const derivedActivityPeriod = deriveActivityPeriod(queryResult);
if (derivedActivityPeriod) { if (derivedActivityPeriod) {
pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows");
@ -337,9 +554,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({ const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan, plan: planner.discovery_plan,
probeResults, probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [], confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [], inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(), unknownFacts: buildLifecycleUnknownFacts(),
sourceRowsSummary, sourceRowsSummary,
queryLimitations, queryLimitations,
recommendedNextProbe: "explain_evidence_basis" recommendedNextProbe: "explain_evidence_basis"
@ -357,6 +574,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence, evidence,
source_rows_summary: sourceRowsSummary, source_rows_summary: sourceRowsSummary,
derived_activity_period: derivedActivityPeriod, derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
query_limitations: queryLimitations, query_limitations: queryLimitations,
reason_codes: reasonCodes reason_codes: reasonCodes
}; };

View File

@ -51,6 +51,7 @@ function hasInternalMechanics(value) {
return (text.includes("query_documents") || return (text.includes("query_documents") ||
text.includes("query_movements") || text.includes("query_movements") ||
text.includes("primitive") || text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") || text.includes("catalog_") ||
@ -67,12 +68,28 @@ function localizeLine(value) {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) { if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности по запрошенному контрагентскому контуру."; return "В 1С найдены строки активности по запрошенному контрагентскому контуру.";
} }
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
if (valueFlowMatch) {
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
}
if (/^1C value-flow 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)) { if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С."; return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
} }
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена."; 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 "Полный оборот за все время без явно проверенного периода не подтвержден.";
}
return value; return value;
} }
function section(title, lines) { function section(title, lines) {

View File

@ -41,6 +41,7 @@ function hasInternalMechanics(value) {
return (text.includes("query_documents") || return (text.includes("query_documents") ||
text.includes("query_movements") || text.includes("query_movements") ||
text.includes("primitive") || text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") || text.includes("catalog_") ||
@ -72,6 +73,14 @@ function isDiscoveryReadyChatCandidate(input, entryPoint) {
turnInput?.should_run_discovery === true && turnInput?.should_run_discovery === true &&
(input.livingChatSource === "llm_chat" || input.currentReplySource === "llm_chat")); (input.livingChatSource === "llm_chat" || input.currentReplySource === "llm_chat"));
} }
function isDiscoveryReadyDeepCandidate(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 === "deep_analysis" || source === "partial_coverage" || source === "normalizer_v2_0_2"));
}
function applyAssistantMcpDiscoveryResponsePolicy(input) { function applyAssistantMcpDiscoveryResponsePolicy(input) {
const currentReply = String(input.currentReply ?? ""); const currentReply = String(input.currentReply ?? "");
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown"; const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
@ -80,6 +89,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const reasonCodes = [...candidate.reason_codes]; const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input); const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint); const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
if (!entryPoint) { if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point"); pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
} }
@ -89,6 +99,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (!discoveryReadyChatCandidate) { if (!discoveryReadyChatCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate"); pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
} }
if (!discoveryReadyDeepCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) { if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed"); pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
} }
@ -102,7 +115,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics"); pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
} }
const canApply = Boolean(entryPoint) && const canApply = Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate) && (unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) && Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -95,12 +95,15 @@ function collectDateScope(predecompose) {
function hasLifecycleSignal(text) { function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(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);
}
function semanticNeedFor(input) { function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`); const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) { if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence"; return "counterparty lifecycle evidence";
} }
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) { if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence"; return "counterparty value-flow evidence";
} }
if (/(?:document|documents|list_documents)/iu.test(combined)) { if (/(?:document|documents|list_documents)/iu.test(combined)) {
@ -115,6 +118,9 @@ function shouldRunDiscovery(input) {
if (input.lifecycleSignal || input.unsupported) { if (input.lifecycleSignal || input.unsupported) {
return true; return true;
} }
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) { if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true; return true;
} }
@ -127,6 +133,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const reasonCodes = []; const reasonCodes = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`); const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText); const lifecycleSignal = hasLifecycleSignal(rawText);
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
@ -135,18 +142,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
domain: rawDomain, domain: rawDomain,
action: rawAction, action: rawAction,
unsupported, unsupported,
lifecycleSignal lifecycleSignal,
valueFlowSignal
}); });
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty); pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization);
}
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
const turnMeaning = { const turnMeaning = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain, asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction, asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
explicit_entity_candidates: entityCandidates, explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization, explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract), explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null), unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value_or_turnover" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal) stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
}; };
const cleanTurnMeaning = {}; const cleanTurnMeaning = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) { if (toNonEmptyString(turnMeaning.asked_domain_family)) {
@ -173,6 +185,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const runDiscovery = shouldRunDiscovery({ const runDiscovery = shouldRunDiscovery({
unsupported, unsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal,
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate explicitIntentCandidate
}); });
@ -182,11 +195,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: predecomposeContract : predecomposeContract
? "predecompose_contract" ? "predecompose_contract"
: lifecycleSignal : lifecycleSignal
? "raw_text"
: valueFlowSignal
? "raw_text" ? "raw_text"
: "none"; : "none";
if (lifecycleSignal) { if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected"); pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
} }
if (valueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
}
if (unsupported) { if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
} }

View File

@ -20,6 +20,7 @@ import {
finalizeAssistantDeepTurn, finalizeAssistantDeepTurn,
type FinalizeAssistantDeepTurnInput type FinalizeAssistantDeepTurnInput
} from "./assistantDeepTurnFinalizeRuntimeAdapter"; } from "./assistantDeepTurnFinalizeRuntimeAdapter";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence"; import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence";
import type { CompanyAnchorSet } from "./companyAnchorResolver"; import type { CompanyAnchorSet } from "./companyAnchorResolver";
import type { import type {
@ -181,7 +182,9 @@ function normalizeAddressRuntimeMetaForDeep(
toolGateDecision: toNullableString(source.toolGateDecision), toolGateDecision: toNullableString(source.toolGateDecision),
toolGateReason: toNullableString(source.toolGateReason), toolGateReason: toNullableString(source.toolGateReason),
predecomposeContract: source.predecomposeContract ?? null, predecomposeContract: source.predecomposeContract ?? null,
orchestrationContract: source.orchestrationContract ?? null semanticExtractionContract: source.semanticExtractionContract ?? null,
orchestrationContract: source.orchestrationContract ?? null,
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
}; };
} }
@ -190,6 +193,7 @@ export function runAssistantDeepTurnResponseRuntime(
): RunAssistantDeepTurnResponseRuntimeOutput { ): RunAssistantDeepTurnResponseRuntimeOutput {
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? runAssistantDeepTurnPackagingRuntime; const runPackagingRuntimeSafe = input.runPackagingRuntime ?? runAssistantDeepTurnPackagingRuntime;
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? finalizeAssistantDeepTurn; const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? finalizeAssistantDeepTurn;
const addressRuntimeMetaForDeep = normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep);
const packagingRuntime = runPackagingRuntimeSafe({ const packagingRuntime = runPackagingRuntimeSafe({
featureInvestigationStateV1: input.featureInvestigationStateV1, featureInvestigationStateV1: input.featureInvestigationStateV1,
@ -225,7 +229,7 @@ export function runAssistantDeepTurnResponseRuntime(
featureContractsV11: input.featureContractsV11, featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11, featureAnswerPolicyV11: input.featureAnswerPolicyV11,
previousInvestigationState: input.previousInvestigationState ?? null, previousInvestigationState: input.previousInvestigationState ?? null,
addressRuntimeMetaForDeep: normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep), addressRuntimeMetaForDeep,
extractDroppedIntentSegments: input.extractDroppedIntentSegments, extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes, buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState, extractExecutionState: input.extractExecutionState,
@ -234,12 +238,36 @@ export function runAssistantDeepTurnResponseRuntime(
messageIdFactory: input.messageIdFactory messageIdFactory: input.messageIdFactory
}); });
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: packagingRuntime.safeAssistantReply,
currentReplySource: "deep_analysis",
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
});
const assistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: packagingRuntime.safeAssistantReply;
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
const debug = {
...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
} as AssistantDebugPayload;
const assistantItem = mcpDiscoveryResponsePolicy.applied
? {
...packagingRuntime.assistantItem,
text: assistantReply,
reply_type: replyType,
debug
}
: packagingRuntime.assistantItem;
const finalization = runFinalizeDeepTurnSafe({ const finalization = runFinalizeDeepTurnSafe({
sessionId: input.sessionId, sessionId: input.sessionId,
assistantReply: packagingRuntime.safeAssistantReply, assistantReply,
replyType: input.composition.reply_type, replyType,
assistantItem: packagingRuntime.assistantItem, assistantItem,
debug: packagingRuntime.debug, debug,
deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails, deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails,
appendItem: input.appendItem, appendItem: input.appendItem,
getSession: input.getSession, getSession: input.getSession,
@ -250,6 +278,6 @@ export function runAssistantDeepTurnResponseRuntime(
return { return {
response: finalization.response, response: finalization.response,
debug: packagingRuntime.debug debug
}; };
} }

View File

@ -62,6 +62,7 @@ function isInternalMechanicsLine(value: string): boolean {
text.includes("probe_coverage") || text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") || text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") || text.includes("pilot_only_executes") ||
text.includes("pilot_") ||
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") text.includes("catalog_")
@ -88,7 +89,10 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
return "checked_sources_only"; return "checked_sources_only";
} }
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode): string { function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
if (mode === "confirmed_with_bounded_inference") { if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк."; return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
} }
@ -119,11 +123,17 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] { function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const claims = [ const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.", "Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false." "Do not claim rows were checked when mcp_execution_performed=false."
]; ];
if (pilot.pilot_scope === "counterparty_lifecycle_query_documents_v1") {
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") {
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.");
}
if (pilot.evidence.confirmed_facts.length === 0) { if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty."); claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
} }
@ -142,6 +152,20 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
].join(" "); ].join(" ");
} }
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_value_flow;
if (!flow) {
return null;
}
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
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С, а не подтверждение полного оборота вне проверенного окна.`;
}
export function buildAssistantMcpDiscoveryAnswerDraft( export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract { ): AssistantMcpDiscoveryAnswerDraftContract {
@ -158,13 +182,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
const inferenceLines = derivedInferenceLine const inferenceLines = derivedInferenceLine
? [derivedInferenceLine] ? [derivedInferenceLine]
: pilot.evidence.inferred_facts; : pilot.evidence.inferred_facts;
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
return { return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter", policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode, answer_mode: mode,
headline: headlineFor(mode), headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts), confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines), inference_lines: uniqueStrings(inferenceLines),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts), unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]), limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),

View File

@ -40,11 +40,27 @@ export interface AssistantMcpDiscoveryDerivedActivityPeriod {
inference_basis: "first_and_latest_confirmed_1c_activity_rows"; inference_basis: "first_and_latest_confirmed_1c_activity_rows";
} }
export interface AssistantMcpDiscoveryDerivedValueFlow {
counterparty: string | null;
period_scope: string | null;
rows_matched: number;
rows_with_amount: number;
total_amount: number;
total_amount_human_ru: string;
first_movement_date: string | null;
latest_movement_date: string | null;
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
}
export type AssistantMcpDiscoveryPilotScope =
| "counterparty_lifecycle_query_documents_v1"
| "counterparty_value_flow_query_movements_v1";
export interface AssistantMcpDiscoveryPilotExecutionContract { export interface AssistantMcpDiscoveryPilotExecutionContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION; schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryPilotExecutor"; policy_owner: "assistantMcpDiscoveryPilotExecutor";
pilot_status: AssistantMcpDiscoveryPilotStatus; pilot_status: AssistantMcpDiscoveryPilotStatus;
pilot_scope: "counterparty_lifecycle_query_documents_v1"; pilot_scope: AssistantMcpDiscoveryPilotScope;
dry_run: AssistantMcpDiscoveryRuntimeDryRunContract; dry_run: AssistantMcpDiscoveryRuntimeDryRunContract;
mcp_execution_performed: boolean; mcp_execution_performed: boolean;
executed_primitives: string[]; executed_primitives: string[];
@ -53,6 +69,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
evidence: AssistantMcpDiscoveryEvidenceContract; evidence: AssistantMcpDiscoveryEvidenceContract;
source_rows_summary: string | null; source_rows_summary: string | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null; derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
query_limitations: string[]; query_limitations: string[];
reason_codes: string[]; reason_codes: string[];
} }
@ -141,6 +158,20 @@ function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): A
}; };
} }
function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean { function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
@ -152,6 +183,22 @@ function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
); );
} }
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported}`;
return (
planner.proposed_primitives.includes("query_movements") &&
(combined.includes("turnover") ||
combined.includes("revenue") ||
combined.includes("payment") ||
combined.includes("payout") ||
combined.includes("value"))
);
}
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult { function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
return { return {
primitive_id: step.primitive_id, primitive_id: step.primitive_id,
@ -175,7 +222,7 @@ function queryResultToProbeResult(
}; };
} }
function summarizeRows(result: AddressMcpQueryExecutorResult): string | null { function summarizeLifecycleRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) { if (result.error) {
return null; return null;
} }
@ -185,6 +232,16 @@ function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`; return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
} }
function summarizeValueFlowRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP value-flow rows fetched";
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function rowDateValue(row: Record<string, unknown>): string | null { function rowDateValue(row: Record<string, unknown>): string | null {
const candidates = [ const candidates = [
row["Период"], row["Период"],
@ -204,6 +261,38 @@ function rowDateValue(row: Record<string, unknown>): string | null {
return null; return null;
} }
function rowAmountValue(row: Record<string, unknown>): number | null {
const candidates = [
row["Сумма"],
row["РЎСѓРјРјР°"],
row["СуммаДокумента"],
row["СуммаДокумента"],
row["Amount"],
row["amount"],
row["Total"],
row["total"]
];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate)) {
return candidate;
}
const text = toNonEmptyString(candidate);
if (!text) {
continue;
}
const normalized = text
.replace(/\s+/g, "")
.replace(/\u00a0/g, "")
.replace(",", ".")
.replace(/[^\d.-]/g, "");
const parsed = Number(normalized);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function monthDiff(firstIsoDate: string, latestIsoDate: string): number { function monthDiff(firstIsoDate: string, latestIsoDate: string): number {
const first = new Date(`${firstIsoDate}T00:00:00.000Z`); const first = new Date(`${firstIsoDate}T00:00:00.000Z`);
const latest = new Date(`${latestIsoDate}T00:00:00.000Z`); const latest = new Date(`${latestIsoDate}T00:00:00.000Z`);
@ -259,7 +348,54 @@ function deriveActivityPeriod(
}; };
} }
function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] { function formatAmountHumanRu(amount: number): string {
const formatted = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 2,
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2
})
.format(amount)
.replace(/\u00a0/g, " ");
return `${formatted} руб.`;
}
function deriveValueFlow(
result: AddressMcpQueryExecutorResult | null,
counterparty: string | null,
periodScope: string | null
): AssistantMcpDiscoveryDerivedValueFlow | null {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
const amount = rowAmountValue(row);
if (amount !== null) {
totalAmount += amount;
rowsWithAmount += 1;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const dates = result.rows
.map((row) => rowDateValue(row))
.filter((value): value is string => Boolean(value))
.sort();
return {
counterparty,
period_scope: periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
total_amount: totalAmount,
total_amount_human_ru: formatAmountHumanRu(totalAmount),
first_movement_date: dates[0] ?? null,
latest_movement_date: dates[dates.length - 1] ?? null,
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) { if (result.error || result.matched_rows <= 0) {
return []; return [];
} }
@ -270,17 +406,43 @@ function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty
]; ];
} }
function buildInferredFacts(result: AddressMcpQueryExecutorResult): string[] { function buildValueFlowConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildLifecycleInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
if (result.error || result.fetched_rows <= 0) { if (result.error || result.fetched_rows <= 0) {
return []; return [];
} }
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"]; return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
} }
function buildUnknownFacts(): string[] { function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueFlow | null): string[] {
if (!derived) {
return [];
}
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
}
function buildLifecycleUnknownFacts(): string[] {
return ["Legal registration date is not proven by this MCP discovery pilot"]; return ["Legal registration date is not proven by this MCP discovery pilot"];
} }
function buildValueFlowUnknownFacts(periodScope: string | null): string[] {
return [
periodScope
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time turnover is not proven without an explicit checked period"
];
}
function buildEmptyEvidence( function buildEmptyEvidence(
planner: AssistantMcpDiscoveryPlannerContract, planner: AssistantMcpDiscoveryPlannerContract,
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract, dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
@ -323,6 +485,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot was blocked before execution"], query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
@ -344,12 +507,16 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"], query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
if (!isLifecyclePilotEligible(planner)) { const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) { for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id); skippedPrimitives.push(step.primitive_id);
@ -369,13 +536,99 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"], query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
let queryResult: AddressMcpQueryExecutorResult | null = null;
const counterparty = firstEntityCandidate(planner); const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
if (valueFlowPilotEligible) {
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildValueFlowFilters(planner);
const selection = selectAddressRecipe("customer_revenue_and_payments", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Value-flow recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_movements") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_value_flow_uses_query_movements_and_derives_aggregate"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
} else {
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
unknownFacts: buildValueFlowUnknownFacts(dateScope),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildLifecycleFilters(planner); const filters = buildLifecycleFilters(planner);
const selection = selectAddressRecipe("counterparty_activity_lifecycle", filters); const selection = selectAddressRecipe("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) { if (!selection.selected_recipe) {
@ -394,6 +647,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence, evidence,
source_rows_summary: null, source_rows_summary: null,
derived_activity_period: null, derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Lifecycle recipe is not available"], query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
@ -421,7 +675,7 @@ export async function executeAssistantMcpDiscoveryPilot(
} }
} }
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null; const sourceRowsSummary = queryResult ? summarizeLifecycleRows(queryResult) : null;
const derivedActivityPeriod = deriveActivityPeriod(queryResult); const derivedActivityPeriod = deriveActivityPeriod(queryResult);
if (derivedActivityPeriod) { if (derivedActivityPeriod) {
pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows");
@ -429,9 +683,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const evidence = resolveAssistantMcpDiscoveryEvidence({ const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan, plan: planner.discovery_plan,
probeResults, probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [], confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [], inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(), unknownFacts: buildLifecycleUnknownFacts(),
sourceRowsSummary, sourceRowsSummary,
queryLimitations, queryLimitations,
recommendedNextProbe: "explain_evidence_basis" recommendedNextProbe: "explain_evidence_basis"
@ -450,6 +704,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence, evidence,
source_rows_summary: sourceRowsSummary, source_rows_summary: sourceRowsSummary,
derived_activity_period: derivedActivityPeriod, derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
query_limitations: queryLimitations, query_limitations: queryLimitations,
reason_codes: reasonCodes reason_codes: reasonCodes
}; };

View File

@ -78,6 +78,7 @@ function hasInternalMechanics(value: string): boolean {
text.includes("query_documents") || text.includes("query_documents") ||
text.includes("query_movements") || text.includes("query_movements") ||
text.includes("primitive") || text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") || text.includes("catalog_") ||
@ -97,12 +98,28 @@ function localizeLine(value: string): string {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) { if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности по запрошенному контрагентскому контуру."; return "В 1С найдены строки активности по запрошенному контрагентскому контуру.";
} }
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
if (valueFlowMatch) {
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
}
if (/^1C value-flow 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)) { if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С."; return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
} }
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена."; 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 "Полный оборот за все время без явно проверенного периода не подтвержден.";
}
return value; return value;
} }

View File

@ -73,6 +73,7 @@ function hasInternalMechanics(value: string): boolean {
text.includes("query_documents") || text.includes("query_documents") ||
text.includes("query_movements") || text.includes("query_movements") ||
text.includes("primitive") || text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") || text.includes("catalog_") ||
@ -122,6 +123,20 @@ function isDiscoveryReadyChatCandidate(
); );
} }
function isDiscoveryReadyDeepCandidate(
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 === "deep_analysis" || source === "partial_coverage" || source === "normalizer_v2_0_2")
);
}
export function applyAssistantMcpDiscoveryResponsePolicy( export function applyAssistantMcpDiscoveryResponsePolicy(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput input: ApplyAssistantMcpDiscoveryResponsePolicyInput
): AssistantMcpDiscoveryResponsePolicyResult { ): AssistantMcpDiscoveryResponsePolicyResult {
@ -133,6 +148,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const reasonCodes = [...candidate.reason_codes]; const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input); const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint); const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
if (!entryPoint) { if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point"); pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@ -143,6 +159,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (!discoveryReadyChatCandidate) { if (!discoveryReadyChatCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate"); pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
} }
if (!discoveryReadyDeepCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) { if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed"); pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
} }
@ -158,7 +177,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const canApply = const canApply =
Boolean(entryPoint) && Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate) && (unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) && Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -138,17 +138,24 @@ 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(
text
);
}
function semanticNeedFor(input: { function semanticNeedFor(input: {
domain: string | null; domain: string | null;
action: string | null; action: string | null;
unsupported: string | null; unsupported: string | null;
lifecycleSignal: boolean; lifecycleSignal: boolean;
valueFlowSignal: boolean;
}): string | null { }): string | null {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`); const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) { if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence"; return "counterparty lifecycle evidence";
} }
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) { if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence"; return "counterparty value-flow evidence";
} }
if (/(?:document|documents|list_documents)/iu.test(combined)) { if (/(?:document|documents|list_documents)/iu.test(combined)) {
@ -163,12 +170,16 @@ function semanticNeedFor(input: {
function shouldRunDiscovery(input: { function shouldRunDiscovery(input: {
unsupported: string | null; unsupported: string | null;
lifecycleSignal: boolean; lifecycleSignal: boolean;
valueFlowSignal: boolean;
semanticDataNeed: string | null; semanticDataNeed: string | null;
explicitIntentCandidate: string | null; explicitIntentCandidate: string | null;
}): boolean { }): boolean {
if (input.lifecycleSignal || input.unsupported) { if (input.lifecycleSignal || input.unsupported) {
return true; return true;
} }
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) { if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true; return true;
} }
@ -184,6 +195,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const reasonCodes: string[] = []; const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`); const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText); const lifecycleSignal = hasLifecycleSignal(rawText);
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
@ -193,19 +205,25 @@ export function buildAssistantMcpDiscoveryTurnInput(
domain: rawDomain, domain: rawDomain,
action: rawAction, action: rawAction,
unsupported, unsupported,
lifecycleSignal lifecycleSignal,
valueFlowSignal
}); });
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty); pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization);
}
const explicitOrganizationScope =
valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = { const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain, asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction, asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
explicit_entity_candidates: entityCandidates, explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization, explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract), explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null), unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value_or_turnover" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal) stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
}; };
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {}; const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
@ -234,6 +252,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const runDiscovery = shouldRunDiscovery({ const runDiscovery = shouldRunDiscovery({
unsupported, unsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal,
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate explicitIntentCandidate
}); });
@ -244,11 +263,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "predecompose_contract" ? "predecompose_contract"
: lifecycleSignal : lifecycleSignal
? "raw_text" ? "raw_text"
: valueFlowSignal
? "raw_text"
: "none"; : "none";
if (lifecycleSignal) { if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected"); pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
} }
if (valueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
}
if (unsupported) { if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
} }

View File

@ -110,7 +110,10 @@ describe("assistant deep turn response runtime adapter", () => {
}) })
); );
expect(runtime.response).toEqual(responsePayload); expect(runtime.response).toEqual(responsePayload);
expect(runtime.debug).toEqual({ trace_id: "trace-1" }); expect(runtime.debug).toMatchObject({
trace_id: "trace-1",
mcp_discovery_response_applied: false
});
}); });
it("passes feature flags and followup flags into packaging stage", () => { it("passes feature flags and followup flags into packaging stage", () => {
@ -160,4 +163,86 @@ describe("assistant deep turn response runtime adapter", () => {
}) })
); );
}); });
it("can replace a bad deep partial answer with a guarded MCP discovery candidate", () => {
const runPackagingRuntime = vi.fn(() => ({
messageId: "msg-a1",
investigationStateSnapshot: null,
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: [],
safeAssistantReplyBase: "bad-base",
safeAssistantReply: "bad deep partial answer",
debug: { trace_id: "trace-1" },
assistantItem: {
message_id: "msg-a1",
session_id: "asst-1",
role: "assistant",
text: "bad deep partial answer",
reply_type: "partial_coverage",
created_at: "2026-04-10T00:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: {}
}));
const runFinalizeDeepTurn = vi.fn((input) => ({
response: {
ok: true,
session_id: "asst-1",
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.assistantItem,
conversation: [],
debug: input.debug
}
}));
const runtime = runAssistantDeepTurnResponseRuntime(
buildBaseInput({
composition: { reply_type: "partial_coverage" },
addressRuntimeMetaForDeep: {
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: { 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 answer",
confirmed_lines: ["Confirmed fact"],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: []
}
},
runPackagingRuntime,
runFinalizeDeepTurn
})
);
expect(runtime.response.assistant_reply).toContain("Discovery answer");
expect(runtime.debug?.mcp_discovery_response_applied).toBe(true);
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
expect.objectContaining({
assistantReply: expect.stringContaining("Discovery answer"),
replyType: "partial_coverage",
assistantItem: expect.objectContaining({
text: expect.stringContaining("Discovery answer")
})
})
);
});
}); });

View File

@ -80,6 +80,36 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
}); });
it("turns value-flow evidence into a bounded turnover answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", 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("3 750,50 руб.");
expect(confirmedText).toContain("2020-01-15");
expect(confirmedText).toContain("2020-02-20");
expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot");
expect(draft.must_not_claim).toContain("Do not claim full all-time turnover unless the checked period and coverage prove it.");
expect(draft.limitation_lines.join("\n")).not.toContain("pilot_");
});
it("does not leak primitive names or query text into user-facing lines", async () => { it("does not leak primitive names or query text into user-facing lines", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {

View File

@ -76,6 +76,49 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled(); expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
}); });
it("executes value-flow query_movements and derives a guarded turnover sum", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
const deps = buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["query_movements"]);
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "aggregate_by_axis", "probe_coverage"]);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.evidence.confirmed_facts[0]).toContain("value-flow rows");
expect(result.source_rows_summary).toBe("2 MCP value-flow rows fetched, 2 matched value-flow scope");
expect(result.derived_value_flow).toMatchObject({
counterparty: "SVK",
period_scope: "2020",
rows_matched: 2,
rows_with_amount: 2,
total_amount: 3750.5,
first_movement_date: "2020-01-15",
latest_movement_date: "2020-02-20",
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
});
expect(result.reason_codes).toContain("pilot_query_movements_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_value_flow_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
expect(String(call?.query ?? "")).toContain("ПоступлениеНаРасчетныйСчет");
expect(call?.limit).toBeGreaterThan(0);
});
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => { it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {

View File

@ -44,6 +44,38 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("primitive"); expect(candidate.reply_text).not.toContain("primitive");
}); });
it("localizes value-flow 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 value-flow rows were found for counterparty SVK",
"По найденным строкам денежных движений в 1С по контрагенту SVK за период 2020 сумма составляет 3 750 руб."
],
inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"],
unknown_lines: ["Full turnover 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("3 750 руб.");
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", () => { it("returns not applicable when discovery was skipped for an exact supported route", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate({ const candidate = buildAssistantMcpDiscoveryResponseCandidate({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",

View File

@ -89,6 +89,26 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_chat_candidate"); expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
}); });
it("applies a guarded candidate for discovery-ready deep partial answers", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "wrong deep partial answer",
currentReplySource: "deep_analysis",
addressRuntimeMeta: {
mcpDiscoveryRuntimeEntryPoint: 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_deep_candidate");
});
it("keeps the current reply when the candidate has no grounded text", () => { it("keeps the current reply when the candidate has no grounded text", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({ const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "route is not wired", currentReply: "route is not wired",

View File

@ -64,12 +64,17 @@ describe("assistant MCP discovery runtime entry point", () => {
entities: { counterparty: "Группа СВК" }, entities: { counterparty: "Группа СВК" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" } period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}, },
deps: buildDeps([]) deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: 2500, Counterparty: "SVK" }
])
}); });
expect(result.entry_status).toBe("bridge_executed"); expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]); expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.bridge?.bridge_status).toBe("unsupported"); expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow?.total_amount).toBe(3750);
expect(result.bridge?.hot_runtime_wired).toBe(false); expect(result.bridge?.hot_runtime_wired).toBe(false);
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn"); expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
}); });

View File

@ -49,6 +49,43 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_lifecycle_signal_detected"); expect(result.reason_codes).toContain("mcp_discovery_lifecycle_signal_detected");
}); });
it("bootstraps value-flow discovery from raw turnover wording when no exact route owns it", () => {
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.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_value_flow_signal_detected");
});
it("treats value-flow organization-shaped target as entity candidate when counterparty is absent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "какой денежный поток был у Группа СВК за 2020 год?",
predecomposeContract: {
entities: { organization: "Группа СВК" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["Группа СВК"]);
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
});
it("does not activate discovery for supported exact current-turn intent", () => { it("does not activate discovery for supported exact current-turn intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: { assistantTurnMeaning: {