ARCH: добавить value-flow pilot MCP discovery
This commit is contained in:
parent
e85d456576
commit
49fd08652c
|
|
@ -1091,6 +1091,69 @@ Validation:
|
|||
- `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`.
|
||||
|
||||
## 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
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"question": "а чем капибара отличается от утки?",
|
||||
"required_answer_patterns_any": [
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.runAssistantDeepTurnResponseRuntime = runAssistantDeepTurnResponseRuntime;
|
||||
const assistantDeepTurnPackagingRuntimeAdapter_1 = require("./assistantDeepTurnPackagingRuntimeAdapter");
|
||||
const assistantDeepTurnFinalizeRuntimeAdapter_1 = require("./assistantDeepTurnFinalizeRuntimeAdapter");
|
||||
const assistantMcpDiscoveryResponsePolicy_1 = require("./assistantMcpDiscoveryResponsePolicy");
|
||||
function toNullableString(value) {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
|
|
@ -68,12 +69,15 @@ function normalizeAddressRuntimeMetaForDeep(value) {
|
|||
toolGateDecision: toNullableString(source.toolGateDecision),
|
||||
toolGateReason: toNullableString(source.toolGateReason),
|
||||
predecomposeContract: source.predecomposeContract ?? null,
|
||||
orchestrationContract: source.orchestrationContract ?? null
|
||||
semanticExtractionContract: source.semanticExtractionContract ?? null,
|
||||
orchestrationContract: source.orchestrationContract ?? null,
|
||||
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
|
||||
};
|
||||
}
|
||||
function runAssistantDeepTurnResponseRuntime(input) {
|
||||
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
|
||||
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
|
||||
const addressRuntimeMetaForDeep = normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep);
|
||||
const packagingRuntime = runPackagingRuntimeSafe({
|
||||
featureInvestigationStateV1: input.featureInvestigationStateV1,
|
||||
sessionId: input.sessionId,
|
||||
|
|
@ -108,7 +112,7 @@ function runAssistantDeepTurnResponseRuntime(input) {
|
|||
featureContractsV11: input.featureContractsV11,
|
||||
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||
previousInvestigationState: input.previousInvestigationState ?? null,
|
||||
addressRuntimeMetaForDeep: normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep),
|
||||
addressRuntimeMetaForDeep,
|
||||
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
|
||||
buildDebugRoutes: input.buildDebugRoutes,
|
||||
extractExecutionState: input.extractExecutionState,
|
||||
|
|
@ -116,12 +120,35 @@ function runAssistantDeepTurnResponseRuntime(input) {
|
|||
persistInvestigationState: input.persistInvestigationState,
|
||||
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({
|
||||
sessionId: input.sessionId,
|
||||
assistantReply: packagingRuntime.safeAssistantReply,
|
||||
replyType: input.composition.reply_type,
|
||||
assistantItem: packagingRuntime.assistantItem,
|
||||
debug: packagingRuntime.debug,
|
||||
assistantReply,
|
||||
replyType,
|
||||
assistantItem,
|
||||
debug,
|
||||
deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails,
|
||||
appendItem: input.appendItem,
|
||||
getSession: input.getSession,
|
||||
|
|
@ -131,6 +158,6 @@ function runAssistantDeepTurnResponseRuntime(input) {
|
|||
});
|
||||
return {
|
||||
response: finalization.response,
|
||||
debug: packagingRuntime.debug
|
||||
debug
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ function isInternalMechanicsLine(value) {
|
|||
text.includes("probe_coverage") ||
|
||||
text.includes("explain_evidence_basis") ||
|
||||
text.includes("pilot_only_executes") ||
|
||||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_"));
|
||||
|
|
@ -58,7 +59,10 @@ function modeFor(pilot) {
|
|||
}
|
||||
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") {
|
||||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||||
}
|
||||
|
|
@ -87,11 +91,17 @@ function nextStepFor(mode, pilot) {
|
|||
}
|
||||
function buildMustNotClaim(pilot) {
|
||||
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 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) {
|
||||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
}
|
||||
|
|
@ -108,6 +118,18 @@ function derivedActivityInferenceLine(pilot) {
|
|||
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
|
||||
].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) {
|
||||
const mode = modeFor(pilot);
|
||||
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
|
||||
|
|
@ -122,12 +144,16 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
const inferenceLines = derivedInferenceLine
|
||||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
answer_mode: mode,
|
||||
headline: headlineFor(mode),
|
||||
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
|
||||
headline: headlineFor(mode, pilot),
|
||||
confirmed_lines: uniqueStrings(confirmedLines),
|
||||
inference_lines: uniqueStrings(inferenceLines),
|
||||
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
|
|
|
|||
|
|
@ -81,6 +81,19 @@ function buildLifecycleFilters(planner) {
|
|||
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) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
|
||||
|
|
@ -89,6 +102,19 @@ function isLifecyclePilotEligible(planner) {
|
|||
return (planner.proposed_primitives.includes("query_documents") &&
|
||||
(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) {
|
||||
return {
|
||||
primitive_id: step.primitive_id,
|
||||
|
|
@ -107,7 +133,7 @@ function queryResultToProbeResult(primitiveId, result) {
|
|||
limitation: result.error
|
||||
};
|
||||
}
|
||||
function summarizeRows(result) {
|
||||
function summarizeLifecycleRows(result) {
|
||||
if (result.error) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -116,6 +142,15 @@ function summarizeRows(result) {
|
|||
}
|
||||
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) {
|
||||
const candidates = [
|
||||
row["Период"],
|
||||
|
|
@ -134,6 +169,37 @@ function rowDateValue(row) {
|
|||
}
|
||||
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) {
|
||||
const first = new Date(`${firstIsoDate}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"
|
||||
};
|
||||
}
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -194,15 +301,38 @@ function buildConfirmedFacts(result, counterparty) {
|
|||
: "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) {
|
||||
return [];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
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) {
|
||||
return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
|
|
@ -235,6 +365,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot was blocked before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
|
|
@ -255,11 +386,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot needs more scope before execution"],
|
||||
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");
|
||||
for (const step of dryRun.execution_steps) {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
|
|
@ -279,12 +413,94 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
let queryResult = null;
|
||||
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 selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", filters);
|
||||
if (!selection.selected_recipe) {
|
||||
|
|
@ -303,6 +519,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["Lifecycle recipe is not available"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
|
|
@ -329,7 +546,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
|
||||
}
|
||||
}
|
||||
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
|
||||
const sourceRowsSummary = queryResult ? summarizeLifecycleRows(queryResult) : null;
|
||||
const derivedActivityPeriod = deriveActivityPeriod(queryResult);
|
||||
if (derivedActivityPeriod) {
|
||||
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)({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
|
||||
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
|
||||
unknownFacts: buildUnknownFacts(),
|
||||
confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
|
||||
inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
|
||||
unknownFacts: buildLifecycleUnknownFacts(),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
|
|
@ -357,6 +574,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
evidence,
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
derived_activity_period: derivedActivityPeriod,
|
||||
derived_value_flow: null,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ function hasInternalMechanics(value) {
|
|||
return (text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("primitive") ||
|
||||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_") ||
|
||||
|
|
@ -67,12 +68,28 @@ function localizeLine(value) {
|
|||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
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)) {
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
function section(title, lines) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function hasInternalMechanics(value) {
|
|||
return (text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("primitive") ||
|
||||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_") ||
|
||||
|
|
@ -72,6 +73,14 @@ function isDiscoveryReadyChatCandidate(input, entryPoint) {
|
|||
turnInput?.should_run_discovery === true &&
|
||||
(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) {
|
||||
const currentReply = String(input.currentReply ?? "");
|
||||
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||
|
|
@ -80,6 +89,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
const reasonCodes = [...candidate.reason_codes];
|
||||
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
|
||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
}
|
||||
|
|
@ -89,6 +99,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
if (!discoveryReadyChatCandidate) {
|
||||
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)) {
|
||||
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");
|
||||
}
|
||||
const canApply = Boolean(entryPoint) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
|
|
|
|||
|
|
@ -95,12 +95,15 @@ function collectDateScope(predecompose) {
|
|||
function hasLifecycleSignal(text) {
|
||||
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
|
||||
}
|
||||
function hasValueFlowSignal(text) {
|
||||
return /(?:оборот|выручк|оплат|плат[её]ж|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|value[-\s]?flow|turnover|revenue|payment|payout|cash\s+flow)/iu.test(text);
|
||||
}
|
||||
function semanticNeedFor(input) {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
|
||||
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";
|
||||
}
|
||||
if (/(?:document|documents|list_documents)/iu.test(combined)) {
|
||||
|
|
@ -115,6 +118,9 @@ function shouldRunDiscovery(input) {
|
|||
if (input.lifecycleSignal || input.unsupported) {
|
||||
return true;
|
||||
}
|
||||
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
|
||||
return true;
|
||||
}
|
||||
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -127,6 +133,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const reasonCodes = [];
|
||||
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
|
||||
const lifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
|
|
@ -135,18 +142,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
domain: rawDomain,
|
||||
action: rawAction,
|
||||
unsupported,
|
||||
lifecycleSignal
|
||||
lifecycleSignal,
|
||||
valueFlowSignal
|
||||
});
|
||||
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||
pushUnique(entityCandidates, predecomposeEntities.counterparty);
|
||||
if (valueFlowSignal && !predecomposeEntities.counterparty) {
|
||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||
}
|
||||
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
|
||||
const turnMeaning = {
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
explicit_organization_scope: predecomposeEntities.organization,
|
||||
explicit_organization_scope: explicitOrganizationScope,
|
||||
explicit_date_scope: collectDateScope(predecomposeContract),
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value_or_turnover" : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
|
||||
};
|
||||
const cleanTurnMeaning = {};
|
||||
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
|
||||
|
|
@ -173,6 +185,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
semanticDataNeed,
|
||||
explicitIntentCandidate
|
||||
});
|
||||
|
|
@ -183,10 +196,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
: valueFlowSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
if (valueFlowSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
finalizeAssistantDeepTurn,
|
||||
type FinalizeAssistantDeepTurnInput
|
||||
} from "./assistantDeepTurnFinalizeRuntimeAdapter";
|
||||
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
|
||||
import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence";
|
||||
import type { CompanyAnchorSet } from "./companyAnchorResolver";
|
||||
import type {
|
||||
|
|
@ -181,7 +182,9 @@ function normalizeAddressRuntimeMetaForDeep(
|
|||
toolGateDecision: toNullableString(source.toolGateDecision),
|
||||
toolGateReason: toNullableString(source.toolGateReason),
|
||||
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 {
|
||||
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? runAssistantDeepTurnPackagingRuntime;
|
||||
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? finalizeAssistantDeepTurn;
|
||||
const addressRuntimeMetaForDeep = normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep);
|
||||
|
||||
const packagingRuntime = runPackagingRuntimeSafe({
|
||||
featureInvestigationStateV1: input.featureInvestigationStateV1,
|
||||
|
|
@ -225,7 +229,7 @@ export function runAssistantDeepTurnResponseRuntime(
|
|||
featureContractsV11: input.featureContractsV11,
|
||||
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
|
||||
previousInvestigationState: input.previousInvestigationState ?? null,
|
||||
addressRuntimeMetaForDeep: normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep),
|
||||
addressRuntimeMetaForDeep,
|
||||
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
|
||||
buildDebugRoutes: input.buildDebugRoutes,
|
||||
extractExecutionState: input.extractExecutionState,
|
||||
|
|
@ -234,12 +238,36 @@ export function runAssistantDeepTurnResponseRuntime(
|
|||
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({
|
||||
sessionId: input.sessionId,
|
||||
assistantReply: packagingRuntime.safeAssistantReply,
|
||||
replyType: input.composition.reply_type,
|
||||
assistantItem: packagingRuntime.assistantItem,
|
||||
debug: packagingRuntime.debug,
|
||||
assistantReply,
|
||||
replyType,
|
||||
assistantItem,
|
||||
debug,
|
||||
deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails,
|
||||
appendItem: input.appendItem,
|
||||
getSession: input.getSession,
|
||||
|
|
@ -250,6 +278,6 @@ export function runAssistantDeepTurnResponseRuntime(
|
|||
|
||||
return {
|
||||
response: finalization.response,
|
||||
debug: packagingRuntime.debug
|
||||
debug
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ function isInternalMechanicsLine(value: string): boolean {
|
|||
text.includes("probe_coverage") ||
|
||||
text.includes("explain_evidence_basis") ||
|
||||
text.includes("pilot_only_executes") ||
|
||||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
text.includes("catalog_")
|
||||
|
|
@ -88,7 +89,10 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
|
|||
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") {
|
||||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||||
}
|
||||
|
|
@ -119,11 +123,17 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
|
||||
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
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 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) {
|
||||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||||
}
|
||||
|
|
@ -142,6 +152,20 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
|
|||
].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(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): AssistantMcpDiscoveryAnswerDraftContract {
|
||||
|
|
@ -158,13 +182,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
const inferenceLines = derivedInferenceLine
|
||||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
answer_mode: mode,
|
||||
headline: headlineFor(mode),
|
||||
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
|
||||
headline: headlineFor(mode, pilot),
|
||||
confirmed_lines: uniqueStrings(confirmedLines),
|
||||
inference_lines: uniqueStrings(inferenceLines),
|
||||
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
|
|
|
|||
|
|
@ -40,11 +40,27 @@ export interface AssistantMcpDiscoveryDerivedActivityPeriod {
|
|||
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 {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor";
|
||||
pilot_status: AssistantMcpDiscoveryPilotStatus;
|
||||
pilot_scope: "counterparty_lifecycle_query_documents_v1";
|
||||
pilot_scope: AssistantMcpDiscoveryPilotScope;
|
||||
dry_run: AssistantMcpDiscoveryRuntimeDryRunContract;
|
||||
mcp_execution_performed: boolean;
|
||||
executed_primitives: string[];
|
||||
|
|
@ -53,6 +69,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
|
|||
evidence: AssistantMcpDiscoveryEvidenceContract;
|
||||
source_rows_summary: string | null;
|
||||
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
|
||||
query_limitations: 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 {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
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 {
|
||||
return {
|
||||
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) {
|
||||
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`;
|
||||
}
|
||||
|
||||
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 {
|
||||
const candidates = [
|
||||
row["Период"],
|
||||
|
|
@ -204,6 +261,38 @@ function rowDateValue(row: Record<string, unknown>): string | 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 {
|
||||
const first = new Date(`${firstIsoDate}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) {
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
|
||||
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(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
|
||||
|
|
@ -323,6 +485,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot was blocked before execution"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
|
|
@ -344,12 +507,16 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot needs more scope before execution"],
|
||||
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");
|
||||
for (const step of dryRun.execution_steps) {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
|
|
@ -369,13 +536,99 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
let queryResult: AddressMcpQueryExecutorResult | null = null;
|
||||
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 selection = selectAddressRecipe("counterparty_activity_lifecycle", filters);
|
||||
if (!selection.selected_recipe) {
|
||||
|
|
@ -394,6 +647,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
evidence,
|
||||
source_rows_summary: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
query_limitations: ["Lifecycle recipe is not available"],
|
||||
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);
|
||||
if (derivedActivityPeriod) {
|
||||
pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows");
|
||||
|
|
@ -429,9 +683,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
|
||||
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
|
||||
unknownFacts: buildUnknownFacts(),
|
||||
confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
|
||||
inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
|
||||
unknownFacts: buildLifecycleUnknownFacts(),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
|
|
@ -450,6 +704,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
evidence,
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
derived_activity_period: derivedActivityPeriod,
|
||||
derived_value_flow: null,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ function hasInternalMechanics(value: string): boolean {
|
|||
text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("primitive") ||
|
||||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
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)) {
|
||||
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)) {
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ function hasInternalMechanics(value: string): boolean {
|
|||
text.includes("query_documents") ||
|
||||
text.includes("query_movements") ||
|
||||
text.includes("primitive") ||
|
||||
text.includes("pilot_") ||
|
||||
text.includes("runtime_") ||
|
||||
text.includes("planner_") ||
|
||||
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(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||
): AssistantMcpDiscoveryResponsePolicyResult {
|
||||
|
|
@ -133,6 +148,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
const reasonCodes = [...candidate.reason_codes];
|
||||
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
|
||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||
|
||||
if (!entryPoint) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||
|
|
@ -143,6 +159,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
if (!discoveryReadyChatCandidate) {
|
||||
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)) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||
}
|
||||
|
|
@ -158,7 +177,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
|
||||
const canApply =
|
||||
Boolean(entryPoint) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate) &&
|
||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
domain: string | null;
|
||||
action: string | null;
|
||||
unsupported: string | null;
|
||||
lifecycleSignal: boolean;
|
||||
valueFlowSignal: boolean;
|
||||
}): string | null {
|
||||
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
|
||||
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
|
||||
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";
|
||||
}
|
||||
if (/(?:document|documents|list_documents)/iu.test(combined)) {
|
||||
|
|
@ -163,12 +170,16 @@ function semanticNeedFor(input: {
|
|||
function shouldRunDiscovery(input: {
|
||||
unsupported: string | null;
|
||||
lifecycleSignal: boolean;
|
||||
valueFlowSignal: boolean;
|
||||
semanticDataNeed: string | null;
|
||||
explicitIntentCandidate: string | null;
|
||||
}): boolean {
|
||||
if (input.lifecycleSignal || input.unsupported) {
|
||||
return true;
|
||||
}
|
||||
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
|
||||
return true;
|
||||
}
|
||||
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -184,6 +195,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const reasonCodes: string[] = [];
|
||||
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
|
||||
const lifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
|
||||
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
|
|
@ -193,19 +205,25 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
domain: rawDomain,
|
||||
action: rawAction,
|
||||
unsupported,
|
||||
lifecycleSignal
|
||||
lifecycleSignal,
|
||||
valueFlowSignal
|
||||
});
|
||||
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||
pushUnique(entityCandidates, predecomposeEntities.counterparty);
|
||||
if (valueFlowSignal && !predecomposeEntities.counterparty) {
|
||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||
}
|
||||
const explicitOrganizationScope =
|
||||
valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
|
||||
|
||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
|
||||
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
|
||||
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
explicit_organization_scope: predecomposeEntities.organization,
|
||||
explicit_organization_scope: explicitOrganizationScope,
|
||||
explicit_date_scope: collectDateScope(predecomposeContract),
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
|
||||
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value_or_turnover" : null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
|
||||
};
|
||||
|
||||
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
|
||||
|
|
@ -234,6 +252,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
semanticDataNeed,
|
||||
explicitIntentCandidate
|
||||
});
|
||||
|
|
@ -244,11 +263,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: valueFlowSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
if (valueFlowSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
|
||||
}
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,10 @@ describe("assistant deep turn response runtime adapter", () => {
|
|||
})
|
||||
);
|
||||
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", () => {
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,49 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
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 () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,38 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "route is not wired",
|
||||
|
|
|
|||
|
|
@ -64,12 +64,17 @@ describe("assistant MCP discovery runtime entry point", () => {
|
|||
entities: { counterparty: "Группа СВК" },
|
||||
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.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.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,6 +49,43 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue