ДОМЕНЫ - ВОПРОСЫ - Исправить обработку коротких debt follow-up и защиту от диагностических LLM rewrite

This commit is contained in:
2026-04-12 22:34:59 +03:00
parent 040a55aaea
commit 98872c2f11
35 changed files with 1745 additions and 195 deletions
@@ -2510,6 +2510,97 @@ export function composeFactualReply(
};
}
if (intent === "vat_payable_confirmed_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedRows = rows.filter((row) => {
const amount = row.amount ?? 0;
if (!Number.isFinite(amount) || amount <= 0) {
return false;
}
const section = extractAccountSectionCode(row.account_kt);
return section === "68";
});
const byAccount = new Map<
string,
{
account: string;
total: number;
operations: number;
lastPeriod: string | null;
refs: Set<string>;
}
>();
for (const row of confirmedRows) {
const account = String(row.account_kt ?? "").trim() || "68*";
const registrator = String(row.registrator ?? "").trim();
const amount = row.amount ?? 0;
const current = byAccount.get(account);
if (!current) {
byAccount.set(account, {
account,
total: amount,
operations: 1,
lastPeriod: row.period,
refs: registrator ? new Set([registrator]) : new Set()
});
continue;
}
current.total += amount;
current.operations += 1;
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if (registrator) {
current.refs.add(registrator);
}
}
const accountRows = Array.from(byAccount.values())
.filter((item) => Number.isFinite(item.total) && item.total > 0)
.sort((a, b) => b.total - a.total || b.operations - a.operations || a.account.localeCompare(b.account, "ru"));
const totalVatPayable = accountRows.reduce((sum, item) => sum + item.total, 0);
const lines: string[] = [
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
"",
"Блок 2. Что учтено",
`- Дата среза: ${formatDateRu(asOfDate)}.`,
"- Контур: остатки по счетам НДС к уплате (68*).",
"",
"Блок 3. Сводка",
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`,
"",
"Блок 4. Подтвержденные позиции"
];
if (accountRows.length > 0) {
lines.push(
...accountRows.slice(0, 12).map((item, index) => {
const refs = Array.from(item.refs).slice(0, 2).join("; ");
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
})
);
} else {
lines.push("- Подтвержденный остаток НДС к уплате на дату среза не найден.");
}
return {
responseType: "FACTUAL_LIST",
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: "strong",
balance_confirmed: true
}
};
}
if (intent === "account_balance_snapshot") {
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
const lines = [
@@ -66,6 +66,14 @@ function hasOpenItemsHint(text: string): boolean {
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
}
function hasVatCue(text: string): boolean {
return /(?:^|[\s,.;:!?()\-])(?:ндс|vat)(?=$|[\s,.;:!?()\-])/iu.test(String(text ?? ""));
}
function hasVatForecastCue(text: string): boolean {
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
}
function hasDocumentSignal(text: string): boolean {
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
}
@@ -437,14 +445,26 @@ function mergeFollowupFilters(
reasons.push("as_of_date_from_followup_context");
}
}
if (!sameDateRequested && !hasExplicitPeriodLiteral(userMessage)) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
}
}
if (
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date"
) {
const hasFollowupSignalForConfirmed = hasAddressFollowupContextSignal(userMessage);
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract);
const shouldInheritContract =
@@ -474,6 +494,16 @@ function mergeFollowupFilters(
reasons.push("as_of_date_from_followup_context");
}
}
if (!sameDateRequested && hasFollowupSignalForConfirmed && !hasExplicitPeriodLiteral(userMessage)) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
}
}
if (allTimeRequested) {
@@ -539,6 +569,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
documents_forming_balance: ["account", "as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
vat_payable_confirmed_as_of_date: ["as_of_date"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"],
@@ -577,6 +608,18 @@ function deriveIntentWithFollowupContext(
const hasPreviousContract = Boolean(previousContract ?? previousContractFromAnchor);
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
const isVatFollowup = hasVatCue(normalizedMessage);
if (detectedIntent.intent === "unknown" && isVatFollowup) {
const vatIntent: AddressIntent = hasVatForecastCue(normalizedMessage)
? "vat_payable_forecast"
: "vat_payable_confirmed_as_of_date";
return {
intent: vatIntent,
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"]
};
}
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
return {
@@ -193,7 +193,8 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date"
) {
return "balance_snapshot";
}