ДОМЕНЫ - ВОПРОСЫ - НДС: развести as-of и tax-period intent, ускорить и стабилизировать VAT source probe, починить M23 тесты

This commit is contained in:
2026-04-13 08:04:02 +03:00
parent f1ef5f9d3c
commit c4f87222a8
27 changed files with 1065 additions and 189 deletions
@@ -114,6 +114,9 @@ function emphasizeNumericTokens(line) {
if (!line) {
return line;
}
const isDigit = (char) => /\d/.test(char);
const isLetter = (char) => /[A-Za-zА-Яа-яЁё]/.test(char);
const dateLikePunctuation = new Set([".", "-", "/", ":"]);
const chunks = line.split(/(`[^`]*`)/g);
return chunks
.map((chunk, index) => {
@@ -123,9 +126,23 @@ function emphasizeNumericTokens(line) {
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
const before = offset > 0 ? source[offset - 1] : "";
const after = offset + match.length < source.length ? source[offset + match.length] : "";
const before2 = offset > 1 ? source[offset - 2] : "";
const after2 = offset + match.length + 1 < source.length ? source[offset + match.length + 1] : "";
if (before === "*" || after === "*") {
return match;
}
if (isLetter(before) || isLetter(after)) {
return match;
}
if (offset === 0 && (after === "." || after === ")")) {
return match;
}
if (dateLikePunctuation.has(before) && isDigit(before2)) {
return match;
}
if (dateLikePunctuation.has(after) && isDigit(after2)) {
return match;
}
return `**${match}**`;
});
})
@@ -1997,11 +2014,24 @@ function composeFactualReply(intent, rows, options = {}) {
];
if (vatProbe && vatProbe.status === "ok") {
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
if (vatProbe.probedSources.length > 0) {
lines.push(...vatProbe.probedSources.slice(0, 6).map((item, index) => {
const statusRank = (status) => status === "ok" ? 0 : status === "empty" ? 1 : 2;
const orderedProbeRows = [...vatProbe.probedSources].sort((a, b) => statusRank(a.status) - statusRank(b.status) ||
a.fullName.localeCompare(b.fullName, "ru"));
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
if (visibleProbeRows.length > 0) {
lines.push(...visibleProbeRows.map((item, index) => {
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`;
const extra = item.status === "ok"
? item.lastPeriod
? ` | последнее движение: ${item.lastPeriod}`
: ""
: item.status === "error" && item.error
? ` | ошибка: ${item.error}`
: "";
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
}));
}
if (vatProbe.errors.length > 0) {
@@ -2044,6 +2074,61 @@ function composeFactualReply(intent, rows, options = {}) {
text: joinLines(lines)
};
}
if (intent === "vat_liability_confirmed_for_tax_period") {
const rowsByMarker = new Map();
for (const row of rows) {
const marker = String(row.registrator ?? "").trim().toUpperCase();
if (!marker) {
continue;
}
const nextValue = (rowsByMarker.get(marker) ?? 0) + (row.amount ?? 0);
rowsByMarker.set(marker, nextValue);
}
const salesVat = rowsByMarker.get("VAT_BOOK_SALES") ?? 0;
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
const netVat = salesVat - purchaseVat;
const vatToPay = Math.max(0, netVat);
const carryoverOrOverpayment = Math.max(0, -netVat);
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const vatProbe = options.vatDirectSourceProbe ?? null;
const lines = [
`Собран подтвержденный расчет НДС к уплате за налоговый период: ${formatConfirmedMoney(vatToPay)}.`,
`Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
`Потенциальный перенос/переплата: ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
"Режим результата: подтвержденный расчет по регистрам книг продаж/покупок (tax-period mode, без surrogate-формулы 68/19).",
"",
"База расчета:",
`- Строк агрегата: ${formatNumberWithDots(rows.length)}.`,
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
];
if (vatProbe && vatProbe.status === "ok") {
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
if (vatProbe.errors.length > 0) {
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
}
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
}
else if (vatProbe && vatProbe.status === "error") {
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, проверьте доступность регистров книг продаж/покупок.");
}
if (rows.length === 0) {
lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0.");
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: "strong",
balance_confirmed: true
}
};
}
if (intent === "vat_payable_confirmed_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedRows = rows.filter((row) => {
@@ -36,6 +36,9 @@ function hasVatCue(text) {
function hasVatForecastCue(text) {
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
}
function hasVatTaxPaymentCue(text) {
return /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|в\s+налогов|в\s+бюджет)/iu.test(String(text ?? ""));
}
function hasDocumentSignal(text) {
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
}
@@ -432,7 +435,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
if (intent === "vat_payable_forecast" && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) {
if ((intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
previousHasPeriod &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage) {
const currentPeriodFrom = toNonEmptyString(merged.period_from);
const currentPeriodTo = toNonEmptyString(merged.period_to);
const todayIso = new Date().toISOString().slice(0, 10);
@@ -465,6 +471,7 @@ function resolveMissingRequiredFilters(intent, filters) {
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"],
vat_liability_confirmed_for_tax_period: ["period_from", "period_to"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"],
@@ -497,9 +504,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
const isVatFollowup = hasVatCue(normalizedMessage);
if (detectedIntent.intent === "unknown" && isVatFollowup) {
const vatIntent = hasVatForecastCue(normalizedMessage)
? "vat_payable_forecast"
: "vat_payable_confirmed_as_of_date";
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
? "vat_liability_confirmed_for_tax_period"
: hasVatForecastCue(normalizedMessage)
? "vat_payable_forecast"
: "vat_payable_confirmed_as_of_date";
return {
intent: vatIntent,
confidence: "low",
@@ -95,7 +95,8 @@ function inferAggregationProfile(intent, shape) {
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date") {
intent === "vat_payable_confirmed_as_of_date" ||
intent === "vat_liability_confirmed_for_tax_period") {
return "balance_snapshot";
}
if (intent === "open_items_by_counterparty_or_contract" ||