Закрепить phase83 planner-brain replay ассистента

This commit is contained in:
dctouch 2026-06-03 15:56:35 +03:00
parent f24cddc492
commit 5ed25bad81
22 changed files with 1439 additions and 28 deletions

View File

@ -465,7 +465,7 @@
"title": "Broad business evaluation becomes grounded summary instead of stale lifecycle dump", "title": "Broad business evaluation becomes grounded summary instead of stale lifecycle dump",
"question": "Как ты оценишь деятельность компании?", "question": "Как ты оценишь деятельность компании?",
"required_answer_patterns_all": [ "required_answer_patterns_all": [
"(?i)коротко|оценк|частичн", "(?i)ограниченн|проверенн|не\\s+аудит|не\\s+аудитор",
"(?i)1с|подтвержд", "(?i)1с|подтвержд",
"(?i)денежн|долг|ндс|контрагент|операц" "(?i)денежн|долг|ндс|контрагент|операц"
], ],
@ -522,7 +522,7 @@
"factual_with_explanation" "factual_with_explanation"
], ],
"required_answer_patterns_all": [ "required_answer_patterns_all": [
"(?i)metadata|метадан", "(?i)схем|метадан|объект",
"(?i)справоч|catalog|directory", "(?i)справоч|catalog|directory",
"(?i)контрагент" "(?i)контрагент"
], ],

View File

@ -224,7 +224,7 @@ function valueFlowZeroResultConfirmedLine(pilot) {
const period = explicitDateScope(pilot); const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : ""; const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне"; const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} ${valueFlowDirectionLabelRu(pilot)} не найдено.`; return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
} }
function valueFlowZeroResultUnknownLine(pilot) { function valueFlowZeroResultUnknownLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) { if (!hasExecutedZeroValueFlowRows(pilot)) {

View File

@ -123,6 +123,14 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) {
} }
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text); return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text);
} }
function hasIncomingVsOutgoingComparisonHint(rawUtterance) {
if (!rawUtterance) {
return false;
}
const mentionsBothDirections = /(?:\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0432\u0445\u043e\u0434\p{L}*|incoming[\s\S]{0,100}outgoing|outgoing[\s\S]{0,100}incoming)/iu.test(rawUtterance);
const asksComparison = /(?:\u0447\u0442\u043e[\s\S]{0,40}\u0431\u043e\u043b\p{L}*|\u0431\u043e\u043b\p{L}*[\s\S]{0,40}\u0447\u0442\u043e|\u043f\u0440\u0435\u0432\u044b\p{L}*|\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u0441\u0440\u0430\u0432\u043d\p{L}*|which[\s\S]{0,40}(?:more|larger|higher|bigger)|what[\s\S]{0,40}(?:more|larger|higher|bigger)|(?:more|larger|higher|bigger)[\s\S]{0,40}(?:incoming|outgoing))/iu.test(rawUtterance);
return mentionsBothDirections && asksComparison;
}
function timeScopeNeedFor(input) { function timeScopeNeedFor(input) {
if (input.explicitDateScope) { if (input.explicitDateScope) {
return "explicit_period"; return "explicit_period";
@ -420,7 +428,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint); const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true; const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? []) const rawSubjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item)) .map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item)); .filter((item) => Boolean(item));
const initialBusinessFactFamily = businessFactFamilyFor({ const initialBusinessFactFamily = businessFactFamilyFor({
@ -434,10 +442,26 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) && Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint; !detailedBusinessOverviewMoneyBreakdownHint;
const action = oneSidedBusinessOverviewValueFlowOverride ? oneSidedValueAction : rawAction; const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride ? "value_flow" : initialBusinessFactFamily; const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" &&
Boolean(oneSidedValueAction) &&
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
const action = bidirectionalBusinessOverviewValueFlowOverride
? "net_value_flow"
: oneSidedBusinessOverviewValueFlowOverride || oneSidedValueFlowActionOverride
? oneSidedValueAction
: rawAction;
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride || bidirectionalBusinessOverviewValueFlowOverride
? "value_flow"
: initialBusinessFactFamily;
const aggregationNeed = aggregationNeedFor(aggregationAxis); const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action); const comparisonNeed = comparisonNeedFor(action);
const organizationScopedValueComparison = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
Boolean(explicitOrganizationScope) &&
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const subjectCandidates = organizationScopedValueComparison ? [] : rawSubjectCandidates;
const effectiveMetadataScopeHint = organizationScopedValueComparison ? null : metadataScopeHint;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance); const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" && const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" && comparisonNeed === "incoming_vs_outgoing" &&
@ -556,6 +580,15 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (oneSidedBusinessOverviewValueFlowOverride) { if (oneSidedBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_one_sided_money_total_routed_to_value_flow"); pushReason(reasonCodes, "data_need_graph_business_overview_one_sided_money_total_routed_to_value_flow");
} }
if (bidirectionalBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow");
}
if (organizationScopedValueComparison) {
pushReason(reasonCodes, "data_need_graph_organization_scoped_value_comparison_ignores_stale_subject");
}
if (oneSidedValueFlowActionOverride) {
pushReason(reasonCodes, "data_need_graph_value_flow_one_sided_money_total_action_normalized");
}
if (clarificationGaps.includes("organization")) { if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization"); pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
} }
@ -566,7 +599,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph", policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates, subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint, metadata_scope_hint: effectiveMetadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined, subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily, business_fact_family: businessFactFamily,
action_family: toNonEmptyString(action), action_family: toNonEmptyString(action),

View File

@ -107,7 +107,10 @@ function scoreExplicitEntityCandidate(raw, cleaned, index) {
return score; return score;
} }
function firstEntityCandidate(planner) { function firstEntityCandidate(planner) {
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? []; const graphCandidates = planner.data_need_graph?.subject_candidates;
const candidates = Array.isArray(graphCandidates)
? graphCandidates
: planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
let best = null; let best = null;
for (let index = 0; index < candidates.length; index += 1) { for (let index = 0; index < candidates.length; index += 1) {
const candidate = candidates[index]; const candidate = candidates[index];
@ -582,10 +585,32 @@ function isEntityResolutionPilotEligible(planner) {
combined.includes("entity discovery") || combined.includes("entity discovery") ||
combined.includes("counterparty search"))); combined.includes("counterparty search")));
} }
function normalizeMetadataScopeHint(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const normalized = text
.replace(/[«»"']/gu, "")
.replace(/\s+/g, " ")
.trim();
const comparable = normalized.toLowerCase().replace(/ё/g, "е");
if (/^контрагент(?:ам|ами|ах|ов|у|а|ом|е|ы)?$/iu.test(comparable)) {
return "контрагент";
}
if (/^(?:справочник\.)?контрагенты$/iu.test(comparable)) {
return "контрагент";
}
return normalized;
}
function metadataScopeForPlanner(planner) { function metadataScopeForPlanner(planner) {
const metadataScopeHint = normalizeMetadataScopeHint(planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint);
if (metadataScopeHint) {
return metadataScopeHint;
}
const entityCandidate = firstEntityCandidate(planner); const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) { if (entityCandidate) {
return entityCandidate; return normalizeMetadataScopeHint(entityCandidate) ?? entityCandidate;
} }
if (planner.selected_chain_id === "catalog_drilldown") { if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref; const surface = planner.metadata_surface_ref;
@ -647,7 +672,9 @@ function valueFlowPilotProfile(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase(); const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase(); const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${action} ${unsupported}`; const graphAction = String(planner.data_need_graph?.action_family ?? "").toLowerCase();
const graphComparison = String(planner.data_need_graph?.comparison_need ?? "").toLowerCase();
const combined = `${action} ${unsupported} ${graphAction} ${graphComparison} ${planner.selected_chain_id}`;
if (combined.includes("net_value_flow") || if (combined.includes("net_value_flow") ||
combined.includes("bidirectional") || combined.includes("bidirectional") ||
combined.includes("netting") || combined.includes("netting") ||

View File

@ -614,13 +614,19 @@ function businessOverviewInventoryLine(overview) {
].filter((item) => Boolean(item)); ].filter((item) => Boolean(item));
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null; return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
} }
function bidirectionalNetLabel(direction) { function bidirectionalNetLabel(direction, scopeKind = "counterparty") {
if (direction === "net_outgoing") { if (direction === "net_outgoing") {
if (scopeKind === "organization") {
return "нетто в сторону исходящих платежей";
}
return "нетто в сторону контрагента"; return "нетто в сторону контрагента";
} }
if (direction === "balanced") { if (direction === "balanced") {
return "нетто около нуля"; return "нетто около нуля";
} }
if (scopeKind === "organization") {
return "нетто в сторону входящих платежей";
}
return "нетто в нашу сторону"; return "нетто в нашу сторону";
} }
function buildCompactBidirectionalValueFlowReply(entryPoint, draft) { function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
@ -642,6 +648,11 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
} }
const counterparty = toNonEmptyString(flow.counterparty); const counterparty = toNonEmptyString(flow.counterparty);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const scopeKind = counterparty
? "counterparty"
: organizationScope
? "organization"
: "generic";
const subjectLead = counterparty const subjectLead = counterparty
? `по контрагенту ${counterparty}` ? `по контрагенту ${counterparty}`
: organizationScope : organizationScope
@ -649,9 +660,9 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
: "по выбранному контуру"; : "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope); const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне"; const periodText = period ? ` за период ${period}` : " в проверенном окне";
const netLabel = bidirectionalNetLabel(flow.net_direction); const netLabel = bidirectionalNetLabel(flow.net_direction, scopeKind);
const lines = [ const lines = [
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.` `${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
]; ];
const basis = []; const basis = [];
if (incomingAmount) { if (incomingAmount) {
@ -661,7 +672,12 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
basis.push("исходящие платежи"); basis.push("исходящие платежи");
} }
if (basis.length > 0) { if (basis.length > 0) {
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`); const basisScope = scopeKind === "counterparty"
? "по этому контрагенту"
: scopeKind === "organization"
? "по этой компании"
: "по выбранному контуру";
lines.push(`Основа: проверенные ${basis.join(" и ")} ${basisScope} в 1С.`);
} }
if (flow.coverage_limited_by_probe_limit === true) { if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода."); lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");

View File

@ -247,6 +247,88 @@ function readTruthAnswerShape(input) {
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1); const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape); return toRecordObject(truthAnswerPolicy?.answer_shape);
} }
function hasCurrentClarificationRequiredAddressReply(input) {
const answerShape = toNonEmptyString(input.addressRuntimeMeta?.answer_shape) ??
toNonEmptyString(input.addressRuntimeMeta?.truth_mode) ??
toNonEmptyString(readTruthAnswerShape(input)?.answer_shape);
if (answerShape === "clarification_required") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const policyAnswerShape = toRecordObject(truthAnswerPolicy?.answer_shape);
return (toNonEmptyString(truthGate?.truth_mode) === "clarification_required" ||
toNonEmptyString(policyAnswerShape?.answer_shape) === "clarification_required");
}
function hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "clarification_candidate") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family) ?? toNonEmptyString(graph?.action_family);
const asksForMovements = discoveryFamily === "movement_evidence" || askedDomain === "movements" || askedAction === "list_movements";
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_")));
return asksForMovements && staleInventoryItemClarification;
}
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.hot_runtime_wired || !candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const routeCandidate = toRecordObject(bridge?.route_candidate);
const selectedChainId = toNonEmptyString(routeCandidate?.selected_chain_id) ??
toNonEmptyString(bridge?.selected_chain_id) ??
toNonEmptyString(pilot?.selected_chain_id);
const pilotScope = toNonEmptyString(pilot?.pilot_scope);
const actionFamily = toNonEmptyString(graph?.action_family);
const comparisonNeed = toNonEmptyString(graph?.comparison_need);
const valueFlowComparison = Boolean(selectedChainId === "value_flow_comparison" ||
pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1" ||
(actionFamily === "net_value_flow" && comparisonNeed === "incoming_vs_outgoing"));
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const guardedBusinessAnswerAllowed = bridge?.user_facing_response_allowed === true &&
bridge?.business_fact_answer_allowed === true &&
(handoff?.can_use_guarded_response === true || toNonEmptyString(bridge?.bridge_status) === "answer_draft_ready");
return valueFlowComparison && mcpExecutionPerformed && guardedBusinessAnswerAllowed;
}
function hasEffectivelyFactualAddressReply(input) { function hasEffectivelyFactualAddressReply(input) {
if (toNonEmptyString(input.currentReplyType) === "factual") { if (toNonEmptyString(input.currentReplyType) === "factual") {
return true; return true;
@ -719,6 +801,12 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint); const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint); const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
const evidenceLaneConflictWithDiscoveryTurnMeaning = hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint); const evidenceLaneConflictWithDiscoveryTurnMeaning = hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint);
const currentClarificationRequiredAddressReply = hasCurrentClarificationRequiredAddressReply(input);
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply &&
!discoveryClarificationCandidatePriority &&
!groundedValueFlowCandidatePriority;
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply && const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning && !semanticConflictWithDiscoveryTurnMeaning &&
!valueFlowActionConflictWithDiscoveryTurnMeaning; !valueFlowActionConflictWithDiscoveryTurnMeaning;
@ -752,6 +840,15 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (evidenceLaneConflictWithDiscoveryTurnMeaning) { if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override"); pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
} }
if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
}
if (discoveryClarificationCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification");
}
if (groundedValueFlowCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification");
}
if (openScopeValueFlowDiscoveryPriority) { if (openScopeValueFlowDiscoveryPriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority"); pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority");
} }
@ -827,6 +924,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!exactDocumentListAddressReply && !exactDocumentListAddressReply &&
!inventoryMarginRankingAddressReply && !inventoryMarginRankingAddressReply &&
!exactInventoryChainProtectsCurrent && !exactInventoryChainProtectsCurrent &&
!currentClarificationProtectsCurrentReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&

View File

@ -60,10 +60,25 @@ function isReferentialEntityPlaceholder(value) {
"этом" "этом"
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value)); ]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
} }
function isNegativeCounterpartyScopePlaceholder(value) {
if (!value) {
return false;
}
const text = compactLower((0, addressTextRepair_1.repairAddressMojibakeText)(value));
if (!text) {
return false;
}
return (/(?:без\s+(?:привязк\p{L}*|конкретн\p{L}*)\s+к\s+контрагент\p{L}*|без\s+контрагент\p{L}*)/iu.test(text) ||
/(?:одн\p{L}*\s+организац\p{L}*\s+без\s+привязк\p{L}*)/iu.test(text) ||
/(?:organization\s+without\s+(?:counterparty|specific\s+counterparty))/iu.test(text));
}
function isReferentialOrganizationPlaceholder(value) { function isReferentialOrganizationPlaceholder(value) {
if (!value) { if (!value) {
return false; return false;
} }
if (isNegativeCounterpartyScopePlaceholder(value)) {
return true;
}
return new Set([ return new Set([
"эта организация", "эта организация",
"этой организации", "этой организации",
@ -129,6 +144,12 @@ function isGarbageSemanticAnchorCandidate(value) {
"этими", "этими",
"итогу", "итогу",
"итогам", "итогам",
"одной",
"одному",
"одним",
"одна",
"один",
"одно",
"всему", "всему",
"всей", "всей",
"всем", "всем",
@ -168,7 +189,8 @@ function isGarbageSemanticAnchorCandidate(value) {
]).has(compact)) { ]).has(compact)) {
return true; return true;
} }
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) || if (isNegativeCounterpartyScopePlaceholder(text) ||
/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) || /^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
/^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) || /^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) || /^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
@ -898,6 +920,9 @@ function normalizeLooseOrganizationAlias(value) {
if (!text) { if (!text) {
return null; return null;
} }
if (isNegativeCounterpartyScopePlaceholder(text)) {
return null;
}
const normalized = text const normalized = text
.replace(/^[«"'“”]+|[»"'“”]+$/gu, "") .replace(/^[«"'“”]+|[»"'“”]+$/gu, "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")

View File

@ -293,7 +293,7 @@ function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecu
const period = explicitDateScope(pilot); const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : ""; const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне"; const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} ${valueFlowDirectionLabelRu( return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(
pilot pilot
)} не найдено.`; )} не найдено.`;
} }

View File

@ -193,6 +193,21 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: {
); );
} }
function hasIncomingVsOutgoingComparisonHint(rawUtterance: string): boolean {
if (!rawUtterance) {
return false;
}
const mentionsBothDirections =
/(?:\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0432\u0445\u043e\u0434\p{L}*|incoming[\s\S]{0,100}outgoing|outgoing[\s\S]{0,100}incoming)/iu.test(
rawUtterance
);
const asksComparison =
/(?:\u0447\u0442\u043e[\s\S]{0,40}\u0431\u043e\u043b\p{L}*|\u0431\u043e\u043b\p{L}*[\s\S]{0,40}\u0447\u0442\u043e|\u043f\u0440\u0435\u0432\u044b\p{L}*|\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u0441\u0440\u0430\u0432\u043d\p{L}*|which[\s\S]{0,40}(?:more|larger|higher|bigger)|what[\s\S]{0,40}(?:more|larger|higher|bigger)|(?:more|larger|higher|bigger)[\s\S]{0,40}(?:incoming|outgoing))/iu.test(
rawUtterance
);
return mentionsBothDirections && asksComparison;
}
function timeScopeNeedFor(input: { function timeScopeNeedFor(input: {
family: string | null; family: string | null;
explicitDateScope: string | null; explicitDateScope: string | null;
@ -557,7 +572,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint); const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true; const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? []) const rawSubjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item)) .map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item)); .filter((item): item is string => Boolean(item));
const initialBusinessFactFamily = businessFactFamilyFor({ const initialBusinessFactFamily = businessFactFamilyFor({
@ -572,10 +587,31 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
initialBusinessFactFamily === "business_overview" && initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) && Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint; !detailedBusinessOverviewMoneyBreakdownHint;
const action = oneSidedBusinessOverviewValueFlowOverride ? oneSidedValueAction! : rawAction; const bidirectionalBusinessOverviewValueFlowOverride =
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride ? "value_flow" : initialBusinessFactFamily; initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const oneSidedValueFlowActionOverride =
initialBusinessFactFamily === "value_flow" &&
Boolean(oneSidedValueAction) &&
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
const action =
bidirectionalBusinessOverviewValueFlowOverride
? "net_value_flow"
: oneSidedBusinessOverviewValueFlowOverride || oneSidedValueFlowActionOverride
? oneSidedValueAction!
: rawAction;
const businessFactFamily =
oneSidedBusinessOverviewValueFlowOverride || bidirectionalBusinessOverviewValueFlowOverride
? "value_flow"
: initialBusinessFactFamily;
const aggregationNeed = aggregationNeedFor(aggregationAxis); const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action); const comparisonNeed = comparisonNeedFor(action);
const organizationScopedValueComparison =
businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
Boolean(explicitOrganizationScope) &&
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const subjectCandidates = organizationScopedValueComparison ? [] : rawSubjectCandidates;
const effectiveMetadataScopeHint = organizationScopedValueComparison ? null : metadataScopeHint;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance); const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime = const subjectScopedBidirectionalAllTime =
businessFactFamily === "value_flow" && businessFactFamily === "value_flow" &&
@ -705,6 +741,15 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
if (oneSidedBusinessOverviewValueFlowOverride) { if (oneSidedBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_one_sided_money_total_routed_to_value_flow"); pushReason(reasonCodes, "data_need_graph_business_overview_one_sided_money_total_routed_to_value_flow");
} }
if (bidirectionalBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow");
}
if (organizationScopedValueComparison) {
pushReason(reasonCodes, "data_need_graph_organization_scoped_value_comparison_ignores_stale_subject");
}
if (oneSidedValueFlowActionOverride) {
pushReason(reasonCodes, "data_need_graph_value_flow_one_sided_money_total_action_normalized");
}
if (clarificationGaps.includes("organization")) { if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization"); pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
} }
@ -716,7 +761,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION, schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph", policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates, subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint, metadata_scope_hint: effectiveMetadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined, subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily, business_fact_family: businessFactFamily,
action_family: toNonEmptyString(action), action_family: toNonEmptyString(action),

View File

@ -752,7 +752,10 @@ function scoreExplicitEntityCandidate(raw: string, cleaned: string, index: numbe
} }
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null { function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? []; const graphCandidates = planner.data_need_graph?.subject_candidates;
const candidates = Array.isArray(graphCandidates)
? graphCandidates
: planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
let best: { text: string; score: number } | null = null; let best: { text: string; score: number } | null = null;
for (let index = 0; index < candidates.length; index += 1) { for (let index = 0; index < candidates.length; index += 1) {
const candidate = candidates[index]; const candidate = candidates[index];
@ -1296,10 +1299,33 @@ function isEntityResolutionPilotEligible(planner: AssistantMcpDiscoveryPlannerCo
); );
} }
function normalizeMetadataScopeHint(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const normalized = text
.replace(/[«»"']/gu, "")
.replace(/\s+/g, " ")
.trim();
const comparable = normalized.toLowerCase().replace(/ё/g, "е");
if (/^контрагент(?:ам|ами|ах|ов|у|а|ом|е|ы)?$/iu.test(comparable)) {
return "контрагент";
}
if (/^(?:справочник\.)?контрагенты$/iu.test(comparable)) {
return "контрагент";
}
return normalized;
}
function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null { function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
const metadataScopeHint = normalizeMetadataScopeHint(planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint);
if (metadataScopeHint) {
return metadataScopeHint;
}
const entityCandidate = firstEntityCandidate(planner); const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) { if (entityCandidate) {
return entityCandidate; return normalizeMetadataScopeHint(entityCandidate) ?? entityCandidate;
} }
if (planner.selected_chain_id === "catalog_drilldown") { if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref; const surface = planner.metadata_surface_ref;
@ -1375,7 +1401,9 @@ function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): V
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase(); const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase(); const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${action} ${unsupported}`; const graphAction = String(planner.data_need_graph?.action_family ?? "").toLowerCase();
const graphComparison = String(planner.data_need_graph?.comparison_need ?? "").toLowerCase();
const combined = `${action} ${unsupported} ${graphAction} ${graphComparison} ${planner.selected_chain_id}`;
if ( if (
combined.includes("net_value_flow") || combined.includes("net_value_flow") ||
combined.includes("bidirectional") || combined.includes("bidirectional") ||

View File

@ -736,13 +736,19 @@ function businessOverviewInventoryLine(overview: Record<string, unknown>): strin
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null; return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
} }
function bidirectionalNetLabel(direction: unknown): string { function bidirectionalNetLabel(direction: unknown, scopeKind: "counterparty" | "organization" | "generic" = "counterparty"): string {
if (direction === "net_outgoing") { if (direction === "net_outgoing") {
if (scopeKind === "organization") {
return "нетто в сторону исходящих платежей";
}
return "нетто в сторону контрагента"; return "нетто в сторону контрагента";
} }
if (direction === "balanced") { if (direction === "balanced") {
return "нетто около нуля"; return "нетто около нуля";
} }
if (scopeKind === "organization") {
return "нетто в сторону входящих платежей";
}
return "нетто в нашу сторону"; return "нетто в нашу сторону";
} }
@ -770,6 +776,11 @@ function buildCompactBidirectionalValueFlowReply(
const counterparty = toNonEmptyString(flow.counterparty); const counterparty = toNonEmptyString(flow.counterparty);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const scopeKind: "counterparty" | "organization" | "generic" = counterparty
? "counterparty"
: organizationScope
? "organization"
: "generic";
const subjectLead = counterparty const subjectLead = counterparty
? `по контрагенту ${counterparty}` ? `по контрагенту ${counterparty}`
: organizationScope : organizationScope
@ -777,9 +788,9 @@ function buildCompactBidirectionalValueFlowReply(
: "по выбранному контуру"; : "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope); const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне"; const periodText = period ? ` за период ${period}` : " в проверенном окне";
const netLabel = bidirectionalNetLabel(flow.net_direction); const netLabel = bidirectionalNetLabel(flow.net_direction, scopeKind);
const lines = [ const lines = [
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.` `${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
]; ];
const basis: string[] = []; const basis: string[] = [];
@ -790,7 +801,13 @@ function buildCompactBidirectionalValueFlowReply(
basis.push("исходящие платежи"); basis.push("исходящие платежи");
} }
if (basis.length > 0) { if (basis.length > 0) {
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`); const basisScope =
scopeKind === "counterparty"
? "по этому контрагенту"
: scopeKind === "organization"
? "по этой компании"
: "по выбранному контуру";
lines.push(`Основа: проверенные ${basis.join(" и ")} ${basisScope} в 1С.`);
} }
if (flow.coverage_limited_by_probe_limit === true) { if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода."); lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");

View File

@ -367,6 +367,116 @@ function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInp
return toRecordObject(truthAnswerPolicy?.answer_shape); return toRecordObject(truthAnswerPolicy?.answer_shape);
} }
function hasCurrentClarificationRequiredAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
const answerShape =
toNonEmptyString(input.addressRuntimeMeta?.answer_shape) ??
toNonEmptyString(input.addressRuntimeMeta?.truth_mode) ??
toNonEmptyString(readTruthAnswerShape(input)?.answer_shape);
if (answerShape === "clarification_required") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const policyAnswerShape = toRecordObject(truthAnswerPolicy?.answer_shape);
return (
toNonEmptyString(truthGate?.truth_mode) === "clarification_required" ||
toNonEmptyString(policyAnswerShape?.answer_shape) === "clarification_required"
);
}
function hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "clarification_candidate") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction =
toNonEmptyString(turnMeaning?.asked_action_family) ?? toNonEmptyString(graph?.action_family);
const asksForMovements =
discoveryFamily === "movement_evidence" || askedDomain === "movements" || askedAction === "list_movements";
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId =
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(
missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_"))
);
return asksForMovements && staleInventoryItemClarification;
}
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.hot_runtime_wired || !candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const routeCandidate = toRecordObject(bridge?.route_candidate);
const selectedChainId =
toNonEmptyString(routeCandidate?.selected_chain_id) ??
toNonEmptyString(bridge?.selected_chain_id) ??
toNonEmptyString(pilot?.selected_chain_id);
const pilotScope = toNonEmptyString(pilot?.pilot_scope);
const actionFamily = toNonEmptyString(graph?.action_family);
const comparisonNeed = toNonEmptyString(graph?.comparison_need);
const valueFlowComparison = Boolean(
selectedChainId === "value_flow_comparison" ||
pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1" ||
(actionFamily === "net_value_flow" && comparisonNeed === "incoming_vs_outgoing")
);
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const guardedBusinessAnswerAllowed =
bridge?.user_facing_response_allowed === true &&
bridge?.business_fact_answer_allowed === true &&
(handoff?.can_use_guarded_response === true || toNonEmptyString(bridge?.bridge_status) === "answer_draft_ready");
return valueFlowComparison && mcpExecutionPerformed && guardedBusinessAnswerAllowed;
}
function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean { function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
if (toNonEmptyString(input.currentReplyType) === "factual") { if (toNonEmptyString(input.currentReplyType) === "factual") {
return true; return true;
@ -971,6 +1081,21 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
input, input,
entryPoint entryPoint
); );
const currentClarificationRequiredAddressReply = hasCurrentClarificationRequiredAddressReply(input);
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(
input,
entryPoint,
candidate
);
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
input,
entryPoint,
candidate
);
const currentClarificationProtectsCurrentReply =
currentClarificationRequiredAddressReply &&
!discoveryClarificationCandidatePriority &&
!groundedValueFlowCandidatePriority;
const exactBankOperationsProtectsCurrent = const exactBankOperationsProtectsCurrent =
exactBankOperationsAddressReply && exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning && !semanticConflictWithDiscoveryTurnMeaning &&
@ -1007,6 +1132,21 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (evidenceLaneConflictWithDiscoveryTurnMeaning) { if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override"); pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
} }
if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
}
if (discoveryClarificationCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification"
);
}
if (groundedValueFlowCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification"
);
}
if (openScopeValueFlowDiscoveryPriority) { if (openScopeValueFlowDiscoveryPriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority"); pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority");
} }
@ -1102,6 +1242,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!exactDocumentListAddressReply && !exactDocumentListAddressReply &&
!inventoryMarginRankingAddressReply && !inventoryMarginRankingAddressReply &&
!exactInventoryChainProtectsCurrent && !exactInventoryChainProtectsCurrent &&
!currentClarificationProtectsCurrentReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&

View File

@ -104,10 +104,28 @@ function isReferentialEntityPlaceholder(value: string): boolean {
]).has(normalizeRussianComparableText(value)); ]).has(normalizeRussianComparableText(value));
} }
function isNegativeCounterpartyScopePlaceholder(value: string | null): boolean {
if (!value) {
return false;
}
const text = compactLower(repairAddressMojibakeText(value));
if (!text) {
return false;
}
return (
/(?:без\s+(?:привязк\p{L}*|конкретн\p{L}*)\s+к\s+контрагент\p{L}*|без\s+контрагент\p{L}*)/iu.test(text) ||
/(?:одн\p{L}*\s+организац\p{L}*\s+без\s+привязк\p{L}*)/iu.test(text) ||
/(?:organization\s+without\s+(?:counterparty|specific\s+counterparty))/iu.test(text)
);
}
function isReferentialOrganizationPlaceholder(value: string | null): boolean { function isReferentialOrganizationPlaceholder(value: string | null): boolean {
if (!value) { if (!value) {
return false; return false;
} }
if (isNegativeCounterpartyScopePlaceholder(value)) {
return true;
}
return new Set([ return new Set([
"эта организация", "эта организация",
"этой организации", "этой организации",
@ -180,6 +198,12 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
"этими", "этими",
"итогу", "итогу",
"итогам", "итогам",
"одной",
"одному",
"одним",
"одна",
"один",
"одно",
"всему", "всему",
"всей", "всей",
"всем", "всем",
@ -221,6 +245,7 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
return true; return true;
} }
if ( if (
isNegativeCounterpartyScopePlaceholder(text) ||
/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) || /^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test( /^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
text text
@ -1294,6 +1319,9 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
if (!text) { if (!text) {
return null; return null;
} }
if (isNegativeCounterpartyScopePlaceholder(text)) {
return null;
}
const normalized = text const normalized = text
.replace(/^[«"']+|[»"']+$/gu, "") .replace(/^[«"']+|[»"']+$/gu, "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")

View File

@ -117,6 +117,28 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty."); expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
}); });
it("states zero rubles for checked payout searches with no matched rows", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.unknown_lines].join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(answerText).toContain("0 руб.");
expect(answerText).toContain("исходящих платежей/списаний не найдено");
expect(answerText).toContain("Группа СВК");
expect(answerText).toContain("2020");
});
it("turns generic document evidence into a bounded document answer draft", async () => { it("turns generic document evidence into a bounded document answer draft", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {

View File

@ -367,6 +367,42 @@ describe("assistant MCP discovery data need graph", () => {
); );
}); });
it("routes broad-overview incoming-vs-outgoing money comparison to value-flow comparison", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
rawUtterance:
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_entity_candidates: ["Группа СВК", "ООО Альтернатива Плюс больше"],
metadata_scope_hint: "Группа СВК",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.metadata_scope_hint).toBeNull();
expect(result.action_family).toBe("net_value_flow");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_incoming_movements",
"collect_outgoing_movements",
"probe_coverage"
]);
expect(result.reason_codes).toContain(
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
);
expect(result.reason_codes).toContain(
"data_need_graph_organization_scoped_value_comparison_ignores_stale_subject"
);
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
});
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => { it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({ const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence", semanticDataNeed: "counterparty value-flow evidence",

View File

@ -1170,6 +1170,41 @@ describe("assistant MCP discovery pilot executor", () => {
}); });
}); });
it("prefers normalized metadata scope hint over inflected entity candidate", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
explicit_entity_candidates: ["контрагентам", "контрагент"],
metadata_scope_hint: "контрагент",
unsupported_but_understood_family: "1c_metadata_surface"
}
});
const deps = buildMetadataDeps([
{
FullName: "Справочник.Контрагенты",
MetaType: "Справочник",
attributes: [{ Name: "Наименование" }, { Name: "ИНН" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "контрагент",
requested_meta_types: ["Справочник"],
matched_rows: 1,
available_entity_sets: ["Справочник"],
matched_objects: ["Справочник.Контрагенты"]
});
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
meta_type: ["Справочник"],
name_mask: "контрагент"
});
});
it("executes catalog drilldown through a narrowed metadata probe seeded from the confirmed surface object", async () => { it("executes catalog drilldown through a narrowed metadata probe seeded from the confirmed surface object", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
metadataSurface: { metadataSurface: {
@ -1750,6 +1785,75 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
}); });
it("executes organization-scoped value-flow comparison without stale counterparty subject", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: null,
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_family_value_flow",
"data_need_graph_comparison_incoming_vs_outgoing",
"data_need_graph_organization_scoped_value_comparison_ignores_stale_subject"
]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_entity_candidates: ["SVK", "OOO Alternative Plus more"],
metadata_scope_hint: "SVK",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const deps = buildSequentialDeps([
{
rows: [{ Period: "2020-01-15T00:00:00", Amount: 10000, Organization: "OOO Alternative Plus" }]
},
{
rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Organization: "OOO Alternative Plus" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
expect(result.derived_value_flow).toBeNull();
expect(result.derived_bidirectional_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
net_amount: 6000,
net_direction: "net_incoming",
incoming_customer_revenue: {
rows_matched: 1,
total_amount: 10000
},
outgoing_supplier_payout: {
rows_matched: 1,
total_amount: 4000
}
});
expect(result.reason_codes).toContain("pilot_bidirectional_value_flow_recipes_selected");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
const serializedCalls = deps.executeAddressMcpQuery.mock.calls
.map((call) => JSON.stringify(call[0]))
.join("\n");
expect(serializedCalls).not.toContain("SVK");
});
it("prefers clean counterparty candidates for bidirectional net value-flow probes", async () => { it("prefers clean counterparty candidates for bidirectional net value-flow probes", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {

View File

@ -1280,6 +1280,10 @@ describe("assistant MCP discovery response candidate", () => {
); );
expect(candidate.reply_text?.split("\n")[0]).toContain("по компании ООО Альтернатива Плюс за период 2020"); expect(candidate.reply_text?.split("\n")[0]).toContain("по компании ООО Альтернатива Плюс за период 2020");
expect(candidate.reply_text).toContain("Основа: проверенные входящие платежи и исходящие платежи по этой компании в 1С.");
expect(candidate.reply_text).toContain("нетто в сторону входящих платежей: 3 865 501,50 руб.");
expect(candidate.reply_text).not.toContain("Коротко:");
expect(candidate.reply_text).not.toContain("по этому контрагенту");
expect(candidate.reply_text).not.toContain("по контрагенту запрошенному контрагенту"); expect(candidate.reply_text).not.toContain("по контрагенту запрошенному контрагенту");
}); });
@ -1918,7 +1922,7 @@ describe("assistant MCP discovery response candidate", () => {
); );
const firstLine = candidate.reply_text?.split("\n")[0] ?? ""; const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("Коротко:"); expect(firstLine).not.toContain("Коротко:");
expect(firstLine).toContain("SVK"); expect(firstLine).toContain("SVK");
expect(firstLine).toContain("получили 12 500,50 руб."); expect(firstLine).toContain("получили 12 500,50 руб.");
expect(firstLine).toContain("заплатили 4 000 руб."); expect(firstLine).toContain("заплатили 4 000 руб.");

View File

@ -57,6 +57,7 @@ describe("assistant MCP discovery response policy", () => {
mcpDiscoveryRuntimeEntryPoint: entryPoint({ mcpDiscoveryRuntimeEntryPoint: entryPoint({
bridge: { bridge: {
bridge_status: "answer_draft_ready", bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true, user_facing_response_allowed: true,
business_fact_answer_allowed: true, business_fact_answer_allowed: true,
requires_user_clarification: false, requires_user_clarification: false,
@ -79,6 +80,56 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reply_text).not.toContain("Коротко:"); expect(result.reply_text).not.toContain("Коротко:");
}); });
it("keeps current organization clarification over a checked-sources value-flow candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Нужно уточнить организацию, чтобы не смешивать компании в одном ответе.\nСейчас в доступном контуре вижу такие организации:\n- ООО Альтернатива Плюс\n- ООО Ромашка",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_open_scope_total_needs_organization"]
}
},
bridge: {
bridge_status: "checked_sources_only",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "checked_sources_only",
headline: "В проверенном срезе 1С входящих денежных поступлений не найдено.",
confirmed_lines: ["В проверенном срезе 1С входящих денежных поступлений не найдено."],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.reply_text).toContain("Нужно уточнить организацию");
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
expect(result.reply_text).not.toContain("не найдено");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("keeps the current reply when the turn is not an unsupported boundary", () => { it("keeps the current reply when the turn is not an unsupported boundary", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({ const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "regular chat", currentReply: "regular chat",
@ -236,6 +287,7 @@ describe("assistant MCP discovery response policy", () => {
}, },
bridge: { bridge: {
bridge_status: "answer_draft_ready", bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true, user_facing_response_allowed: true,
business_fact_answer_allowed: true, business_fact_answer_allowed: true,
requires_user_clarification: false, requires_user_clarification: false,
@ -341,6 +393,68 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply"); expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
}); });
it("uses a movement clarification candidate over a stale inventory-item clarification", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Need item before I can continue.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
detected_intent: "inventory_purchase_documents_for_item",
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
capability_id: "inventory_inventory_purchase_documents_for_item",
truth_mode: "clarification_required",
answer_shape: "clarification_required",
missing_required_filters: ["item"],
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage",
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Group SVK"],
unsupported_but_understood_family: "movement_evidence"
},
data_need_graph: {
business_fact_family: "movement_evidence",
action_family: "list_movements",
clarification_gaps: ["period"]
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "I can continue by movements for Group SVK, but I need a period.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["The period is required before searching movements."],
limitation_lines: [],
next_step_line: "Specify the period and I will search movements for Group SVK."
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("movements for Group SVK");
expect(result.reply_text).not.toContain("Need item");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => { it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({ const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.", currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
@ -1509,4 +1623,95 @@ describe("assistant MCP discovery response policy", () => {
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate" "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
); );
}); });
it("lets a grounded bidirectional value-flow candidate override a stale address clarification reply", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Проверка данных за 2020 год по контрагенту «Группа СВК» не удалась — такой точный или аналогичный вариант имени не найден.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
detected_intent: "counterparty_population_and_roles",
selected_recipe: "address_counterparty_population_roles_v1",
assistant_mcp_discovery_entry_point_v1: entryPoint({
hot_runtime_wired: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
},
data_need_graph: {
business_fact_family: "value_flow",
action_family: "net_value_flow",
comparison_need: "incoming_vs_outgoing",
clarification_gaps: []
}
},
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
route_candidate: {
selected_chain_id: "value_flow_comparison"
},
execution_handoff: {
mcp_execution_performed: true,
can_use_guarded_response: true,
must_keep_internal_mechanics_hidden: true
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
mcp_execution_performed: true,
derived_bidirectional_value_flow: {
counterparty: "Группа СВК",
period_scope: "2020",
net_amount_human_ru: "12 093 465 руб.",
net_direction: "net_incoming",
coverage_limited_by_probe_limit: false,
incoming_customer_revenue: {
total_amount_human_ru: "12 093 465 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "0 руб."
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Двусторонний денежный поток подтвержден.",
confirmed_lines: [
"1C bidirectional value-flow rows were checked for counterparty Группа СВК: incoming=found, outgoing=found"
],
inference_lines: [
"Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"
],
unknown_lines: ["Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Группа СВК");
expect(result.reply_text).toContain("получили 12 093 465 руб.");
expect(result.reply_text).toContain("заплатили 0 руб.");
expect(result.reply_text).not.toContain("не найден");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
}); });

View File

@ -88,6 +88,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined(); expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
}); });
it("does not treat no-counterparty organization scope as a counterparty entity", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "counterparty_value_or_turnover",
explicit_entity_candidates: ["одной организации без привязки"],
explicit_organization_scope: "без привязки к контрагенту",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: {},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Ромашка"]
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.clarification_gaps).toContain("organization");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_raw_scope");
});
it("keeps payout wording as outgoing supplier-payout discovery", () => { it("keeps payout wording as outgoing supplier-payout discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько мы заплатили Группа СВК за 2020 год?", userMessage: "сколько мы заплатили Группа СВК за 2020 год?",

View File

@ -1,4 +1,94 @@
[ [
{
"generation_id": "gen-ag06031252-8c5319",
"created_at": "2026-06-03T12:52:51+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"count": 20,
"domain": "planner_autonomy_consolidation",
"questions": [
"привет, ты на связи?",
"найди в 1С контрагента СВК",
"сколько получили по нему за 2020 год",
"а теперь сколько заплатили?",
"а какое нетто?",
"а по документам?",
"а по движениям?",
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?",
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
"Как ты оценишь деятельность компании?",
"какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"какие справочники 1С есть по контрагентам?",
"давай дальше",
"а чем капибара отличается от утки?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603125251_gen-ag06031252-8c5319.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260603125251_gen-ag06031252-8c5319.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Mixed AGENT replay for Planner Autonomy Consolidation. The pack interleaves selected-counterparty catalog-alignment probes, open-organization money flow, ranking, broad-evaluation continuity, metadata drilldown, and off-domain living-chat safety.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase83_planner_brain_alignment_mix.json",
"scenario_id": "address_truth_harness_phase83_planner_brain_alignment_mix",
"semantic_tags": [
"alias_grounding",
"all_time_followup",
"broad_business_evaluation",
"broad_eval_bridge_preserved",
"catalog_drilldown",
"catalog_metadata_surface",
"company_activity_lifecycle",
"counterparty_catalog_scope",
"counterparty_net_cash_flow",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_context_seed",
"grounded_summary",
"human_answer",
"human_dialog",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"neutral_followup",
"off_domain_living_chat",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_comparison",
"value_flow_pivot",
"value_flow_ranking",
"year_switch"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6",
"saved_after_validated_replay": true
}
},
{ {
"generation_id": "gen-ag06031054-1bd955", "generation_id": "gen-ag06031054-1bd955",
"created_at": "2026-06-03T10:54:28+00:00", "created_at": "2026-06-03T10:54:28+00:00",

View File

@ -0,0 +1,377 @@
{
"saved_at": "2026-06-03T12:52:51+00:00",
"generation_id": "gen-ag06031252-8c5319",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"agent_run": true,
"questions": [
"привет, ты на связи?",
"найди в 1С контрагента СВК",
"сколько получили по нему за 2020 год",
"а теперь сколько заплатили?",
"а какое нетто?",
"а по документам?",
"а по движениям?",
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?",
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
"Как ты оценишь деятельность компании?",
"какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"какие справочники 1С есть по контрагентам?",
"давай дальше",
"а чем капибара отличается от утки?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Mixed AGENT replay for Planner Autonomy Consolidation. The pack interleaves selected-counterparty catalog-alignment probes, open-organization money flow, ranking, broad-evaluation continuity, metadata drilldown, and off-domain living-chat safety.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase83_planner_brain_alignment_mix.json",
"scenario_id": "address_truth_harness_phase83_planner_brain_alignment_mix",
"semantic_tags": [
"alias_grounding",
"all_time_followup",
"broad_business_evaluation",
"broad_eval_bridge_preserved",
"catalog_drilldown",
"catalog_metadata_surface",
"company_activity_lifecycle",
"counterparty_catalog_scope",
"counterparty_net_cash_flow",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_context_seed",
"grounded_summary",
"human_answer",
"human_dialog",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"neutral_followup",
"off_domain_living_chat",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_comparison",
"value_flow_pivot",
"value_flow_ranking",
"year_switch"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 20,
"steps_passed": 20,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "f24cddc492138745aa896f52d1a6ebee8bab08ab",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "привет, ты на связи?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "найди в 1С контрагента СВК",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "сколько получили по нему за 2020 год",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а теперь сколько заплатили?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а какое нетто?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а по документам?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "а по движениям?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "По ООО Альтернатива Плюс.",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "Понял, тогда за все время.",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "И кто больше всего принес денег этой организации в 2020 году?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "А в 2021 году?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-016",
"role": "user",
"text": "Как ты оценишь деятельность компании?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-017",
"role": "user",
"text": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-018",
"role": "user",
"text": "какие справочники 1С есть по контрагентам?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-019",
"role": "user",
"text": "давай дальше",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-020",
"role": "user",
"text": "а чем капибара отличается от утки?",
"created_at": "2026-06-03T12:52:51+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Mixed AGENT replay for Planner Autonomy Consolidation. The pack interleaves selected-counterparty catalog-alignment probes, open-organization money flow, ranking, broad-evaluation continuity, metadata drilldown, and off-domain living-chat safety.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase83_planner_brain_alignment_mix.json",
"scenario_id": "address_truth_harness_phase83_planner_brain_alignment_mix",
"semantic_tags": [
"alias_grounding",
"all_time_followup",
"broad_business_evaluation",
"broad_eval_bridge_preserved",
"catalog_drilldown",
"catalog_metadata_surface",
"company_activity_lifecycle",
"counterparty_catalog_scope",
"counterparty_net_cash_flow",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_context_seed",
"grounded_summary",
"human_answer",
"human_dialog",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"neutral_followup",
"off_domain_living_chat",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_comparison",
"value_flow_pivot",
"value_flow_ranking",
"year_switch"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 20,
"steps_passed": 20,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live6\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "f24cddc492138745aa896f52d1a6ebee8bab08ab",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,85 @@
{
"suite_id": "assistant_saved_session_gen-ag06031252-8c5319",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-03T12:52:51+00:00",
"generation_id": "gen-ag06031252-8c5319",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"domain": "planner_autonomy_consolidation",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет, ты на связи?"
},
{
"user_message": "найди в 1С контрагента СВК"
},
{
"user_message": "сколько получили по нему за 2020 год"
},
{
"user_message": "а теперь сколько заплатили?"
},
{
"user_message": "а какое нетто?"
},
{
"user_message": "а по документам?"
},
{
"user_message": "а по движениям?"
},
{
"user_message": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?"
},
{
"user_message": "По ООО Альтернатива Плюс."
},
{
"user_message": "Понял, тогда за все время."
},
{
"user_message": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?"
},
{
"user_message": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?"
},
{
"user_message": "И кто больше всего принес денег этой организации в 2020 году?"
},
{
"user_message": "А в 2021 году?"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
},
{
"user_message": "какие справочники 1С есть по контрагентам?"
},
{
"user_message": "давай дальше"
},
{
"user_message": "а чем капибара отличается от утки?"
}
]
}
]
}