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

This commit is contained in:
2026-06-03 15:56:35 +03:00
parent f24cddc492
commit 5ed25bad81
22 changed files with 1439 additions and 28 deletions
@@ -224,7 +224,7 @@ function valueFlowZeroResultConfirmedLine(pilot) {
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
}
function valueFlowZeroResultUnknownLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) {
@@ -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);
}
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) {
if (input.explicitDateScope) {
return "explicit_period";
@@ -420,7 +428,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
const rawSubjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
const initialBusinessFactFamily = businessFactFamilyFor({
@@ -434,10 +442,26 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint;
const action = oneSidedBusinessOverviewValueFlowOverride ? oneSidedValueAction : rawAction;
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride ? "value_flow" : initialBusinessFactFamily;
const bidirectionalBusinessOverviewValueFlowOverride = 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 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 subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
@@ -556,6 +580,15 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (oneSidedBusinessOverviewValueFlowOverride) {
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")) {
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,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint,
metadata_scope_hint: effectiveMetadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(action),
@@ -107,7 +107,10 @@ function scoreExplicitEntityCandidate(raw, cleaned, index) {
return score;
}
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;
for (let index = 0; index < candidates.length; index += 1) {
const candidate = candidates[index];
@@ -582,10 +585,32 @@ function isEntityResolutionPilotEligible(planner) {
combined.includes("entity discovery") ||
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) {
const metadataScopeHint = normalizeMetadataScopeHint(planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint);
if (metadataScopeHint) {
return metadataScopeHint;
}
const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) {
return entityCandidate;
return normalizeMetadataScopeHint(entityCandidate) ?? entityCandidate;
}
if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref;
@@ -647,7 +672,9 @@ function valueFlowPilotProfile(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${action} ${unsupported}`;
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") ||
combined.includes("bidirectional") ||
combined.includes("netting") ||
@@ -614,13 +614,19 @@ function businessOverviewInventoryLine(overview) {
].filter((item) => Boolean(item));
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
}
function bidirectionalNetLabel(direction) {
function bidirectionalNetLabel(direction, scopeKind = "counterparty") {
if (direction === "net_outgoing") {
if (scopeKind === "organization") {
return "нетто в сторону исходящих платежей";
}
return "нетто в сторону контрагента";
}
if (direction === "balanced") {
return "нетто около нуля";
}
if (scopeKind === "organization") {
return "нетто в сторону входящих платежей";
}
return "нетто в нашу сторону";
}
function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
@@ -642,6 +648,11 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
}
const counterparty = toNonEmptyString(flow.counterparty);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const scopeKind = counterparty
? "counterparty"
: organizationScope
? "organization"
: "generic";
const subjectLead = counterparty
? `по контрагенту ${counterparty}`
: organizationScope
@@ -649,9 +660,9 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
: "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне";
const netLabel = bidirectionalNetLabel(flow.net_direction);
const netLabel = bidirectionalNetLabel(flow.net_direction, scopeKind);
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 = [];
if (incomingAmount) {
@@ -661,7 +672,12 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
basis.push("исходящие платежи");
}
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) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
@@ -247,6 +247,88 @@ function readTruthAnswerShape(input) {
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
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) {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
@@ -719,6 +801,12 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(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 &&
!semanticConflictWithDiscoveryTurnMeaning &&
!valueFlowActionConflictWithDiscoveryTurnMeaning;
@@ -752,6 +840,15 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
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) {
pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority");
}
@@ -827,6 +924,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!exactDocumentListAddressReply &&
!inventoryMarginRankingAddressReply &&
!exactInventoryChainProtectsCurrent &&
!currentClarificationProtectsCurrentReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
@@ -60,10 +60,25 @@ function isReferentialEntityPlaceholder(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) {
if (!value) {
return false;
}
if (isNegativeCounterpartyScopePlaceholder(value)) {
return true;
}
return new Set([
"эта организация",
"этой организации",
@@ -129,6 +144,12 @@ function isGarbageSemanticAnchorCandidate(value) {
"этими",
"итогу",
"итогам",
"одной",
"одному",
"одним",
"одна",
"один",
"одно",
"всему",
"всей",
"всем",
@@ -168,7 +189,8 @@ function isGarbageSemanticAnchorCandidate(value) {
]).has(compact)) {
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+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/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) {
return null;
}
if (isNegativeCounterpartyScopePlaceholder(text)) {
return null;
}
const normalized = text
.replace(/^[«"'“”]+|[»"'“”]+$/gu, "")
.replace(/\s+/g, " ")