Завершить phase99-105 schema primitive closure
This commit is contained in:
@@ -715,6 +715,33 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||
const isLowQualityTimeToken = (token) => lowQualityTimeTokens.has(token) ||
|
||||
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||
const lowQualityGenericTokens = new Set([
|
||||
"или",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"деньги",
|
||||
"денег",
|
||||
"деньгам",
|
||||
@@ -1240,6 +1267,10 @@ function isLowQualityWarehouseAnchorValue(rawValue) {
|
||||
"лежали",
|
||||
"на",
|
||||
"по",
|
||||
"остатка",
|
||||
"остаткам",
|
||||
"остатками",
|
||||
"остатков",
|
||||
"компания",
|
||||
"компании",
|
||||
"компанию",
|
||||
@@ -1320,7 +1351,7 @@ function extractInventoryWarehouseAnchor(text) {
|
||||
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||
normalizedCandidate.startsWith("по состоянию") ||
|
||||
isTemporalWarehousePhrase(candidate) ||
|
||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {
|
||||
/^(?:сейчас|на|дату|дате|остат(?:ок|ки|ка|кам|ками|ков)|по\s+остат(?:кам|ки|ку|ка|ков))$/iu.test(candidate)) {
|
||||
continue;
|
||||
}
|
||||
return candidate;
|
||||
|
||||
@@ -1865,7 +1865,9 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||
}
|
||||
if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
!/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))) {
|
||||
(hasMoneyCue ||
|
||||
hasRankingCue ||
|
||||
/заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution(/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile", "high", /(?:хвост|задержк|проблем)/iu.test(normalized)
|
||||
? "supplier_tail_risk_signal_detected"
|
||||
: "unicode_supplier_payouts_bridge_signal_detected");
|
||||
@@ -2003,7 +2005,7 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||
return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))) {
|
||||
(hasMoneyCue || hasRankingCue || /заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("supplier_payouts_profile", "high", "unicode_supplier_payouts_bridge_signal_detected");
|
||||
}
|
||||
if ((/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) &&
|
||||
|
||||
@@ -306,8 +306,43 @@ function bankOperationDirectionLabel(direction) {
|
||||
}
|
||||
return "банковская операция без надежно распознанного направления";
|
||||
}
|
||||
function bankOperationEvidenceLine(rows) {
|
||||
const sample = rows[0];
|
||||
function summarizeBankOperationDirections(rows) {
|
||||
const summary = {
|
||||
incoming: { count: 0, amount: 0 },
|
||||
outgoing: { count: 0, amount: 0 },
|
||||
unknown: { count: 0, amount: 0 }
|
||||
};
|
||||
for (const row of rows) {
|
||||
const direction = bankOperationDirection(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? Math.abs(row.amount) : 0;
|
||||
summary[direction].count += 1;
|
||||
summary[direction].amount += amount;
|
||||
}
|
||||
const parts = [];
|
||||
if (summary.incoming.count > 0) {
|
||||
parts.push(`входящие: ${formatMoneyRub(summary.incoming.amount)} (${summary.incoming.count} строк)`);
|
||||
}
|
||||
if (summary.outgoing.count > 0) {
|
||||
parts.push(`исходящие: ${formatMoneyRub(summary.outgoing.amount)} (${summary.outgoing.count} строк)`);
|
||||
}
|
||||
if (summary.unknown.count > 0) {
|
||||
parts.push(`без распознанного направления: ${formatMoneyRub(summary.unknown.amount)} (${summary.unknown.count} строк)`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `Сводка по направлению: ${parts.join("; ")}.`
|
||||
: "Сводка по направлению: подтвержденные строки не найдены.";
|
||||
}
|
||||
function preferredBankEvidenceDirection(userMessage) {
|
||||
if (hasBankIncomingRoleBoundaryQuestion(userMessage)) {
|
||||
return "incoming";
|
||||
}
|
||||
if (hasBankOutgoingRoleBoundaryQuestion(userMessage)) {
|
||||
return "outgoing";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function bankOperationEvidenceLine(rows, preferredDirection = null) {
|
||||
const sample = (preferredDirection ? rows.find((row) => bankOperationDirection(row) === preferredDirection) : null) ?? rows[0];
|
||||
if (!sample) {
|
||||
return "Проверенная строка 1С не найдена.";
|
||||
}
|
||||
@@ -341,12 +376,12 @@ function bankRoleBoundaryLine(userMessage, rows) {
|
||||
const hasOutgoingRow = directions.includes("outgoing");
|
||||
if (incomingBoundary) {
|
||||
return hasIncomingRow
|
||||
? "Выручкой от обычного клиента это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
|
||||
? "Это не обычный клиент и не клиентская выручка автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
|
||||
: hasOutgoingRow
|
||||
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; клиентскую выручку, кредит или депозит по этой строке не доказываю."
|
||||
: "Входящее поступление от банка в найденных строках не подтверждено; клиентскую выручку, кредитный или депозитный смысл без вида операции/назначения платежа не доказываю.";
|
||||
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; это не подтвержденная клиентская выручка, кредит или депозит."
|
||||
: "Входящее поступление от банка в найденных строках не подтверждено; это не подтвержденная клиентская выручка, кредитный или депозитный смысл.";
|
||||
}
|
||||
return "Обычным поставщиком это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
|
||||
return "Это не обычный поставщик автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
|
||||
}
|
||||
function hasInventoryPurchaseDateActionFocus(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
@@ -3896,23 +3931,34 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
.filter((item) => Boolean(item)));
|
||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(rows, rows.length)
|
||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "bank_operations_by_contract") {
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций по договору — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
...formatTopRows(rows, rows.length)
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
||||
@@ -139,6 +139,7 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"что",
|
||||
"все",
|
||||
"всё",
|
||||
"или",
|
||||
"кроме",
|
||||
"помимо",
|
||||
"этого",
|
||||
@@ -157,6 +158,30 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"договора",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"еще",
|
||||
"ещё",
|
||||
"другие",
|
||||
@@ -654,6 +679,19 @@ function hasBroadCounterpartyRankingCue(text) {
|
||||
}
|
||||
return /(?:\bкто\b|\bкакие\b|\bкакой\b|\bтоп\b|\bсписок\b|\bвсе\b|\bвсех\b|\bвсего\b|\bclients?\b|\bcounterpart(?:y|ies)\b|контрагент|клиент|заказчик)/iu.test(normalized);
|
||||
}
|
||||
function isBroadDebtPolarityQuestion(intent, text) {
|
||||
if (intent !== "payables_confirmed_as_of_date" && intent !== "receivables_confirmed_as_of_date") {
|
||||
return false;
|
||||
}
|
||||
const normalized = textWithRepairedVariant(String(text ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||
if (!/(?:долж|задолж|дебитор|кредитор|обязательств)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:по\s+(?:нему|ней|ним|этому|этой|этому\s+контрагенту|этой\s+компании|поставщику|клиенту|покупателю|заказчику)|\bон\b|\bона\b)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(normalized);
|
||||
}
|
||||
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const merged = { ...current };
|
||||
const reasons = [];
|
||||
@@ -823,10 +861,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const inheritedCounterparty = previousCounterparty ??
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||
const shouldInheritCounterparty = !currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
|
||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||
const shouldInheritCounterparty = !suppressCounterpartyForBroadDebtQuestion &&
|
||||
(!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
|
||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
}
|
||||
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||
|
||||
@@ -69,6 +69,7 @@ async function runAssistantAddressAttemptRuntime(input) {
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ function buildAssistantLivingChatAttemptRuntimeInput(input) {
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ async function runAssistantLivingChatAttemptRuntime(input) {
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ function buildAssistantLivingChatHandlerRuntimeInput(input) {
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ async function tryHandleAssistantLivingChatRuntime(input) {
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
||||
@@ -11,6 +11,34 @@ function hasPriorAssistantTurn(items) {
|
||||
}
|
||||
return items.some((item) => item && typeof item === "object" && item.role === "assistant");
|
||||
}
|
||||
function shouldProbeBareOrganizationScopeCandidate(input) {
|
||||
if (input.selectedOrganization ||
|
||||
input.activeOrganization ||
|
||||
input.dataScopeMetaQuery ||
|
||||
input.capabilityMetaQuery ||
|
||||
input.destructiveSignal ||
|
||||
input.dangerSignal ||
|
||||
input.operationalSignal) {
|
||||
return false;
|
||||
}
|
||||
const raw = String(input.userMessage ?? "").trim();
|
||||
if (!raw || raw.length > 80 || /[?!]/u.test(raw) || /\d/u.test(raw) || !/\p{L}/u.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = raw.split(/\s+/u).filter(Boolean).length;
|
||||
if (tokenCount < 1 || tokenCount > 5) {
|
||||
return false;
|
||||
}
|
||||
const normalized = raw
|
||||
.toLowerCase()
|
||||
.replace(/\u0451/gu, "\u0435")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
if (/^(?:\u043f\u0440\u0438\u0432\u0435\u0442|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435|\u0434\u0430|\u043d\u0435\u0442|\u043e\u043a|\u043e\u043a\u0435\u0439|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u043f\u043e\u043a\u0430|\u0433\u043e|\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u043d\u044f\u043b|\u043f\u043e\u043d\u044f\u043b\u0430)(?:\s|$)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return !/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043f\u043e\u043a\u0430\u0436\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438|\u0447\u0442\u043e|\u043a\u0430\u043a|\u0433\u0434\u0435|\u043a\u043e\u0433\u0434\u0430|\u043f\u043e\u0447\u0435\u043c\u0443|\u0437\u0430\u0447\u0435\u043c|\u043c\u043e\u0436\u0435\u0448\u044c|\u0443\u043c\u0435\u0435\u0448\u044c|\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u0445\u043e\u0447\u0443|\u043e\u0441\u0442\u0430\u0442\u043a|\u043d\u0434\u0441|\u0434\u043e\u043b\u0433|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043f\u0440\u0438\u0431\u044b\u043b)/iu.test(normalized);
|
||||
}
|
||||
function buildDeterministicSmalltalkLeadReply() {
|
||||
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
|
||||
}
|
||||
@@ -80,6 +108,8 @@ async function runAssistantLivingChatRuntime(input) {
|
||||
let livingChatGroundingGuardApplied = false;
|
||||
let livingChatGroundingGuardReason = null;
|
||||
let livingChatProactiveScopeOfferApplied = false;
|
||||
let livingChatBareScopeProbeAttempted = false;
|
||||
let livingChatBareScopeProbeMatchedOrganization = null;
|
||||
const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization;
|
||||
let knownOrganizations = [...organizationAuthority.knownOrganizations];
|
||||
let selectedOrganization = organizationAuthority.selectedOrganization;
|
||||
@@ -101,6 +131,29 @@ async function runAssistantLivingChatRuntime(input) {
|
||||
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
|
||||
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
||||
const lastAnswerInspectionAddressDebug = memoryRecapContext.lastAnswerInspectionAddressDebug;
|
||||
if (shouldProbeBareOrganizationScopeCandidate({
|
||||
userMessage,
|
||||
selectedOrganization,
|
||||
activeOrganization,
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
destructiveSignal,
|
||||
dangerSignal,
|
||||
operationalSignal
|
||||
})) {
|
||||
dataScopeProbe = await input.resolveDataScopeProbe();
|
||||
livingChatBareScopeProbeAttempted = true;
|
||||
knownOrganizations = input.mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations : [])
|
||||
]);
|
||||
const probedOrganization = input.resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations);
|
||||
if (probedOrganization) {
|
||||
selectedOrganization = probedOrganization;
|
||||
activeOrganization = probedOrganization;
|
||||
livingChatBareScopeProbeMatchedOrganization = probedOrganization;
|
||||
}
|
||||
}
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = input.buildAssistantSafetyRefusalReply();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
@@ -302,6 +355,8 @@ async function runAssistantLivingChatRuntime(input) {
|
||||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
|
||||
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
|
||||
living_chat_bare_scope_probe_attempted: livingChatBareScopeProbeAttempted,
|
||||
living_chat_bare_scope_probe_matched_organization: livingChatBareScopeProbeMatchedOrganization,
|
||||
living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null,
|
||||
living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null,
|
||||
living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations)
|
||||
|
||||
+21
-17
@@ -505,7 +505,7 @@ function businessOverviewOutgoingLeaderLine(overview) {
|
||||
function businessOverviewSupplierBoundaryBasis(overview) {
|
||||
const leader = overview.top_suppliers?.[0] ?? null;
|
||||
if (!leader) {
|
||||
return "есть только общий срез исходящих платежей без надежного vendor-risk профиля";
|
||||
return "есть только общий срез исходящих платежей без надежного профиля поставщицкого риска";
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
@@ -540,9 +540,9 @@ function businessOverviewHeadlineMetricsLine(overview) {
|
||||
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
|
||||
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
|
||||
const margin = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} 90/91/99 ${amount}; ${margin}`);
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} по закрытию счетов 90/91/99 ${amount}; ${margin}`);
|
||||
}
|
||||
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||
if (strongestIncomingYear) {
|
||||
@@ -551,7 +551,7 @@ function businessOverviewHeadlineMetricsLine(overview) {
|
||||
return parts.length > 0
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: `${parts.join("; ")}. Это операционный денежный сигнал по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: null;
|
||||
}
|
||||
function businessOverviewAccountingFinancialResultText(overview) {
|
||||
@@ -568,12 +568,12 @@ function businessOverviewAccountingFinancialResultText(overview) {
|
||||
? `минус ${result.final_result_amount_human_ru}`
|
||||
: result.final_result_amount_human_ru;
|
||||
const marginText = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
const basis = result.final_transfer_basis === "account_99_to_84_period_close"
|
||||
? "по закрытию 99 на 84"
|
||||
: "по закрытию 90/91 на 99";
|
||||
return `По бухгалтерскому маршруту 90/91/99 за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
return `Нет: денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
}
|
||||
function businessOverviewDebtDueDateAgingText(overview) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
@@ -637,12 +637,12 @@ function businessOverviewVendorProcurementQualityText(overview) {
|
||||
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
|
||||
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
|
||||
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим срезом не доказаны.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_procurement_concentration") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
}
|
||||
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period} нашла исходящие платежи на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный аудит поставщицкого риска не подтвержден.`;
|
||||
}
|
||||
function businessOverviewInventoryQualityEventsText(overview) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
@@ -684,7 +684,7 @@ function headlineFor(mode, pilot) {
|
||||
if (accountingFinancialResultText) {
|
||||
return accountingFinancialResultText;
|
||||
}
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финрезультат.";
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финрезультат.";
|
||||
}
|
||||
if (isDebtDueDateBoundaryTurn(pilot)) {
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
@@ -1375,6 +1375,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`);
|
||||
}
|
||||
if (overview.incoming_customer_revenue.coverage_recovered_by_period_chunking ||
|
||||
overview.outgoing_supplier_payout.coverage_recovered_by_period_chunking) {
|
||||
lines.push("Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос.");
|
||||
}
|
||||
if (overview.activity_period) {
|
||||
lines.push(`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`);
|
||||
}
|
||||
@@ -1536,7 +1540,7 @@ function businessOverviewSupplierConcentrationLine(overview) {
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал концентрации закупок/исходящих платежей по найденным строкам, а не полный аудит поставщицкого риска или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
}
|
||||
function businessOverviewYearlyOperatingLine(overview) {
|
||||
@@ -1561,7 +1565,7 @@ function businessOverviewYearlyOperatingLine(overview) {
|
||||
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
|
||||
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
|
||||
}
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это операционный денежный сигнал, не бухгалтерская прибыль и не финрезультат.`;
|
||||
}
|
||||
function businessOverviewRiskSynthesisLine(overview) {
|
||||
const signals = [];
|
||||
@@ -1587,9 +1591,9 @@ function businessOverviewRiskSynthesisLine(overview) {
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const marginText = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} по закрытию счетов 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
|
||||
+11
-1
@@ -3584,6 +3584,10 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
||||
if (derived.yearly_breakdown.length > 0) {
|
||||
facts.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`);
|
||||
}
|
||||
if (derived.incoming_customer_revenue.coverage_recovered_by_period_chunking ||
|
||||
derived.outgoing_supplier_payout.coverage_recovered_by_period_chunking) {
|
||||
facts.push("Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос.");
|
||||
}
|
||||
if (derived.activity_period) {
|
||||
facts.push(`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.`);
|
||||
}
|
||||
@@ -3820,7 +3824,7 @@ function buildBusinessOverviewUnknownFacts(derived) {
|
||||
: null
|
||||
].filter((item) => Boolean(item));
|
||||
if (derived?.coverage_limited_by_probe_limit) {
|
||||
unknowns.unshift("Полное покрытие бизнес-обзора не подтверждено: хотя бы один денежный probe достиг лимита строк.");
|
||||
unknowns.unshift("Полное покрытие бизнес-обзора не подтверждено: хотя бы один денежный запрос достиг верхней границы выборки.");
|
||||
}
|
||||
return unknowns;
|
||||
}
|
||||
@@ -4735,6 +4739,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
if (!incomingResult?.error || !outgoingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_movements_mcp_executed");
|
||||
}
|
||||
if (incomingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_incoming_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (outgoingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (taxResult?.error) {
|
||||
pushUnique(queryLimitations, taxResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_error");
|
||||
|
||||
@@ -5,6 +5,7 @@ exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery;
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1";
|
||||
const CHUNKED_COVERAGE_PROBE_BUDGET = 30;
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
@@ -385,12 +386,14 @@ function budgetOverrideFor(input, recipe) {
|
||||
(recipe.semanticDataNeed === "counterparty value-flow evidence" ||
|
||||
recipe.semanticDataNeed === "bidirectional value-flow comparison evidence" ||
|
||||
recipe.semanticDataNeed === "ranked value-flow evidence");
|
||||
if (!isValueFlowRecipe) {
|
||||
const isBusinessOverviewRecipe = recipe.primitives.includes("query_movements") &&
|
||||
recipe.chainId === "business_overview";
|
||||
if (!isValueFlowRecipe && !isBusinessOverviewRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: 30
|
||||
maxProbeCount: CHUNKED_COVERAGE_PROBE_BUDGET
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
||||
+18
-9
@@ -403,8 +403,9 @@ function businessOverviewCoverageLimitLine(overview) {
|
||||
if (outgoing?.coverage_limited_by_probe_limit === true) {
|
||||
limited.push("исходящие");
|
||||
}
|
||||
const continuation = "Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога.";
|
||||
return limited.length > 0
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}`
|
||||
: null;
|
||||
}
|
||||
function joinBusinessReplyLines(lines) {
|
||||
@@ -560,6 +561,8 @@ function bidirectionalNetLabel(direction) {
|
||||
return "нетто в нашу сторону";
|
||||
}
|
||||
function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||
const turnInput = toRecordObject(entryPoint.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
@@ -574,7 +577,13 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||
if (!incomingAmount && !outgoingAmount && !netAmount) {
|
||||
return null;
|
||||
}
|
||||
const counterparty = toNonEmptyString(flow.counterparty) ?? "запрошенному контрагенту";
|
||||
const counterparty = toNonEmptyString(flow.counterparty);
|
||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const subjectLead = counterparty
|
||||
? `по контрагенту ${counterparty}`
|
||||
: organizationScope
|
||||
? `по компании ${organizationScope}`
|
||||
: "по выбранному контуру";
|
||||
const period = toNonEmptyString(flow.period_scope);
|
||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||
const incomingRows = sideRowsText(incoming);
|
||||
@@ -583,7 +592,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||
const outgoingDates = sideDateText(outgoing);
|
||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||
const lines = [
|
||||
`Коротко: по контрагенту ${counterparty}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
];
|
||||
const basis = [];
|
||||
if (incomingRows) {
|
||||
@@ -771,7 +780,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
? `минус ${amount}`
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(`Коротко: по бухгалтерскому маршруту 90/91/99 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`);
|
||||
lines.push(`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`);
|
||||
lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
@@ -779,7 +788,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат.");
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат.");
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
...toStringList(draft.inference_lines),
|
||||
@@ -790,7 +799,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push("Для точного P&L нужны отдельный маршрут по себестоимости, расходам, закрытию периода и финрезультату; текущий proxy нельзя выдавать за подтвержденную чистую прибыль или маржу.");
|
||||
lines.push("Для точного отчета о прибыли нужны отдельная проверка себестоимости, расходов, закрытия периода и финрезультата; текущий ограниченный сигнал нельзя выдавать за подтвержденную чистую прибыль или маржу.");
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
@@ -890,7 +899,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
|
||||
: "есть только ограниченный срез исходящих платежей без полного профиля поставщицкого риска";
|
||||
const proxyLabel = topSupplierLooksFinancial
|
||||
? "сигнал концентрации исходящих денег"
|
||||
: "сигнал концентрации закупок/исходящих платежей";
|
||||
@@ -925,7 +934,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
lines.push(`Можно утверждать: по компании подтвержден operating-flow proxy по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`);
|
||||
lines.push(`Можно утверждать: по компании подтвержден операционный денежный сигнал по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`);
|
||||
lines.push(`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
@@ -962,7 +971,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('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
|
||||
@@ -317,6 +317,34 @@ function hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPo
|
||||
hasConfirmedAddressExecution(input) &&
|
||||
hasBusinessOverviewDirectMoneyClarification(entryPoint));
|
||||
}
|
||||
function hasExactBankOperationsAddressReply(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
|
||||
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
const isBankIntent = detectedIntent === "bank_operations_by_counterparty" || detectedIntent === "bank_operations_by_contract";
|
||||
const isBankRecipe = selectedRecipe === "address_bank_operations_by_counterparty_v1" ||
|
||||
selectedRecipe === "address_bank_operations_by_contract_v1";
|
||||
if (!isBankIntent || !isBankRecipe) {
|
||||
return false;
|
||||
}
|
||||
const grounding = toRecordObject(input.addressRuntimeMeta?.answer_grounding_check);
|
||||
const groundingStatus = toNonEmptyString(grounding?.status);
|
||||
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||
const routeMode = toNonEmptyString(input.addressRuntimeMeta?.capability_route_mode);
|
||||
return Boolean(mcpCallStatus === "matched_non_empty" ||
|
||||
groundingStatus === "grounded" ||
|
||||
routeMode === "exact" ||
|
||||
hasFullConfirmedTruth(input));
|
||||
}
|
||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
@@ -330,6 +358,9 @@ function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
||||
if (askedDomain !== "counterparty_value") {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
if (askedAction === "payout") {
|
||||
return detectedIntent !== "supplier_payouts_profile";
|
||||
@@ -470,6 +501,9 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
||||
if (hasRuntimeMatchedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
@@ -566,6 +600,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||
const runtimeMatchedExactReply = hasRuntimeMatchedExactReply(input, entryPoint);
|
||||
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
||||
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||
@@ -627,6 +662,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||
if (exactValueFlowReplyForBusinessOverviewDirectMoneyNeed) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_value_flow_reply_over_business_overview_direct_money_clarification");
|
||||
}
|
||||
if (exactBankOperationsAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
|
||||
}
|
||||
@@ -653,6 +691,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||
!runtimeMatchedExactReply &&
|
||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||
!exactBankOperationsAddressReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
|
||||
@@ -168,6 +168,7 @@ function isGarbageSemanticAnchorCandidate(value) {
|
||||
}
|
||||
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
|
||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
|
||||
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
||||
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)) {
|
||||
return true;
|
||||
@@ -899,6 +900,7 @@ function rawEntityResolutionCandidate(text) {
|
||||
function rawScopedEntityCandidateFromText(text) {
|
||||
const source = (0, addressTextRepair_1.repairAddressMojibakeText)(String(text ?? ""));
|
||||
const patterns = [
|
||||
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+(.+?)(?=$|[,.;:!?]|\s+(?:за|на|в|во|к|по|сколько|скок|как|какое|какой|какая|какие|получ\p{L}*|заплат\p{L}*|нетто|документ\p{L}*|движени\p{L}*|операц\p{L}*|плат[её]ж\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu,
|
||||
/(?:документ(?:ам|ы)?|движени(?:ям|я)?|операци(?:ям|и)?|плат[её]ж(?:ам|и)?)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||
];
|
||||
|
||||
@@ -4955,6 +4955,7 @@ class AssistantService {
|
||||
hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||
applyScriptGuard: applyLivingChatScriptGuardFromPolicy,
|
||||
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
|
||||
|
||||
@@ -955,7 +955,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate));
|
||||
const carryoverTargetIntent = (0, assistantContinuityPolicy_1.resolveFollowupTargetIntent)(inventoryPurchaseDateVatBridge, selectedObjectRetargetIntent, explicitIntent, sourceIntent, followupSelectionMode, deps.toNonEmptyString(inventoryRootFrame?.intent), displayedEntityTargetIntent, previousIntent, explicitInventorySameDatePivot);
|
||||
const explicitIntentForCarryover = debtRoleSwapIntent ? debtRoleSwapIntent : explicitIntent;
|
||||
const carryoverTargetIntent = (0, assistantContinuityPolicy_1.resolveFollowupTargetIntent)(inventoryPurchaseDateVatBridge, selectedObjectRetargetIntent, explicitIntentForCarryover, sourceIntent, followupSelectionMode, deps.toNonEmptyString(inventoryRootFrame?.intent), displayedEntityTargetIntent, previousIntent, explicitInventorySameDatePivot);
|
||||
return {
|
||||
followupContext: {
|
||||
previous_intent: previousIntent ?? undefined,
|
||||
|
||||
@@ -54,6 +54,7 @@ function buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps) {
|
||||
hasLivingChatSignal: deps.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: deps.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: deps.resolveDataScopeProbe,
|
||||
applyScriptGuard: deps.applyScriptGuard,
|
||||
applyGroundingGuard: deps.applyGroundingGuard,
|
||||
|
||||
@@ -818,6 +818,33 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
lowQualityTimeTokens.has(token) ||
|
||||
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||
const lowQualityGenericTokens = new Set([
|
||||
"или",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"деньги",
|
||||
"денег",
|
||||
"деньгам",
|
||||
@@ -1427,6 +1454,10 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
||||
"лежали",
|
||||
"на",
|
||||
"по",
|
||||
"остатка",
|
||||
"остаткам",
|
||||
"остатками",
|
||||
"остатков",
|
||||
"компания",
|
||||
"компании",
|
||||
"компанию",
|
||||
@@ -1524,7 +1555,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
|
||||
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||
normalizedCandidate.startsWith("по состоянию") ||
|
||||
isTemporalWarehousePhrase(candidate) ||
|
||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)
|
||||
/^(?:сейчас|на|дату|дате|остат(?:ок|ки|ка|кам|ками|ков)|по\s+остат(?:кам|ки|ку|ка|ков))$/iu.test(candidate)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2586,7 +2586,11 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
if (
|
||||
/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
!/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))
|
||||
(hasMoneyCue ||
|
||||
hasRankingCue ||
|
||||
/заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(
|
||||
normalized
|
||||
))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile",
|
||||
@@ -2955,7 +2959,7 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
|
||||
if (
|
||||
/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))
|
||||
(hasMoneyCue || hasRankingCue || /заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"supplier_payouts_profile",
|
||||
|
||||
@@ -456,8 +456,51 @@ function bankOperationDirectionLabel(direction: "incoming" | "outgoing" | "unkno
|
||||
return "банковская операция без надежно распознанного направления";
|
||||
}
|
||||
|
||||
function bankOperationEvidenceLine(rows: ComposeStageRow[]): string {
|
||||
const sample = rows[0];
|
||||
function summarizeBankOperationDirections(rows: ComposeStageRow[]): string {
|
||||
const summary = {
|
||||
incoming: { count: 0, amount: 0 },
|
||||
outgoing: { count: 0, amount: 0 },
|
||||
unknown: { count: 0, amount: 0 }
|
||||
};
|
||||
for (const row of rows) {
|
||||
const direction = bankOperationDirection(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? Math.abs(row.amount) : 0;
|
||||
summary[direction].count += 1;
|
||||
summary[direction].amount += amount;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (summary.incoming.count > 0) {
|
||||
parts.push(`входящие: ${formatMoneyRub(summary.incoming.amount)} (${summary.incoming.count} строк)`);
|
||||
}
|
||||
if (summary.outgoing.count > 0) {
|
||||
parts.push(`исходящие: ${formatMoneyRub(summary.outgoing.amount)} (${summary.outgoing.count} строк)`);
|
||||
}
|
||||
if (summary.unknown.count > 0) {
|
||||
parts.push(`без распознанного направления: ${formatMoneyRub(summary.unknown.amount)} (${summary.unknown.count} строк)`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `Сводка по направлению: ${parts.join("; ")}.`
|
||||
: "Сводка по направлению: подтвержденные строки не найдены.";
|
||||
}
|
||||
|
||||
function preferredBankEvidenceDirection(
|
||||
userMessage: string | null | undefined
|
||||
): "incoming" | "outgoing" | null {
|
||||
if (hasBankIncomingRoleBoundaryQuestion(userMessage)) {
|
||||
return "incoming";
|
||||
}
|
||||
if (hasBankOutgoingRoleBoundaryQuestion(userMessage)) {
|
||||
return "outgoing";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bankOperationEvidenceLine(
|
||||
rows: ComposeStageRow[],
|
||||
preferredDirection: "incoming" | "outgoing" | null = null
|
||||
): string {
|
||||
const sample =
|
||||
(preferredDirection ? rows.find((row) => bankOperationDirection(row) === preferredDirection) : null) ?? rows[0];
|
||||
if (!sample) {
|
||||
return "Проверенная строка 1С не найдена.";
|
||||
}
|
||||
@@ -494,13 +537,13 @@ function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: Comp
|
||||
|
||||
if (incomingBoundary) {
|
||||
return hasIncomingRow
|
||||
? "Выручкой от обычного клиента это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
|
||||
? "Это не обычный клиент и не клиентская выручка автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
|
||||
: hasOutgoingRow
|
||||
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; клиентскую выручку, кредит или депозит по этой строке не доказываю."
|
||||
: "Входящее поступление от банка в найденных строках не подтверждено; клиентскую выручку, кредитный или депозитный смысл без вида операции/назначения платежа не доказываю.";
|
||||
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; это не подтвержденная клиентская выручка, кредит или депозит."
|
||||
: "Входящее поступление от банка в найденных строках не подтверждено; это не подтвержденная клиентская выручка, кредитный или депозитный смысл.";
|
||||
}
|
||||
|
||||
return "Обычным поставщиком это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
|
||||
return "Это не обычный поставщик автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean {
|
||||
@@ -4970,12 +5013,17 @@ function composeFactualReplyBody(
|
||||
);
|
||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(rows, rows.length)
|
||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
@@ -4983,11 +5031,17 @@ function composeFactualReplyBody(
|
||||
}
|
||||
|
||||
if (intent === "bank_operations_by_contract") {
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций по договору — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
...formatTopRows(rows, rows.length)
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
||||
@@ -233,6 +233,7 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"что",
|
||||
"все",
|
||||
"всё",
|
||||
"или",
|
||||
"кроме",
|
||||
"помимо",
|
||||
"этого",
|
||||
@@ -251,6 +252,30 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"договора",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"еще",
|
||||
"ещё",
|
||||
"другие",
|
||||
@@ -853,6 +878,22 @@ function hasBroadCounterpartyRankingCue(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isBroadDebtPolarityQuestion(intent: AddressIntent, text: string): boolean {
|
||||
if (intent !== "payables_confirmed_as_of_date" && intent !== "receivables_confirmed_as_of_date") {
|
||||
return false;
|
||||
}
|
||||
const normalized = textWithRepairedVariant(String(text ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||
if (!/(?:долж|задолж|дебитор|кредитор|обязательств)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:по\s+(?:нему|ней|ним|этому|этой|этому\s+контрагенту|этой\s+компании|поставщику|клиенту|покупателю|заказчику)|\bон\b|\bона\b)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function mergeFollowupFilters(
|
||||
current: AddressFilterSet,
|
||||
intent: AddressIntent,
|
||||
@@ -1062,11 +1103,16 @@ function mergeFollowupFilters(
|
||||
previousCounterparty ??
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||
const shouldInheritCounterparty =
|
||||
!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
|
||||
!suppressCounterpartyForBroadDebtQuestion &&
|
||||
(!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
|
||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
}
|
||||
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
|
||||
hasLivingChatSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
|
||||
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
|
||||
applyScriptGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyScriptGuard"];
|
||||
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
|
||||
@@ -185,6 +186,7 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface BuildAssistantLivingChatAttemptRuntimeInputInput<ResponseType =
|
||||
hasLivingChatSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
|
||||
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
|
||||
applyScriptGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyScriptGuard"];
|
||||
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
|
||||
@@ -79,6 +80,7 @@ export function buildAssistantLivingChatAttemptRuntimeInput<ResponseType = unkno
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
||||
@@ -78,6 +78,7 @@ export async function runAssistantLivingChatAttemptRuntime<ResponseType = unknow
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
||||
@@ -59,6 +59,7 @@ export function buildAssistantLivingChatHandlerRuntimeInput<ResponseType = unkno
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface TryHandleAssistantLivingChatRuntimeInput<ResponseType = unknown
|
||||
hasLivingChatSignal: AssistantLivingChatRuntimeInput["hasLivingChatSignal"];
|
||||
shouldEmitOrganizationSelectionReply: AssistantLivingChatRuntimeInput["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage: AssistantLivingChatRuntimeInput["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: AssistantLivingChatRuntimeInput["resolveDataScopeProbe"];
|
||||
executeLlmChat: AssistantLivingChatRuntimeInput["executeLlmChat"];
|
||||
applyScriptGuard: AssistantLivingChatRuntimeInput["applyScriptGuard"];
|
||||
@@ -81,6 +82,7 @@ export async function tryHandleAssistantLivingChatRuntime<ResponseType = unknown
|
||||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface AssistantLivingChatRuntimeInput {
|
||||
hasLivingChatSignal: (message: string) => boolean;
|
||||
shouldEmitOrganizationSelectionReply: (message: string, activeOrganization: string | null) => boolean;
|
||||
hasAssistantCapabilityQuestionSignal: (message: string) => boolean;
|
||||
resolveOrganizationSelectionFromMessage: (message: string, knownOrganizations: unknown[]) => string | null;
|
||||
resolveDataScopeProbe: () => Promise<Record<string, unknown> | null>;
|
||||
executeLlmChat: () => Promise<string>;
|
||||
applyScriptGuard: (chatText: string, userMessage: string) => {
|
||||
@@ -78,6 +79,54 @@ function hasPriorAssistantTurn(items: unknown[]): boolean {
|
||||
return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant");
|
||||
}
|
||||
|
||||
function shouldProbeBareOrganizationScopeCandidate(input: {
|
||||
userMessage: string;
|
||||
selectedOrganization: string | null;
|
||||
activeOrganization: string | null;
|
||||
dataScopeMetaQuery: boolean;
|
||||
capabilityMetaQuery: boolean;
|
||||
destructiveSignal: boolean;
|
||||
dangerSignal: boolean;
|
||||
operationalSignal: boolean;
|
||||
}): boolean {
|
||||
if (
|
||||
input.selectedOrganization ||
|
||||
input.activeOrganization ||
|
||||
input.dataScopeMetaQuery ||
|
||||
input.capabilityMetaQuery ||
|
||||
input.destructiveSignal ||
|
||||
input.dangerSignal ||
|
||||
input.operationalSignal
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const raw = String(input.userMessage ?? "").trim();
|
||||
if (!raw || raw.length > 80 || /[?!]/u.test(raw) || /\d/u.test(raw) || !/\p{L}/u.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = raw.split(/\s+/u).filter(Boolean).length;
|
||||
if (tokenCount < 1 || tokenCount > 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalized = raw
|
||||
.toLowerCase()
|
||||
.replace(/\u0451/gu, "\u0435")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
if (
|
||||
/^(?:\u043f\u0440\u0438\u0432\u0435\u0442|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435|\u0434\u0430|\u043d\u0435\u0442|\u043e\u043a|\u043e\u043a\u0435\u0439|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u043f\u043e\u043a\u0430|\u0433\u043e|\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u043d\u044f\u043b|\u043f\u043e\u043d\u044f\u043b\u0430)(?:\s|$)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043f\u043e\u043a\u0430\u0436\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438|\u0447\u0442\u043e|\u043a\u0430\u043a|\u0433\u0434\u0435|\u043a\u043e\u0433\u0434\u0430|\u043f\u043e\u0447\u0435\u043c\u0443|\u0437\u0430\u0447\u0435\u043c|\u043c\u043e\u0436\u0435\u0448\u044c|\u0443\u043c\u0435\u0435\u0448\u044c|\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u0445\u043e\u0447\u0443|\u043e\u0441\u0442\u0430\u0442\u043a|\u043d\u0434\u0441|\u0434\u043e\u043b\u0433|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043f\u0440\u0438\u0431\u044b\u043b)/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function buildDeterministicSmalltalkLeadReply(): string {
|
||||
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
|
||||
}
|
||||
@@ -160,6 +209,8 @@ export async function runAssistantLivingChatRuntime(
|
||||
let livingChatGroundingGuardApplied = false;
|
||||
let livingChatGroundingGuardReason: string | null = null;
|
||||
let livingChatProactiveScopeOfferApplied = false;
|
||||
let livingChatBareScopeProbeAttempted = false;
|
||||
let livingChatBareScopeProbeMatchedOrganization: string | null = null;
|
||||
const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization;
|
||||
let knownOrganizations = [...organizationAuthority.knownOrganizations];
|
||||
let selectedOrganization = organizationAuthority.selectedOrganization;
|
||||
@@ -186,6 +237,32 @@ export async function runAssistantLivingChatRuntime(
|
||||
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
||||
const lastAnswerInspectionAddressDebug = memoryRecapContext.lastAnswerInspectionAddressDebug;
|
||||
|
||||
if (
|
||||
shouldProbeBareOrganizationScopeCandidate({
|
||||
userMessage,
|
||||
selectedOrganization,
|
||||
activeOrganization,
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
destructiveSignal,
|
||||
dangerSignal,
|
||||
operationalSignal
|
||||
})
|
||||
) {
|
||||
dataScopeProbe = await input.resolveDataScopeProbe();
|
||||
livingChatBareScopeProbeAttempted = true;
|
||||
knownOrganizations = input.mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(dataScopeProbe?.organizations) ? (dataScopeProbe.organizations as unknown[]) : [])
|
||||
]);
|
||||
const probedOrganization = input.resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations);
|
||||
if (probedOrganization) {
|
||||
selectedOrganization = probedOrganization;
|
||||
activeOrganization = probedOrganization;
|
||||
livingChatBareScopeProbeMatchedOrganization = probedOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = input.buildAssistantSafetyRefusalReply();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
@@ -388,6 +465,8 @@ export async function runAssistantLivingChatRuntime(
|
||||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
|
||||
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
|
||||
living_chat_bare_scope_probe_attempted: livingChatBareScopeProbeAttempted,
|
||||
living_chat_bare_scope_probe_matched_organization: livingChatBareScopeProbeMatchedOrganization,
|
||||
living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null,
|
||||
living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null,
|
||||
living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations)
|
||||
|
||||
@@ -633,7 +633,7 @@ function businessOverviewOutgoingLeaderLine(overview: BusinessOverview): string
|
||||
function businessOverviewSupplierBoundaryBasis(overview: BusinessOverview): string {
|
||||
const leader = overview.top_suppliers?.[0] ?? null;
|
||||
if (!leader) {
|
||||
return "есть только общий срез исходящих платежей без надежного vendor-risk профиля";
|
||||
return "есть только общий срез исходящих платежей без надежного профиля поставщицкого риска";
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
@@ -672,9 +672,9 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
||||
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
|
||||
const margin =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} 90/91/99 ${amount}; ${margin}`);
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} по закрытию счетов 90/91/99 ${amount}; ${margin}`);
|
||||
}
|
||||
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||
if (strongestIncomingYear) {
|
||||
@@ -685,7 +685,7 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
||||
return parts.length > 0
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: `${parts.join("; ")}. Это операционный денежный сигнал по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -706,13 +706,13 @@ function businessOverviewAccountingFinancialResultText(overview: BusinessOvervie
|
||||
: result.final_result_amount_human_ru;
|
||||
const marginText =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
const basis =
|
||||
result.final_transfer_basis === "account_99_to_84_period_close"
|
||||
? "по закрытию 99 на 84"
|
||||
: "по закрытию 90/91 на 99";
|
||||
return `По бухгалтерскому маршруту 90/91/99 за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
return `Нет: денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
}
|
||||
|
||||
function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): string | null {
|
||||
@@ -780,12 +780,12 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview
|
||||
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
|
||||
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
|
||||
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим срезом не доказаны.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_procurement_concentration") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
}
|
||||
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period} нашла исходящие платежи на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный аудит поставщицкого риска не подтвержден.`;
|
||||
}
|
||||
|
||||
function businessOverviewInventoryQualityEventsText(overview: BusinessOverview): string | null {
|
||||
@@ -831,7 +831,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
if (accountingFinancialResultText) {
|
||||
return accountingFinancialResultText;
|
||||
}
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финрезультат.";
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финрезультат.";
|
||||
}
|
||||
if (isDebtDueDateBoundaryTurn(pilot)) {
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
@@ -1588,6 +1588,14 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
||||
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
|
||||
);
|
||||
}
|
||||
if (
|
||||
overview.incoming_customer_revenue.coverage_recovered_by_period_chunking ||
|
||||
overview.outgoing_supplier_payout.coverage_recovered_by_period_chunking
|
||||
) {
|
||||
lines.push(
|
||||
"Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос."
|
||||
);
|
||||
}
|
||||
if (overview.activity_period) {
|
||||
lines.push(
|
||||
`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`
|
||||
@@ -1782,7 +1790,7 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал концентрации закупок/исходящих платежей по найденным строкам, а не полный аудит поставщицкого риска или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
@@ -1808,7 +1816,7 @@ function businessOverviewYearlyOperatingLine(overview: BusinessOverview): string
|
||||
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
|
||||
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
|
||||
}
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это операционный денежный сигнал, не бухгалтерская прибыль и не финрезультат.`;
|
||||
}
|
||||
|
||||
function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | null {
|
||||
@@ -1838,9 +1846,9 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
||||
: "нулевой учетный финрезультат";
|
||||
const marginText =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} по закрытию счетов 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection =
|
||||
|
||||
@@ -4879,6 +4879,14 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`
|
||||
);
|
||||
}
|
||||
if (
|
||||
derived.incoming_customer_revenue.coverage_recovered_by_period_chunking ||
|
||||
derived.outgoing_supplier_payout.coverage_recovered_by_period_chunking
|
||||
) {
|
||||
facts.push(
|
||||
"Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос."
|
||||
);
|
||||
}
|
||||
if (derived.activity_period) {
|
||||
facts.push(
|
||||
`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.`
|
||||
@@ -5159,7 +5167,7 @@ function buildBusinessOverviewUnknownFacts(derived: AssistantMcpDiscoveryDerived
|
||||
: null
|
||||
].filter((item): item is string => Boolean(item));
|
||||
if (derived?.coverage_limited_by_probe_limit) {
|
||||
unknowns.unshift("Полное покрытие бизнес-обзора не подтверждено: хотя бы один денежный probe достиг лимита строк.");
|
||||
unknowns.unshift("Полное покрытие бизнес-обзора не подтверждено: хотя бы один денежный запрос достиг верхней границы выборки.");
|
||||
}
|
||||
return unknowns;
|
||||
}
|
||||
@@ -6183,6 +6191,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
if (!incomingResult?.error || !outgoingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_movements_mcp_executed");
|
||||
}
|
||||
if (incomingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_incoming_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (outgoingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (taxResult?.error) {
|
||||
pushUnique(queryLimitations, taxResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_error");
|
||||
|
||||
@@ -110,6 +110,8 @@ interface PlannerBudgetOverride {
|
||||
maxProbeCount?: number;
|
||||
}
|
||||
|
||||
const CHUNKED_COVERAGE_PROBE_BUDGET = 30;
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
@@ -607,12 +609,15 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla
|
||||
(recipe.semanticDataNeed === "counterparty value-flow evidence" ||
|
||||
recipe.semanticDataNeed === "bidirectional value-flow comparison evidence" ||
|
||||
recipe.semanticDataNeed === "ranked value-flow evidence");
|
||||
if (!isValueFlowRecipe) {
|
||||
const isBusinessOverviewRecipe =
|
||||
recipe.primitives.includes("query_movements") &&
|
||||
recipe.chainId === "business_overview";
|
||||
if (!isValueFlowRecipe && !isBusinessOverviewRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: 30
|
||||
maxProbeCount: CHUNKED_COVERAGE_PROBE_BUDGET
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -471,8 +471,10 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
|
||||
if (outgoing?.coverage_limited_by_probe_limit === true) {
|
||||
limited.push("исходящие");
|
||||
}
|
||||
const continuation =
|
||||
"Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога.";
|
||||
return limited.length > 0
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}`
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -649,6 +651,8 @@ function buildCompactBidirectionalValueFlowReply(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
|
||||
draft: Record<string, unknown>
|
||||
): string | null {
|
||||
const turnInput = toRecordObject(entryPoint.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
@@ -665,7 +669,13 @@ function buildCompactBidirectionalValueFlowReply(
|
||||
return null;
|
||||
}
|
||||
|
||||
const counterparty = toNonEmptyString(flow.counterparty) ?? "запрошенному контрагенту";
|
||||
const counterparty = toNonEmptyString(flow.counterparty);
|
||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const subjectLead = counterparty
|
||||
? `по контрагенту ${counterparty}`
|
||||
: organizationScope
|
||||
? `по компании ${organizationScope}`
|
||||
: "по выбранному контуру";
|
||||
const period = toNonEmptyString(flow.period_scope);
|
||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||
const incomingRows = sideRowsText(incoming);
|
||||
@@ -674,7 +684,7 @@ function buildCompactBidirectionalValueFlowReply(
|
||||
const outgoingDates = sideDateText(outgoing);
|
||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||
const lines = [
|
||||
`Коротко: по контрагенту ${counterparty}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
];
|
||||
|
||||
const basis: string[] = [];
|
||||
@@ -904,7 +914,7 @@ function buildCompactBusinessOverviewReply(
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(
|
||||
`Коротко: по бухгалтерскому маршруту 90/91/99 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`
|
||||
`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
|
||||
);
|
||||
lines.push(
|
||||
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
|
||||
@@ -916,7 +926,7 @@ function buildCompactBusinessOverviewReply(
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат."
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
|
||||
);
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
@@ -929,7 +939,7 @@ function buildCompactBusinessOverviewReply(
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push(
|
||||
"Для точного P&L нужны отдельный маршрут по себестоимости, расходам, закрытию периода и финрезультату; текущий proxy нельзя выдавать за подтвержденную чистую прибыль или маржу."
|
||||
"Для точного отчета о прибыли нужны отдельная проверка себестоимости, расходов, закрытия периода и финрезультата; текущий ограниченный сигнал нельзя выдавать за подтвержденную чистую прибыль или маржу."
|
||||
);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
@@ -1056,7 +1066,7 @@ function buildCompactBusinessOverviewReply(
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
|
||||
: "есть только ограниченный срез исходящих платежей без полного профиля поставщицкого риска";
|
||||
const proxyLabel = topSupplierLooksFinancial
|
||||
? "сигнал концентрации исходящих денег"
|
||||
: "сигнал концентрации закупок/исходящих платежей";
|
||||
@@ -1106,7 +1116,7 @@ function buildCompactBusinessOverviewReply(
|
||||
);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
lines.push(
|
||||
`Можно утверждать: по компании подтвержден operating-flow proxy по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`
|
||||
`Можно утверждать: по компании подтвержден операционный денежный сигнал по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`
|
||||
);
|
||||
lines.push(
|
||||
`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`
|
||||
@@ -1150,7 +1160,7 @@ function buildCompactBusinessOverviewReply(
|
||||
lines.push(
|
||||
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
|
||||
);
|
||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(
|
||||
topCustomerLooksFinancial
|
||||
|
||||
@@ -455,6 +455,42 @@ function hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(
|
||||
);
|
||||
}
|
||||
|
||||
function hasExactBankOperationsAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
|
||||
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
const isBankIntent =
|
||||
detectedIntent === "bank_operations_by_counterparty" || detectedIntent === "bank_operations_by_contract";
|
||||
const isBankRecipe =
|
||||
selectedRecipe === "address_bank_operations_by_counterparty_v1" ||
|
||||
selectedRecipe === "address_bank_operations_by_contract_v1";
|
||||
if (!isBankIntent || !isBankRecipe) {
|
||||
return false;
|
||||
}
|
||||
const grounding = toRecordObject(input.addressRuntimeMeta?.answer_grounding_check);
|
||||
const groundingStatus = toNonEmptyString(grounding?.status);
|
||||
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||
const routeMode = toNonEmptyString(input.addressRuntimeMeta?.capability_route_mode);
|
||||
return Boolean(
|
||||
mcpCallStatus === "matched_non_empty" ||
|
||||
groundingStatus === "grounded" ||
|
||||
routeMode === "exact" ||
|
||||
hasFullConfirmedTruth(input)
|
||||
);
|
||||
}
|
||||
|
||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
@@ -471,6 +507,9 @@ function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
if (askedDomain !== "counterparty_value") {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
if (askedAction === "payout") {
|
||||
return detectedIntent !== "supplier_payouts_profile";
|
||||
@@ -647,6 +686,9 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
|
||||
if (hasRuntimeMatchedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
@@ -771,6 +813,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
input,
|
||||
entryPoint
|
||||
);
|
||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
@@ -851,6 +894,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
"mcp_discovery_response_policy_keep_exact_value_flow_reply_over_business_overview_direct_money_clarification"
|
||||
);
|
||||
}
|
||||
if (exactBankOperationsAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
@@ -882,6 +928,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
!runtimeMatchedExactReply &&
|
||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||
!exactBankOperationsAddressReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
|
||||
@@ -222,6 +222,9 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
|
||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
||||
text
|
||||
) ||
|
||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(
|
||||
text
|
||||
) ||
|
||||
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
||||
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)
|
||||
) {
|
||||
@@ -1303,6 +1306,7 @@ function rawEntityResolutionCandidate(text: string): string | null {
|
||||
function rawScopedEntityCandidateFromText(text: string): string | null {
|
||||
const source = repairAddressMojibakeText(String(text ?? ""));
|
||||
const patterns = [
|
||||
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+(.+?)(?=$|[,.;:!?]|\s+(?:за|на|в|во|к|по|сколько|скок|как|какое|какой|какая|какие|получ\p{L}*|заплат\p{L}*|нетто|документ\p{L}*|движени\p{L}*|операц\p{L}*|плат[её]ж\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu,
|
||||
/(?:документ(?:ам|ы)?|движени(?:ям|я)?|операци(?:ям|и)?|плат[её]ж(?:ам|и)?)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||
];
|
||||
|
||||
@@ -4913,6 +4913,7 @@ export class AssistantService {
|
||||
hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||
applyScriptGuard: applyLivingChatScriptGuardFromPolicy,
|
||||
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
|
||||
|
||||
@@ -1332,10 +1332,11 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate)
|
||||
);
|
||||
const explicitIntentForCarryover = debtRoleSwapIntent ? debtRoleSwapIntent : explicitIntent;
|
||||
const carryoverTargetIntent = resolveFollowupTargetIntent(
|
||||
inventoryPurchaseDateVatBridge,
|
||||
selectedObjectRetargetIntent,
|
||||
explicitIntent,
|
||||
explicitIntentForCarryover,
|
||||
sourceIntent,
|
||||
followupSelectionMode,
|
||||
deps.toNonEmptyString(inventoryRootFrame?.intent),
|
||||
|
||||
@@ -72,6 +72,8 @@ export interface AssistantTurnRuntimeBuilderDeps<ResponseType = unknown> {
|
||||
AddressAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal:
|
||||
AddressAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage:
|
||||
AddressAttemptRuntimeInput<ResponseType>["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: AddressAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
|
||||
applyScriptGuard: AddressAttemptRuntimeInput<ResponseType>["applyScriptGuard"];
|
||||
applyGroundingGuard: AddressAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
|
||||
@@ -170,6 +172,7 @@ export function buildAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
|
||||
hasLivingChatSignal: deps.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: deps.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: deps.resolveDataScopeProbe,
|
||||
applyScriptGuard: deps.applyScriptGuard,
|
||||
applyGroundingGuard: deps.applyGroundingGuard,
|
||||
|
||||
@@ -584,11 +584,39 @@ describe("address compose stage utf8 headers", () => {
|
||||
|
||||
expect(reply.text).toContain("по СБЕРБАНК");
|
||||
expect(reply.text).toContain("входящее поступление от банка в этом срезе не подтверждено");
|
||||
expect(reply.text).toContain("клиентскую выручку");
|
||||
expect(reply.text).toContain("не подтвержденная клиентская выручка");
|
||||
expect(reply.text).toContain("Сводка по направлению");
|
||||
expect(reply.text).toContain("Основание 1С");
|
||||
expect(reply.text).toContain("вид операции/назначение платежа/договор");
|
||||
});
|
||||
|
||||
it("keeps bank operation drilldown compact when many rows are available", () => {
|
||||
const rows = Array.from({ length: 8 }, (_, index) => ({
|
||||
period: `2020-01-${String(index + 1).padStart(2, "0")}T12:00:00Z`,
|
||||
registrator:
|
||||
index % 2 === 0
|
||||
? `Поступление на расчетный счет 0000000000${index}`
|
||||
: `Списание с расчетного счета 0000000000${index}`,
|
||||
account_dt: "0",
|
||||
account_kt: "0",
|
||||
amount: 100 + index,
|
||||
analytics: ["СБЕРБАНК, ПАО", "0"],
|
||||
counterparty: "СБЕРБАНК, ПАО",
|
||||
operation_kind: index % 2 === 0 ? "Прочее поступление" : "Прочее списание",
|
||||
payment_purpose: index % 2 === 0 ? "Депозит" : "Комиссия банка"
|
||||
}));
|
||||
|
||||
const reply = composeFactualReply("bank_operations_by_counterparty", rows, {
|
||||
counterpartyHint: "СБЕРБАНК",
|
||||
userMessage: "СБЕРБАНК это поставщик или финансовые списания?"
|
||||
});
|
||||
|
||||
expect(reply.text).toContain("Сводка по направлению");
|
||||
expect(reply.text).toContain("Это не обычный поставщик автоматически");
|
||||
expect(reply.text).toContain("Показаны первые 5 из 8");
|
||||
expect(reply.text).not.toContain("00000000007");
|
||||
});
|
||||
|
||||
it("renders readable russian header for contracts-by-counterparty list", () => {
|
||||
const reply = composeFactualReply("list_contracts_by_counterparty", [
|
||||
{
|
||||
@@ -2692,6 +2720,13 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("supplier_payouts_profile");
|
||||
});
|
||||
|
||||
it("resolves explicit supplier payment amount question into supplier payouts profile", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
|
||||
);
|
||||
expect(result.intent).toBe("supplier_payouts_profile");
|
||||
});
|
||||
|
||||
it("resolves contract usage and value intent", () => {
|
||||
const result = resolveAddressIntent("договоры по обороту ранкни и дай топ-20");
|
||||
expect(result.intent).toBe("contract_usage_and_value");
|
||||
@@ -2989,6 +3024,38 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
});
|
||||
|
||||
it("drops generic customer/supplier role tail from broad company overview wording", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||
"customer_revenue_and_payments"
|
||||
);
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(extracted.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(extracted.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("clears generic customer/supplier role tail from inherited follow-up anchor", () => {
|
||||
const result = runAddressDecomposeStage(
|
||||
"Покажи документы по нему за 2020",
|
||||
{
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "или поставщик"
|
||||
}
|
||||
);
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(result?.filters.warnings).toContain("counterparty_cleared_low_quality_followup_anchor");
|
||||
});
|
||||
|
||||
it("keeps real counterparty after explicit supplier role prefix", () => {
|
||||
const extracted = extractAddressFilters("Покажи платежи по поставщику Альфа за июль 2020", "supplier_payouts_profile");
|
||||
expect(extracted.extracted_filters.counterparty).toBe("Альфа");
|
||||
expect(extracted.warnings).not.toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
|
||||
@@ -4751,6 +4818,65 @@ describe("address decompose stage follow-up carryover", () => {
|
||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("does not inherit stale counterparty for broad receivables mirror question", () => {
|
||||
const result = runAddressDecomposeStage("а нам кто должен на конец 2020?", {
|
||||
previous_intent: "payables_confirmed_as_of_date",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31",
|
||||
as_of_date: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-12-31");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.baseReasons).toContain("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
expect(result?.baseReasons).not.toContain("counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("does not inherit stale counterparty for broad payables mirror question", () => {
|
||||
const result = runAddressDecomposeStage("кому мы должны на конец 2020?", {
|
||||
previous_intent: "supplier_payouts_profile",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.baseReasons).toContain("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
});
|
||||
|
||||
it("keeps referential counterparty carryover for debt question that explicitly says по нему", () => {
|
||||
const result = runAddressDecomposeStage("по нему сколько он нам должен на конец 2020?", {
|
||||
previous_intent: "customer_revenue_and_payments",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||
expect(result?.baseReasons).toContain("counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => {
|
||||
const result = runAddressDecomposeStage("а теперь банковские операции", {
|
||||
previous_intent: "list_documents_by_contract",
|
||||
@@ -5542,6 +5668,13 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||
expect(filters.as_of_date).toBe("2020-03-31");
|
||||
});
|
||||
|
||||
it("does not treat generic 'по остаткам' as a warehouse anchor", () => {
|
||||
const result = runAddressDecomposeStage("кайф - что там на складе по остаткам?");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds exact balance query for inventory-on-hand snapshot", () => {
|
||||
const selected = selectAddressRecipe("inventory_on_hand_as_of_date", {
|
||||
as_of_date: "2020-03-31"
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("assistant address follow-up carryover", () => {
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(["factual", "factual_with_explanation"]).toContain(second.reply_type);
|
||||
expect(["factual", "factual_with_explanation", "partial_coverage"]).toContain(second.reply_type);
|
||||
expect(second.debug?.detected_mode).toBe("address_query");
|
||||
expect(second.debug?.detected_intent).toBe("list_documents_by_counterparty");
|
||||
expect(second.debug?.extracted_filters?.counterparty).toBe("свк");
|
||||
|
||||
@@ -28,13 +28,16 @@ function buildInput(overrides: Record<string, unknown> = {}) {
|
||||
hasOperationalAdminActionRequestSignal: () => false,
|
||||
hasOrganizationFactLookupSignal: () => false,
|
||||
hasOrganizationFactFollowupSignal: () => false,
|
||||
hasLivingChatSignal: () => false,
|
||||
shouldEmitOrganizationSelectionReply: () => false,
|
||||
hasAssistantCapabilityQuestionSignal: () => false,
|
||||
resolveOrganizationSelectionFromMessage: () => null,
|
||||
resolveDataScopeProbe: () => null,
|
||||
applyScriptGuard: (chatText: string) => chatText,
|
||||
applyGroundingGuard: (guardInput: Record<string, unknown>) => guardInput,
|
||||
buildAssistantSafetyRefusalReply: () => "safety",
|
||||
buildAssistantDataScopeContractReply: () => "scope",
|
||||
buildAssistantProactiveOrganizationOfferReply: () => "offer",
|
||||
buildAssistantOrganizationFactBoundaryReply: () => "boundary",
|
||||
buildAssistantDataScopeSelectionReply: () => "selection",
|
||||
buildAssistantOperationalBoundaryReply: () => "operational",
|
||||
|
||||
@@ -46,8 +46,10 @@ describe("assistant living chat attempt runtime input builder", () => {
|
||||
hasOperationalAdminActionRequestSignal: vi.fn(() => false),
|
||||
hasOrganizationFactLookupSignal: vi.fn(() => false),
|
||||
hasOrganizationFactFollowupSignal: vi.fn(() => false),
|
||||
hasLivingChatSignal: vi.fn(() => false),
|
||||
shouldEmitOrganizationSelectionReply: vi.fn(() => false),
|
||||
hasAssistantCapabilityQuestionSignal: vi.fn(() => false),
|
||||
resolveOrganizationSelectionFromMessage: vi.fn(() => null),
|
||||
resolveDataScopeProbe: vi.fn(async () => null),
|
||||
executeLlmChat,
|
||||
applyScriptGuard: vi.fn((text: string) => ({ text, applied: false, reason: null })),
|
||||
@@ -58,6 +60,7 @@ describe("assistant living chat attempt runtime input builder", () => {
|
||||
})),
|
||||
buildAssistantSafetyRefusalReply: vi.fn(() => "safety"),
|
||||
buildAssistantDataScopeContractReply: vi.fn(() => "scope"),
|
||||
buildAssistantProactiveOrganizationOfferReply: vi.fn(() => "offer"),
|
||||
buildAssistantOrganizationFactBoundaryReply: vi.fn(() => "boundary"),
|
||||
buildAssistantDataScopeSelectionReply: vi.fn(() => "selection"),
|
||||
buildAssistantOperationalBoundaryReply: vi.fn(() => "operational"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAssistantLivingChatRuntime } from "../src/services/assistantLivingChatRuntimeAdapter";
|
||||
import { resolveOrganizationSelectionFromMessage } from "../src/services/assistantOrganizationMatcher";
|
||||
|
||||
function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
@@ -37,6 +38,7 @@ function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
|
||||
hasLivingChatSignal: () => true,
|
||||
shouldEmitOrganizationSelectionReply: () => false,
|
||||
hasAssistantCapabilityQuestionSignal: () => false,
|
||||
resolveOrganizationSelectionFromMessage: () => null,
|
||||
resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: (chatText: string) => ({
|
||||
@@ -87,6 +89,47 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(1);
|
||||
});
|
||||
|
||||
it("probes data scope before LLM when a fresh chat turn is a bare organization name", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
const resolveDataScopeProbe = vi.fn(async () => ({
|
||||
status: "resolved",
|
||||
channel: "default",
|
||||
organizations: [
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
"\u041e\u041e\u041e \u041b\u0430\u0439\u0441\u0432\u0443\u0434"
|
||||
],
|
||||
error: null
|
||||
}));
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
modeDecision: { mode: "chat", reason: "non_domain_query_indexed" },
|
||||
hasLivingChatSignal: () => false,
|
||||
shouldEmitOrganizationSelectionReply: (_message: string, organization: string | null) => Boolean(organization),
|
||||
resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
buildAssistantDataScopeSelectionReply: (organization: string | null) => `selection:${organization ?? "none"}`
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toBe(
|
||||
"selection:\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_data_scope_selection_contract");
|
||||
expect(output.debug?.living_chat_bare_scope_probe_attempted).toBe(true);
|
||||
expect(output.debug?.living_chat_bare_scope_probe_matched_organization).toBe(
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(output.debug?.assistant_active_organization).toBe(
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(2);
|
||||
expect(resolveDataScopeProbe).toHaveBeenCalledTimes(1);
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("selects safety refusal branch for dangerous capability meta query", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
const input = buildRuntimeInput({
|
||||
|
||||
@@ -311,6 +311,104 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
it("recovers explicit-year business overview money coverage through monthly value-flow chunks", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "broad_evaluation",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
const broadIncomingRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 1,
|
||||
Counterparty: "Клиент из широкого запроса"
|
||||
}));
|
||||
const broadOutgoingRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 1,
|
||||
Counterparty: "Поставщик из широкого запроса"
|
||||
}));
|
||||
const incomingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
|
||||
Amount: (index + 1) * 100,
|
||||
Counterparty: index % 2 === 0 ? "Клиент А" : "Клиент Б"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const outgoingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-10T00:00:00`,
|
||||
Amount: (index + 1) * 50,
|
||||
Counterparty: index % 2 === 0 ? "Поставщик А" : "Поставщик Б"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: broadIncomingRows },
|
||||
...incomingMonthlyResults,
|
||||
{ rows: broadOutgoingRows },
|
||||
...outgoingMonthlyResults
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(planner.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.derived_business_overview).toMatchObject({
|
||||
organization_scope: "ООО Альтернатива Плюс",
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount: 7800,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month"
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount: 3900,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month"
|
||||
},
|
||||
net_amount: 3900,
|
||||
coverage_limited_by_probe_limit: false
|
||||
});
|
||||
expect(result.evidence.confirmed_facts).toContain(
|
||||
"Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос."
|
||||
);
|
||||
expect(result.evidence.unknown_facts).not.toContain(
|
||||
"Полное покрытие бизнес-обзора не подтверждено: хотя бы один денежный запрос достиг верхней границы выборки."
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_incoming_monthly_period_chunking_recovered_coverage");
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
});
|
||||
|
||||
it("marks bank-like counterparties in business-overview rankings before evidence wording", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
||||
@@ -288,6 +288,48 @@ describe("assistant MCP discovery planner", () => {
|
||||
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_business_overview");
|
||||
});
|
||||
|
||||
it("enables chunked coverage budget for explicit-year business overviews", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "broad_evaluation",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: [
|
||||
"no_raw_model_claims",
|
||||
"no_unchecked_business_health_claim",
|
||||
"no_profit_or_margin_claim_without_evidence"
|
||||
],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.selected_chain_id).toBe("business_overview");
|
||||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
||||
});
|
||||
|
||||
it("keeps bidirectional value-flow comparison executable when checked totals are derived without aggregate_by_axis", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
||||
@@ -159,6 +159,60 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).not.toContain("47 628 853");
|
||||
});
|
||||
|
||||
it("answers profit follow-ups with a direct cash-flow boundary before accounting result detail", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "profit_margin_boundary",
|
||||
unsupported_but_understood_family: "profit_margin_boundary",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
ranking_need: null,
|
||||
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: {
|
||||
accounting_financial_result: {
|
||||
period_scope: "2020",
|
||||
final_result_direction: "loss",
|
||||
final_result_amount_human_ru: "7 136 815,85 руб.",
|
||||
net_margin_to_revenue_pct: -59.41
|
||||
}
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Коротко: по бухгалтерскому маршруту 90/91/99 за 2020 подтвержден учетный убыток.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("нет, денежное операционное нетто не стоит считать чистой прибылью");
|
||||
expect(candidate.reply_text).toContain("по закрытию счетов 90/91/99 в 1С за 2020");
|
||||
expect(candidate.reply_text).toContain("учетный убыток");
|
||||
expect(candidate.reply_text).toContain("маржа к подтвержденной выручке -59.41%");
|
||||
expect(candidate.reply_text).not.toContain("бухгалтерскому маршруту");
|
||||
expect(candidate.reply_text).not.toContain("маржа к выручке 90.01");
|
||||
});
|
||||
|
||||
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
@@ -392,6 +446,8 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
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("лимит выборки MCP");
|
||||
expect(candidate.reply_text).not.toContain("MCP-срез");
|
||||
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
||||
@@ -465,11 +521,65 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).toContain("12 474 036,91 руб");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("крупнейший источник входящих денег: ГКУ УКРиС");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("крупнейший получатель исходящих денег: ООО Поставщик");
|
||||
expect(candidate.reply_text).toContain("денежный операционный показатель");
|
||||
expect(candidate.reply_text).toContain("операционный денежный показатель");
|
||||
expect(candidate.reply_text).not.toContain("Что можно сказать только как вывод:");
|
||||
expect(candidate.reply_text).not.toContain("Складской срез");
|
||||
});
|
||||
|
||||
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "value_flow",
|
||||
comparison_need: "incoming_vs_outgoing",
|
||||
reason_codes: ["data_need_graph_family_value_flow"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
derived_bidirectional_value_flow: {
|
||||
counterparty: null,
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб.",
|
||||
rows_with_amount: 44
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб.",
|
||||
rows_with_amount: 299
|
||||
},
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
coverage_limited_by_probe_limit: false
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Денежный поток подтвержден.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("по компании ООО Альтернатива Плюс за период 2020");
|
||||
expect(candidate.reply_text).not.toContain("по контрагенту запрошенному контрагенту");
|
||||
});
|
||||
|
||||
it("does not present bank-like incoming leaders as ordinary client revenue", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
||||
@@ -606,6 +606,64 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||
});
|
||||
|
||||
it("keeps exact bank operation replies over generic value-flow discovery candidates", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply:
|
||||
"Exact bank operation answer: incoming/outgoing rows include operation kind, payment purpose, and contract; do not classify the bank as a regular customer or supplier automatically.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "bank_operations_by_counterparty",
|
||||
selected_recipe: "address_bank_operations_by_counterparty_v1",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
capability_route_mode: "exact",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
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: ["SBERBANK"],
|
||||
reason_codes: ["data_need_graph_built"]
|
||||
},
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SBERBANK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
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: "Generic value-flow answer.",
|
||||
confirmed_lines: ["Outgoing payout total only."],
|
||||
inference_lines: ["Generic supplier payout interpretation."],
|
||||
unknown_lines: ["Incoming role is unknown."],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toContain("operation kind");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
});
|
||||
|
||||
it("overrides an exact ranking-shaped address reply when open-scope ranking still needs organization", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply:
|
||||
|
||||
@@ -196,6 +196,32 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps multi-token scoped counterparty from net wording when LLM entities are empty", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||
followupContext: {
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_raw_scope");
|
||||
});
|
||||
|
||||
it("overrides a supported exact current-turn payout route when the question asks for a payment amount", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
@@ -1733,6 +1759,36 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
||||
});
|
||||
|
||||
it("does not turn generic role tails into predecompose counterparties for business overview", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: {
|
||||
counterparty: "или поставщик",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
period: {
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31",
|
||||
has_explicit_period: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||
});
|
||||
|
||||
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
|
||||
@@ -2013,4 +2013,48 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["\u041d\u0414\u0421"]);
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
|
||||
});
|
||||
|
||||
it("lets short receivables-to-payables mirror override an LLM open-items expansion", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043d\u0430\u043c \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 13.05.2026.",
|
||||
debug: {
|
||||
detected_intent: "receivables_confirmed_as_of_date",
|
||||
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
|
||||
extracted_filters: {
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
as_of_date: "2026-05-13"
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
resolveDebtRoleSwapFollowupIntent: (message: string, previousIntent: string) =>
|
||||
message === "\u0430 \u043c\u044b \u043a\u043e\u043c\u0443?" &&
|
||||
previousIntent === "receivables_confirmed_as_of_date"
|
||||
? "payables_confirmed_as_of_date"
|
||||
: null,
|
||||
resolveAddressIntent: () => ({
|
||||
intent: "open_items_by_counterparty_or_contract"
|
||||
})
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"\u0430 \u043c\u044b \u043a\u043e\u043c\u0443?",
|
||||
[],
|
||||
"\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u043a\u043e\u043c\u0443 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0438\u0442 \u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c",
|
||||
{
|
||||
predecomposeContract: {
|
||||
intent: "open_items_by_counterparty_or_contract"
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
as_of_date: "2026-05-13"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user