АДРЕСНЫЙ РЕЖИМ - стабилизация LLM fallback якорей и периода

This commit is contained in:
2026-04-02 00:30:38 +03:00
parent 2d95ce6332
commit 7dd6607ded
19 changed files with 2727 additions and 49 deletions
@@ -1828,9 +1828,24 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"show",
"list",
"выведи",
"что",
"чо",
"которые",
"какие",
"какой",
"были",
"был",
"была",
"было",
"ли",
"списания",
"списание",
"поступления",
"поступление",
"расчетного",
"расчётного",
"счета",
"счёта",
"есть",
"est",
"kakie",
@@ -1910,6 +1925,8 @@ const ADDRESS_MONTH_ALIAS_MAP = {
dec: "12"
};
const ADDRESS_DOCS_SIGNAL_PATTERN = /(?:док|доки|документ|документы|документов|docs?|documents?|bank|выписк|плат[её]ж|оплат|поступлен|списан|операц)/i;
const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|плат[её]ж|оплат|поступлен|списан|операц|расчетн)/i;
const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|\bcontract\b)/iu;
const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i;
const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu;
function normalizeAddressMonthAliasToken(token) {
@@ -1975,14 +1992,21 @@ function extractAddressFallbackYear(text) {
return fullYearMatch[1];
}
const shortYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/iu);
if (!shortYearMatch) {
if (shortYearMatch) {
const shortYear = Number(shortYearMatch[1]);
if (Number.isFinite(shortYear) && shortYear >= 0 && shortYear <= 99) {
return String(2000 + shortYear);
}
}
const shortOrdinalYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:[-\s]?(?:й|ый|ой|th))(?=$|[^a-zа-яё0-9])/iu);
if (!shortOrdinalYearMatch) {
return null;
}
const shortYear = Number(shortYearMatch[1]);
if (!Number.isFinite(shortYear) || shortYear < 0 || shortYear > 99) {
const shortOrdinalYear = Number(shortOrdinalYearMatch[1]);
if (!Number.isFinite(shortOrdinalYear) || shortOrdinalYear < 0 || shortOrdinalYear > 99) {
return null;
}
return String(2000 + shortYear);
return String(2000 + shortOrdinalYear);
}
function extractAddressFallbackMonthYear(text) {
const source = String(text ?? "");
@@ -1998,14 +2022,14 @@ function extractAddressFallbackMonthYear(text) {
const year = numericMonthYear[2];
return `${year}-${month}`;
}
const namedMonthYear = source.match(/\b([a-zа-яё]+)\s+(20\d{2})\b/iu);
const namedMonthYear = source.match(/(?:^|[^a-zа-яё0-9])([a-zа-яё]+)\s+(20\d{2})(?=$|[^a-zа-яё0-9])/iu);
if (namedMonthYear) {
const month = normalizeAddressMonthAliasToken(namedMonthYear[1]);
if (month) {
return `${namedMonthYear[2]}-${month}`;
}
}
const yearNamedMonth = source.match(/\b(20\d{2})\s+([a-zа-яё]+)\b/iu);
const yearNamedMonth = source.match(/(?:^|[^a-zа-яё0-9])(20\d{2})\s+([a-zа-яё]+)(?=$|[^a-zа-яё0-9])/iu);
if (yearNamedMonth) {
const month = normalizeAddressMonthAliasToken(yearNamedMonth[2]);
if (month) {
@@ -2014,7 +2038,42 @@ function extractAddressFallbackMonthYear(text) {
}
return null;
}
function extractAddressFallbackAccountToken(text) {
const source = String(text ?? "");
const explicitMatch = source.match(/(?:сч[её]т(?:а|у|ом|е)?|account)\D{0,12}(\d{2}(?:[.,]\d{1,2})?)/iu);
if (explicitMatch && explicitMatch[1]) {
return String(explicitMatch[1]).replace(",", ".");
}
const tokenPattern = /\b(\d{2}(?:[.,]\d{1,2})?)\b/giu;
let match = tokenPattern.exec(source);
while (match) {
const raw = String(match[1] ?? "");
const start = match.index;
const end = start + raw.length;
const prev = start > 0 ? source[start - 1] : " ";
const next = end < source.length ? source[end] : " ";
if (!/[./-]/.test(prev) && !/[./-]/.test(next) && !/\d/.test(prev) && !/\d/.test(next)) {
return raw.replace(",", ".");
}
match = tokenPattern.exec(source);
}
return null;
}
function pickAddressFallbackCounterpartyToken(text) {
const source = String(text ?? "");
const byAnchor = source.match(/(?:^|[\s,.;:!?()\-])(?:по|от)\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=$|[\s,.;:!?()\-])/iu);
if (byAnchor && byAnchor[1]) {
const byToken = String(byAnchor[1]).trim();
const normalizedByToken = byToken.toLowerCase();
if (byToken &&
!ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(normalizedByToken) &&
!/^\d{2}(?:\.\d{1,2})?$/.test(normalizedByToken) &&
!/^(?:19|20)\d{2}$/.test(normalizedByToken) &&
!/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalizedByToken) &&
!/^(?:договор|договора|договору|договором|договоре|contract)$/.test(normalizedByToken)) {
return byToken;
}
}
const candidates = extractAddressAnchorTokens(text);
for (const token of candidates) {
const normalized = String(token ?? "").toLowerCase();
@@ -2027,10 +2086,44 @@ function pickAddressFallbackCounterpartyToken(text) {
if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalized)) {
continue;
}
if (/^(?:договор|договора|договору|договором|договоре|contract)$/.test(normalized)) {
continue;
}
return token;
}
return null;
}
function extractAddressFallbackContractToken(text) {
const source = String(text ?? "");
const patterns = [
/(?:договор(?:а|у|ом|е)?|\bcontract\b)\s*(?:№|#|n|no\.?)?\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})/iu,
/(?:№|#|n|no\.?)\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})\s*(?:договор(?:а|у|ом|е)?|\bcontract\b)/iu
];
for (const pattern of patterns) {
const match = pattern.exec(source);
if (!match || !match[1]) {
continue;
}
const candidate = String(match[1]).replace(/^[^a-zа-я0-9]+|[^a-zа-я0-9/_-]+$/giu, "");
if (!candidate || candidate.length < 2) {
continue;
}
if (/^(?:19|20)\d{2}$/.test(candidate)) {
continue;
}
if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])(?:[./-](?:0?[1-9]|[12]\d|3[01]))?$/.test(candidate)) {
continue;
}
return candidate;
}
if (ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source)) {
const generic = source.match(/\b([a-zа-я0-9]{1,10}[/-][a-zа-я0-9]{1,10}(?:[/-][a-zа-я0-9]{1,10})?)\b/iu);
if (generic && generic[1]) {
return generic[1];
}
}
return null;
}
function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) {
const sourceRaw = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
const source = compactWhitespace(String(sanitizedUserMessage ?? sourceRaw).toLowerCase());
@@ -2040,9 +2133,10 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage)
const monthYear = extractAddressFallbackMonthYear(source);
const year = extractAddressFallbackYear(source);
const allTime = ADDRESS_ALL_TIME_PATTERN.test(source);
const accountMatch = source.match(/\b(\d{2}(?:[.,]\d{1,2})?)\b/);
const account = accountMatch ? String(accountMatch[1]).replace(",", ".") : null;
const account = extractAddressFallbackAccountToken(source);
const docsSignal = ADDRESS_DOCS_SIGNAL_PATTERN.test(source);
const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source);
const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source);
if (balanceSignal && account) {
let periodClause = "";
@@ -2064,28 +2158,59 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage)
}
}
if (docsSignal) {
const counterparty = pickAddressFallbackCounterpartyToken(source);
if (counterparty) {
let periodClause = "";
let rule = "documents_counterparty_rewrite";
if (allTime) {
periodClause = " за все время";
rule = "documents_counterparty_all_time_rewrite";
if (contractSignal) {
const contract = extractAddressFallbackContractToken(sourceRaw || source);
if (contract) {
let periodClause = "";
let rule = bankSignal ? "bank_operations_contract_rewrite" : "documents_contract_rewrite";
if (allTime) {
periodClause = " за все время";
rule = bankSignal ? "bank_operations_contract_all_time_rewrite" : "documents_contract_all_time_rewrite";
}
else if (monthYear) {
periodClause = ` за ${monthYear}`;
rule = bankSignal ? "bank_operations_contract_month_rewrite" : "documents_contract_month_rewrite";
}
else if (year) {
periodClause = ` за ${year} год`;
rule = bankSignal ? "bank_operations_contract_year_rewrite" : "documents_contract_year_rewrite";
}
const subject = bankSignal ? "банковские операции" : "документы";
const candidate = compactWhitespace(`${subject} по договору ${contract}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
}
else if (monthYear) {
periodClause = ` за ${monthYear}`;
rule = "documents_counterparty_month_rewrite";
}
else if (year) {
periodClause = ` за ${year} год`;
rule = "documents_counterparty_year_rewrite";
}
const candidate = compactWhitespace(`документы по контрагенту ${counterparty}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
else {
const counterparty = pickAddressFallbackCounterpartyToken(source);
if (counterparty) {
let periodClause = "";
const subject = bankSignal ? "банковские операции" : "документы";
const rulePrefix = bankSignal ? "bank_operations_counterparty" : "documents_counterparty";
let rule = `${rulePrefix}_rewrite`;
if (allTime) {
periodClause = " за все время";
rule = `${rulePrefix}_all_time_rewrite`;
}
else if (monthYear) {
periodClause = ` за ${monthYear}`;
rule = `${rulePrefix}_month_rewrite`;
}
else if (year) {
periodClause = ` за ${year} год`;
rule = `${rulePrefix}_year_rewrite`;
}
const candidate = compactWhitespace(`${subject} по контрагенту ${counterparty}${periodClause}`);
if (candidate && candidate !== sourceRaw.toLowerCase()) {
return {
candidate,
rule
};
}
}
}
}
@@ -2620,12 +2745,13 @@ export class AssistantService {
};
this.sessions.appendItem(sessionId, userItem);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? "");
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
const assistantItem = {
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
session_id: sessionId,
role: "assistant",
text: addressLane.reply_text,
text: safeAddressReply,
reply_type: addressLane.reply_type,
created_at: new Date().toISOString(),
trace_id: debug.trace_id,