ДОМЕНЫ - ВОПРОСЫ - НДС: Исправить роутинг НДС-запросов с формулировкой мы должны и добавить регрессионные тесты
This commit is contained in:
parent
4205c6b3e6
commit
fd159e13ac
|
|
@ -159,7 +159,7 @@ function toIsoDate(year, month, day) {
|
|||
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
function extractAsOfDate(text) {
|
||||
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);
|
||||
}
|
||||
const ymd = text.match(DATE_YMD_PATTERN);
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@ function hasVatLiabilityConfirmedTaxPeriodSignal(text) {
|
|||
if (!hasVatLexeme) {
|
||||
return false;
|
||||
}
|
||||
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(text);
|
||||
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить|мы\s+должн[аы]?|должн[аы]?\s+мы)/iu.test(text);
|
||||
if (!hasPaymentCue) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -584,7 +584,7 @@ function hasVatPayableConfirmedSignal(text) {
|
|||
if (!hasVatLexeme) {
|
||||
return false;
|
||||
}
|
||||
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(text);
|
||||
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить|мы\s+должн[аы]?|должн[аы]?\s+мы)/iu.test(text);
|
||||
if (!hasPaymentCue) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2973,7 +2973,15 @@ 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 = hadBaseRows &&
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ function hasSameDateHint(text) {
|
|||
function hasExplicitPeriodLiteral(text) {
|
||||
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitCurrentDateHint(text) {
|
||||
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) {
|
||||
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
||||
}
|
||||
|
|
@ -358,7 +361,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
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);
|
||||
|
|
@ -401,7 +404,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
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);
|
||||
|
|
@ -433,6 +439,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
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);
|
||||
if ((intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
||||
|
|
@ -453,7 +465,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal) {
|
||||
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !(asOfPrimaryIntent && hasExplicitCurrentDateInMessage)) {
|
||||
if (previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,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) {
|
||||
|
|
@ -2594,6 +2608,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;
|
||||
}
|
||||
|
|
@ -2646,6 +2665,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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -2093,6 +2093,13 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
|||
expect(result.intent).toBe("contract_usage_and_value");
|
||||
});
|
||||
|
||||
it("resolves VAT wording with debt-phrase as confirmed VAT payable intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
|
||||
);
|
||||
expect(result.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("vat_payable_confirmed_signal_detected");
|
||||
});
|
||||
it("resolves contracts-by-counterparty intent from list wording", () => {
|
||||
const result = resolveAddressIntent("покажи договора все по жуковке 51");
|
||||
expect(result.intent).toBe("list_contracts_by_counterparty");
|
||||
|
|
@ -3335,6 +3342,27 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
|||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
});
|
||||
|
||||
it("returns factual confirmed VAT snapshot instead of partial when payable rows are absent", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("скок ндс платить надо на март 2020");
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||||
expect(result?.reply_type).not.toBe("partial_coverage");
|
||||
expect(result?.debug.result_mode).toBe("confirmed_balance");
|
||||
expect(result?.debug.balance_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps mixed VAT + debt wording in VAT lane (not payables/contracts)", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result?.debug.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
||||
expect(result?.debug.result_mode).toBe("confirmed_balance");
|
||||
});
|
||||
it("routes contracts-by-counterparty intent into dedicated catalog recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("покажи договора все по жуковке 51");
|
||||
|
|
@ -3755,6 +3783,27 @@ describe("address decompose stage follow-up carryover", () => {
|
|||
expect(result?.baseReasons).toContain("intent_adjusted_to_vat_followup_context");
|
||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
||||
});
|
||||
|
||||
it("keeps explicit current-date VAT follow-up and does not inherit stale as-of date", () => {
|
||||
const result = runAddressDecomposeStage("а на текущую дату", {
|
||||
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||
previous_filters: {
|
||||
period_from: "2016-03-01",
|
||||
period_to: "2016-03-31",
|
||||
as_of_date: "2016-03-31"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
});
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.mode.mode).toBe("address_query");
|
||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe(todayIso);
|
||||
expect(result?.filters.extracted_filters.as_of_date).not.toBe("2016-03-31");
|
||||
expect(result?.filters.extracted_filters.period_from).toBeUndefined();
|
||||
expect(result?.filters.extracted_filters.period_to).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("address recipe catalog counterparty filtering", () => {
|
||||
|
|
@ -4033,3 +4082,4 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1415,6 +1415,187 @@ describe("assistant address follow-up carryover", () => {
|
|||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps month-only VAT follow-up phrase in address lane", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "\u0441\u043a\u043e\u043a \u043d\u0434\u0441 \u043d\u0430\u0434\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0432 \u043d\u0430\u043b\u043e\u0433\u043e\u0432\u0443\u044e \u043d\u0430 \u0444\u0435\u0432\u0440\u0430\u043b\u044c 2017";
|
||||
const followupMessage = "\u0430 \u043d\u0430 \u043c\u0430\u0440\u0442";
|
||||
|
||||
const firstVatResult = buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-03-31"
|
||||
},
|
||||
selected_recipe: "address_vat_liability_confirmed_tax_period_v1",
|
||||
response_type: "FACTUAL_SUMMARY",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true
|
||||
}
|
||||
});
|
||||
|
||||
const followupVatResult = buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-03-31"
|
||||
},
|
||||
selected_recipe: "address_vat_liability_confirmed_tax_period_v1",
|
||||
response_type: "FACTUAL_SUMMARY",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true,
|
||||
reasons: [
|
||||
"address_action_detected",
|
||||
"vat_liability_confirmed_tax_period_signal_detected",
|
||||
"address_followup_context_applied"
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return firstVatResult;
|
||||
}
|
||||
if (!options?.followupContext) {
|
||||
return null;
|
||||
}
|
||||
return followupVatResult;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-vat-march-${Date.now()}`;
|
||||
const first = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: firstMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.reply_type).toBe("factual");
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.detected_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(second.debug?.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1");
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1].message).toBe(followupMessage);
|
||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.period_from).toBe("2017-01-01");
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.period_to).toBe("2017-03-31");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps 'a na tekushuyu datu' VAT follow-up in address lane", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "\u0441\u043a\u043e\u043a \u043d\u0430\u0434\u043e \u043d\u0434\u0441 \u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0441 \u0430\u043f\u0440\u0435\u043b\u0435 2017";
|
||||
const followupMessage = "\u0430 \u043d\u0430 \u0442\u0435\u043a\u0443\u0449\u0443\u044e \u0434\u0430\u0442\u0443";
|
||||
|
||||
const vatAsOfResult = buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "vat_payable_confirmed_as_of_date",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-04-01",
|
||||
period_to: "2017-04-30",
|
||||
as_of_date: "2017-04-30"
|
||||
},
|
||||
selected_recipe: "address_vat_payable_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_SUMMARY",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true
|
||||
}
|
||||
});
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return vatAsOfResult;
|
||||
}
|
||||
if (!options?.followupContext) {
|
||||
return null;
|
||||
}
|
||||
return vatAsOfResult;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-vat-current-date-${Date.now()}`;
|
||||
const first = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: firstMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.reply_type).toBe("factual");
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1].message).toBe(followupMessage);
|
||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-04-30");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes active organization scope into address lane follow-up context", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const addressQueryService = {
|
||||
|
|
|
|||
|
|
@ -558,6 +558,40 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а на текущую дату",
|
||||
effectiveAddressUserMessage: "а на текущую дату",
|
||||
followupContext: {
|
||||
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||
previous_filters: {
|
||||
period_from: "2016-03-01",
|
||||
period_to: "2016-03-31",
|
||||
as_of_date: "2016-03-31"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
applied: false,
|
||||
reason: "normalized_fragment_rejected_semantic_guard",
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage:
|
||||
|
|
|
|||
Loading…
Reference in New Issue