ДОМЕНЫ - ВОПРОСЫ - fix(ui-assistant-chat): render markdown bold, add readable block spacing, force scroll-to-bottom on send + ЮИ

This commit is contained in:
2026-04-12 17:53:31 +03:00
parent a717ea6b26
commit ae9daa50e7
10 changed files with 289 additions and 86 deletions
@@ -81,6 +81,32 @@ function formatMoney(value) {
}
return value.toFixed(2);
}
function formatNumberWithDots(value, fractionDigits = 0) {
if (!Number.isFinite(value)) {
if (fractionDigits > 0) {
return `0,${"0".repeat(fractionDigits)}`;
}
return "0";
}
const sign = value < 0 ? "-" : "";
const absolute = Math.abs(value);
const fixed = absolute.toFixed(Math.max(0, fractionDigits));
const [intPartRaw, fractionPartRaw] = fixed.split(".");
const groupedInt = intPartRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
if (fractionDigits <= 0) {
return `${sign}${groupedInt}`;
}
return `${sign}${groupedInt},${fractionPartRaw ?? "0".repeat(fractionDigits)}`;
}
function formatMoneyRub(value) {
return `${formatNumberWithDots(value, 2)}`;
}
function emphasizeNumericTokens(line) {
if (!line) {
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
}
function parseIsoDateToken(value) {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
@@ -1891,6 +1917,7 @@ function composeFactualReply(intent, rows, options = {}) {
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
: null;
@@ -1902,9 +1929,10 @@ function composeFactualReply(intent, rows, options = {}) {
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const lines = [
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route).",
"- Эвристический shortlist в этом режиме не используется."
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
];
lines.push("");
lines.push("Блок 2. Что учтено");
@@ -1918,25 +1946,25 @@ function composeFactualReply(intent, rows, options = {}) {
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push(`- Строк в выборке: ${rows.length}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${confirmedBalances.length}.`);
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
}
else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
}
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: lines.join("\n"),
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
@@ -1958,7 +1986,7 @@ function composeFactualReply(intent, rows, options = {}) {
const carryoverLine = asOfDate || periodFrom || periodTo
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
: null;
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
const pushCategorySlice = (lines, title, items, limit) => {
if (items.length === 0) {
return;
@@ -1971,9 +1999,9 @@ function composeFactualReply(intent, rows, options = {}) {
const lines = [
"Блок 1. Статус результата",
forcedFallbackFromConfirmed
? "- Режим результата: эвристический скоринг в рамках fallback, потому что подтвержденный срез обязательств к оплате недоступен."
: "- Режим результата: эвристический скоринг (shortlist кандидатов по признакам незакрытых обязательств в контуре 60/76).",
"- Тип результата: кандидаты для ручной проверки, а не финальный платежный реестр.",
? "- Точный реестр обязательств сейчас недоступен, поэтому показан предварительный список на проверку."
: "- Формат результата: предварительный список на проверку.",
"- Это рабочий список для проверки, а не финальный платежный реестр.",
"",
"Блок 2. Как читать результат",
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
@@ -1982,8 +2010,8 @@ function composeFactualReply(intent, rows, options = {}) {
...(carryoverLine ? [carryoverLine] : []),
"",
"Блок 3. Сводка выборки",
`- Строк в выборке: ${rows.length}.`,
`- Контрагентов-кандидатов: ${counterparties.length}.`
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Контрагентов-кандидатов: ${formatNumberWithDots(counterparties.length)}.`
];
if (counterparties.length > 0) {
const categoryCounts = counterparties.reduce((acc, item) => {
@@ -1996,10 +2024,10 @@ function composeFactualReply(intent, rows, options = {}) {
const other = counterparties.filter((item) => item.category === "other");
lines.push("");
lines.push("Блок 4. Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`);
lines.push("");
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
pushCategorySlice(lines, "5.1 Поставщики/подрядчики:", suppliers, 6);
@@ -2027,7 +2055,10 @@ function composeFactualReply(intent, rows, options = {}) {
acc[item.category] += 1;
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const lines = [
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
"- Тип результата: подтвержденные остатки к оплате.",
@@ -2041,21 +2072,21 @@ function composeFactualReply(intent, rows, options = {}) {
...(carryoverLine ? [carryoverLine] : []),
"",
"Блок 3. Сводка выборки",
`- Строк в выборке: ${rows.length}.`,
`- Контрагентов с подтвержденным остатком: ${confirmedBalances.length}.`,
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Контрагентов с подтвержденным остатком: ${formatNumberWithDots(confirmedBalances.length)}.`,
"",
"Блок 4. Категории обязательств",
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`,
`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`,
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`,
`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`,
`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`,
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
"",
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n"),
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: "strong",
@@ -2066,7 +2097,7 @@ function composeFactualReply(intent, rows, options = {}) {
const fallbackLines = buildHeuristicLines(true);
return {
responseType: "FACTUAL_LIST",
text: fallbackLines.join("\n"),
text: fallbackLines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "heuristic_candidates",
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
@@ -2077,7 +2108,7 @@ function composeFactualReply(intent, rows, options = {}) {
const lines = buildHeuristicLines(false);
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n"),
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "heuristic_candidates",
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
@@ -153,6 +153,35 @@ function formatMoney(value: number): string {
return value.toFixed(2);
}
function formatNumberWithDots(value: number, fractionDigits = 0): string {
if (!Number.isFinite(value)) {
if (fractionDigits > 0) {
return `0,${"0".repeat(fractionDigits)}`;
}
return "0";
}
const sign = value < 0 ? "-" : "";
const absolute = Math.abs(value);
const fixed = absolute.toFixed(Math.max(0, fractionDigits));
const [intPartRaw, fractionPartRaw] = fixed.split(".");
const groupedInt = intPartRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
if (fractionDigits <= 0) {
return `${sign}${groupedInt}`;
}
return `${sign}${groupedInt},${fractionPartRaw ?? "0".repeat(fractionDigits)}`;
}
function formatMoneyRub(value: number): string {
return `${formatNumberWithDots(value, 2)}`;
}
function emphasizeNumericTokens(line: string): string {
if (!line) {
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
}
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
@@ -2412,6 +2441,7 @@ export function composeFactualReply(
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const periodScopeLine =
!asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
@@ -2429,9 +2459,10 @@ export function composeFactualReply(
);
const lines: string[] = [
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route).",
"- Эвристический shortlist в этом режиме не используется."
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
];
lines.push("");
@@ -2447,15 +2478,15 @@ export function composeFactualReply(
lines.push("");
lines.push("Блок 3. Сводка");
lines.push(`- Строк в выборке: ${rows.length}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${confirmedBalances.length}.`);
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к оплате");
@@ -2463,7 +2494,7 @@ export function composeFactualReply(
lines.push(
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
);
} else {
@@ -2472,7 +2503,7 @@ export function composeFactualReply(
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: lines.join("\n"),
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
@@ -2498,7 +2529,7 @@ export function composeFactualReply(
: null;
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
`${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
`${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
const pushCategorySlice = (
lines: string[],
@@ -2518,9 +2549,9 @@ export function composeFactualReply(
const lines = [
"Блок 1. Статус результата",
forcedFallbackFromConfirmed
? "- Режим результата: эвристический скоринг в рамках fallback, потому что подтвержденный срез обязательств к оплате недоступен."
: "- Режим результата: эвристический скоринг (shortlist кандидатов по признакам незакрытых обязательств в контуре 60/76).",
"- Тип результата: кандидаты для ручной проверки, а не финальный платежный реестр.",
? "- Точный реестр обязательств сейчас недоступен, поэтому показан предварительный список на проверку."
: "- Формат результата: предварительный список на проверку.",
"- Это рабочий список для проверки, а не финальный платежный реестр.",
"",
"Блок 2. Как читать результат",
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
@@ -2529,8 +2560,8 @@ export function composeFactualReply(
...(carryoverLine ? [carryoverLine] : []),
"",
"Блок 3. Сводка выборки",
`- Строк в выборке: ${rows.length}.`,
`- Контрагентов-кандидатов: ${counterparties.length}.`
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Контрагентов-кандидатов: ${formatNumberWithDots(counterparties.length)}.`
];
if (counterparties.length > 0) {
@@ -2548,10 +2579,10 @@ export function composeFactualReply(
lines.push("");
lines.push("Блок 4. Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`);
lines.push("");
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
@@ -2585,7 +2616,10 @@ export function composeFactualReply(
},
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
);
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const lines: string[] = [
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
"- Тип результата: подтвержденные остатки к оплате.",
@@ -2599,24 +2633,24 @@ export function composeFactualReply(
...(carryoverLine ? [carryoverLine] : []),
"",
"Блок 3. Сводка выборки",
`- Строк в выборке: ${rows.length}.`,
`- Контрагентов с подтвержденным остатком: ${confirmedBalances.length}.`,
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Контрагентов с подтвержденным остатком: ${formatNumberWithDots(confirmedBalances.length)}.`,
"",
"Блок 4. Категории обязательств",
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`,
`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`,
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`,
`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`,
`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`,
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
"",
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n"),
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: "strong",
@@ -2628,7 +2662,7 @@ export function composeFactualReply(
const fallbackLines = buildHeuristicLines(true);
return {
responseType: "FACTUAL_LIST",
text: fallbackLines.join("\n"),
text: fallbackLines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "heuristic_candidates",
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
@@ -2640,7 +2674,7 @@ export function composeFactualReply(
const lines = buildHeuristicLines(false);
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n"),
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "heuristic_candidates",
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
@@ -2544,11 +2544,12 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(reply.toLowerCase()).toContain("shortlist");
} else {
expect(result?.debug.balance_confirmed).toBe(true);
expect(reply).toContain("Блок 1. Статус результата");
expect(reply).toContain("\n\nБлок 2. Что учтено");
expect(reply).toContain("\n\nБлок 3. Сводка");
expect(reply).toContain("\n\nБлок 4. Категории обязательств");
expect(reply).toContain("\n\nБлок 5. Подтвержденные позиции к оплате");
expect(reply).toContain("Итого подтвержденный долг");
expect(reply).toMatch(/Блок\s+(?:\*\*1\*\*|1)\.\s+Статус результата/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*2\*\*|2)\.\s+Что учтено/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*3\*\*|3)\.\s+Сводка/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*4\*\*|4)\.\s+Категории обязательств/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*5\*\*|5)\.\s+Подтвержденные позиции к оплате/u);
expect(reply).not.toContain("эвристический");
}
});