Усилить семантический контур агента и большой прогон

This commit is contained in:
2026-05-25 13:00:50 +03:00
parent 7cc65e808e
commit 8ce007724a
87 changed files with 4941 additions and 14691 deletions
@@ -1664,6 +1664,27 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
}
function countBroadBusinessOverviewBridgeAxes(text) {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewBridgeSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasBroadCue = /(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(normalized);
const hasCompanyScope = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(normalized);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewBridgeAxes(normalized) >= 3;
}
function hasNomenclatureMarginRankingSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
@@ -1776,6 +1797,9 @@ function resolveUnicodeAddressIntentBridge(text) {
if (!hasContractCue && (hasTopYearRevenueRankingCue || hasCustomerRevenueRankingBridgeSignal(normalized))) {
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_ranking_bridge_signal_detected");
}
if (hasBroadBusinessOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_multi_surface_deferred_to_discovery");
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}
+187 -12
View File
@@ -11,6 +11,7 @@ const MAX_RESULT_SETS = 40;
const MAX_NAVIGATION_EVENTS = 120;
const MAX_ENTITY_REFS_PER_RESULT_SET = 40;
const DISPLAY_ENTITY_TYPE_BY_INTENT = {
business_overview: "organization",
counterparty_activity_lifecycle: "counterparty",
customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty",
@@ -33,6 +34,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
inventory_aging_by_purchase_date: "item"
};
const RESULT_SET_TYPE_BY_INTENT = {
business_overview: "profile_summary",
counterparty_activity_lifecycle: "counterparty_list",
customer_revenue_and_payments: "counterparty_list",
supplier_payouts_profile: "counterparty_list",
@@ -69,6 +71,18 @@ function toObject(value) {
}
return value;
}
function cloneRecord(value) {
const record = toObject(value);
if (!record) {
return null;
}
try {
return JSON.parse(JSON.stringify(record));
}
catch {
return { ...record };
}
}
function toNonEmptyString(value) {
if (typeof value !== "string") {
return null;
@@ -76,6 +90,40 @@ function toNonEmptyString(value) {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function candidateLabel(value) {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toObject(value);
if (!record) {
return null;
}
return (toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text));
}
function readNavigationDiscoveryCounterparty(debug) {
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entry?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const dataNeedGraph = toObject(turnInput?.data_need_graph);
const candidates = [
...(Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: []),
...(Array.isArray(turnMeaning?.explicit_entity_candidates) ? turnMeaning.explicit_entity_candidates : []),
...(Array.isArray(dataNeedGraph?.subject_candidates) ? dataNeedGraph.subject_candidates : [])
];
for (const candidate of candidates) {
const label = candidateLabel(candidate);
if (label) {
return label;
}
}
return null;
}
function toAddressFocusObjectType(value) {
const normalized = toNonEmptyString(value);
if (!normalized) {
@@ -194,6 +242,21 @@ function cloneFocusObject(value) {
selected_at: value.selected_at
};
}
function cloneComparisonProofBundles(value) {
const record = toObject(value);
if (!record) {
return null;
}
const counterpartyValueFlowBundle = cloneRecord(record.counterparty_value_flow_bundle ?? record.previous_counterparty_value_flow_bundle);
const counterpartyDocumentBundle = cloneRecord(record.counterparty_document_bundle ?? record.previous_counterparty_document_bundle);
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function cloneResultSet(input) {
return {
result_set_id: input.result_set_id,
@@ -252,8 +315,63 @@ function buildFocusObject(objectType, label, resultSetId, createdAt) {
selected_at: createdAt
};
}
function cloneComparisonScope(value) {
if (!value || typeof value !== "object") {
return null;
}
return {
organization: cloneFocusObject(value.organization),
counterparty: cloneFocusObject(value.counterparty),
proof_bundles: cloneComparisonProofBundles(value.proof_bundles)
};
}
function sameBusinessLabel(left, right) {
const normalizedLeft = toNonEmptyString(left)?.toLocaleLowerCase("ru-RU");
const normalizedRight = toNonEmptyString(right)?.toLocaleLowerCase("ru-RU");
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
}
function readBusinessOverviewComparisonProofBundles(debug) {
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty) ??
toNonEmptyString(counterpartyDocumentBundle?.counterparty) ??
readNavigationDiscoveryCounterparty(debug);
if (!counterparty) {
return null;
}
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
if (organization) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
}
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
if (canonicalType === "item") {
@@ -282,9 +400,13 @@ function capNavigationEvents(events) {
return events.slice(events.length - MAX_NAVIGATION_EVENTS);
}
function isAddressAssistantItem(item) {
return (item.role === "assistant" &&
Boolean(item.debug) &&
toNonEmptyString(item.debug?.detected_mode) === "address_query");
if (item.role !== "assistant" || !item.debug) {
return false;
}
if (toNonEmptyString(item.debug.detected_mode) === "address_query") {
return true;
}
return item.debug.mcp_discovery_response_applied === true && Boolean(toNonEmptyString(item.debug.mcp_discovery_selected_chain_id));
}
function createEmptyAddressNavigationState(sessionId, nowIso = new Date().toISOString()) {
return {
@@ -294,6 +416,7 @@ function createEmptyAddressNavigationState(sessionId, nowIso = new Date().toISOS
session_context: {
active_result_set_id: null,
active_focus_object: null,
comparison_scope: null,
last_confirmed_route: null,
date_scope: {
as_of_date: null,
@@ -317,6 +440,7 @@ function cloneAddressNavigationState(value) {
session_context: {
active_result_set_id: value.session_context.active_result_set_id,
active_focus_object: cloneFocusObject(value.session_context.active_focus_object),
comparison_scope: cloneComparisonScope(value.session_context.comparison_scope),
last_confirmed_route: value.session_context.last_confirmed_route,
date_scope: {
as_of_date: value.session_context.date_scope.as_of_date,
@@ -347,6 +471,7 @@ function normalizeAddressNavigationState(value, sessionId) {
session_context: {
active_result_set_id: toNonEmptyString(context.active_result_set_id),
active_focus_object: cloneFocusObject(context.active_focus_object),
comparison_scope: cloneComparisonScope(context.comparison_scope),
last_confirmed_route: toNonEmptyString(context.last_confirmed_route),
date_scope: {
as_of_date: toNonEmptyString(dateScope.as_of_date),
@@ -400,21 +525,40 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
return state;
}
const debug = item.debug;
const intent = toAddressIntent(debug.detected_intent);
if (intent === "unknown") {
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
const discoveryIntent = selectedDiscoveryChain === "business_overview"
? "business_overview"
: selectedDiscoveryChain === "value_flow_comparison"
? "customer_revenue_and_payments"
: "unknown";
const detectedIntent = toNonEmptyString(debug.detected_intent);
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
if (intent === "unknown" && !trackableDiscoveryTurn) {
return state;
}
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe);
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
const derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
: null;
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
...filters,
organization: derivedOrganizationScope
organization: derivedOrganizationScope,
...(derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? { counterparty: derivedCounterpartyScope }
: {})
}
: filters;
: derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? {
...filters,
counterparty: derivedCounterpartyScope
}
: filters;
const sourceRefs = routeId ? [routeId] : [];
const entityRefs = extractEntityRefsFromAssistantReply(item.text, intent);
const resultSet = {
@@ -430,6 +574,34 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
};
const previousResultSetId = state.session_context.active_result_set_id;
const focusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
const comparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readNavigationDiscoveryCounterparty(debug)
: null;
const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
: null;
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug)
: null;
const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles = comparisonCounterparty &&
sameBusinessLabel(inheritedComparisonScope?.counterparty?.label, comparisonCounterparty)
? cloneComparisonProofBundles(inheritedComparisonScope?.proof_bundles)
: null;
const comparisonProofBundles = currentComparisonProofBundles ?? inheritedComparisonProofBundles;
const comparisonOrganizationObject = comparisonOrganization
? buildFocusObject("organization", comparisonOrganization, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.organization ?? null);
const comparisonCounterpartyObject = comparisonCounterparty
? buildFocusObject("counterparty", comparisonCounterparty, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.counterparty ?? null);
const comparisonScope = comparisonOrganizationObject || comparisonCounterpartyObject || comparisonProofBundles
? {
organization: comparisonOrganizationObject,
counterparty: comparisonCounterpartyObject,
proof_bundles: comparisonProofBundles
}
: null;
const action = resolveNavigationAction(debug, Boolean(focusObject));
const navigationEvent = {
event_id: `nav-${(0, nanoid_1.nanoid)(10)}`,
@@ -440,10 +612,11 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
turn_index: turnIndex,
created_at: createdAt
};
const discoveryTemporalScope = (0, assistantContinuityPolicy_1.readAddressDebugTemporalScope)(debug, toNonEmptyString);
const normalizedDateScope = {
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date) ?? discoveryTemporalScope.asOfDate,
period_from: toNonEmptyString(filtersWithDerivedScope.period_from) ?? discoveryTemporalScope.periodFrom,
period_to: toNonEmptyString(filtersWithDerivedScope.period_to) ?? discoveryTemporalScope.periodTo
};
const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
const nextResultSets = capResultSets([...state.result_sets.filter((itemSet) => itemSet.result_set_id !== resultSetId), resultSet].sort((left, right) => left.created_from_turn - right.created_from_turn));
@@ -452,6 +625,7 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
? {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? null,
comparison_scope: comparisonScope,
last_confirmed_route: routeId ?? null,
date_scope: {
as_of_date: normalizedDateScope.as_of_date,
@@ -463,6 +637,7 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
: {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? state.session_context.active_focus_object,
comparison_scope: comparisonScope ?? state.session_context.comparison_scope,
last_confirmed_route: routeId ?? state.session_context.last_confirmed_route,
date_scope: {
as_of_date: normalizedDateScope.as_of_date ?? state.session_context.date_scope.as_of_date,
@@ -542,6 +542,33 @@ function needsVatCalendarDetails(userMessage) {
}
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
}
function needsVatPurchaseDateAnchorDisclosure(userMessage) {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
}
function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
return null;
}
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const asOfTs = toUtcDayTimestamp(asOfDate);
const fromTs = toUtcDayTimestamp(periodFrom);
const toTs = toUtcDayTimestamp(periodTo);
if (asOfDate &&
asOfTs !== null &&
fromTs !== null &&
toTs !== null &&
asOfTs >= fromTs &&
asOfTs <= toTs) {
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
}
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
}
function detectRankingLimit(userMessage, fallback = 20) {
const text = normalizeQuestionText(userMessage);
if (!text) {
@@ -3033,6 +3060,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
"Расчет сделан по книгам продаж и покупок.",
@@ -3040,6 +3068,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
"Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
@@ -31,6 +31,121 @@ function toNullableBoolean(value) {
function normalizeAddressReplyType(value) {
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
}
function sameBusinessLabel(left, right) {
const normalizedLeft = toNullableString(left)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
const normalizedRight = toNullableString(right)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
return Boolean(normalizedLeft &&
normalizedRight &&
(normalizedLeft === normalizedRight ||
normalizedLeft.includes(normalizedRight) ||
normalizedRight.includes(normalizedLeft)));
}
function firstString(values) {
for (const value of values) {
const text = toNullableString(value);
if (text) {
return text;
}
}
return null;
}
function legalOrganizationLabelFromClarification(value) {
const text = toNullableString(value);
if (!text) {
return null;
}
const compact = text.replace(/\s+/gu, " ").replace(/[.!?]+$/u, "").trim();
if (compact.length > 120 || !/^(?:ООО|ПАО|АО|ИП)\s+\S/iu.test(compact)) {
return null;
}
return compact;
}
function cleanComparisonScopeCompanyLine(line, organization) {
let clean = String(line ?? "")
.replace(/\bcompany-level\b/giu, "общий по компании")
.replace(/\breusable bundle\b/giu, "сохраненный подтвержденный срез");
if (organization) {
clean = clean.replace(/по компании\s+Альтернатива Плюс/iu, `по компании ${organization}`);
}
return clean.trim();
}
function buildComparisonScopeProofReply(input) {
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const isBusinessOverview = toNullableString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview) {
return null;
}
const separateCandidates = Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: [];
const separateSubject = firstString([...separateCandidates, turnMeaning?.metadata_scope_hint]);
if (!separateSubject) {
return null;
}
const sessionRecord = toRecordObject(input.session);
const addressNavigationState = toRecordObject(sessionRecord?.address_navigation_state);
const sessionContext = toRecordObject(addressNavigationState?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle || !sameBusinessLabel(separateSubject, valueBundle.counterparty ?? comparisonCounterparty?.label)) {
return null;
}
const incoming = toRecordObject(valueBundle.incoming_customer_revenue);
const outgoing = toRecordObject(valueBundle.outgoing_supplier_payout);
const incomingAmount = toNullableString(incoming?.total_amount_human_ru);
const outgoingAmount = toNullableString(outgoing?.total_amount_human_ru);
const netAmount = toNullableString(valueBundle.net_amount_human_ru);
if (!incomingAmount && !outgoingAmount && !netAmount) {
return null;
}
const organization = legalOrganizationLabelFromClarification(input.userMessage)
?? toNullableString(turnMeaning?.explicit_organization_scope)
?? toNullableString(toRecordObject(comparisonScope?.organization)?.label);
const documentCount = Number(toRecordObject(documentBundle)?.document_count);
const documentText = Number.isFinite(documentCount) && documentCount > 0 ? `, документы: ${documentCount}` : "";
const lines = String(input.baseReply ?? "")
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const companyLine = cleanComparisonScopeCompanyLine(lines[0] ?? `Коротко: по компании ${organization ?? "выбранной организации"} подтвержден общий денежный срез.`, organization);
const netDirection = valueBundle.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в нашу сторону";
const counterparty = toNullableString(valueBundle.counterparty) ?? separateSubject;
return {
reply: [
`${companyLine}; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ${netDirection} ${netAmount ?? "0 руб."}${documentText}.`,
`Отдельно по контрагенту ${counterparty}: это ранее подтвержденный контрагентский срез, а не перенос общих сумм компании на контрагента.`,
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${counterparty} из общих сумм компании без отдельного контрагентского среза.`
].join("\n"),
audit: {
applied: true,
source: "address_navigation_state.comparison_scope.proof_bundles",
counterparty,
organization: organization ?? null,
document_count: Number.isFinite(documentCount) && documentCount > 0 ? documentCount : null
}
};
}
function buildAppliedMcpDiscoveryRoutePatch(debug, applied) {
if (!applied) {
return {};
}
const selectedChain = toNullableString(debug.mcp_discovery_selected_chain_id);
if (selectedChain === "business_overview") {
return {
detected_intent: "business_overview",
detected_intent_confidence: "high",
selected_recipe: "business_overview",
response_type: "LIMITED_WITH_REASON",
mcp_discovery_effective_response_route: "business_overview"
};
}
return {};
}
function normalizeAddressLaneDebug(value) {
return (toRecordObject(value) ?? {});
}
@@ -231,18 +346,27 @@ function runAssistantAddressLaneResponseRuntime(input) {
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: guardedResponse.assistantReply;
const comparisonScopeProofReply = buildComparisonScopeProofReply({
baseReply: finalAssistantReply,
debug: debugWithResponseGuard,
session: input.getSession(input.sessionId),
userMessage: input.userMessage
});
const finalAssistantReplyWithComparisonProof = comparisonScopeProofReply?.reply ?? finalAssistantReply;
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
const finalDebug = {
...debugWithResponseGuard,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
assistantReply: finalAssistantReply,
assistantReply: finalAssistantReplyWithComparisonProof,
replyType: finalReplyType,
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
debug: finalDebug,
@@ -151,6 +151,256 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
}
};
}
function firstString(values, toNonEmptyString) {
for (const value of values) {
const text = toNonEmptyString(value);
if (text) {
return text;
}
}
return null;
}
function comparableEntityName(value) {
const text = compactLower(value);
return text ? text.replace(/["'«»„“”]+/g, "") : null;
}
function sameEntityHint(expected, actual) {
const left = comparableEntityName(expected);
const right = comparableEntityName(actual);
if (!left || !right) {
return true;
}
return left === right || left.includes(right) || right.includes(left);
}
function isBusinessOverviewDiscoveryFollowup(followupContext, toNonEmptyString) {
if (!followupContext) {
return false;
}
return [
followupContext.previous_discovery_pilot_scope,
followupContext.previous_discovery_loop_selected_chain_id,
followupContext.previous_discovery_loop_asked_domain_family,
followupContext.previous_intent,
followupContext.target_intent
]
.map((value) => toNonEmptyString(value))
.some((value) => value === "business_overview" || value === "business_overview_route_template_v1");
}
function businessOverviewCounterpartyHint(followupContext, toNonEmptyString) {
const previousFilters = toRecordObject(followupContext.previous_filters);
const rootFilters = toRecordObject(followupContext.root_filters);
return (toNonEmptyString(followupContext.previous_discovery_loop_metadata_scope_hint) ??
(toNonEmptyString(followupContext.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext.previous_anchor_value)
: null) ??
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty));
}
function turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, toNonEmptyString) {
const separateEntities = Array.isArray(turnMeaningRef.business_overview_separate_entity_candidates)
? turnMeaningRef.business_overview_separate_entity_candidates
: [];
return (toNonEmptyString(valueBundle?.counterparty) ??
toNonEmptyString(documentBundle?.counterparty) ??
toNonEmptyString(turnMeaningRef.metadata_scope_hint) ??
firstString(separateEntities, toNonEmptyString));
}
function parseBusinessOverviewProofBundlesFromText(value, toNonEmptyString) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const valueMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu);
const directDocumentMatch = text.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено\s+документов:\s*(\d+)/iu);
const summaryDocumentMatch = text.match(/документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
const counterparty = toNonEmptyString(valueMatch?.[1]) ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueMatch
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueMatch[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueMatch[3])
},
net_amount_human_ru: toNonEmptyString(valueMatch[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function parseBusinessOverviewProofBundlesFromTextV2(value, counterpartyHint, toNonEmptyString) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const comparableHint = comparableEntityName(counterpartyHint);
const lines = text
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const candidateLines = comparableHint
? lines.filter((line) => comparableEntityName(line)?.includes(comparableHint))
: lines;
const rubAmountPattern = /[0-9][0-9\s.,]*\s*\u0440\u0443\u0431\.?/giu;
const valueLine = candidateLines.find((line) => (line.match(rubAmountPattern) ?? []).length >= 3) ?? null;
const valueAmounts = valueLine?.match(rubAmountPattern) ?? [];
const directDocumentMatch = candidateLines
.map((line) => line.match(/\u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^.\n]+)\.\s*\u041d\u0430\u0439\u0434\u0435\u043d\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432:\s*(\d+)/iu))
.find(Boolean);
const summaryDocumentMatch = candidateLines
.map((line) => line.match(/\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b\s+\u043f\u043e\s+\u0446\u0435\u043f\u043e\u0447\u043a\u0435:\s*\u043d\u0430\u0439\u0434\u0435\u043d\u043e\s*(\d+)/iu))
.find(Boolean);
const counterparty = counterpartyHint ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueAmounts.length >= 3
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueAmounts[0])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueAmounts[1])
},
net_amount_human_ru: toNonEmptyString(valueAmounts[2]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function findRecentBusinessOverviewProofBundles(input) {
if (!input.counterpartyHint) {
return null;
}
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(input.sessionItems[index]);
if (input.toNonEmptyString(item?.role) !== "assistant") {
continue;
}
const debug = toRecordObject(item?.debug);
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
if (turnMeaningRef) {
const valueBundle = toRecordObject(turnMeaningRef.previous_counterparty_value_flow_bundle);
const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle);
if (valueBundle || documentBundle) {
const bundleCounterparty = turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, input.toNonEmptyString);
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
return { valueBundle, documentBundle };
}
}
}
const parsedBundles = parseBusinessOverviewProofBundlesFromTextV2(item?.text, input.counterpartyHint, input.toNonEmptyString) ??
parseBusinessOverviewProofBundlesFromText(item?.text, input.toNonEmptyString);
if (!parsedBundles) {
continue;
}
const parsedCounterparty = input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
continue;
}
return parsedBundles;
}
return null;
}
function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const state = toRecordObject(input.sessionAddressNavigationState);
const sessionContext = toRecordObject(state?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle && !documentBundle) {
return input.followupContext;
}
const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext;
}
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const counterpartyHint = (input.followupContext ? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString) : null) ??
input.toNonEmptyString(comparisonCounterparty?.label);
const bundleCounterparty = input.toNonEmptyString(valueBundle?.counterparty) ?? input.toNonEmptyString(documentBundle?.counterparty);
if (!counterpartyHint || !sameEntityHint(counterpartyHint, bundleCounterparty)) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
previous_discovery_pilot_scope: input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
"business_overview_route_template_v1",
previous_discovery_loop_status: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_status) ?? "awaiting_clarification",
previous_discovery_loop_selected_chain_id: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
previous_discovery_loop_pending_axes: Array.isArray(input.followupContext?.previous_discovery_loop_pending_axes)
? input.followupContext?.previous_discovery_loop_pending_axes
: ["organization"],
previous_discovery_loop_asked_domain_family: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
previous_discovery_loop_asked_action_family: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
previous_discovery_loop_metadata_scope_hint: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? counterpartyHint,
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? "counterparty",
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? counterpartyHint,
previous_filters: toRecordObject(input.followupContext?.previous_filters) ?? {},
previous_discovery_bidirectional_value_flow: currentValueBundle ?? valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? documentBundle ?? undefined
};
}
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
return input.followupContext;
}
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const counterpartyHint = input.followupContext
? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString)
: null;
const proofBundles = findRecentBusinessOverviewProofBundles({
sessionItems: input.sessionItems,
counterpartyHint,
toNonEmptyString: input.toNonEmptyString
});
if (!proofBundles) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_discovery_bidirectional_value_flow: currentValueBundle ?? proofBundles.valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
};
}
function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? ""));
}
@@ -324,6 +574,17 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
followupContext: discoveryFollowupContext,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithStateProofBundles = mergeBusinessOverviewProofBundlesFromNavigationState({
followupContext: discoveryFollowupContextWithProofBundles,
sessionAddressNavigationState: input.sessionAddressNavigationState,
predecomposeContract,
toNonEmptyString: input.toNonEmptyString
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint;
let mcpDiscoveryRuntimeEntryPoint = null;
@@ -334,7 +595,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract,
followupContext: discoveryFollowupContext,
followupContext: discoveryFollowupContextWithStateProofBundles,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
}));
}
@@ -238,6 +238,9 @@ function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(debug, toNonEmptyS
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) {
if (pilotScope === "business_overview_route_template_v1" || actionFamily === "broad_evaluation") {
return "business_overview";
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}
@@ -42,6 +42,17 @@ function shouldProbeBareOrganizationScopeCandidate(input) {
function buildDeterministicSmalltalkLeadReply() {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function hasFirstTurnSmalltalkGreetingSignal(value) {
const normalized = String(value ?? "")
.toLowerCase()
.replace(/\u0451/gu, "\u0435")
.replace(/\s+/gu, " ")
.trim();
if (!normalized) {
return false;
}
return /^(?:привет(?:ик)?|здравствуй(?:те)?|хай|йо|yo|hello|hi|че\s+как|че\s+там)(?:[\s,.!?;:()\-]+(?:как|дела|там|че|что|у\s+тебя|как\s+там|как\s+дела))*[\s,.!?;:()\-]*$/iu.test(normalized);
}
function hasConversationExecutiveSummarySignal(value) {
const normalized = String(value ?? "")
.toLowerCase()
@@ -114,6 +125,13 @@ async function runAssistantLivingChatRuntime(input) {
let knownOrganizations = [...organizationAuthority.knownOrganizations];
let selectedOrganization = organizationAuthority.selectedOrganization;
let activeOrganization = organizationAuthority.activeOrganization;
const shouldHandleFirstTurnSmalltalkDeterministically = !selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
hasFirstTurnSmalltalkGreetingSignal(userMessage) &&
input.hasLivingChatSignal(userMessage);
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
? input.addressRuntimeMeta
: {});
@@ -263,6 +281,28 @@ async function runAssistantLivingChatRuntime(input) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
}
else if (shouldHandleFirstTurnSmalltalkDeterministically) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? proactiveScopeProbe.organizations : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = Boolean(proactiveOffer);
livingChatSource = proactiveOffer
? "deterministic_smalltalk_with_proactive_scope_offer"
: "deterministic_smalltalk";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
else {
chatText = await input.executeLlmChat();
const scriptGuard = input.applyScriptGuard(chatText, userMessage);
@@ -283,34 +323,6 @@ async function runAssistantLivingChatRuntime(input) {
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope = !selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? proactiveScopeProbe.organizations : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {
return {
@@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryExecutionHandoff = buildAssistantMcpDiscoveryExecutionHandoff;
exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = "assistant_mcp_discovery_execution_handoff_v1";
const HOT_HANDOFF_CHAIN_ALLOWLIST = ["value_flow"];
const HOT_HANDOFF_CHAIN_ALLOWLIST = [
"value_flow",
"value_flow_comparison",
"business_overview"
];
function uniqueStrings(values) {
const result = [];
for (const value of values) {
@@ -24,16 +24,51 @@ function normalizeQuestionText(value) {
.replace(/\s+/g, " ")
.trim();
}
function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
const text = normalizeQuestionText([
function normalizedTurnAndGraphText(turnMeaning, graph) {
return normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
}
function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
}
function requestsBroadBusinessOverviewSurface(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:не\s+обзор|просто\s+ден\p{L}*|одной\s+строк\p{L}*|только\s+итог|без\s+разбив\p{L}*)/iu.test(text)) {
return false;
}
if (/(?:бизнес[-\s]*обзор|взросл\p{L}{0,10}\s+бизнес|что\s+(?:пока\s+)?нельзя\s+утвержд)/iu.test(text)) {
return true;
}
const markers = [
/(?:ндс|налог\p{L}*)/iu,
/(?:долг\p{L}*|дебитор|кредитор)/iu,
/(?:склад|остатк|товар\p{L}*)/iu,
/(?:клиент|заказчик|покупател)/iu,
/(?:поставщик|получател)/iu,
/(?:оборот\p{L}*)/iu,
/(?:ограничен|не\s+подтвержд|нельзя\s+утвержд)/iu
];
return markers.filter((marker) => marker.test(text)).length >= 3;
}
function requestsCounterpartyLeaderSurface(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:кто|кому)[\s\S]{0,60}(?:больше\s+всего|крупнее\s+всего|основн\p{L}*)[\s\S]{0,60}(?:зан[её]с|прин[её]с|платил|ушло|получил|перев[её]л|заплатил|внес|вн[её]с)/iu.test(text)) {
return true;
}
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
}
function requestsCompactCashflowAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
@@ -716,6 +751,39 @@ function buildPreviousCounterpartyValueFlowSummary(flow, separateSubject, docume
`${basisText} Это не перенос сумм компании на контрагента, а отдельный ранее подтвержденный контрагентский срез.`
};
}
function buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint) {
const turnInput = toRecordObject(entryPoint.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const graph = toRecordObject(turnInput?.data_need_graph);
const bridge = toRecordObject(entryPoint.bridge);
const pilot = toRecordObject(bridge?.pilot);
const overview = toRecordObject(pilot?.derived_business_overview);
const isBusinessOverview = toNonEmptyString(graph?.business_fact_family) === "business_overview" ||
toNonEmptyString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview || overview) {
return null;
}
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
if (!separateSubject || !previousCounterpartySummary) {
return null;
}
const lines = organizationScope
? [
`Коротко: по компании ${organizationScope} в этом шаге нет нового полного company-level расчета; отдельно по выбранному контрагенту ${separateSubject} есть ранее подтвержденный контрагентский срез.`,
previousCounterpartySummary.line,
`Можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не подтверждает чистую прибыль, полный оборот или общую бизнес-роль ${separateSubject}; также нельзя смешивать этот контрагентский срез с выводами по компании без отдельного company-level расчета.`
]
: [
`Коротко: уточните, по какой компании/организации сравнить выбранного контрагента ${separateSubject}; company-level вывод без организации не подтверждаю.`,
previousCounterpartySummary.line,
`Уже можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не чистая прибыль, не полный оборот компании и не доказанная бизнес-роль ${separateSubject}; контрагентский срез нельзя смешивать с company-level выводом без выбранной компании.`
];
return joinBusinessReplyLines(lines);
}
function buildCompactBusinessOverviewReply(entryPoint, draft) {
const turnInput = toRecordObject(entryPoint.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
@@ -773,6 +841,11 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
: null;
const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const broadOverviewSurfaceRequested = requestsBroadBusinessOverviewSurface(turnMeaning, graph);
const counterpartyLeaderSurfaceRequested = requestsCounterpartyLeaderSurface(turnMeaning, graph);
const directMoneyOnlyAnswer = directMoneyAnswer && !broadOverviewSurfaceRequested && !counterpartyLeaderSurfaceRequested;
const shouldIncludeCounterpartyLeaders = !directMoneyOnlyAnswer || counterpartyLeaderSurfaceRequested || broadOverviewSurfaceRequested;
const shouldIncludeOverviewSurface = !directMoneyAnswer || broadOverviewSurfaceRequested;
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
const lines = [];
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
@@ -781,9 +854,20 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const rawMessageComparable = compactComparable(rawMessage);
const organizationScopeComparable = compactComparable(organizationScope);
const plainOrganizationClarificationSelection = Boolean(separateSubject &&
organizationScope &&
rawMessage &&
rawMessageComparable &&
organizationScopeComparable &&
rawMessageComparable.includes(organizationScopeComparable) &&
rawMessage.length <= 90 &&
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage));
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
@@ -798,6 +882,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.");
return joinBusinessReplyLines(lines);
}
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
if (previousCounterpartySummary) {
lines.push(previousCounterpartySummary.line);
}
else {
lines.push(`Отдельно по выбранному контрагенту ${separateSubject}: суммы компании на него не переношу; в этом шаге держу только границу, что это отдельный контрагентский контур.`);
}
lines.push(`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${separateSubject} на основе company-level сумм без отдельного контрагентского среза.`);
return joinBusinessReplyLines(lines);
}
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) {
@@ -973,7 +1068,11 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
return joinBusinessReplyLines(lines);
}
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
if (!separateSubject &&
!crossScopeExecutiveSummary &&
!counterpartyLeaderSurfaceRequested &&
!rankingNeed &&
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
const subject = organizationScope ?? "компания";
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
lines.push(`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`);
@@ -997,6 +1096,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
? `- крупнейший получатель исходящих денег: ${topSupplier}; это похоже на финансовый контур, не на обычного поставщика;`
: `- крупнейший получатель исходящих денег: ${topSupplier};`);
}
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
lines.push(`- ${localizeLine(taxLine)}`);
}
const inventoryLine = businessOverviewInventoryLine(overview);
if (inventoryLine) {
lines.push(`- ${localizeLine(inventoryLine)}`);
@@ -1007,7 +1110,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
}
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
const missingOverviewFamilies = [];
if (!businessOverviewTaxLine(overview)) {
if (!taxLine) {
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
}
if (!debtLine) {
@@ -1040,7 +1143,22 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
(incomingAmount || outgoingAmount || netAmount);
if (explicitPeriodRankingOverview) {
lines.push(`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`);
if (counterpartyLeaderSurfaceRequested) {
const incomingLeaderText = customerName && customerAmount
? topCustomerLooksFinancial
? `${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это банк/финансовый контур, не называю его обычной клиентской выручкой без назначения платежа${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
: `${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
: "не распознан";
const outgoingLeaderText = topSupplier
? topSupplierLooksFinancial
? `${topSupplier}; это банк/финансовый контур, не называю его обычным поставщиком без назначения платежа/договора${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: topSupplier
: "не распознан";
lines.push(`Коротко: ${organizationPrefix}${period} больше всего занес ${incomingLeaderText}; больше всего ушло ${outgoingLeaderText}.`);
}
else {
lines.push(`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`);
}
lines.push(`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
if (customerName && customerAmount) {
lines.push(topCustomerLooksFinancial
@@ -1093,7 +1211,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
else if (incomingAmount || outgoingAmount || netAmount) {
lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`);
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) {
if (shouldIncludeCounterpartyLeaders && customerName && customerAmount) {
lines.push(topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
@@ -1109,17 +1227,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push(previousCounterpartySummary?.line ??
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
}
if (!directMoneyAnswer && topSupplier) {
if (shouldIncludeCounterpartyLeaders && topSupplier) {
lines.push(topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
}
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
if (shouldIncludeCounterpartyLeaders && (topCustomer || topSupplier)) {
lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
}
if (!directMoneyAnswer) {
if (shouldIncludeOverviewSurface) {
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
@@ -1205,6 +1323,10 @@ function buildReplyText(entryPoint, status) {
}
return null;
}
const previousCounterpartyBoundaryReply = buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint);
if (previousCounterpartyBoundaryReply) {
return previousCounterpartyBoundaryReply;
}
const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft);
if (compactBidirectionalValueFlowReply) {
return compactBidirectionalValueFlowReply;
@@ -361,6 +361,11 @@ function hasExactDocumentListAddressReply(input, entryPoint) {
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
return false;
}
if (hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const isDocumentIntent = detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
@@ -271,7 +271,9 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
});
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(reasonCodes, executionHandoff.can_use_guarded_response
? "runtime_bridge_wired_to_guarded_hot_assistant_answer"
: "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
@@ -73,6 +73,7 @@ function isReferentialOrganizationPlaceholder(value) {
"этой компании",
"этой компанией",
"эту компанию",
"в целом",
"наша организация",
"нашей организации",
"нашей компанией"
@@ -188,7 +189,10 @@ function normalizeFollowupCounterpartyCandidate(value) {
if (!text || isInvalidEntityCandidate(text)) {
return null;
}
return text;
const cleaned = text
.replace(/^(?:\u043f\u043e\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435|\u044b|\u0430\u043c|\u0430\u043c\u0438|\u0430\u0445)?\s+/iu, "")
.trim();
return cleaned && !isInvalidEntityCandidate(cleaned) ? cleaned : text;
}
function pushScopedEntityCandidate(target, value, groundedFollowupEntity) {
const text = candidateValue(value);
@@ -543,10 +547,11 @@ function collectFollowupDiscoverySeed(followupContext) {
const normalizedDiscoveryEntities = discoveryEntities
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
.filter((entity) => Boolean(entity));
const normalizedLoopMetadataScopeHint = normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const groundedDiscoveryCounterparty = ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
? null
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const metadataScopeHint = loopMetadataScopeHint ??
: normalizedDiscoveryEntities[0] ?? normalizedLoopMetadataScopeHint;
const metadataScopeHint = normalizedLoopMetadataScopeHint ??
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
@@ -561,8 +566,10 @@ function collectFollowupDiscoverySeed(followupContext) {
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null);
const dateScope = collectDateScopeFromFilters(previousFilters) ??
collectDateScopeFromFilters(rootFilters);
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
const dateScope = loopProvidedAllTimeScope
? "all_time_scope"
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
return {
pilotScope: effectivePilotScope,
domain: mapped.domain,
@@ -713,7 +720,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text) {
return hasSupplierScopeCue && hasSupplierQualityCue && hasCompanyScopeCue;
}
function hasCrossScopeExecutiveSummarySignal(text) {
return (/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary)/iu.test(text) &&
return (/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0441\u0440\u0430\u0432\u043d\p{L}*|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary|brief(?:ly)?\s+compare)/iu.test(text) &&
/(?:\u0447\u0442\u043e\s+(?:\u043c\u044b\s+)?\u043f\u043e\u0434\u0442\u0432\u0435\u0440\p{L}*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043f\u043e\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|confirmed|company|organization)/iu.test(text) &&
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0433\u0440\u0443\u043f\u043f\p{L}*\s+\u0441\u0432\u043a|\u0441\u0432\u043a|counterpart(?:y|ies)?)/iu.test(text) &&
/(?:\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\p{L}*|\u0447\u0442\u043e\s+\u043d\u0435\u043b\u044c\u0437\p{L}*|\u0432\u044b\u0432\u043e\u0434\p{L}*|allowed|forbidden|cannot|can\s+say)/iu.test(text));
@@ -723,6 +730,27 @@ function hasPlainBusinessOverviewSignal(text) {
const hasCompanyOrOperatingScopeCue = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\p{L}*|(?:19|20)\d{2}|company|organization|business)/iu.test(text);
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
}
function countBroadBusinessOverviewAxes(text) {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewSurfaceSignal(text) {
const normalized = compactLower(text);
if (!normalized) {
return false;
}
const hasBroadCue = /(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(normalized);
const hasCompanyScope = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(normalized);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewAxes(normalized) >= 3;
}
function hasBusinessOverviewSignal(text) {
if (hasCrossScopeExecutiveSummarySignal(text) ||
hasOrganizationLevelEarningsOverviewSignal(text) ||
@@ -730,6 +758,7 @@ function hasBusinessOverviewSignal(text) {
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
hasPlainBusinessOverviewSignal(text) ||
hasBroadBusinessOverviewSurfaceSignal(text) ||
hasOrganizationLevelSupplierQualityOverviewSignal(text)) {
return true;
}
@@ -774,6 +803,12 @@ function hasBusinessOverviewSeparateCounterpartySignal(text) {
return (/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|counterpart(?:y|ies)?)/iu.test(text) &&
/(?:\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|company|organization|\u0438\u0442\u043e\u0433|summary|\u0432\u044b\u0432\u043e\u0434\p{L}*)/iu.test(text));
}
function isGenericSelectedCounterpartyReference(value) {
if (!value) {
return false;
}
return /^(?:(?:\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*|\u044d\u0442\p{L}*|\u0434\u0430\u043d\p{L}*|\u0442\u0435\u043a\u0443\u0449\p{L}*)\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*$|^(?:selected|chosen|current|this)\s+counterpart(?:y|ies)?$/iu.test(value);
}
function businessOverviewSeparateCounterpartyCandidateFromText(text) {
const source = (0, addressTextRepair_1.repairAddressMojibakeText)(String(text ?? ""));
const patterns = [
@@ -782,7 +817,7 @@ function businessOverviewSeparateCounterpartyCandidateFromText(text) {
];
for (const pattern of patterns) {
const candidate = normalizeFollowupCounterpartyCandidate(source.match(pattern)?.[1]);
if (candidate && !isInvalidEntityCandidate(candidate)) {
if (candidate && !isInvalidEntityCandidate(candidate) && !isGenericSelectedCounterpartyReference(candidate)) {
return candidate;
}
}
@@ -868,6 +903,9 @@ function normalizeLooseOrganizationAlias(value) {
if (hasYearOnlyTimeTail) {
return null;
}
if (new Set(["в целом", "компания в целом", "организация в целом"]).has(comparable)) {
return null;
}
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
return null;
}
@@ -1370,10 +1408,18 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: "broad_business_evaluation";
const businessOverviewSignal = !businessOverviewCounterpartyValueFlowPivot &&
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
const organizationClarificationBusinessOverviewLoop = Boolean(followupSeed.loopStatus === "awaiting_clarification" &&
followupSeed.loopSelectedChainId === "business_overview" &&
followupSeed.loopPendingAxes.includes("organization") &&
currentTurnOrganizationScope &&
!rawLifecycleSignal &&
!rawMetadataSignal);
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
: null;
: organizationClarificationBusinessOverviewLoop
? followupSeed.counterparty ?? followupSeed.discoveryEntity ?? followupSeed.metadataScopeHint
: null;
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
const currentTurnMovementLaneSignal = rawAction === "list_movements";
@@ -1397,6 +1443,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
sameScopedName(followupSeed.counterparty, currentTurnOrganizationScope)));
const businessOverviewSuppressesFollowupCounterparty = Boolean(businessOverviewSignal &&
!businessOverviewSeparateCounterpartySignal &&
!organizationClarificationBusinessOverviewLoop &&
(rawBusinessOverviewSignal ||
businessOverviewContinuationSignal ||
broadBusinessEvaluationUnsupported ||
@@ -1831,7 +1878,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
}
const businessOverviewSeparateCounterpartyDisplayCandidate = businessOverviewSeparateCounterpartySignal
const shouldPreserveBusinessOverviewSeparateCounterparty = businessOverviewSeparateCounterpartySignal || organizationClarificationBusinessOverviewLoop;
const businessOverviewSeparateCounterpartyDisplayCandidate = shouldPreserveBusinessOverviewSeparateCounterparty
? preferredScopedDisplayName(businessOverviewSeparateCounterpartyCandidate, [
groundedFollowupEntity,
effectiveFollowupCounterparty,
@@ -1840,7 +1888,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
])
]) ??
preferredScopedDisplayName(groundedFollowupEntity ??
effectiveFollowupCounterparty ??
followupSeed.discoveryEntity ??
normalizedPredecomposeCounterparty ??
rawScopedEntityCandidate ??
rawEntityCandidate, [
groundedFollowupEntity,
effectiveFollowupCounterparty,
followupSeed.discoveryEntity,
normalizedPredecomposeCounterparty,
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
])
: null;
const businessOverviewSeparateEntityCandidates = businessOverviewSeparateCounterpartyDisplayCandidate
? [businessOverviewSeparateCounterpartyDisplayCandidate]
@@ -1936,6 +1998,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: predecomposeDateScope;
const normalizedAssistantTurnMeaningDateScope = rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(organizationClarificationBusinessOverviewLoop && !currentTurnCarriesExplicitPeriod) ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
? null
: assistantTurnMeaningDateScope;
@@ -1950,7 +2013,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
/^\d{4}$/.test(rawDateScope) &&
normalizedPredecomposeDateScope &&
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
const explicitDateScope = rawAllTimeScopeSignal
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
? null
: normalizedAssistantTurnMeaningDateScope ??
(businessOverviewRawYearOverridesPredecomposeAsOf ? rawDateScope : normalizedPredecomposeDateScope) ??
@@ -1960,7 +2024,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!normalizedAssistantTurnMeaningDateScope &&
!normalizedPredecomposeDateScope &&
!rawDateScope &&
normalizedFollowupDateScope);
normalizedFollowupDateScope &&
normalizedFollowupDateScope !== "all_time_scope");
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
const turnMeaning = {
raw_message: repairedUserText ?? rawUserText ?? null,
@@ -2007,7 +2072,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed
: undefined,
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
explicit_entity_candidates: businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
previous_counterparty_value_flow_bundle: businessOverviewSignal && followupSeed.previousBidirectionalValueFlow
? followupSeed.previousBidirectionalValueFlow
@@ -2222,6 +2287,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (followupAllTimeScopeApplied) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_from_followup_context");
}
if (suppressNegatedTaxOnlyDateScope) {
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
}
@@ -2324,7 +2392,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (businessOverviewSuppressesFollowupCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty");
}
if (businessOverviewSeparateCounterpartySignal) {
if (shouldPreserveBusinessOverviewSeparateCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope");
}
if (businessOverviewSeparateCounterpartyCandidate) {
@@ -2346,7 +2414,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty && !businessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
}
if (effectiveFollowupCounterparty &&
@@ -75,6 +75,19 @@ function createAssistantTransitionPolicy(deps) {
}
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
}
function hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage = null) {
return [userMessage, alternateMessage]
.filter((value) => deps.toNonEmptyString(value))
.map((value) => normalizeFollowupText(value).replace(/С‘/g, "Рµ"))
.some((normalized) => {
if (!normalized) {
return false;
}
const hasDocumentCue = /(?:\u0434\u043e\u043a\p{L}*|\u0441\u0447\p{L}*|\u043d\u0430\u043a\u043b\u0430\u0434\p{L}*|\u0430\u043a\u0442|document|docs?|invoice|receipt)/iu.test(normalized) || hasReadableDocumentsPivotCue(normalized);
const hasSelectedCounterpartyCue = /(?:\u043f\u043e\s+\u043d(?:\u0435\u043c\u0443|\u0435\u0439)|\u043f\u043e\s+\u044d\u0442(?:\u043e\u043c\u0443|\u043e\u0439)|\u0442\u0435\u043a\u0443\u0449\p{L}*\s+\u043e\u0431\u044a\u0435\u043a\p{L}*|\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*\s+(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u044a\u0435\u043a\u0442)|selected\s+(?:counterparty|object)|current\s+object)/iu.test(normalized);
return hasDocumentCue && hasSelectedCounterpartyCue;
});
}
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
return null;
@@ -298,32 +311,115 @@ function createAssistantTransitionPolicy(deps) {
}
return flow;
}
function readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_value_flow_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_document_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readCounterpartyDocumentSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
const match = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (!match?.[1] || !match?.[2]) {
const directMatch = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (directMatch?.[1] && directMatch?.[2]) {
return {
counterparty: deps.toNonEmptyString(directMatch[1]),
document_count: Number(directMatch[2]),
direct_answer: firstLine
};
}
const summaryMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):[\s\S]{0,260}документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
if (!summaryMatch?.[1] || !summaryMatch?.[2]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(summaryMatch[1]),
document_count: Number(summaryMatch[2]),
direct_answer: summaryMatch[0].replace(/\s+/g, " ").trim()
};
}
function readCounterpartyValueFlowSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const match = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu);
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(match[1]),
document_count: Number(match[2]),
direct_answer: firstLine
incoming_customer_revenue: {
total_amount_human_ru: deps.toNonEmptyString(match[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: deps.toNonEmptyString(match[3])
},
net_amount_human_ru: deps.toNonEmptyString(match[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_confirmed_counterparty_boundary_summary"
};
}
function findRecentDiscoveryValueFlowBundle(items) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
if (!item || item.role !== "assistant" || !debug || typeof debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
if (debug && typeof debug === "object") {
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
}
}
const parsedFlow = readCounterpartyValueFlowSummaryFromItem(item);
if (parsedFlow) {
return parsedFlow;
}
}
return null;
}
function sameCounterpartyHint(expected, actual) {
const left = normalizeFollowupText(expected);
const right = normalizeFollowupText(actual);
if (!left || !right) {
return false;
}
return left === right || left.includes(right) || right.includes(left);
}
function findRecentPreviousCounterpartyValueFlowBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
@@ -331,7 +427,8 @@ function createAssistantTransitionPolicy(deps) {
function findRecentCounterpartyDocumentBundle(items) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const summary = readCounterpartyDocumentSummaryFromItem(item);
@@ -341,6 +438,28 @@ function createAssistantTransitionPolicy(deps) {
}
return null;
}
function findRecentPreviousCounterpartyDocumentBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
}
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
!hasInventoryItemFocusHint &&
@@ -504,9 +623,15 @@ function createAssistantTransitionPolicy(deps) {
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false);
const earlyNavigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const earlyNavigationFocusObject = earlyNavigationSessionState.focusObject;
const selectedCounterpartyDocumentFollowupSignal = Boolean(deps.toNonEmptyString(earlyNavigationFocusObject?.label) &&
deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty" &&
hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage));
if (assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal) {
!compactCashflowFollowupSignal &&
!selectedCounterpartyDocumentFollowupSignal) {
return null;
}
const latestAddressItem = deps.findLastAddressAssistantItem(items);
@@ -574,7 +699,7 @@ function createAssistantTransitionPolicy(deps) {
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationSessionState = earlyNavigationSessionState;
const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
@@ -662,6 +787,7 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@@ -686,6 +812,7 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@@ -713,6 +840,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@@ -729,6 +857,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@@ -762,8 +891,19 @@ function createAssistantTransitionPolicy(deps) {
const sourceDiscoveryLoopSubjectResolutionOptional = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSubjectResolutionOptional)(carryoverSourceDebug);
const sourceDiscoveryRankingNeed = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryRankingNeed)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryBidirectionalValueFlow = readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ?? findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary = findRecentCounterpartyDocumentBundle(items);
const sourceDiscoveryCounterpartyHint = sourceDiscoveryLoopMetadataScopeHint ??
(deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty"
? deps.toNonEmptyString(earlyNavigationFocusObject?.label)
: null);
const sourceDiscoveryBidirectionalValueFlow = readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ??
readMcpDiscoveryPreviousCounterpartyValueFlowBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyValueFlowBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyValueFlowSummaryFromItem(previousAddressItem) ??
findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary = readMcpDiscoveryPreviousCounterpartyDocumentBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyDocumentBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyDocumentSummaryFromItem(previousAddressItem) ??
findRecentCounterpartyDocumentBundle(items);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent;
@@ -917,6 +1057,14 @@ function createAssistantTransitionPolicy(deps) {
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(carryoverSourceDebug, deps.toNonEmptyString);
const navigationCounterpartyFocus = navigationFocusObjectType === "counterparty" ? navigationFocusObjectLabel : null;
const hasNavigationCounterpartyFocusCarryover = Boolean(navigationCounterpartyFocus &&
(hasValueFlowCarryoverSourceHint ||
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "open_items_by_counterparty_or_contract" ||
sourceDiscoveryLoopSelectedChainIdHint === "value_flow_comparison"));
const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
@@ -924,6 +1072,15 @@ function createAssistantTransitionPolicy(deps) {
sourceIntentHint === "bank_operations_by_contract" ||
sourceIntentHint === "open_items_by_counterparty_or_contract";
previousFilters = (0, assistantContinuityPolicy_1.applyHistoricalPartyCarryoverFilters)(previousFilters, shouldBackfillHistoricalPartyAnchors, deps.findRecentAddressFilterValue(items, "contract"), deps.findRecentAddressFilterValue(items, "counterparty"), deps.toNonEmptyString);
if (hasNavigationCounterpartyFocusCarryover && navigationCounterpartyFocus) {
if (!previousAnchor) {
previousAnchorType = "counterparty";
previousAnchor = navigationCounterpartyFocus;
}
if (!deps.toNonEmptyString(previousFilters.counterparty)) {
previousFilters.counterparty = navigationCounterpartyFocus;
}
}
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
const authorityActiveOrganization = deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??
deps.normalizeOrganizationScopeValue(organizationAuthority.continuityActiveOrganization);
@@ -2152,6 +2152,35 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
}
function countBroadBusinessOverviewBridgeAxes(text: string): number {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasBroadCue =
/(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(
normalized
);
const hasCompanyScope =
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(
normalized
);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewBridgeAxes(normalized) >= 3;
}
function hasNomenclatureMarginRankingSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
@@ -2357,6 +2386,10 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
);
}
if (hasBroadBusinessOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_multi_surface_deferred_to_discovery");
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}
@@ -3,7 +3,9 @@ import type { AssistantConversationItem } from "../types/assistant";
import type { AddressIntent } from "../types/addressQuery";
import {
readAddressDebugCounterparty,
readAddressDebugItem
readAddressDebugItem,
readAddressDebugOrganization,
readAddressDebugTemporalScope
} from "./assistantContinuityPolicy";
import {
ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
@@ -21,7 +23,11 @@ const MAX_RESULT_SETS = 40;
const MAX_NAVIGATION_EVENTS = 120;
const MAX_ENTITY_REFS_PER_RESULT_SET = 40;
type AddressComparisonScope = NonNullable<AddressNavigationState["session_context"]["comparison_scope"]>;
type AddressComparisonProofBundles = NonNullable<AddressComparisonScope["proof_bundles"]>;
const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusObjectType>> = {
business_overview: "organization",
counterparty_activity_lifecycle: "counterparty",
customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty",
@@ -45,6 +51,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
};
const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetType>> = {
business_overview: "profile_summary",
counterparty_activity_lifecycle: "counterparty_list",
customer_revenue_and_payments: "counterparty_list",
supplier_payouts_profile: "counterparty_list",
@@ -83,6 +90,18 @@ function toObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function cloneRecord(value: unknown): Record<string, unknown> | null {
const record = toObject(value);
if (!record) {
return null;
}
try {
return JSON.parse(JSON.stringify(record)) as Record<string, unknown>;
} catch {
return { ...record };
}
}
function toNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
@@ -91,6 +110,44 @@ function toNonEmptyString(value: unknown): string | null {
return trimmed.length > 0 ? trimmed : null;
}
function candidateLabel(value: unknown): string | null {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toObject(value);
if (!record) {
return null;
}
return (
toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text)
);
}
function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): string | null {
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entry?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const dataNeedGraph = toObject(turnInput?.data_need_graph);
const candidates = [
...(Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: []),
...(Array.isArray(turnMeaning?.explicit_entity_candidates) ? turnMeaning.explicit_entity_candidates : []),
...(Array.isArray(dataNeedGraph?.subject_candidates) ? dataNeedGraph.subject_candidates : [])
];
for (const candidate of candidates) {
const label = candidateLabel(candidate);
if (label) {
return label;
}
}
return null;
}
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
const normalized = toNonEmptyString(value);
if (!normalized) {
@@ -228,6 +285,26 @@ function cloneFocusObject(value: AddressFocusObject | null): AddressFocusObject
};
}
function cloneComparisonProofBundles(value: unknown): AddressComparisonProofBundles | null {
const record = toObject(value);
if (!record) {
return null;
}
const counterpartyValueFlowBundle = cloneRecord(
record.counterparty_value_flow_bundle ?? record.previous_counterparty_value_flow_bundle
);
const counterpartyDocumentBundle = cloneRecord(
record.counterparty_document_bundle ?? record.previous_counterparty_document_bundle
);
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function cloneResultSet(input: AddressResultSet): AddressResultSet {
return {
result_set_id: input.result_set_id,
@@ -296,8 +373,71 @@ function buildFocusObject(
};
}
function cloneComparisonScope(
value: AddressNavigationState["session_context"]["comparison_scope"]
): AddressNavigationState["session_context"]["comparison_scope"] {
if (!value || typeof value !== "object") {
return null;
}
return {
organization: cloneFocusObject(value.organization),
counterparty: cloneFocusObject(value.counterparty),
proof_bundles: cloneComparisonProofBundles(value.proof_bundles)
};
}
function sameBusinessLabel(left: unknown, right: unknown): boolean {
const normalizedLeft = toNonEmptyString(left)?.toLocaleLowerCase("ru-RU");
const normalizedRight = toNonEmptyString(right)?.toLocaleLowerCase("ru-RU");
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
}
function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknown>): AddressComparisonProofBundles | null {
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const counterpartyValueFlowBundle =
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
const counterparty =
toNonEmptyString(counterpartyValueFlowBundle?.counterparty) ??
toNonEmptyString(counterpartyDocumentBundle?.counterparty) ??
readNavigationDiscoveryCounterparty(debug);
if (!counterparty) {
return null;
}
if (!counterpartyValueFlowBundle && !counterpartyDocumentBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (organization) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty =
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
}
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
if (canonicalType === "item") {
@@ -330,11 +470,13 @@ function capNavigationEvents(events: AddressNavigationEvent[]): AddressNavigatio
}
function isAddressAssistantItem(item: AssistantConversationItem): boolean {
return (
item.role === "assistant" &&
Boolean(item.debug) &&
toNonEmptyString(item.debug?.detected_mode) === "address_query"
);
if (item.role !== "assistant" || !item.debug) {
return false;
}
if (toNonEmptyString(item.debug.detected_mode) === "address_query") {
return true;
}
return item.debug.mcp_discovery_response_applied === true && Boolean(toNonEmptyString(item.debug.mcp_discovery_selected_chain_id));
}
export function createEmptyAddressNavigationState(
@@ -348,6 +490,7 @@ export function createEmptyAddressNavigationState(
session_context: {
active_result_set_id: null,
active_focus_object: null,
comparison_scope: null,
last_confirmed_route: null,
date_scope: {
as_of_date: null,
@@ -372,6 +515,7 @@ export function cloneAddressNavigationState(value: AddressNavigationState | null
session_context: {
active_result_set_id: value.session_context.active_result_set_id,
active_focus_object: cloneFocusObject(value.session_context.active_focus_object),
comparison_scope: cloneComparisonScope(value.session_context.comparison_scope),
last_confirmed_route: value.session_context.last_confirmed_route,
date_scope: {
as_of_date: value.session_context.date_scope.as_of_date,
@@ -406,6 +550,9 @@ export function normalizeAddressNavigationState(
session_context: {
active_result_set_id: toNonEmptyString(context.active_result_set_id),
active_focus_object: cloneFocusObject(context.active_focus_object as AddressFocusObject | null),
comparison_scope: cloneComparisonScope(
context.comparison_scope as AddressNavigationState["session_context"]["comparison_scope"]
),
last_confirmed_route: toNonEmptyString(context.last_confirmed_route),
date_scope: {
as_of_date: toNonEmptyString(dateScope.as_of_date),
@@ -464,20 +611,42 @@ export function evolveAddressNavigationStateWithAssistantItem(
return state;
}
const debug = item.debug as unknown as Record<string, unknown>;
const intent = toAddressIntent(debug.detected_intent);
if (intent === "unknown") {
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
const discoveryIntent =
selectedDiscoveryChain === "business_overview"
? "business_overview"
: selectedDiscoveryChain === "value_flow_comparison"
? "customer_revenue_and_payments"
: "unknown";
const detectedIntent = toNonEmptyString(debug.detected_intent);
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
if (intent === "unknown" && !trackableDiscoveryTurn) {
return state;
}
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe);
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text);
const derivedOrganizationScope =
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
const derivedCounterpartyScope =
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
: null;
const filtersWithDerivedScope =
derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
...filters,
organization: derivedOrganizationScope
organization: derivedOrganizationScope,
...(derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? { counterparty: derivedCounterpartyScope }
: {})
}
: derivedCounterpartyScope && !toNonEmptyString(filters.counterparty)
? {
...filters,
counterparty: derivedCounterpartyScope
}
: filters;
const sourceRefs = routeId ? [routeId] : [];
@@ -495,6 +664,41 @@ export function evolveAddressNavigationStateWithAssistantItem(
};
const previousResultSetId = state.session_context.active_result_set_id;
const focusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
const comparisonCounterparty =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readNavigationDiscoveryCounterparty(debug)
: null;
const comparisonOrganization =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
: null;
const currentComparisonProofBundles =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug)
: null;
const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles =
comparisonCounterparty &&
sameBusinessLabel(inheritedComparisonScope?.counterparty?.label, comparisonCounterparty)
? cloneComparisonProofBundles(inheritedComparisonScope?.proof_bundles)
: null;
const comparisonProofBundles = currentComparisonProofBundles ?? inheritedComparisonProofBundles;
const comparisonOrganizationObject =
comparisonOrganization
? buildFocusObject("organization", comparisonOrganization, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.organization ?? null);
const comparisonCounterpartyObject =
comparisonCounterparty
? buildFocusObject("counterparty", comparisonCounterparty, resultSetId, createdAt)
: cloneFocusObject(inheritedComparisonScope?.counterparty ?? null);
const comparisonScope =
comparisonOrganizationObject || comparisonCounterpartyObject || comparisonProofBundles
? {
organization: comparisonOrganizationObject,
counterparty: comparisonCounterpartyObject,
proof_bundles: comparisonProofBundles
}
: null;
const action = resolveNavigationAction(debug, Boolean(focusObject));
const navigationEvent: AddressNavigationEvent = {
event_id: `nav-${nanoid(10)}`,
@@ -505,10 +709,11 @@ export function evolveAddressNavigationStateWithAssistantItem(
turn_index: turnIndex,
created_at: createdAt
};
const discoveryTemporalScope = readAddressDebugTemporalScope(debug, toNonEmptyString);
const normalizedDateScope = {
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date),
period_from: toNonEmptyString(filtersWithDerivedScope.period_from),
period_to: toNonEmptyString(filtersWithDerivedScope.period_to)
as_of_date: toNonEmptyString(filtersWithDerivedScope.as_of_date) ?? discoveryTemporalScope.asOfDate,
period_from: toNonEmptyString(filtersWithDerivedScope.period_from) ?? discoveryTemporalScope.periodFrom,
period_to: toNonEmptyString(filtersWithDerivedScope.period_to) ?? discoveryTemporalScope.periodTo
};
const organizationScope = toNonEmptyString(filtersWithDerivedScope.organization);
const nextResultSets = capResultSets(
@@ -522,6 +727,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
? {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? null,
comparison_scope: comparisonScope,
last_confirmed_route: routeId ?? null,
date_scope: {
as_of_date: normalizedDateScope.as_of_date,
@@ -533,6 +739,7 @@ export function evolveAddressNavigationStateWithAssistantItem(
: {
active_result_set_id: resultSetId,
active_focus_object: focusObject ?? state.session_context.active_focus_object,
comparison_scope: comparisonScope ?? state.session_context.comparison_scope,
last_confirmed_route: routeId ?? state.session_context.last_confirmed_route,
date_scope: {
as_of_date: normalizedDateScope.as_of_date ?? state.session_context.date_scope.as_of_date,
@@ -733,6 +733,44 @@ function needsVatCalendarDetails(userMessage: string | null | undefined): boolea
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
}
function needsVatPurchaseDateAnchorDisclosure(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(
text
);
}
function buildVatPurchaseDateAnchorDisclosureLine(
options: ComposeFactualReplyOptions,
periodWindowLabel: string | null
): string | null {
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
return null;
}
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const asOfTs = toUtcDayTimestamp(asOfDate);
const fromTs = toUtcDayTimestamp(periodFrom);
const toTs = toUtcDayTimestamp(periodTo);
if (
asOfDate &&
asOfTs !== null &&
fromTs !== null &&
toTs !== null &&
asOfTs >= fromTs &&
asOfTs <= toTs
) {
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
}
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
}
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
const text = normalizeQuestionText(userMessage);
if (!text) {
@@ -3896,6 +3934,7 @@ function composeFactualReplyBody(
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
@@ -3904,6 +3943,7 @@ function composeFactualReplyBody(
"Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
@@ -76,6 +76,141 @@ function normalizeAddressReplyType(value: unknown): AssistantReplyType {
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
}
function sameBusinessLabel(left: unknown, right: unknown): boolean {
const normalizedLeft = toNullableString(left)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
const normalizedRight = toNullableString(right)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
return Boolean(
normalizedLeft &&
normalizedRight &&
(normalizedLeft === normalizedRight ||
normalizedLeft.includes(normalizedRight) ||
normalizedRight.includes(normalizedLeft))
);
}
function firstString(values: unknown[]): string | null {
for (const value of values) {
const text = toNullableString(value);
if (text) {
return text;
}
}
return null;
}
function legalOrganizationLabelFromClarification(value: unknown): string | null {
const text = toNullableString(value);
if (!text) {
return null;
}
const compact = text.replace(/\s+/gu, " ").replace(/[.!?]+$/u, "").trim();
if (compact.length > 120 || !/^(?:ООО|ПАО|АО|ИП)\s+\S/iu.test(compact)) {
return null;
}
return compact;
}
function cleanComparisonScopeCompanyLine(line: string, organization: string | null): string {
let clean = String(line ?? "")
.replace(/\bcompany-level\b/giu, "общий по компании")
.replace(/\breusable bundle\b/giu, "сохраненный подтвержденный срез");
if (organization) {
clean = clean.replace(/по компании\s+Альтернатива Плюс/iu, `по компании ${organization}`);
}
return clean.trim();
}
function buildComparisonScopeProofReply(input: {
baseReply: string;
debug: Record<string, unknown>;
session: unknown;
userMessage?: unknown;
}): { reply: string; audit: Record<string, unknown> } | null {
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const isBusinessOverview = toNullableString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview) {
return null;
}
const separateCandidates = Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: [];
const separateSubject = firstString([...separateCandidates, turnMeaning?.metadata_scope_hint]);
if (!separateSubject) {
return null;
}
const sessionRecord = toRecordObject(input.session);
const addressNavigationState = toRecordObject(sessionRecord?.address_navigation_state);
const sessionContext = toRecordObject(addressNavigationState?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle || !sameBusinessLabel(separateSubject, valueBundle.counterparty ?? comparisonCounterparty?.label)) {
return null;
}
const incoming = toRecordObject(valueBundle.incoming_customer_revenue);
const outgoing = toRecordObject(valueBundle.outgoing_supplier_payout);
const incomingAmount = toNullableString(incoming?.total_amount_human_ru);
const outgoingAmount = toNullableString(outgoing?.total_amount_human_ru);
const netAmount = toNullableString(valueBundle.net_amount_human_ru);
if (!incomingAmount && !outgoingAmount && !netAmount) {
return null;
}
const organization = legalOrganizationLabelFromClarification(input.userMessage)
?? toNullableString(turnMeaning?.explicit_organization_scope)
?? toNullableString(toRecordObject(comparisonScope?.organization)?.label);
const documentCount = Number(toRecordObject(documentBundle)?.document_count);
const documentText = Number.isFinite(documentCount) && documentCount > 0 ? `, документы: ${documentCount}` : "";
const lines = String(input.baseReply ?? "")
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const companyLine = cleanComparisonScopeCompanyLine(
lines[0] ?? `Коротко: по компании ${organization ?? "выбранной организации"} подтвержден общий денежный срез.`,
organization
);
const netDirection =
valueBundle.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в нашу сторону";
const counterparty = toNullableString(valueBundle.counterparty) ?? separateSubject;
return {
reply: [
`${companyLine}; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ${netDirection} ${netAmount ?? "0 руб."}${documentText}.`,
`Отдельно по контрагенту ${counterparty}: это ранее подтвержденный контрагентский срез, а не перенос общих сумм компании на контрагента.`,
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${counterparty} из общих сумм компании без отдельного контрагентского среза.`
].join("\n"),
audit: {
applied: true,
source: "address_navigation_state.comparison_scope.proof_bundles",
counterparty,
organization: organization ?? null,
document_count: Number.isFinite(documentCount) && documentCount > 0 ? documentCount : null
}
};
}
function buildAppliedMcpDiscoveryRoutePatch(
debug: Record<string, unknown>,
applied: boolean
): Record<string, unknown> {
if (!applied) {
return {};
}
const selectedChain = toNullableString(debug.mcp_discovery_selected_chain_id);
if (selectedChain === "business_overview") {
return {
detected_intent: "business_overview",
detected_intent_confidence: "high",
selected_recipe: "business_overview",
response_type: "LIMITED_WITH_REASON",
mcp_discovery_effective_response_route: "business_overview"
};
}
return {};
}
function normalizeAddressLaneDebug(value: unknown): AddressExecutionDebug {
return (toRecordObject(value) ?? {}) as unknown as AddressExecutionDebug;
}
@@ -290,18 +425,27 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: guardedResponse.assistantReply;
const comparisonScopeProofReply = buildComparisonScopeProofReply({
baseReply: finalAssistantReply,
debug: debugWithResponseGuard,
session: input.getSession(input.sessionId),
userMessage: input.userMessage
});
const finalAssistantReplyWithComparisonProof = comparisonScopeProofReply?.reply ?? finalAssistantReply;
const finalReplyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : guardedResponse.replyType;
const finalDebug = {
...debugWithResponseGuard,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
assistantReply: finalAssistantReply,
assistantReply: finalAssistantReplyWithComparisonProof,
replyType: finalReplyType,
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
debug: finalDebug,
@@ -254,6 +254,332 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
};
}
function firstString(
values: unknown[],
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): string | null {
for (const value of values) {
const text = toNonEmptyString(value);
if (text) {
return text;
}
}
return null;
}
function comparableEntityName(value: string | null): string | null {
const text = compactLower(value);
return text ? text.replace(/["'«»„“”]+/g, "") : null;
}
function sameEntityHint(expected: string | null, actual: string | null): boolean {
const left = comparableEntityName(expected);
const right = comparableEntityName(actual);
if (!left || !right) {
return true;
}
return left === right || left.includes(right) || right.includes(left);
}
function isBusinessOverviewDiscoveryFollowup(
followupContext: Record<string, unknown> | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): boolean {
if (!followupContext) {
return false;
}
return [
followupContext.previous_discovery_pilot_scope,
followupContext.previous_discovery_loop_selected_chain_id,
followupContext.previous_discovery_loop_asked_domain_family,
followupContext.previous_intent,
followupContext.target_intent
]
.map((value) => toNonEmptyString(value))
.some((value) => value === "business_overview" || value === "business_overview_route_template_v1");
}
function businessOverviewCounterpartyHint(
followupContext: Record<string, unknown>,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): string | null {
const previousFilters = toRecordObject(followupContext.previous_filters);
const rootFilters = toRecordObject(followupContext.root_filters);
return (
toNonEmptyString(followupContext.previous_discovery_loop_metadata_scope_hint) ??
(toNonEmptyString(followupContext.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext.previous_anchor_value)
: null) ??
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty)
);
}
function turnMeaningCounterpartyName(
turnMeaningRef: Record<string, unknown>,
valueBundle: Record<string, unknown> | null,
documentBundle: Record<string, unknown> | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): string | null {
const separateEntities = Array.isArray(turnMeaningRef.business_overview_separate_entity_candidates)
? turnMeaningRef.business_overview_separate_entity_candidates
: [];
return (
toNonEmptyString(valueBundle?.counterparty) ??
toNonEmptyString(documentBundle?.counterparty) ??
toNonEmptyString(turnMeaningRef.metadata_scope_hint) ??
firstString(separateEntities, toNonEmptyString)
);
}
function parseBusinessOverviewProofBundlesFromText(
value: unknown,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const valueMatch = text.match(
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
);
const directDocumentMatch = text.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено\s+документов:\s*(\d+)/iu);
const summaryDocumentMatch = text.match(/документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
const counterparty =
toNonEmptyString(valueMatch?.[1]) ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueMatch
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueMatch[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueMatch[3])
},
net_amount_human_ru: toNonEmptyString(valueMatch[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function parseBusinessOverviewProofBundlesFromTextV2(
value: unknown,
counterpartyHint: string | null,
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const comparableHint = comparableEntityName(counterpartyHint);
const lines = text
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
const candidateLines = comparableHint
? lines.filter((line) => comparableEntityName(line)?.includes(comparableHint))
: lines;
const rubAmountPattern = /[0-9][0-9\s.,]*\s*\u0440\u0443\u0431\.?/giu;
const valueLine = candidateLines.find((line) => (line.match(rubAmountPattern) ?? []).length >= 3) ?? null;
const valueAmounts = valueLine?.match(rubAmountPattern) ?? [];
const directDocumentMatch = candidateLines
.map((line) =>
line.match(
/\u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^.\n]+)\.\s*\u041d\u0430\u0439\u0434\u0435\u043d\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432:\s*(\d+)/iu
)
)
.find(Boolean);
const summaryDocumentMatch = candidateLines
.map((line) =>
line.match(
/\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b\s+\u043f\u043e\s+\u0446\u0435\u043f\u043e\u0447\u043a\u0435:\s*\u043d\u0430\u0439\u0434\u0435\u043d\u043e\s*(\d+)/iu
)
)
.find(Boolean);
const counterparty = counterpartyHint ?? toNonEmptyString(directDocumentMatch?.[1]);
if (!counterparty) {
return null;
}
const valueBundle = valueAmounts.length >= 3
? {
counterparty,
incoming_customer_revenue: {
total_amount_human_ru: toNonEmptyString(valueAmounts[0])
},
outgoing_supplier_payout: {
total_amount_human_ru: toNonEmptyString(valueAmounts[1])
},
net_amount_human_ru: toNonEmptyString(valueAmounts[2]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_business_overview_summary"
}
: null;
const documentCount = Number(directDocumentMatch?.[2] ?? summaryDocumentMatch?.[1]);
const documentBundle = Number.isFinite(documentCount) && documentCount > 0
? {
counterparty,
document_count: documentCount
}
: null;
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
}
function findRecentBusinessOverviewProofBundles(input: {
sessionItems: unknown[];
counterpartyHint: string | null;
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
if (!input.counterpartyHint) {
return null;
}
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(input.sessionItems[index]);
if (input.toNonEmptyString(item?.role) !== "assistant") {
continue;
}
const debug = toRecordObject(item?.debug);
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
if (turnMeaningRef) {
const valueBundle = toRecordObject(turnMeaningRef.previous_counterparty_value_flow_bundle);
const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle);
if (valueBundle || documentBundle) {
const bundleCounterparty = turnMeaningCounterpartyName(
turnMeaningRef,
valueBundle,
documentBundle,
input.toNonEmptyString
);
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
return { valueBundle, documentBundle };
}
}
}
const parsedBundles =
parseBusinessOverviewProofBundlesFromTextV2(item?.text, input.counterpartyHint, input.toNonEmptyString) ??
parseBusinessOverviewProofBundlesFromText(item?.text, input.toNonEmptyString);
if (!parsedBundles) {
continue;
}
const parsedCounterparty =
input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
continue;
}
return parsedBundles;
}
return null;
}
function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
followupContext: Record<string, unknown> | null;
sessionAddressNavigationState: unknown;
predecomposeContract: Record<string, unknown> | null;
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): Record<string, unknown> | null {
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const state = toRecordObject(input.sessionAddressNavigationState);
const sessionContext = toRecordObject(state?.session_context);
const comparisonScope = toRecordObject(sessionContext?.comparison_scope);
const proofBundles = toRecordObject(comparisonScope?.proof_bundles);
const valueBundle = toRecordObject(proofBundles?.counterparty_value_flow_bundle);
const documentBundle = toRecordObject(proofBundles?.counterparty_document_bundle);
if (!valueBundle && !documentBundle) {
return input.followupContext;
}
const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext;
}
const comparisonCounterparty = toRecordObject(comparisonScope?.counterparty);
const counterpartyHint =
(input.followupContext ? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString) : null) ??
input.toNonEmptyString(comparisonCounterparty?.label);
const bundleCounterparty =
input.toNonEmptyString(valueBundle?.counterparty) ?? input.toNonEmptyString(documentBundle?.counterparty);
if (!counterpartyHint || !sameEntityHint(counterpartyHint, bundleCounterparty)) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
previous_discovery_pilot_scope:
input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
"business_overview_route_template_v1",
previous_discovery_loop_status:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_status) ?? "awaiting_clarification",
previous_discovery_loop_selected_chain_id:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
previous_discovery_loop_pending_axes: Array.isArray(input.followupContext?.previous_discovery_loop_pending_axes)
? input.followupContext?.previous_discovery_loop_pending_axes
: ["organization"],
previous_discovery_loop_asked_domain_family:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
previous_discovery_loop_asked_action_family:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
previous_discovery_loop_metadata_scope_hint:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? counterpartyHint,
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? "counterparty",
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? counterpartyHint,
previous_filters: toRecordObject(input.followupContext?.previous_filters) ?? {},
previous_discovery_bidirectional_value_flow: currentValueBundle ?? valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? documentBundle ?? undefined
};
}
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
followupContext: Record<string, unknown> | null;
sessionItems: unknown[];
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): Record<string, unknown> | null {
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
return input.followupContext;
}
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
const currentDocumentBundle = toRecordObject(input.followupContext?.previous_discovery_document_summary);
if (currentValueBundle && currentDocumentBundle) {
return input.followupContext;
}
const counterpartyHint = input.followupContext
? businessOverviewCounterpartyHint(input.followupContext, input.toNonEmptyString)
: null;
const proofBundles = findRecentBusinessOverviewProofBundles({
sessionItems: input.sessionItems,
counterpartyHint,
toNonEmptyString: input.toNonEmptyString
});
if (!proofBundles) {
return input.followupContext;
}
return {
...(input.followupContext ?? {}),
previous_discovery_bidirectional_value_flow:
currentValueBundle ?? proofBundles.valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
};
}
function hasSelectedObjectInventorySignal(text: string | null): boolean {
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
String(text ?? "")
@@ -525,6 +851,17 @@ export async function buildAssistantAddressOrchestrationRuntime(
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
followupContext: discoveryFollowupContext,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const discoveryFollowupContextWithStateProofBundles = mergeBusinessOverviewProofBundlesFromNavigationState({
followupContext: discoveryFollowupContextWithProofBundles,
sessionAddressNavigationState: input.sessionAddressNavigationState,
predecomposeContract,
toNonEmptyString: input.toNonEmptyString
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
input.userMessage,
addressInputMessage,
@@ -540,7 +877,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract,
followupContext: discoveryFollowupContext,
followupContext: discoveryFollowupContextWithStateProofBundles,
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
})) as Record<string, unknown>;
} catch (error) {
@@ -414,6 +414,9 @@ function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
pilotScope: string | null,
actionFamily: string | null
): string | null {
if (pilotScope === "business_overview_route_template_v1" || actionFamily === "broad_evaluation") {
return "business_overview";
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}
@@ -131,6 +131,20 @@ function buildDeterministicSmalltalkLeadReply(): string {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function hasFirstTurnSmalltalkGreetingSignal(value: unknown): boolean {
const normalized = String(value ?? "")
.toLowerCase()
.replace(/\u0451/gu, "\u0435")
.replace(/\s+/gu, " ")
.trim();
if (!normalized) {
return false;
}
return /^(?:привет(?:ик)?|здравствуй(?:те)?|хай|йо|yo|hello|hi|че\s+как|че\s+там)(?:[\s,.!?;:()\-]+(?:как|дела|там|че|что|у\s+тебя|как\s+там|как\s+дела))*[\s,.!?;:()\-]*$/iu.test(
normalized
);
}
function hasConversationExecutiveSummarySignal(value: unknown): boolean {
const normalized = String(value ?? "")
.toLowerCase()
@@ -215,6 +229,14 @@ export async function runAssistantLivingChatRuntime(
let knownOrganizations = [...organizationAuthority.knownOrganizations];
let selectedOrganization = organizationAuthority.selectedOrganization;
let activeOrganization = organizationAuthority.activeOrganization;
const shouldHandleFirstTurnSmalltalkDeterministically =
!selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
hasFirstTurnSmalltalkGreetingSignal(userMessage) &&
input.hasLivingChatSignal(userMessage);
const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object"
? input.addressRuntimeMeta
: {}) as Record<string, unknown>;
@@ -365,6 +387,27 @@ export async function runAssistantLivingChatRuntime(
} else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
} else if (shouldHandleFirstTurnSmalltalkDeterministically) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = Boolean(proactiveOffer);
livingChatSource = proactiveOffer
? "deterministic_smalltalk_with_proactive_scope_offer"
: "deterministic_smalltalk";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
} else {
chatText = await input.executeLlmChat();
const scriptGuard = input.applyScriptGuard(chatText, userMessage);
@@ -385,36 +428,6 @@ export async function runAssistantLivingChatRuntime(
livingChatGroundingGuardReason = groundingGuard.reason;
livingChatSource = "llm_chat_grounding_guard";
}
const shouldOfferProactiveOrganizationScope =
!selectedOrganization &&
!activeOrganization &&
!continuitySnapshot.hasGroundedAddressContext &&
!hasPriorAssistantTurn(input.sessionItems) &&
input.modeDecision?.mode === "chat" &&
input.hasLivingChatSignal(userMessage);
if (shouldOfferProactiveOrganizationScope) {
const proactiveScopeProbe = await input.resolveDataScopeProbe();
const mergedKnownOrganizations = input.mergeKnownOrganizations([
...knownOrganizations,
...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : [])
]);
knownOrganizations = mergedKnownOrganizations;
if (!activeOrganization && mergedKnownOrganizations.length === 1) {
activeOrganization = mergedKnownOrganizations[0];
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
}
}
}
if (!chatText) {
@@ -42,7 +42,11 @@ export interface AssistantMcpDiscoveryExecutionHandoffContract {
reason_codes: string[];
}
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = ["value_flow"];
const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = [
"value_flow",
"value_flow_comparison",
"business_overview"
];
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
@@ -47,19 +47,66 @@ function normalizeQuestionText(value: unknown): string {
.trim();
}
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
const text = normalizeQuestionText([
function normalizedTurnAndGraphText(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): string {
return normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
}
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
return (
/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)
);
}
function requestsBroadBusinessOverviewSurface(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:не\s+обзор|просто\s+ден\p{L}*|одной\s+строк\p{L}*|только\s+итог|без\s+разбив\p{L}*)/iu.test(text)) {
return false;
}
if (/(?:бизнес[-\s]*обзор|взросл\p{L}{0,10}\s+бизнес|что\s+(?:пока\s+)?нельзя\s+утвержд)/iu.test(text)) {
return true;
}
const markers = [
/(?:ндс|налог\p{L}*)/iu,
/(?:долг\p{L}*|дебитор|кредитор)/iu,
/(?:склад|остатк|товар\p{L}*)/iu,
/(?:клиент|заказчик|покупател)/iu,
/(?:поставщик|получател)/iu,
/(?:оборот\p{L}*)/iu,
/(?:ограничен|не\s+подтвержд|нельзя\s+утвержд)/iu
];
return markers.filter((marker) => marker.test(text)).length >= 3;
}
function requestsCounterpartyLeaderSurface(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
if (!text) {
return false;
}
if (/(?:кто|кому)[\s\S]{0,60}(?:больше\s+всего|крупнее\s+всего|основн\p{L}*)[\s\S]{0,60}(?:зан[её]с|прин[её]с|платил|ушло|получил|перев[её]л|заплатил|внес|вн[её]с)/iu.test(text)) {
return true;
}
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
}
function requestsCompactCashflowAnswer(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
@@ -850,6 +897,49 @@ function buildPreviousCounterpartyValueFlowSummary(
};
}
function buildBoundarySummaryFromPreviousCounterpartyBundles(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract
): string | null {
const turnInput = toRecordObject(entryPoint.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const graph = toRecordObject(turnInput?.data_need_graph);
const bridge = toRecordObject(entryPoint.bridge);
const pilot = toRecordObject(bridge?.pilot);
const overview = toRecordObject(pilot?.derived_business_overview);
const isBusinessOverview =
toNonEmptyString(graph?.business_fact_family) === "business_overview" ||
toNonEmptyString(turnMeaning?.asked_domain_family) === "business_overview";
if (!isBusinessOverview || overview) {
return null;
}
const organizationScope = businessOverviewOrganizationScopeLabel(turnMeaning?.explicit_organization_scope);
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
separateSubject,
toRecordObject(turnMeaning?.previous_counterparty_document_bundle)
);
if (!separateSubject || !previousCounterpartySummary) {
return null;
}
const lines = organizationScope
? [
`Коротко: по компании ${organizationScope} в этом шаге нет нового полного company-level расчета; отдельно по выбранному контрагенту ${separateSubject} есть ранее подтвержденный контрагентский срез.`,
previousCounterpartySummary.line,
`Можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не подтверждает чистую прибыль, полный оборот или общую бизнес-роль ${separateSubject}; также нельзя смешивать этот контрагентский срез с выводами по компании без отдельного company-level расчета.`
]
: [
`Коротко: уточните, по какой компании/организации сравнить выбранного контрагента ${separateSubject}; company-level вывод без организации не подтверждаю.`,
previousCounterpartySummary.line,
`Уже можно утверждать: по ${separateSubject} отдельно подтверждены входящие/исходящие денежные строки, расчетное нетто и документы из предыдущего контрагентского среза.`,
`Нельзя утверждать: это не чистая прибыль, не полный оборот компании и не доказанная бизнес-роль ${separateSubject}; контрагентский срез нельзя смешивать с company-level выводом без выбранной компании.`
];
return joinBusinessReplyLines(lines);
}
function buildCompactBusinessOverviewReply(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
draft: Record<string, unknown>
@@ -921,6 +1011,13 @@ function buildCompactBusinessOverviewReply(
: null;
const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const broadOverviewSurfaceRequested = requestsBroadBusinessOverviewSurface(turnMeaning, graph);
const counterpartyLeaderSurfaceRequested = requestsCounterpartyLeaderSurface(turnMeaning, graph);
const directMoneyOnlyAnswer =
directMoneyAnswer && !broadOverviewSurfaceRequested && !counterpartyLeaderSurfaceRequested;
const shouldIncludeCounterpartyLeaders =
!directMoneyOnlyAnswer || counterpartyLeaderSurfaceRequested || broadOverviewSurfaceRequested;
const shouldIncludeOverviewSurface = !directMoneyAnswer || broadOverviewSurfaceRequested;
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
const lines: string[] = [];
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
@@ -931,9 +1028,22 @@ function buildCompactBusinessOverviewReply(
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const compactCashflowRequested = directMoneyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const rawMessageComparable = compactComparable(rawMessage);
const organizationScopeComparable = compactComparable(organizationScope);
const plainOrganizationClarificationSelection = Boolean(
separateSubject &&
organizationScope &&
rawMessage &&
rawMessageComparable &&
organizationScopeComparable &&
rawMessageComparable.includes(organizationScopeComparable) &&
rawMessage.length <= 90 &&
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)
);
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
@@ -955,6 +1065,23 @@ function buildCompactBusinessOverviewReply(
return joinBusinessReplyLines(lines);
}
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(
`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
);
if (previousCounterpartySummary) {
lines.push(previousCounterpartySummary.line);
} else {
lines.push(
`Отдельно по выбранному контрагенту ${separateSubject}: суммы компании на него не переношу; в этом шаге держу только границу, что это отдельный контрагентский контур.`
);
}
lines.push(
`Нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические роли клиентов/поставщиков и выводы по ${separateSubject} на основе company-level сумм без отдельного контрагентского среза.`
);
return joinBusinessReplyLines(lines);
}
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) {
@@ -1178,7 +1305,13 @@ function buildCompactBusinessOverviewReply(
return joinBusinessReplyLines(lines);
}
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
if (
!separateSubject &&
!crossScopeExecutiveSummary &&
!counterpartyLeaderSurfaceRequested &&
!rankingNeed &&
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")
) {
const subject = organizationScope ?? "компания";
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
lines.push(
@@ -1208,6 +1341,10 @@ function buildCompactBusinessOverviewReply(
: `- крупнейший получатель исходящих денег: ${topSupplier};`
);
}
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
lines.push(`- ${localizeLine(taxLine)}`);
}
const inventoryLine = businessOverviewInventoryLine(overview);
if (inventoryLine) {
lines.push(`- ${localizeLine(inventoryLine)}`);
@@ -1220,7 +1357,7 @@ function buildCompactBusinessOverviewReply(
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
);
const missingOverviewFamilies: string[] = [];
if (!businessOverviewTaxLine(overview)) {
if (!taxLine) {
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
}
if (!debtLine) {
@@ -1264,9 +1401,26 @@ function buildCompactBusinessOverviewReply(
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
(incomingAmount || outgoingAmount || netAmount);
if (explicitPeriodRankingOverview) {
lines.push(
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
);
if (counterpartyLeaderSurfaceRequested) {
const incomingLeaderText =
customerName && customerAmount
? topCustomerLooksFinancial
? `${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это банк/финансовый контур, не называю его обычной клиентской выручкой без назначения платежа${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
: `${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
: "не распознан";
const outgoingLeaderText = topSupplier
? topSupplierLooksFinancial
? `${topSupplier}; это банк/финансовый контур, не называю его обычным поставщиком без назначения платежа/договора${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: topSupplier
: "не распознан";
lines.push(
`Коротко: ${organizationPrefix}${period} больше всего занес ${incomingLeaderText}; больше всего ушло ${outgoingLeaderText}.`
);
} else {
lines.push(
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
);
}
lines.push(
`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
);
@@ -1332,7 +1486,7 @@ function buildCompactBusinessOverviewReply(
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
);
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) {
if (shouldIncludeCounterpartyLeaders && customerName && customerAmount) {
lines.push(
topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
@@ -1353,21 +1507,21 @@ function buildCompactBusinessOverviewReply(
);
}
if (!directMoneyAnswer && topSupplier) {
if (shouldIncludeCounterpartyLeaders && topSupplier) {
lines.push(
topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
);
}
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
if (shouldIncludeCounterpartyLeaders && (topCustomer || topSupplier)) {
lines.push(
topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
);
}
if (!directMoneyAnswer) {
if (shouldIncludeOverviewSurface) {
lines.push(
`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`
);
@@ -1466,6 +1620,11 @@ function buildReplyText(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContra
return null;
}
const previousCounterpartyBoundaryReply = buildBoundarySummaryFromPreviousCounterpartyBundles(entryPoint);
if (previousCounterpartyBoundaryReply) {
return previousCounterpartyBoundaryReply;
}
const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft);
if (compactBidirectionalValueFlowReply) {
return compactBidirectionalValueFlowReply;
@@ -510,6 +510,13 @@ function hasExactDocumentListAddressReply(
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
return false;
}
if (
hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint) ||
hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)
) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const isDocumentIntent =
@@ -432,7 +432,12 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(
reasonCodes,
executionHandoff.can_use_guarded_response
? "runtime_bridge_wired_to_guarded_hot_assistant_answer"
: "runtime_bridge_not_wired_to_hot_assistant_answer"
);
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
@@ -117,6 +117,7 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
"этой компании",
"этой компанией",
"эту компанию",
"в целом",
"наша организация",
"нашей организации",
"нашей компанией"
@@ -250,7 +251,13 @@ function normalizeFollowupCounterpartyCandidate(value: unknown): string | null {
if (!text || isInvalidEntityCandidate(text)) {
return null;
}
return text;
const cleaned = text
.replace(
/^(?:\u043f\u043e\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435|\u044b|\u0430\u043c|\u0430\u043c\u0438|\u0430\u0445)?\s+/iu,
""
)
.trim();
return cleaned && !isInvalidEntityCandidate(cleaned) ? cleaned : text;
}
function pushScopedEntityCandidate(
@@ -702,12 +709,13 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
const normalizedDiscoveryEntities = discoveryEntities
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
.filter((entity): entity is string => Boolean(entity));
const normalizedLoopMetadataScopeHint = normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const groundedDiscoveryCounterparty =
ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
? null
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
: normalizedDiscoveryEntities[0] ?? normalizedLoopMetadataScopeHint;
const metadataScopeHint =
loopMetadataScopeHint ??
normalizedLoopMetadataScopeHint ??
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
@@ -724,9 +732,10 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null);
const dateScope =
collectDateScopeFromFilters(previousFilters) ??
collectDateScopeFromFilters(rootFilters);
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
const dateScope = loopProvidedAllTimeScope
? "all_time_scope"
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
return {
pilotScope: effectivePilotScope,
domain: mapped.domain,
@@ -986,7 +995,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text: string): boolea
function hasCrossScopeExecutiveSummarySignal(text: string): boolean {
return (
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary)/iu.test(
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0441\u0440\u0430\u0432\u043d\p{L}*|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary|brief(?:ly)?\s+compare)/iu.test(
text
) &&
/(?:\u0447\u0442\u043e\s+(?:\u043c\u044b\s+)?\u043f\u043e\u0434\u0442\u0432\u0435\u0440\p{L}*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043f\u043e\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|confirmed|company|organization)/iu.test(
@@ -1013,6 +1022,35 @@ function hasPlainBusinessOverviewSignal(text: string): boolean {
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
}
function countBroadBusinessOverviewAxes(text: string): number {
const axisPatterns = [
/(?:\u0434\u0435\u043d\p{L}*|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|\u043f\u043b\u0430\u0442\p{L}*|money|cash|revenue|turnover)/iu,
/(?:\u043d\u0434\u0441|vat)/iu,
/(?:\u0434\u043e\u043b\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\p{L}*|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\p{L}*|receivable|payable|debt)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440|warehouse|stock|inventory)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|customer|client|buyer)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|\u0437\u0430\u043a\u0443\u043f|supplier|vendor|procurement)/iu,
/(?:\u0433\u0434\u0435[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0434\u0435\u043b\u0430\p{L}*|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|\u0447\u0442\u043e[\s\S]{0,80}(?:\u043d\u0435\u043b\u044c\u0437\u044f|\u043d\u0435\s+\u0445\u0432\u0430\p{L}*)|cannot|unknown|missing|limitation)/iu
];
return axisPatterns.reduce((count, pattern) => count + (pattern.test(text) ? 1 : 0), 0);
}
function hasBroadBusinessOverviewSurfaceSignal(text: string): boolean {
const normalized = compactLower(text);
if (!normalized) {
return false;
}
const hasBroadCue =
/(?:\u043f\u043e[-\s]*\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\p{L}*|\u0447\u0442\u043e\s+\u043f\u043e\s+\u0431\u0438\u0437\u043d\u0435\u0441\u0443\s+\u0432\u0438\u0434\u043d\p{L}*|\u043f\u043e\u0441\u043c\u043e\u0442\p{L}*[\s\S]{0,100}(?:\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\p{L}*)|\u0431\u0438\u0437\u043d\u0435\u0441[\s\S]{0,80}(?:\u0432\u0438\u0434\u043d\p{L}*|\u0432\u044b\u0432\u043e\u0434|\u0441\u0440\u0435\u0437)|human\s+readable\s+business\s+view)/iu.test(
normalized
);
const hasCompanyScope =
/(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+1\s?\u0441|1\s?c|company|organization|business|(?:19|20)\d{2})/iu.test(
normalized
);
return hasBroadCue && hasCompanyScope && countBroadBusinessOverviewAxes(normalized) >= 3;
}
function hasBusinessOverviewSignal(text: string): boolean {
if (
hasCrossScopeExecutiveSummarySignal(text) ||
@@ -1021,6 +1059,7 @@ function hasBusinessOverviewSignal(text: string): boolean {
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
hasPlainBusinessOverviewSignal(text) ||
hasBroadBusinessOverviewSurfaceSignal(text) ||
hasOrganizationLevelSupplierQualityOverviewSignal(text)
) {
return true;
@@ -1101,6 +1140,15 @@ function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
);
}
function isGenericSelectedCounterpartyReference(value: string | null): boolean {
if (!value) {
return false;
}
return /^(?:(?:\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*|\u044d\u0442\p{L}*|\u0434\u0430\u043d\p{L}*|\u0442\u0435\u043a\u0443\u0449\p{L}*)\s+)?\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*$|^(?:selected|chosen|current|this)\s+counterpart(?:y|ies)?$/iu.test(
value
);
}
function businessOverviewSeparateCounterpartyCandidateFromText(text: string): string | null {
const source = repairAddressMojibakeText(String(text ?? ""));
const patterns = [
@@ -1109,7 +1157,7 @@ function businessOverviewSeparateCounterpartyCandidateFromText(text: string): st
];
for (const pattern of patterns) {
const candidate = normalizeFollowupCounterpartyCandidate(source.match(pattern)?.[1]);
if (candidate && !isInvalidEntityCandidate(candidate)) {
if (candidate && !isInvalidEntityCandidate(candidate) && !isGenericSelectedCounterpartyReference(candidate)) {
return candidate;
}
}
@@ -1245,6 +1293,9 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
if (hasYearOnlyTimeTail) {
return null;
}
if (new Set(["в целом", "компания в целом", "организация в целом"]).has(comparable)) {
return null;
}
if (/^(?:\u0438|\u0432|\u0432\u043e|\u0437\u0430|\u043d\u0430|\u043f\u043e|\u043a\u0442\u043e|\u0447\u0442\u043e|\u043a\u0430\u043a(?:\u043e\u0439|\u0430\u044f|\u0438\u0435)?|\u0433\u043b\u0430\u0432\u043d\p{L}*)\b/iu.test(comparable)) {
return null;
}
@@ -1910,11 +1961,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
const businessOverviewSignal =
!businessOverviewCounterpartyValueFlowPivot &&
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
const organizationClarificationBusinessOverviewLoop = Boolean(
followupSeed.loopStatus === "awaiting_clarification" &&
followupSeed.loopSelectedChainId === "business_overview" &&
followupSeed.loopPendingAxes.includes("organization") &&
currentTurnOrganizationScope &&
!rawLifecycleSignal &&
!rawMetadataSignal
);
const businessOverviewSeparateCounterpartySignal = Boolean(
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
);
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
: organizationClarificationBusinessOverviewLoop
? followupSeed.counterparty ?? followupSeed.discoveryEntity ?? followupSeed.metadataScopeHint
: null;
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
@@ -1944,6 +2005,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const businessOverviewSuppressesFollowupCounterparty = Boolean(
businessOverviewSignal &&
!businessOverviewSeparateCounterpartySignal &&
!organizationClarificationBusinessOverviewLoop &&
(rawBusinessOverviewSignal ||
businessOverviewContinuationSignal ||
broadBusinessEvaluationUnsupported ||
@@ -2452,7 +2514,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
}
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
}
const businessOverviewSeparateCounterpartyDisplayCandidate = businessOverviewSeparateCounterpartySignal
const shouldPreserveBusinessOverviewSeparateCounterparty =
businessOverviewSeparateCounterpartySignal || organizationClarificationBusinessOverviewLoop;
const businessOverviewSeparateCounterpartyDisplayCandidate = shouldPreserveBusinessOverviewSeparateCounterparty
? preferredScopedDisplayName(businessOverviewSeparateCounterpartyCandidate, [
groundedFollowupEntity,
effectiveFollowupCounterparty,
@@ -2461,7 +2525,24 @@ export function buildAssistantMcpDiscoveryTurnInput(
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
])
]) ??
preferredScopedDisplayName(
groundedFollowupEntity ??
effectiveFollowupCounterparty ??
followupSeed.discoveryEntity ??
normalizedPredecomposeCounterparty ??
rawScopedEntityCandidate ??
rawEntityCandidate,
[
groundedFollowupEntity,
effectiveFollowupCounterparty,
followupSeed.discoveryEntity,
normalizedPredecomposeCounterparty,
rawScopedEntityCandidate,
rawEntityCandidate,
...entityCandidates
]
)
: null;
const businessOverviewSeparateEntityCandidates = businessOverviewSeparateCounterpartyDisplayCandidate
? [businessOverviewSeparateCounterpartyDisplayCandidate]
@@ -2586,6 +2667,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const normalizedAssistantTurnMeaningDateScope =
rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(organizationClarificationBusinessOverviewLoop && !currentTurnCarriesExplicitPeriod) ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
? null
: assistantTurnMeaningDateScope;
@@ -2603,8 +2685,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
normalizedPredecomposeDateScope &&
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
);
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
const explicitDateScope =
rawAllTimeScopeSignal
rawAllTimeScopeSignal || followupAllTimeScopeApplied
? null
: normalizedAssistantTurnMeaningDateScope ??
(businessOverviewRawYearOverridesPredecomposeAsOf ? rawDateScope : normalizedPredecomposeDateScope) ??
@@ -2615,7 +2698,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
!normalizedAssistantTurnMeaningDateScope &&
!normalizedPredecomposeDateScope &&
!rawDateScope &&
normalizedFollowupDateScope
normalizedFollowupDateScope &&
normalizedFollowupDateScope !== "all_time_scope"
);
const clarificationLoopSeedApplied = Boolean(
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId
@@ -2668,7 +2752,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed
: undefined,
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
explicit_entity_candidates:
businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
previous_counterparty_value_flow_bundle:
businessOverviewSignal && followupSeed.previousBidirectionalValueFlow
@@ -2897,6 +2982,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (followupAllTimeScopeApplied) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_from_followup_context");
}
if (suppressNegatedTaxOnlyDateScope) {
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
}
@@ -2999,7 +3087,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (businessOverviewSuppressesFollowupCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty");
}
if (businessOverviewSeparateCounterpartySignal) {
if (shouldPreserveBusinessOverviewSeparateCounterparty) {
pushReason(reasonCodes, "mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope");
}
if (businessOverviewSeparateCounterpartyCandidate) {
@@ -3023,7 +3111,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty && !businessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
}
if (
@@ -136,6 +136,26 @@ export function createAssistantTransitionPolicy(deps) {
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
}
function hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage = null) {
return [userMessage, alternateMessage]
.filter((value) => deps.toNonEmptyString(value))
.map((value) => normalizeFollowupText(value).replace(/С‘/g, "Рµ"))
.some((normalized) => {
if (!normalized) {
return false;
}
const hasDocumentCue =
/(?:\u0434\u043e\u043a\p{L}*|\u0441\u0447\p{L}*|\u043d\u0430\u043a\u043b\u0430\u0434\p{L}*|\u0430\u043a\u0442|document|docs?|invoice|receipt)/iu.test(
normalized
) || hasReadableDocumentsPivotCue(normalized);
const hasSelectedCounterpartyCue =
/(?:\u043f\u043e\s+\u043d(?:\u0435\u043c\u0443|\u0435\u0439)|\u043f\u043e\s+\u044d\u0442(?:\u043e\u043c\u0443|\u043e\u0439)|\u0442\u0435\u043a\u0443\u0449\p{L}*\s+\u043e\u0431\u044a\u0435\u043a\p{L}*|\u0432\u044b\u0431\u0440\u0430\u043d\p{L}*\s+(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u044a\u0435\u043a\u0442)|selected\s+(?:counterparty|object)|current\s+object)/iu.test(
normalized
);
return hasDocumentCue && hasSelectedCounterpartyCue;
});
}
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
return null;
@@ -425,20 +445,71 @@ export function createAssistantTransitionPolicy(deps) {
return flow;
}
function readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_value_flow_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug) {
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
const bundle = entryPoint?.turn_input?.turn_meaning_ref?.previous_counterparty_document_bundle;
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
return null;
}
return bundle;
}
function readCounterpartyDocumentSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
const match = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (!match?.[1] || !match?.[2]) {
const directMatch = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
if (directMatch?.[1] && directMatch?.[2]) {
return {
counterparty: deps.toNonEmptyString(directMatch[1]),
document_count: Number(directMatch[2]),
direct_answer: firstLine
};
}
const summaryMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):[\s\S]{0,260}документы\s+по\s+цепочке:\s*найдено\s*(\d+)/iu);
if (!summaryMatch?.[1] || !summaryMatch?.[2]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(summaryMatch[1]),
document_count: Number(summaryMatch[2]),
direct_answer: summaryMatch[0].replace(/\s+/g, " ").trim()
};
}
function readCounterpartyValueFlowSummaryFromItem(item) {
const text = deps.toNonEmptyString(item?.text);
if (!text) {
return null;
}
const match = text.match(
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
);
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
return null;
}
return {
counterparty: deps.toNonEmptyString(match[1]),
document_count: Number(match[2]),
direct_answer: firstLine
incoming_customer_revenue: {
total_amount_human_ru: deps.toNonEmptyString(match[2])
},
outgoing_supplier_payout: {
total_amount_human_ru: deps.toNonEmptyString(match[3])
},
net_amount_human_ru: deps.toNonEmptyString(match[4]),
net_direction: "net_incoming",
inference_basis: "parsed_from_previous_confirmed_counterparty_boundary_summary"
};
}
@@ -446,12 +517,51 @@ export function createAssistantTransitionPolicy(deps) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
if (!item || item.role !== "assistant" || !debug || typeof debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
if (debug && typeof debug === "object") {
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
if (flow) {
return flow;
}
}
const parsedFlow = readCounterpartyValueFlowSummaryFromItem(item);
if (parsedFlow) {
return parsedFlow;
}
}
return null;
}
function sameCounterpartyHint(expected, actual) {
const left = normalizeFollowupText(expected);
const right = normalizeFollowupText(actual);
if (!left || !right) {
return false;
}
return left === right || left.includes(right) || right.includes(left);
}
function findRecentPreviousCounterpartyValueFlowBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyValueFlowBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
@@ -460,7 +570,8 @@ export function createAssistantTransitionPolicy(deps) {
function findRecentCounterpartyDocumentBundle(items) {
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem) {
continue;
}
const summary = readCounterpartyDocumentSummaryFromItem(item);
@@ -471,6 +582,29 @@ export function createAssistantTransitionPolicy(deps) {
return null;
}
function findRecentPreviousCounterpartyDocumentBundle(items, counterpartyHint = null) {
const expectedCounterparty = deps.toNonEmptyString(counterpartyHint);
if (!expectedCounterparty) {
return null;
}
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
const item = items[index];
const debug = item?.debug;
const isAssistantItem = item?.role === "assistant" || item?.kind === "assistant";
if (!item || !isAssistantItem || !debug || typeof debug !== "object") {
continue;
}
const bundle = readMcpDiscoveryPreviousCounterpartyDocumentBundle(debug);
if (!bundle) {
continue;
}
if (sameCounterpartyHint(expectedCounterparty, deps.toNonEmptyString(bundle.counterparty))) {
return bundle;
}
}
return null;
}
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
if (
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
@@ -681,10 +815,22 @@ export function createAssistantTransitionPolicy(deps) {
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ||
hasCompactCashflowFollowupCue(String(alternateMessage ?? ""))
: false);
const earlyNavigationSessionState = resolveNavigationSessionContextState(
addressNavigationState,
deps.toNonEmptyString,
deps.normalizeOrganizationScopeValue
);
const earlyNavigationFocusObject = earlyNavigationSessionState.focusObject;
const selectedCounterpartyDocumentFollowupSignal = Boolean(
deps.toNonEmptyString(earlyNavigationFocusObject?.label) &&
deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty" &&
hasSelectedCounterpartyDocumentFollowupSignal(userMessage, alternateMessage)
);
if (
assistantTurnMeaning?.stale_replay_forbidden === true &&
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage) &&
!compactCashflowFollowupSignal
!compactCashflowFollowupSignal &&
!selectedCounterpartyDocumentFollowupSignal
) {
return null;
}
@@ -784,11 +930,7 @@ export function createAssistantTransitionPolicy(deps) {
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const hasBusinessOverviewCarryoverSourceHint =
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = resolveNavigationSessionContextState(
addressNavigationState,
deps.toNonEmptyString,
deps.normalizeOrganizationScopeValue
);
const navigationSessionState = earlyNavigationSessionState;
const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
@@ -912,6 +1054,7 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@@ -937,6 +1080,7 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
selectedCounterpartyDocumentFollowupSignal ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
inventoryMarginRankingFollowup ||
@@ -969,6 +1113,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@@ -987,6 +1132,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
@@ -1062,9 +1208,22 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryCounterpartyHint =
sourceDiscoveryLoopMetadataScopeHint ??
(deps.toNonEmptyString(earlyNavigationFocusObject?.objectType) === "counterparty"
? deps.toNonEmptyString(earlyNavigationFocusObject?.label)
: null);
const sourceDiscoveryBidirectionalValueFlow =
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ?? findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary = findRecentCounterpartyDocumentBundle(items);
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ??
readMcpDiscoveryPreviousCounterpartyValueFlowBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyValueFlowBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyValueFlowSummaryFromItem(previousAddressItem) ??
findRecentDiscoveryValueFlowBundle(items);
const sourceDiscoveryDocumentSummary =
readMcpDiscoveryPreviousCounterpartyDocumentBundle(carryoverSourceDebug) ??
findRecentPreviousCounterpartyDocumentBundle(items, sourceDiscoveryCounterpartyHint) ??
readCounterpartyDocumentSummaryFromItem(previousAddressItem) ??
findRecentCounterpartyDocumentBundle(items);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@@ -1240,6 +1399,17 @@ export function createAssistantTransitionPolicy(deps) {
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
let previousFilters = resolveAddressDebugCarryoverFilters(carryoverSourceDebug, deps.toNonEmptyString);
const navigationCounterpartyFocus =
navigationFocusObjectType === "counterparty" ? navigationFocusObjectLabel : null;
const hasNavigationCounterpartyFocusCarryover = Boolean(
navigationCounterpartyFocus &&
(hasValueFlowCarryoverSourceHint ||
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "open_items_by_counterparty_or_contract" ||
sourceDiscoveryLoopSelectedChainIdHint === "value_flow_comparison")
);
const shouldBackfillHistoricalPartyAnchors =
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
@@ -1254,6 +1424,15 @@ export function createAssistantTransitionPolicy(deps) {
deps.findRecentAddressFilterValue(items, "counterparty"),
deps.toNonEmptyString
);
if (hasNavigationCounterpartyFocusCarryover && navigationCounterpartyFocus) {
if (!previousAnchor) {
previousAnchorType = "counterparty";
previousAnchor = navigationCounterpartyFocus;
}
if (!deps.toNonEmptyString(previousFilters.counterparty)) {
previousFilters.counterparty = navigationCounterpartyFocus;
}
}
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
const authorityActiveOrganization =
deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??
@@ -65,6 +65,14 @@ export interface AddressNavigationEvent {
export interface AddressNavigationSessionContext {
active_result_set_id: string | null;
active_focus_object: AddressFocusObject | null;
comparison_scope: {
organization: AddressFocusObject | null;
counterparty: AddressFocusObject | null;
proof_bundles: {
counterparty_value_flow_bundle: Record<string, unknown> | null;
counterparty_document_bundle: Record<string, unknown> | null;
} | null;
} | null;
last_confirmed_route: string | null;
date_scope: {
as_of_date: string | null;
@@ -7,6 +7,7 @@ import type {
} from "./assistantRuntimeContracts";
export type AddressIntent =
| "business_overview"
| "period_coverage_profile"
| "document_type_and_account_section_profile"
| "counterparty_population_and_roles"
@@ -529,6 +529,11 @@ export interface AssistantDebugPayload {
fa_live_route_audit?: FaLiveRouteAuditDebug;
eligibility_time_basis?: GroundedAnswerEligibilityGuardDebug["eligibility_time_basis"];
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
mcp_discovery_response_applied?: boolean;
mcp_discovery_selected_chain_id?: string | null;
mcp_discovery_effective_response_route?: string | null;
assistant_mcp_discovery_entry_point_v1?: unknown;
mcp_discovery_response_candidate_v1?: unknown;
followup_state_usage?: FollowupStateUsageDebug;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
@@ -72,6 +72,15 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.reasons).toContain("unicode_business_overview_debt_position_deferred_to_discovery");
});
it("defers colloquial multi-surface company overview to discovery before exact VAT", () => {
const result = resolveAddressIntent(
"\u041f\u043e-\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0447\u0442\u043e \u043f\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u0432\u0438\u0434\u043d\u043e \u0432 1\u0421 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0430\u043c, \u0441\u043a\u043b\u0430\u0434\u0443, \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c \u0438 \u0433\u0434\u0435 \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c \u0432\u044b\u0432\u043e\u0434?"
);
expect(result.intent).toBe("unknown");
expect(result.reasons).toContain("unicode_business_overview_multi_surface_deferred_to_discovery");
});
it("detects specific counterparty turnover wording as revenue profile", () => {
const result = resolveAddressIntent(
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a"
@@ -55,6 +55,231 @@ describe("address navigation state", () => {
expect(evolved.navigation_history[0]?.action).toBe("open");
});
it("captures organization focus from applied business overview discovery turns", () => {
const base = createEmptyAddressNavigationState("asst-bo", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-bo1",
session_id: "asst-bo",
role: "assistant",
text: "Коротко: по данным ООО Альтернатива Плюс за 2020 подтвержден бизнес-обзор.",
reply_type: "partial_coverage",
created_at: "2026-04-12T10:10:00.000Z",
trace_id: "address-bo",
debug: {
detected_mode: "address_query",
detected_intent: "unknown",
selected_recipe: null,
extracted_filters: {},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "business_overview",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
},
dialog_continuation_contract_v2: {
decision: "new_topic"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
expect(evolved.result_sets[0]?.route_id).toBe("business_overview");
expect(evolved.result_sets[0]?.filters.organization).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.active_focus_object?.object_type).toBe("organization");
expect(evolved.session_context.active_focus_object?.label).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.organization_scope).toBe("ООО Альтернатива Плюс");
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => {
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-vf1",
session_id: "asst-vf",
role: "assistant",
text: "\u041f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438.",
reply_type: "partial_coverage",
created_at: "2026-04-12T10:12:00.000Z",
trace_id: "chat-vf",
debug: {
detected_mode: "chat",
detected_intent: "unknown",
selected_recipe: null,
extracted_filters: {},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "value_flow_comparison",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "compare_incoming_outgoing",
explicit_entity_candidates: ["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"],
stale_replay_forbidden: true,
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
answer_draft: {
answer_mode: "confirmed_factual"
}
}
},
dialog_continuation_contract_v2: {
decision: "new_topic"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
expect(evolved.result_sets[0]?.route_id).toBe("value_flow_comparison");
expect(evolved.result_sets[0]?.intent).toBe("customer_revenue_and_payments");
expect(evolved.result_sets[0]?.filters.counterparty).toBe("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
expect(evolved.session_context.active_focus_object?.label).toBe("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("keeps selected counterparty focus for company boundary summaries", () => {
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-boundary",
session_id: "asst-boundary",
role: "assistant",
text:
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез.\n" +
"Отдельно по выбранному контрагенту Группа СВК: суммы компании на него не переношу.",
created_at: "2026-04-12T10:05:00.000Z",
debug: {
detected_intent: "unknown",
mcp_discovery_selected_chain_id: "business_overview",
mcp_discovery_response_applied: true,
extracted_filters: {
organization: "Альтернатива Плюс"
},
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "Альтернатива Плюс"
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: []
}
}
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(initial, assistantItem, 1);
expect(evolved.session_context.organization_scope).toBe("Альтернатива Плюс");
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(evolved.session_context.comparison_scope?.organization?.label).toBe("Альтернатива Плюс");
expect(evolved.session_context.comparison_scope?.counterparty?.label).toBe("Группа СВК");
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
});
it("carries comparison proof bundles through organization clarification", () => {
const org = "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const counterparty = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
const valueFlowBundle = {
counterparty,
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
};
const documentBundle = { counterparty, document_count: 19 };
const initial = createEmptyAddressNavigationState("asst-proof", "2026-04-12T10:00:00.000Z");
const boundaryItem = {
message_id: "msg-boundary-proof",
session_id: "asst-proof",
role: "assistant",
text: `Коротко: уточните организацию для сравнения ${counterparty}.`,
created_at: "2026-04-12T10:05:00.000Z",
debug: {
detected_intent: "unknown",
mcp_discovery_selected_chain_id: "business_overview",
mcp_discovery_response_applied: true,
extracted_filters: {},
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterparty],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle
}
}
}
}
} as any;
const afterBoundary = evolveAddressNavigationStateWithAssistantItem(initial, boundaryItem, 1);
expect(afterBoundary.session_context.comparison_scope?.organization).toBeNull();
expect(afterBoundary.session_context.comparison_scope?.counterparty?.label).toBe(counterparty);
expect(afterBoundary.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
const clarifiedItem = {
message_id: "msg-boundary-org",
session_id: "asst-proof",
role: "assistant",
text: `Коротко: по компании ${org} подтвержден company-level срез. Отдельно по ${counterparty}: границу не теряю.`,
created_at: "2026-04-12T10:06:00.000Z",
debug: {
detected_intent: "business_overview",
mcp_discovery_selected_chain_id: "business_overview",
mcp_discovery_response_applied: true,
extracted_filters: { organization: org },
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterparty],
explicit_organization_scope: org
}
}
}
}
} as any;
const afterClarification = evolveAddressNavigationStateWithAssistantItem(afterBoundary, clarifiedItem, 2);
expect(afterClarification.session_context.comparison_scope?.organization?.label).toBe(org);
expect(afterClarification.session_context.comparison_scope?.counterparty?.label).toBe(counterparty);
expect(afterClarification.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
expect(afterClarification.session_context.comparison_scope?.proof_bundles?.counterparty_document_bundle).toEqual(
documentBundle
);
});
it("tracks drilldown event for follow-up continuation turn", () => {
const initial = normalizeAddressNavigationState(
{
@@ -2145,6 +2145,43 @@ describe("address compose stage utf8 headers", () => {
expect(reply.semantics?.balance_confirmed).toBe(true);
});
it("explains purchase-date anchor for confirmed VAT tax-period reply", () => {
const reply = composeFactualReply(
"vat_liability_confirmed_for_tax_period",
[
{
period: "2015-02-28T23:59:59Z",
registrator: "VAT_BOOK_SALES",
account_dt: "68.02",
account_kt: "",
amount: 3500000,
analytics: []
},
{
period: "2015-02-28T23:59:59Z",
registrator: "VAT_BOOK_PURCHASES",
account_dt: "19",
account_kt: "",
amount: 868612,
analytics: []
}
],
{
userMessage: "ндс можешь прикинуть на дату покупки рабочей станции?",
periodFrom: "2015-02-01",
periodTo: "2015-02-28",
asOfDate: "2016-03-31",
organizationHint: "ООО Альтернатива Плюс",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Якорь периода: дата покупки из вопроса/контекста");
expect(reply.text).toContain("01.02.2015..28.02.2015");
expect(reply.text).toContain("книг продаж/покупок");
});
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
@@ -236,6 +236,216 @@ describe("assistant address lane response runtime adapter", () => {
expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний");
});
it("aligns final route metadata when a business-overview discovery candidate replaces a stale exact reply", () => {
const finalizeAddressTurn = vi.fn((input) => ({
response: {
ok: true,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
debug: input.debug
}
}));
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-business-overview-address",
userMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
effectiveAddressUserMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
addressLane: {
handled: true,
reply_text: "Контрагент: Группа СВК. Найдено документов: 19.",
reply_type: "factual",
debug: {
detected_intent: "list_documents_by_counterparty",
selected_recipe: "address_documents_by_counterparty_v1",
response_type: "FACTUAL_LIST"
}
},
llmPreDecomposeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
raw_message: "Собери короткий бизнес-итог по компании и отдельно по СВК."
},
data_need_graph: {
business_fact_family: "business_overview",
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
planner: {
selected_chain_id: "business_overview"
},
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Business overview answer",
confirmed_lines: ["Confirmed company business overview facts"],
inference_lines: [],
unknown_lines: ["Pure profit is not proven"],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: (debug) => ({ ...(debug as Record<string, unknown>) }),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
appendItem: () => {},
getSession: () => ({ session_id: "asst-business-overview-address", updated_at: "", items: [], investigation_state: null } as any),
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-business-overview-address",
finalizeAddressTurn
});
expect((runtime.response as any).debug).toEqual(
expect.objectContaining({
mcp_discovery_response_applied: true,
detected_intent: "business_overview",
selected_recipe: "business_overview",
mcp_discovery_effective_response_route: "business_overview"
})
);
expect(String((runtime.response as any).assistant_reply)).toContain("47 628 853,03");
});
it("keeps comparison-scope proof augmentation business-facing for organization clarification", () => {
const finalizeAddressTurn = vi.fn((input) => ({
response: {
ok: true,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
debug: input.debug
}
}));
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-comparison-proof",
userMessage: "ООО Альтернатива Плюс",
effectiveAddressUserMessage: "ООО Альтернатива Плюс",
addressLane: {
handled: true,
reply_text:
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез: входящие 285 819 547,57 руб.",
reply_type: "partial_coverage",
debug: {}
},
llmPreDecomposeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: true,
discovery_attempted: true,
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
business_overview_separate_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "Альтернатива Плюс"
}
},
bridge: {
bridge_status: "not_applicable",
user_facing_response_allowed: false,
business_fact_answer_allowed: false,
requires_user_clarification: false
},
reason_codes: []
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: () => ({}),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
appendItem: () => {},
getSession: () =>
({
session_id: "asst-comparison-proof",
updated_at: "",
items: [],
investigation_state: null,
address_navigation_state: {
session_context: {
comparison_scope: {
organization: { label: "Альтернатива Плюс" },
counterparty: { label: "Группа СВК" },
proof_bundles: {
counterparty_value_flow_bundle: {
counterparty: "Группа СВК",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб.",
net_direction: "net_incoming"
},
counterparty_document_bundle: {
document_count: 19
}
}
}
}
}
}) as any,
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-comparison-proof",
finalizeAddressTurn
});
const reply = String((runtime.response as any).assistant_reply);
expect(reply).toContain("по компании ООО Альтернатива Плюс");
expect(reply).toContain("получили 20 653 490 руб.");
expect(reply).toContain("документы: 19");
expect(reply).not.toContain("company-level");
expect(reply).not.toContain("reusable bundle");
expect((runtime.response as any).debug.comparison_scope_response_augmentation_v1).toEqual(
expect.objectContaining({
applied: true,
organization: "ООО Альтернатива Плюс",
counterparty: "Группа СВК",
document_count: 19
})
);
});
it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => {
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-2",
@@ -307,6 +307,320 @@ describe("assistant address orchestration runtime adapter", () => {
);
});
it("recovers business overview proof bundles from session history before MCP discovery", async () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: "Group SVK",
document_count: 19
};
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
debug: {
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Group SVK"],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
metadata_scope_hint: "Group SVK"
}
}
}
}
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" },
semantics: { anchor_kind: "organization", anchor_value: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_anchor_type: "counterparty",
previous_anchor_value: "Group SVK",
previous_filters: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
})
})
);
});
it("recovers business overview proof bundles from previous assistant summary text", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
text:
"\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443 Group SVK: " +
"\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 20 653 490 \u0440\u0443\u0431., " +
"\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 2 129 651 \u0440\u0443\u0431., " +
"\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e \u0432 \u043d\u0430\u0448\u0443 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 18 523 839 \u0440\u0443\u0431. " +
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u0446\u0435\u043f\u043e\u0447\u043a\u0435: \u043d\u0430\u0439\u0434\u0435\u043d\u043e 19."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_anchor_type: "counterparty",
previous_anchor_value: "Group SVK",
previous_filters: {}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_bidirectional_value_flow: expect.objectContaining({
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 \u0440\u0443\u0431." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 \u0440\u0443\u0431." },
net_amount_human_ru: "18 523 839 \u0440\u0443\u0431."
}),
previous_discovery_document_summary: expect.objectContaining({
counterparty: "Group SVK",
document_count: 19
})
})
})
);
});
it("recovers business overview proof bundles from navigation comparison state", async () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
};
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionAddressNavigationState: {
session_context: {
comparison_scope: {
organization: null,
counterparty: { label: "Group SVK" },
proof_bundles: {
counterparty_value_flow_bundle: valueFlowBundle,
counterparty_document_bundle: documentBundle
}
}
}
},
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: null })),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
})
})
);
});
it("does not infer a counterparty proof bundle from company-only overview totals", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "who brought money",
sessionItems: [
{
role: "assistant",
text:
"Деньги: входящие 47 628 853,03 руб., исходящие 43 763 351,53 руб., расчетное операционное нетто 3 865 501,50 руб.\n" +
"НДС: продажи 2 002 138,93 руб., покупки 1 784 850,48 руб., НДС к уплате 217 288,45 руб."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "who brought money",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "business_overview",
target_intent: "business_overview",
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_filters: { organization: "Alt Plus" }
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
const call = runMcpDiscoveryRuntimeEntryPoint.mock.calls[0]?.[0] as any;
expect(call.followupContext.previous_discovery_bidirectional_value_flow).toBeUndefined();
expect(call.followupContext.previous_discovery_document_summary).toBeUndefined();
});
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
@@ -176,6 +176,31 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("handles first-turn smalltalk deterministically without relying on LLM", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
channel: "default",
organizations: [],
error: null
}));
const input = buildRuntimeInput({
userMessage: "приветик - че как там дела",
resolveDataScopeProbe,
buildAssistantProactiveOrganizationOfferReply: () => ""
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Привет! Всё нормально.");
expect(output.chatText).not.toContain("llm-text");
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(false);
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(0);
expect(input.__spies.executeLlmChat).not.toHaveBeenCalled();
expect(resolveDataScopeProbe).toHaveBeenCalledTimes(1);
});
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
@@ -438,6 +463,7 @@ describe("assistant living chat runtime adapter", () => {
"ООО Лайсвуд",
"РАЙМ"
]);
expect(input.__spies.executeLlmChat).not.toHaveBeenCalled();
});
it("does not add proactive organization offer after the session already has assistant context", async () => {
@@ -475,6 +475,7 @@ describe("assistant MCP discovery response candidate", () => {
effective_message:
"Дать краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и банковскую границу.",
explicit_organization_scope: "ООО Альтернатива Плюс"
,asked_action_family: "broad_evaluation"
},
data_need_graph: {
business_fact_family: "business_overview",
@@ -778,6 +779,199 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("wide overview");
});
it("keeps broad overview layers when direct-money reason is present", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020 год: деньги, нетто, НДС, долги, склад, клиенты, поставщики и что нельзя утверждать.",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
is_likely_financial_institution: true
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб."
}
],
top_suppliers: [
{
axis_value: "Департамент капитального ремонта города Москвы",
total_amount_human_ru: "9 612 904,90 руб."
}
],
tax_position: {
sales_vat_amount_human_ru: "2 717 288,45 руб.",
purchase_vat_amount_human_ru: "2 500 000 руб.",
net_vat_amount_human_ru: "217 288,45 руб.",
net_vat_direction: "vat_to_pay"
},
debt_position: {
receivables: { total_amount_human_ru: "25 000 000 руб." },
payables: { total_amount_human_ru: "3 318 824,40 руб." },
net_debt_position_amount_human_ru: "21 681 175,60 руб.",
net_debt_position_direction: "net_receivable"
},
inventory_position: {
total_amount_human_ru: "716 418,33 руб.",
rows_matched: 34
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "wide overview should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("Что видно:");
expect(candidate.reply_text).toContain("НДС:");
expect(candidate.reply_text).toContain("Долги:");
expect(candidate.reply_text).toContain("Склад:");
expect(candidate.reply_text).toContain("Группа СВК");
expect(candidate.reply_text).not.toContain("по деньгам плюс");
expect(candidate.reply_text).not.toContain("wide overview");
});
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в 2020.",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
},
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
is_likely_financial_institution: true
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб."
}
],
top_suppliers: [
{
axis_value: "Департамент капитального ремонта города Москвы",
total_amount_human_ru: "9 612 904,90 руб."
}
],
tax_position: {
net_vat_amount_human_ru: "217 288,45 руб.",
net_vat_direction: "vat_to_pay"
},
debt_position: {
receivables: { total_amount_human_ru: "25 000 000 руб." },
payables: { total_amount_human_ru: "3 318 824,40 руб." },
net_debt_position_amount_human_ru: "21 681 175,60 руб.",
net_debt_position_direction: "net_receivable"
},
inventory_position: {
total_amount_human_ru: "716 418,33 руб.",
rows_matched: 34
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "wide overview should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("47 628 853,03");
expect(candidate.reply_text).toContain("43 763 351,53");
expect(candidate.reply_text).toContain("3 865 501,50");
expect(candidate.reply_text).toContain("СБЕРБАНК, ПАО");
expect(candidate.reply_text).toContain("Группа СВК");
expect(candidate.reply_text).toContain("Департамент капитального ремонта города Москвы");
expect(candidate.reply_text).not.toContain("по деньгам плюс");
expect(candidate.reply_text).not.toContain("НДС:");
expect(candidate.reply_text).not.toContain("Склад:");
expect(candidate.reply_text).not.toContain("wide overview");
});
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@@ -902,6 +1096,83 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("Самый крупный подтвержденный клиент");
});
it("answers colloquial money leader follow-ups with leaders first", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки.",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: "top_desc",
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
counterparty_role_hint: "financial_institution"
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб.",
counterparty_role_hint: "ordinary_counterparty"
}
],
top_suppliers: [
{
axis_value: "Департамент капитального ремонта города Москвы.",
total_amount_human_ru: "9 612 904,90 руб."
}
],
yearly_breakdown: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Company summary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("больше всего занес СБЕРБАНК, ПАО");
expect(firstLine).toContain("не называю его обычной клиентской выручкой");
expect(firstLine).toContain("крупнейший небанковский входящий контрагент: Группа СВК");
expect(firstLine).toContain("больше всего ушло Департамент капитального ремонта города Москвы.");
expect(firstLine).not.toContain("выглядит как бизнес");
});
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@@ -1107,6 +1378,128 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).toContain("ранее подтвержденный контрагентский срез");
});
it("answers selected counterparty boundary summaries from previous bundles without asking for organization", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Группа СВК"],
previous_counterparty_value_flow_bundle: {
counterparty: "Группа СВК",
incoming_customer_revenue: {
total_amount_human_ru: "20 653 490 руб.",
rows_with_amount: 26,
rows_matched: 26
},
outgoing_supplier_payout: {
total_amount_human_ru: "2 129 651 руб.",
rows_with_amount: 1,
rows_matched: 1
},
net_amount_human_ru: "18 523 839 руб.",
net_direction: "net_incoming"
},
previous_counterparty_document_bundle: {
counterparty: "Группа СВК",
document_count: 19
}
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: [],
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_family_business_overview", "data_need_graph_has_clarification_gaps"]
}
},
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: "Нужно уточнить контекст перед поиском в 1С.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: "Уточните организацию."
}
}
})
);
expect(candidate.candidate_status).toBe("clarification_candidate");
expect(candidate.reply_text).toContain("уточните, по какой компании");
expect(candidate.reply_text).toContain("Группа СВК");
expect(candidate.reply_text).toContain("20 653 490 руб.");
expect(candidate.reply_text).toContain("документы по цепочке: найдено 19");
expect(candidate.reply_text).toContain("Нельзя утверждать");
expect(candidate.reply_text).not.toContain("перед поиском");
});
it("keeps organization clarification boundary summaries compact", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "ООО Альтернатива Плюс",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "Альтернатива Плюс"
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: [],
metadata_scope_hint: "Группа СВК",
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "all_time",
incoming_customer_revenue: { total_amount_human_ru: "285 819 547,57 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "137 963 720,37 руб." },
net_amount_human_ru: "147 855 827,20 руб.",
net_direction: "net_incoming",
top_customers: [{ axis_value: "Комитет государственных услуг г. Москвы", total_amount_human_ru: "133 839 880,82 руб." }],
top_suppliers: [{ axis_value: "ГТК-Интер, ООО", total_amount_human_ru: "44 943 460 руб." }]
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Company summary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
const lines = candidate.reply_text?.split("\n").filter(Boolean) ?? [];
expect(lines.length).toBeLessThanOrEqual(3);
expect(lines[0]).toContain("по компании Альтернатива Плюс");
expect(lines[0]).toContain("285 819 547,57");
expect(candidate.reply_text).toContain("Отдельно по выбранному контрагенту Группа СВК");
expect(candidate.reply_text).toContain("Нельзя утверждать");
expect(candidate.reply_text).not.toContain("Метод:");
expect(candidate.reply_text).not.toContain("Для ответа именно про чистую прибыль");
});
it("localizes value-flow evidence without leaking pilot mechanics", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@@ -310,6 +310,67 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "list_documents_by_counterparty",
selected_recipe: "address_documents_by_counterparty_v1",
mcp_call_status: "matched_non_empty",
truth_mode: "confirmed",
capability_binding_status: "bound",
capability_binding_violations: [],
answer_shape_contract: {
reply_type: "factual",
capability_contract_id: "documents_drilldown"
},
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: ["Группа СВК"],
reason_codes: ["data_need_graph_built", "data_need_graph_bidirectional_value_flow"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Группа СВК: получили 20 653 490 руб., заплатили 2 129 651 руб.; нетто 18 523 839 руб. в нашу сторону.",
confirmed_lines: ["получили 20 653 490 руб.; заплатили 2 129 651 руб.; нетто 18 523 839 руб."],
inference_lines: [],
unknown_lines: [],
limitation_lines: ["Это денежный срез, не бухгалтерская прибыль."],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
expect(result.reply_text).toContain("20 653 490");
expect(result.reply_text).toContain("2 129 651");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
});
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
@@ -328,8 +328,17 @@ describe("assistant MCP discovery runtime bridge", () => {
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.hot_runtime_wired).toBe(true);
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.hot_runtime_wired).toBe(true);
expect(result.execution_handoff).toMatchObject({
handoff_status: "ready_for_guarded_response",
selected_chain_id: "value_flow_comparison",
allowed_hot_chain: true,
can_use_guarded_response: true
});
expect(result.reason_codes).toContain("runtime_bridge_wired_to_guarded_hot_assistant_answer");
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
period_scope: "2020",
incoming_customer_revenue: {
@@ -389,6 +398,14 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.requires_user_clarification).toBe(false);
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.hot_runtime_wired).toBe(true);
expect(result.execution_handoff).toMatchObject({
handoff_status: "ready_for_guarded_response",
selected_chain_id: "value_flow_comparison",
allowed_hot_chain: true,
can_use_guarded_response: true
});
expect(result.reason_codes).toContain("runtime_bridge_wired_to_guarded_hot_assistant_answer");
expect(result.planner.required_axes).toContain("all_time_scope");
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
@@ -581,6 +598,11 @@ describe("assistant MCP discovery runtime bridge", () => {
executable_now: true,
enablement_reason: null
});
expect(result.execution_handoff).toMatchObject({
handoff_status: "ready_for_guarded_response",
allowed_hot_chain: true,
can_use_guarded_response: true
});
expect(result.pilot.derived_business_overview?.inventory_quality_events?.evidence_status).toBe(
"reviewed_no_quality_events_found"
);
@@ -166,6 +166,34 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
});
it("normalizes role-prefixed counterparty carryover before document discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "Покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс.",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "list_documents",
explicit_intent_candidate: "list_documents_by_counterparty"
},
predecomposeContract: {
entities: { counterparty: null, organization: null },
period: { scope: "unspecified", period_from: null, period_to: null }
},
followupContext: {
previous_intent: "customer_revenue_and_payments",
target_intent: "list_documents_by_counterparty",
previous_discovery_pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
previous_discovery_loop_metadata_scope_hint: "контрагенту группа свк",
previous_anchor_type: "counterparty",
previous_anchor_value: "контрагенту группа свк",
previous_filters: { counterparty: "контрагенту группа свк" }
}
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["группа свк"]);
expect(result.turn_meaning_ref?.metadata_scope_hint).toBe("группа свк");
expect(result.data_need_graph?.subject_candidates).toEqual(["группа свк"]);
});
it("extracts compact scoped counterparty from net follow-up wording when LLM entities are empty", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
@@ -1653,6 +1681,7 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
@@ -1699,6 +1728,22 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected");
});
it("routes colloquial multi-surface company overview into business overview over exact VAT", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u041f\u043e-\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0447\u0442\u043e \u043f\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u0432\u0438\u0434\u043d\u043e \u0432 1\u0421 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0430\u043c, \u0441\u043a\u043b\u0430\u0434\u0443, \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c \u0438 \u0433\u0434\u0435 \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c \u0432\u044b\u0432\u043e\u0434?",
assistantTurnMeaning: {
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
}
});
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref?.asked_action_family).toBe("broad_evaluation");
expect(result.turn_meaning_ref?.explicit_intent_candidate).toBeUndefined();
});
it("keeps explicit year out of the organization scope for raw business overview wording", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
@@ -1789,6 +1834,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it("keeps previous organization scope when all-time business overview says company in general", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за 2020 как подтвержденную общую налоговую позицию.",
assistantTurnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "в целом",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_discovery_loop_provided_axes: ["organization"],
previous_filters: {
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_signal_detected");
expect(result.reason_codes).not.toContain("mcp_discovery_organization_scope_from_semantic_text");
});
it("grounds organization aliases from known organizations for clean-session earnings questions", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "скока денег альтернатива заработала за 20 год?",
@@ -3690,6 +3765,135 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("routes brief compare boundary wording to business overview without implicit current date", () => {
const today = new Date().toISOString().slice(0, 10);
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
const valueFlowBundle = {
counterparty: counterpartyName,
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: counterpartyName,
document_count: 19
};
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0418 \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u0441\u0440\u0430\u0432\u043d\u0438: \u0447\u0442\u043e \u0443 \u043d\u0430\u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438, \u0447\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443, \u0438 \u043a\u0430\u043a\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u044b \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_intent_candidate: "list_documents_by_counterparty",
explicit_entity_candidates: ["\u043d\u0430\u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e"]
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1",
previous_intent: "list_documents_by_counterparty",
target_intent: "list_documents_by_counterparty",
previous_filters: {
organization: orgName,
counterparty: counterpartyName,
as_of_date: today
},
previous_anchor_type: "counterparty",
previous_anchor_value: counterpartyName,
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterpartyName],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.reason_codes).toContain(
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
);
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_raw_scope");
});
it("continues business overview organization clarification without current-date reset", () => {
const today = new Date().toISOString().slice(0, 10);
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
const valueFlowBundle = {
counterparty: counterpartyName,
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: counterpartyName,
document_count: 19
};
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: orgName,
assistantTurnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_date_scope: today,
stale_replay_forbidden: true
},
predecomposeContract: {
entities: { organization: orgName },
period: { as_of_date: today }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_provided_axes: ["all_time_scope", "metadata_scope"],
previous_discovery_loop_asked_domain_family: "business_overview",
previous_discovery_loop_asked_action_family: "broad_evaluation",
previous_discovery_loop_unsupported_family: "broad_business_evaluation",
previous_discovery_loop_metadata_scope_hint: counterpartyName,
previous_anchor_type: "counterparty",
previous_anchor_value: counterpartyName,
previous_discovery_bidirectional_value_flow: valueFlowBundle,
previous_discovery_document_summary: documentBundle
}
});
expect(result.source_signal).toBe("assistant_turn_meaning");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterpartyName],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_from_followup_context");
expect(result.reason_codes).toContain(
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
);
expect(result.reason_codes).not.toContain("mcp_discovery_date_scope_from_followup_context");
});
it("preserves explicit counterparty scope for company plus counterparty business summaries", () => {
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
@@ -339,6 +339,71 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.previous_anchor_value).toBe("Workstation Focus");
});
it("keeps selected counterparty document follow-up through stale discovery guard", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Confirmed value-flow profile for SVK Group.",
debug: {
detected_intent: "customer_revenue_and_payments",
extracted_filters: {
organization: "ACME",
period_from: "2017-01-01",
period_to: "2017-12-31"
},
assistant_mcp_discovery_entry_point_v1: {
bridge: {
loop_state: {
selected_chain_id: "value_flow_comparison"
}
}
}
}
}),
resolveAssistantTurnMeaning: () => ({
stale_replay_forbidden: true,
explicit_intent_candidate: "list_documents_by_counterparty"
}),
resolveAddressIntent: () => ({ intent: "list_documents_by_counterparty" }),
resolveAddressIntentFamily: (intent: unknown) =>
["customer_revenue_and_payments", "list_documents_by_counterparty"].includes(String(intent ?? ""))
? "counterparty"
: toNonEmptyString(intent),
hasAddressFollowupContextSignal: () => false,
hasReferentialPointer: () => false,
findRecentInventoryRootFrame: () => null,
findRecentAddressFilterValue: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"Show docs for selected counterparty",
[],
null,
{
predecomposeContract: {
intent: "list_documents_by_counterparty"
}
},
{
session_context: {
active_focus_object: {
object_type: "counterparty",
label: "SVK Group"
}
}
}
);
expect(carryover?.followupContext?.target_intent).toBe("list_documents_by_counterparty");
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.followupContext?.previous_anchor_value).toBe("SVK Group");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization: "ACME",
counterparty: "SVK Group",
period_from: "2017-01-01",
period_to: "2017-12-31"
});
});
it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
@@ -1628,6 +1693,164 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});
it("carries selected counterparty proof bundles through business overview organization clarification", () => {
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
const valueFlowBundle = {
counterparty: counterpartyName,
incoming_total: 20653490,
outgoing_total: 2129651,
net_amount: 18523839
};
const documentBundle = {
counterparty: counterpartyName,
document_count: 19
};
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: "Коротко: уточните, по какой компании/организации сравнить выбранного контрагента Группа СВК.",
debug: {
execution_lane: "living_chat",
detected_intent: "business_overview",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: [counterpartyName],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
metadata_scope_hint: counterpartyName,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "business_overview_route_template_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
loop_status: "awaiting_clarification",
selected_chain_id: "business_overview",
pilot_scope: "business_overview_route_template_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
pending_axes: ["organization"],
provided_axes: ["all_time_scope", "metadata_scope"],
metadata_scope_hint: counterpartyName
}
}
}
}
}),
resolveOrganizationSelectionFromMessage: (message: string) =>
String(message ?? "").includes("Альтернатива") ? orgName : null,
hasAddressFollowupContextSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
orgName,
[],
null,
{
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
null
);
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("business_overview");
expect(carryover?.followupContext?.previous_discovery_loop_provided_axes).toEqual([
"all_time_scope",
"metadata_scope"
]);
expect(carryover?.followupContext?.previous_discovery_loop_metadata_scope_hint).toBe(counterpartyName);
expect(carryover?.followupContext?.previous_discovery_bidirectional_value_flow).toEqual(valueFlowBundle);
expect(carryover?.followupContext?.previous_discovery_document_summary).toEqual(documentBundle);
});
it("recovers selected counterparty proof bundles from prior boundary-summary text", () => {
const orgName = "ООО Альтернатива Плюс";
const counterpartyName = "Группа СВК";
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text:
"Коротко: уточните, по какой компании/организации сравнить выбранного контрагента Группа СВК.\n" +
"Отдельно по контрагенту Группа СВК: подтверждено получили 20 653 490 руб., заплатили 2 129 651 руб., расчетное нетто в нашу сторону 18 523 839 руб. Основа: проверенные входящие платежи и исходящие платежи и документы по цепочке: найдено 19.",
debug: {
execution_lane: "living_chat",
detected_intent: "business_overview",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "business_overview_route_template_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
loop_status: "awaiting_clarification",
selected_chain_id: "business_overview",
pilot_scope: "business_overview_route_template_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
pending_axes: ["organization"],
provided_axes: ["all_time_scope", "metadata_scope"],
metadata_scope_hint: counterpartyName
}
}
}
}
}),
resolveOrganizationSelectionFromMessage: (message: string) =>
String(message ?? "").includes("Альтернатива") ? orgName : null,
hasAddressFollowupContextSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
orgName,
[],
null,
{
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
null
);
expect(carryover?.followupContext?.previous_discovery_bidirectional_value_flow).toMatchObject({
counterparty: counterpartyName,
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
net_amount_human_ru: "18 523 839 руб."
});
expect(carryover?.followupContext?.previous_discovery_document_summary).toMatchObject({
counterparty: counterpartyName,
document_count: 19
});
});
it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,