ДОМЕНЫ - ВОПРОСЫ - НДС: Исправить роутинг НДС-запросов с формулировкой мы должны и добавить регрессионные тесты

This commit is contained in:
2026-04-13 10:32:02 +03:00
parent 4205c6b3e6
commit fd159e13ac
13 changed files with 375 additions and 15 deletions
@@ -176,7 +176,11 @@ function toIsoDate(year: number, month: number, day: number): string | null {
}
function extractAsOfDate(text: string): string | undefined {
if (/\b(сегодня|на\s+сегодня|today|as\s+of\s+today)\b/i.test(text)) {
if (
/\b(сегодня|на\s+сегодня|на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)\b/i.test(
text
)
) {
return new Date().toISOString().slice(0, 10);
}
@@ -1,4 +1,4 @@
import type { AddressIntentResolution } from "../types/addressQuery";
import type { AddressIntentResolution } from "../types/addressQuery";
const RECEIVABLES_STRONG = [
"кто должен нам",
@@ -608,7 +608,7 @@ function hasVatLiabilityConfirmedTaxPeriodSignal(text: string): boolean {
return false;
}
const hasPaymentCue =
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить|мы\s+должн[аы]?|должн[аы]?\s+мы)/iu.test(
text
);
if (!hasPaymentCue) {
@@ -646,7 +646,7 @@ function hasVatPayableConfirmedSignal(text: string): boolean {
return false;
}
const hasPaymentCue =
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить|мы\s+должн[аы]?|должн[аы]?\s+мы)/iu.test(
text
);
if (!hasPaymentCue) {
@@ -3620,7 +3620,16 @@ export class AddressQueryService {
}
}
if (filteredRows.length === 0) {
const allowConfirmedAsOfZeroSnapshot =
filteredRows.length === 0 &&
(composeIntent === "vat_payable_confirmed_as_of_date" ||
composeIntent === "payables_confirmed_as_of_date" ||
composeIntent === "receivables_confirmed_as_of_date") &&
(stageStatus === "no_raw_rows" || stageStatus === "materialized_but_filtered_out_by_recipe") &&
!toNonEmptyFilterValue(filters.extracted_filters.counterparty) &&
!toNonEmptyFilterValue(filters.extracted_filters.contract) &&
!toNonEmptyFilterValue(filters.extracted_filters.document_ref);
if (filteredRows.length === 0 && !allowConfirmedAsOfZeroSnapshot) {
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
const hadAnchorMatchedRows = filterByAnchors.length > 0;
const isVisibilityGapCandidate =
@@ -62,6 +62,12 @@ function hasExplicitPeriodLiteral(text: string): boolean {
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
}
function hasExplicitCurrentDateHint(text: string): boolean {
return /(?:на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодня|сегодня|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)/iu.test(
String(text ?? "")
);
}
function hasOpenItemsHint(text: string): boolean {
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
}
@@ -449,7 +455,7 @@ function mergeFollowupFilters(
reasons.push("as_of_date_from_followup_context");
}
}
if (!sameDateRequested && !hasExplicitPeriodLiteral(userMessage)) {
if (!sameDateRequested && !hasExplicitPeriodLiteral(userMessage) && !hasExplicitCurrentDateHint(userMessage)) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
@@ -498,7 +504,12 @@ function mergeFollowupFilters(
reasons.push("as_of_date_from_followup_context");
}
}
if (!sameDateRequested && hasFollowupSignalForConfirmed && !hasExplicitPeriodLiteral(userMessage)) {
if (
!sameDateRequested &&
hasFollowupSignalForConfirmed &&
!hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage)
) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
@@ -535,6 +546,13 @@ function mergeFollowupFilters(
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const asOfPrimaryIntent =
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
@@ -559,7 +577,7 @@ function mergeFollowupFilters(
}
}
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal) {
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !(asOfPrimaryIntent && hasExplicitCurrentDateInMessage)) {
if (previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
}
@@ -998,6 +998,20 @@ function countTokens(text) {
function hasPeriodLiteral(text) {
return /\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/.test(text);
}
function hasShortNamedPeriodFollowupLiteral(text) {
const normalized = compactWhitespace(String(text ?? "").toLowerCase());
if (!normalized) {
return false;
}
return /^(?:(?:\u0430|a|\u0438|i)\s+)?(?:\u043d\u0430|\u0437\u0430|\u043f\u043e|na|za|for)\s+(?:(?:\u044f\u043d\u0432(?:\u0430\u0440)?|\u0444\u0435\u0432(?:\u0440\u0430\u043b)?|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440(?:\u0435\u043b)?|\u043c\u0430(?:\u0439|\u044f)|\u0438\u044e\u043d(?:\u044c)?|\u0438\u044e\u043b(?:\u044c)?|\u0430\u0432\u0433(?:\u0443\u0441\u0442)?|\u0441\u0435\u043d\u0442(?:\u044f\u0431\u0440)?|\u043e\u043a\u0442(?:\u044f\u0431\u0440)?|\u043d\u043e\u044f(?:\u0431\u0440)?|\u0434\u0435\u043a(?:\u0430\u0431\u0440)?)(?:[\u0430-\u044f\u0451]*)?|q[1-4]|(?:[1-4]|[ivx]{1,4})\s*(?:-?\u0439)?\s*\u043a\u0432(?:\.|\u0430\u0440\u0442(?:\u0430\u043b(?:\u0430|\u0435|\u0443|\u043e\u043c)?)?)?|(?:20\d{2})\s*(?:\u0433(?:\.|\u043e\u0434(?:\u0430|\u0443|\u043e\u043c)?)?))(?=$|[\s,.;:!?])/iu.test(normalized);
}
function hasShortCurrentDateFollowupLiteral(text) {
const normalized = compactWhitespace(String(text ?? "").toLowerCase());
if (!normalized) {
return false;
}
return /^(?:(?:\u0430|a|\u0438|i)\s+)?(?:(?:\u043d\u0430|na)\s+)?(?:(?:\u0442\u0435\u043a\u0443\u0449(?:[\u0430-\u044f\u0451]+)?\s+\u0434\u0430\u0442(?:[\u0430-\u044f\u0451]+)?|(?:\u043d\u0430\s+)?\u0441\u0435\u0433\u043e\u0434\u043d(?:\u044f|\u0435\u0448\u043d(?:[\u0430-\u044f\u0451]+)?\s+\u0434\u0430\u0442(?:[\u0430-\u044f\u0451]+)?)|(?:\u043d\u0430\s+)?\u0442\u0435\u043a\u0443\u0449(?:[\u0430-\u044f\u0451]+)?\s+\u043c\u043e\u043c\u0435\u043d\u0442|today|current\s+date|current\s+moment|now)|(?:\u043f\u043e\s+\u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e\s+\u043d\u0430|as\s+of)\s+(?:\u0441\u0435\u0433\u043e\u0434\u043d\u044f|today|current\s+date))(?=$|[\s,.;:!?])/iu.test(normalized);
}
function hasStandaloneAddressTopicSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
@@ -2551,6 +2565,11 @@ function hasAddressFollowupContextSignal(userMessage) {
if (shortVatCue) {
return true;
}
const shortCurrentDateCue = shortFollowup &&
samples.some((sample) => hasShortCurrentDateFollowupLiteral(sample));
if (shortCurrentDateCue) {
return true;
}
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu)) {
return true;
}
@@ -2603,6 +2622,9 @@ function hasAddressFollowupContextSignal(userMessage) {
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
return true;
}
if (shortFollowup && samples.some((sample) => hasShortNamedPeriodFollowupLiteral(sample))) {
return true;
}
return false;
}
function hasShortDebtMirrorFollowupSignal(userMessage) {