АДРЕСНЫЙ РЕЖИМ - ADDRESS: стабилизация интерпретации сленга и mixed RU/EN, расширение intent/mode-роутинга, регресс-тесты и live stress 102/102

This commit is contained in:
2026-04-02 11:01:28 +03:00
parent 7dd6607ded
commit da8a4eb872
68 changed files with 29777 additions and 3220 deletions
@@ -28,14 +28,20 @@ const ACCOUNT_BALANCE_HINTS = [
"account balance",
"balance by account",
"saldo",
"баланс",
"остаток по счет",
"сальдо по счет",
"по счету"
"по счету",
"что на счете",
"что на счёте",
"на конец"
];
const DOCUMENTS_FORMING_BALANCE_HINTS = [
"documents forming balance",
"docs forming balance",
"documents form balance",
"docs form balance",
"balance documents",
"documents for balance",
"which documents form balance",
@@ -61,6 +67,8 @@ const OPEN_ITEMS_HINTS = [
"висят",
"незакрыт",
"открыт",
"долг",
"задолж",
"позици"
];
@@ -93,14 +101,21 @@ const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
"bank operations by customer",
"show bank operations by counterparty",
"bank ops",
"bank oper",
"transactions by counterparty",
"транзак",
"банк",
"банков",
"по банку",
"опер",
"выписк",
"платеж",
"платёж",
"оплат",
"списан",
"списани",
"поступлен",
"поступлени",
"движени"
];
const DOCUMENTS_BY_CONTRACT_HINTS = [
@@ -126,7 +141,10 @@ const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
];
const BANK_OPERATION_CORE_HINTS = [
"банк",
"банков",
"операц",
"опер",
"выписк",
"платеж",
"платёж",
@@ -148,18 +166,62 @@ function hasAny(text: string, patterns: string[]): boolean {
return patterns.some((item) => text.includes(item));
}
function hasCompactAccountCodeToken(text: string): boolean {
// Match compact account tokens like 60.01 / 62, while avoiding date fragments.
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})(?![\d-])/u.test(text);
}
function hasDocumentsFormingBalanceSignal(text: string): boolean {
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
return true;
}
const hasDocLexeme = text.includes("документ") || text.includes("доки");
const hasLooseAccountCodeToken = hasCompactAccountCodeToken(text);
const hasDocLexeme = /(?:документ|док(?:и|ам|ах|ов|а)?)/u.test(text);
const hasFormingLexeme = text.includes("формир");
const hasBalanceLexeme = text.includes("остат");
const hasAccountLexeme = text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text);
const hasAccountLexeme =
text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text) || hasLooseAccountCodeToken;
if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) {
return true;
}
return hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит");
if (
hasDocLexeme &&
hasBalanceLexeme &&
hasAccountLexeme &&
(text.includes("раскрой") || text.includes("раскид") || text.includes("под остатк"))
) {
return true;
}
if (hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит")) {
return true;
}
return hasBalanceLexeme && hasAccountLexeme && /из\s+чего\s+остат/u.test(text);
}
function hasDocumentsFormingBalanceAccountAnchor(text: string): boolean {
if (hasAccountNumberAnchor(text) || text.includes("счет") || text.includes("счёт")) {
return true;
}
// Allow compact account mentions like "60.01" in slang prompts without explicit "счет".
return hasCompactAccountCodeToken(text);
}
function hasAccountBalanceSignal(text: string): boolean {
if (hasAny(text, ACCOUNT_BALANCE_HINTS)) {
return true;
}
const hasAccountLexeme =
hasAccountNumberAnchor(text) || hasCompactAccountCodeToken(text) || /(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/u.test(text);
const hasBalanceLexeme =
text.includes("баланс") ||
text.includes("остат") ||
text.includes("сальд") ||
text.includes("saldo") ||
text.includes("balance") ||
text.includes("скока") ||
text.includes("сколько") ||
/на\s+конец/u.test(text);
return hasAccountLexeme && hasBalanceLexeme;
}
function isLikelyCounterpartyToken(rawToken: string): boolean {
@@ -241,6 +303,8 @@ function hasPartyAnchorMention(text: string): boolean {
function hasContractAnchorMention(text: string): boolean {
return (
text.includes("договор") ||
text.includes("контракт") ||
/\bдог\.?\b/iu.test(text) ||
text.includes("дог.") ||
text.includes("contract") ||
text.includes("dogovor")
@@ -265,6 +329,10 @@ function hasContractNumberLikeToken(text: string): boolean {
if (!token) {
continue;
}
if (/^\d{1,2}\.\d{1,2}$/u.test(token)) {
// Likely an account code like 60.01/51.00, not a contract number.
continue;
}
const parts = token.split(/[./_-]+/u).map((part) => Number(part));
if (!parts.every((part) => Number.isFinite(part))) {
return true;
@@ -381,7 +449,7 @@ function hasDocumentSignal(text: string): boolean {
}
function hasHeuristicCounterpartyAnchor(text: string): boolean {
if (!hasDocsOrBankSignal(text)) {
if (!hasDocsOrBankSignal(text) && !hasBankOperationSignal(text)) {
return false;
}
const tokens = String(text ?? "")
@@ -441,7 +509,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasDocumentsFormingBalanceSignal(text) && (hasAccountNumberAnchor(text) || text.includes("счет"))) {
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
confidence: "high",
@@ -449,6 +517,17 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (
hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))
) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
reasons: ["open_items_signal_detected"]
};
}
if (
hasContractAnchorSignal(text) &&
hasBankOperationSignal(text)
@@ -496,7 +575,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasAny(text, ACCOUNT_BALANCE_HINTS) || hasAccountNumberAnchor(text)) {
if (hasAccountBalanceSignal(text)) {
return {
intent: "account_balance_snapshot",
confidence: "high",
@@ -512,14 +591,6 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) && (text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
reasons: ["open_items_signal_detected"]
};
}
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) {
return {
intent: "list_open_contracts",