Укрепить автономные reviewed-маршруты и срезы задолженности
This commit is contained in:
@@ -266,6 +266,17 @@ function normalizeQuestionText(value) {
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function isReportStyleBusinessQuestion(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
return /(?:обзор|анализ|подроб|разверн|оцен|аудит|report|review|analysis)/iu.test(text);
|
||||
}
|
||||
function isDirectBalanceQuestion(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text || isReportStyleBusinessQuestion(text)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:кто|кому|сколько|какой|какая|какие|есть\s+ли|долж|дебитор|кредитор|payables?|receivables?|who|how\s+much)/iu.test(text);
|
||||
}
|
||||
function hasInventoryPurchaseDateActionFocus(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
@@ -579,25 +590,42 @@ function extractRequestedYearFromQuestion(userMessage) {
|
||||
return 2000 + shortYear;
|
||||
}
|
||||
function extractCounterpartyName(row) {
|
||||
const skipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
const isCounterpartyLikeToken = (value, skipPattern) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
if (!/[a-zа-я]/iu.test(normalized)) {
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
if (skipTokenPattern.test(normalized)) {
|
||||
continue;
|
||||
if (skipPattern.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
const hardSkipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|касса|расчетн|проводк|movement|invoice|payment)/iu;
|
||||
const skipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
|
||||
const directCounterparty = isCounterpartyLikeToken(row.counterparty, hardSkipTokenPattern);
|
||||
if (directCounterparty) {
|
||||
return directCounterparty;
|
||||
}
|
||||
if (/остатки\s+на\s+дату/iu.test(row.registrator)) {
|
||||
const balancePrimaryCounterparty = isCounterpartyLikeToken(row.analytics[0], hardSkipTokenPattern);
|
||||
if (balancePrimaryCounterparty) {
|
||||
return balancePrimaryCounterparty;
|
||||
}
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = isCounterpartyLikeToken(token, skipTokenPattern);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
@@ -1151,6 +1179,16 @@ function hasReceivablesSectionPrefix(account) {
|
||||
const section = extractAccountSectionCode(account);
|
||||
return section === "62" || section === "76";
|
||||
}
|
||||
function normalizeSettlementAccount(value) {
|
||||
const normalized = String(value ?? "")
|
||||
.trim()
|
||||
.replace(",", ".");
|
||||
return normalized || null;
|
||||
}
|
||||
function extractSettlementOrganizationName(row) {
|
||||
const direct = String(row.organization ?? "").trim();
|
||||
return direct || null;
|
||||
}
|
||||
function resolvePayablesAsOfDate(options) {
|
||||
const explicit = normalizeIsoDateOnly(options.asOfDate);
|
||||
if (explicit) {
|
||||
@@ -1430,6 +1468,211 @@ function buildReceivablesConfirmedBalanceAggregate(rows, asOfDate) {
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
function buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasRelevantSectionPrefix, positiveSide) {
|
||||
const bySettlementKey = new Map();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
for (const row of rows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
const debitAccount = normalizeSettlementAccount(row.account_dt);
|
||||
const creditAccount = normalizeSettlementAccount(row.account_kt);
|
||||
const contributions = [];
|
||||
if (debitAccount && hasRelevantSectionPrefix(debitAccount)) {
|
||||
contributions.push({ side: "debit", account: debitAccount });
|
||||
}
|
||||
if (creditAccount && hasRelevantSectionPrefix(creditAccount)) {
|
||||
contributions.push({ side: "credit", account: creditAccount });
|
||||
}
|
||||
if (contributions.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const contract = extractSettlementBalanceAnalyticKey(row, name);
|
||||
const organization = extractSettlementOrganizationName(row);
|
||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
|
||||
for (const contribution of contributions) {
|
||||
const key = [
|
||||
normalizeEntityToken(organization),
|
||||
normalizeEntityToken(contribution.account),
|
||||
normalizeEntityToken(name),
|
||||
normalizeEntityToken(contract)
|
||||
].join("|");
|
||||
const current = bySettlementKey.get(key);
|
||||
if (!current) {
|
||||
bySettlementKey.set(key, {
|
||||
name,
|
||||
account: contribution.account,
|
||||
contract,
|
||||
organization,
|
||||
debitAmount: contribution.side === "debit" ? absAmount : 0,
|
||||
creditAmount: contribution.side === "credit" ? absAmount : 0,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
categoryScores: {
|
||||
supplier_or_contractor: classified.scores.supplier_or_contractor,
|
||||
bank_or_credit: classified.scores.bank_or_credit,
|
||||
tax_or_state: classified.scores.tax_or_state,
|
||||
other: classified.scores.other
|
||||
},
|
||||
reasons: new Set(classified.reasons),
|
||||
contracts: new Set(contract ? [contract] : []),
|
||||
documents: new Set(row.registrator ? [row.registrator] : []),
|
||||
sourceRefs: new Set(sourceRefs)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (contribution.side === "debit") {
|
||||
current.debitAmount += absAmount;
|
||||
}
|
||||
else {
|
||||
current.creditAmount += absAmount;
|
||||
}
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
|
||||
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
|
||||
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
|
||||
current.categoryScores.other += classified.scores.other;
|
||||
for (const reason of classified.reasons) {
|
||||
current.reasons.add(reason);
|
||||
}
|
||||
if (contract) {
|
||||
current.contracts.add(contract);
|
||||
}
|
||||
if (row.registrator) {
|
||||
current.documents.add(row.registrator);
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
const byCounterparty = new Map();
|
||||
const mirrorGroups = [];
|
||||
let mirroredOffsetAmount = 0;
|
||||
for (const group of bySettlementKey.values()) {
|
||||
const offsetAmount = Math.min(group.debitAmount, group.creditAmount);
|
||||
const netDebitMinusCredit = group.debitAmount - group.creditAmount;
|
||||
if (offsetAmount > 0.005) {
|
||||
mirroredOffsetAmount += offsetAmount;
|
||||
mirrorGroups.push({
|
||||
name: group.name,
|
||||
account: group.account,
|
||||
contract: group.contract,
|
||||
organization: group.organization,
|
||||
debitAmount: group.debitAmount,
|
||||
creditAmount: group.creditAmount,
|
||||
offsetAmount,
|
||||
netAmount: netDebitMinusCredit,
|
||||
operations: group.operations,
|
||||
sourceRefs: Array.from(group.sourceRefs).slice(0, 3)
|
||||
});
|
||||
}
|
||||
const sideNetAmount = positiveSide === "credit" ? group.creditAmount - group.debitAmount : group.debitAmount - group.creditAmount;
|
||||
if (sideNetAmount <= 0.005) {
|
||||
continue;
|
||||
}
|
||||
const current = byCounterparty.get(group.name);
|
||||
if (!current) {
|
||||
byCounterparty.set(group.name, {
|
||||
outstandingAmount: sideNetAmount,
|
||||
operations: group.operations,
|
||||
firstPeriod: group.firstPeriod,
|
||||
lastPeriod: group.lastPeriod,
|
||||
categoryScores: {
|
||||
supplier_or_contractor: group.categoryScores.supplier_or_contractor,
|
||||
bank_or_credit: group.categoryScores.bank_or_credit,
|
||||
tax_or_state: group.categoryScores.tax_or_state,
|
||||
other: group.categoryScores.other
|
||||
},
|
||||
reasons: new Set(group.reasons),
|
||||
contracts: new Set(group.contracts),
|
||||
documents: new Set(group.documents),
|
||||
sourceRefs: new Set(group.sourceRefs)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.outstandingAmount += sideNetAmount;
|
||||
current.operations += group.operations;
|
||||
if ((group.firstPeriod ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = group.firstPeriod;
|
||||
}
|
||||
if ((group.lastPeriod ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = group.lastPeriod;
|
||||
}
|
||||
current.categoryScores.supplier_or_contractor += group.categoryScores.supplier_or_contractor;
|
||||
current.categoryScores.bank_or_credit += group.categoryScores.bank_or_credit;
|
||||
current.categoryScores.tax_or_state += group.categoryScores.tax_or_state;
|
||||
current.categoryScores.other += group.categoryScores.other;
|
||||
for (const reason of group.reasons) {
|
||||
current.reasons.add(reason);
|
||||
}
|
||||
for (const contract of group.contracts) {
|
||||
current.contracts.add(contract);
|
||||
}
|
||||
for (const document of group.documents) {
|
||||
current.documents.add(document);
|
||||
}
|
||||
for (const ref of group.sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
return {
|
||||
balances: Array.from(byCounterparty.entries())
|
||||
.map(([name, item]) => ({
|
||||
name,
|
||||
outstandingAmount: item.outstandingAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||
categoryReasons: Array.from(item.reasons).slice(0, 2),
|
||||
contracts: Array.from(item.contracts).slice(0, 2),
|
||||
documents: Array.from(item.documents).slice(0, 2),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
|
||||
}))
|
||||
.filter((item) => item.outstandingAmount > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.outstandingAmount !== left.outstandingAmount) {
|
||||
return right.outstandingAmount - left.outstandingAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
}),
|
||||
mirrorGroups: mirrorGroups.sort((left, right) => {
|
||||
if (right.offsetAmount !== left.offsetAmount) {
|
||||
return right.offsetAmount - left.offsetAmount;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
}),
|
||||
mirroredOffsetAmount
|
||||
};
|
||||
}
|
||||
function buildPayablesConfirmedBalanceSnapshot(rows, asOfDate) {
|
||||
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasPayablesSectionPrefix, "credit");
|
||||
}
|
||||
function buildReceivablesConfirmedBalanceSnapshot(rows, asOfDate) {
|
||||
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasReceivablesSectionPrefix, "debit");
|
||||
}
|
||||
function buildCounterpartyRiskAggregate(rows) {
|
||||
const byCounterparty = new Map();
|
||||
for (const row of rows) {
|
||||
@@ -1603,6 +1846,45 @@ function extractContractName(row) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractSettlementBalanceAnalyticKey(row, counterparty) {
|
||||
const counterpartyToken = normalizeSettlementComparableToken(counterparty);
|
||||
const organizationToken = normalizeSettlementComparableToken(extractSettlementOrganizationName(row));
|
||||
const contract = extractContractName(row);
|
||||
if (contract) {
|
||||
const contractToken = normalizeSettlementComparableToken(contract);
|
||||
if (contractToken &&
|
||||
contractToken !== counterpartyToken &&
|
||||
contractToken !== organizationToken &&
|
||||
!(Boolean(organizationToken) && contractToken.includes(organizationToken)) &&
|
||||
!/^организац/.test(contractToken)) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
const normalizedToken = normalizeSettlementComparableToken(normalized);
|
||||
if (!normalized || !normalizedToken) {
|
||||
continue;
|
||||
}
|
||||
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(normalized) || /^\d+(?:[.,]\d+)?$/.test(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (/^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedToken === counterpartyToken ||
|
||||
normalizedToken === organizationToken ||
|
||||
(Boolean(organizationToken) && normalizedToken.includes(organizationToken)) ||
|
||||
/^организац/.test(normalizedToken)) {
|
||||
continue;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function normalizeEntityToken(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
@@ -1610,6 +1892,12 @@ function normalizeEntityToken(value) {
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function normalizeSettlementComparableToken(value) {
|
||||
return normalizeEntityToken(value)
|
||||
.replace(/[^\p{L}0-9]+/giu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function extractPayablesSourceRefs(row, counterparty, contract) {
|
||||
const refs = new Set();
|
||||
const counterpartyToken = normalizeEntityToken(counterparty);
|
||||
@@ -1656,6 +1944,41 @@ function formatPayablesEvidenceSuffix(item) {
|
||||
}
|
||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||
}
|
||||
function formatDebtMirrorGroupLine(item) {
|
||||
const details = [
|
||||
item.account ? `счет ${item.account}` : null,
|
||||
item.contract ? `договор/аналитика: ${item.contract}` : null,
|
||||
item.organization ? `организация: ${item.organization}` : null
|
||||
].filter((part) => Boolean(part));
|
||||
const netText = Math.abs(item.netAmount) <= 0.005
|
||||
? "чисто: 0 ₽"
|
||||
: item.netAmount > 0
|
||||
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
|
||||
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
|
||||
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
|
||||
}
|
||||
function debtMirrorCleanScopeLabel(kind) {
|
||||
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
|
||||
}
|
||||
function appendDebtMirrorCompactDisclosure(lines, snapshot, kind) {
|
||||
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
||||
return;
|
||||
}
|
||||
lines.push(`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`);
|
||||
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||
if (leadingMirror) {
|
||||
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||
}
|
||||
}
|
||||
function appendDebtMirrorDisclosure(lines, snapshot, kind) {
|
||||
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
||||
return;
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("Встречные остатки к сверке");
|
||||
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`);
|
||||
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
|
||||
}
|
||||
function deriveOperationalYearWindow(yearDocs, yearOps) {
|
||||
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
|
||||
const fallbackSeries = [...yearOps].sort((a, b) => a.year - b.year);
|
||||
@@ -2955,7 +3278,8 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
@@ -2970,6 +3294,35 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||
if (isDirectBalanceQuestion(options.userMessage)) {
|
||||
const leading = confirmedBalances[0] ?? null;
|
||||
const compactLines = leading
|
||||
? [
|
||||
`Коротко: на ${formatDateRu(payablesAsOfDate)} мы должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
|
||||
"Крупнейшие позиции к оплате:"
|
||||
]
|
||||
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
|
||||
if (leading) {
|
||||
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
|
||||
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
|
||||
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
|
||||
}));
|
||||
if (confirmedBalances.length > 5) {
|
||||
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
|
||||
}
|
||||
}
|
||||
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "payables");
|
||||
compactLines.push(`Основа: подтвержденный остаток по счетам 60/76, срез ${formatDateRu(payablesAsOfDate)}.`);
|
||||
return {
|
||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(compactLines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
const lines = [
|
||||
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"Это подтвержденный срез обязательств к оплате по точному остатку."
|
||||
@@ -2988,6 +3341,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
lines.push("Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
||||
lines.push("");
|
||||
lines.push("Категории обязательств");
|
||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
||||
@@ -3020,7 +3374,8 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
|
||||
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
|
||||
const balanceSnapshot = buildReceivablesConfirmedBalanceSnapshot(rows, receivablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
@@ -3035,6 +3390,35 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||
if (isDirectBalanceQuestion(options.userMessage)) {
|
||||
const leading = confirmedBalances[0] ?? null;
|
||||
const compactLines = leading
|
||||
? [
|
||||
`Коротко: на ${formatDateRu(receivablesAsOfDate)} нам должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
|
||||
"Крупнейшие позиции к получению:"
|
||||
]
|
||||
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
|
||||
if (leading) {
|
||||
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
|
||||
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
|
||||
return `${index + 1}. ${item.name} — ${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
|
||||
}));
|
||||
if (confirmedBalances.length > 5) {
|
||||
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
|
||||
}
|
||||
}
|
||||
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "receivables");
|
||||
compactLines.push(`Основа: подтвержденный остаток по счетам 62/76, срез ${formatDateRu(receivablesAsOfDate)}.`);
|
||||
return {
|
||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(compactLines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
const lines = [
|
||||
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
|
||||
@@ -3053,6 +3437,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
lines.push("Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
|
||||
lines.push("");
|
||||
lines.push("Категории дебиторской задолженности");
|
||||
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
||||
@@ -3160,7 +3545,8 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
return lines;
|
||||
};
|
||||
if (options.requestedResultMode === "confirmed_balance") {
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
if (confirmedBalances.length > 0) {
|
||||
const categoryCounts = confirmedBalances.reduce((acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
@@ -3195,6 +3581,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||
...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)}`)
|
||||
];
|
||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: joinLines(lines),
|
||||
|
||||
+5
-1
@@ -391,7 +391,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
||||
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(normalizedQuestion);
|
||||
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
|
||||
const asksExplicitRankingList = /(?:\btop\b|топ|рейтинг|список|первые|покажи\s+(?:топ|список)|дай\s+(?:топ|список)|show\s+(?:top|list))/iu.test(normalizedQuestion);
|
||||
const hasSingleBestCounterpartyCue = /(?:сам\p{L}*|больше\s+всего|наибольш|прин[её]с|определ\p{L}*|найд\p{L}*|highest|largest|most)/iu.test(normalizedQuestion) &&
|
||||
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
|
||||
const semanticSingleBestCounterparty = focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||
const byCounterparty = new Map();
|
||||
const byYear = new Map();
|
||||
const deals = [];
|
||||
|
||||
@@ -126,6 +126,14 @@ function hasExplicitLooseByAnchorToken(text) {
|
||||
return !pronounTokens.has(token) && !genericTokens.has(token);
|
||||
}
|
||||
const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"есть",
|
||||
"же",
|
||||
"что",
|
||||
@@ -1148,6 +1156,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
previousOrganization ??
|
||||
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
|
||||
const finalCounterparty = toNonEmptyString(merged.counterparty);
|
||||
if (finalCounterparty && isLowQualityCounterpartyAnchor(finalCounterparty)) {
|
||||
delete merged.counterparty;
|
||||
if (!reasons.includes("counterparty_cleared_low_quality_followup_anchor")) {
|
||||
reasons.push("counterparty_cleared_low_quality_followup_anchor");
|
||||
}
|
||||
}
|
||||
if (shouldSuppressInventoryCounterpartyAlias(intent, finalCounterparty, finalOrganizationReference)) {
|
||||
delete merged.counterparty;
|
||||
if (!reasons.includes("counterparty_cleared_as_organization_scope_alias")) {
|
||||
|
||||
Reference in New Issue
Block a user