Завершить phase99-105 schema primitive closure

This commit is contained in:
2026-05-17 21:49:02 +03:00
parent b1a5e4435d
commit a5fa940953
83 changed files with 5289 additions and 111 deletions
@@ -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,