1250 lines
51 KiB
JavaScript
1250 lines
51 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.resolveAddressIntent = resolveAddressIntent;
|
||
const RECEIVABLES_STRONG = [
|
||
"кто должен нам",
|
||
"нам должны",
|
||
"who owes us",
|
||
"receivable",
|
||
"receivables",
|
||
"debtor",
|
||
"debtors",
|
||
"дебитор",
|
||
"дебиторск"
|
||
];
|
||
const PAYABLES_STRONG = [
|
||
"кому должны мы",
|
||
"мы должны",
|
||
"who we owe",
|
||
"payable",
|
||
"payables",
|
||
"creditor",
|
||
"creditors",
|
||
"кредитор",
|
||
"кредиторск"
|
||
];
|
||
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",
|
||
"из чего состоит остаток",
|
||
"какие документы формируют остаток",
|
||
"раскрой остаток по документам",
|
||
"документы под остатком"
|
||
];
|
||
const OPEN_CONTRACTS_HINTS = [
|
||
"open contracts",
|
||
"unclosed contracts",
|
||
"незакрыт",
|
||
"не закрыт",
|
||
"открыт",
|
||
"договор",
|
||
"контракт"
|
||
];
|
||
const OPEN_ITEMS_HINTS = [
|
||
"open items",
|
||
"unclosed items",
|
||
"хвост",
|
||
"висят",
|
||
"незакрыт",
|
||
"открыт",
|
||
"долг",
|
||
"задолж",
|
||
"позици"
|
||
];
|
||
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
|
||
"documents by counterparty",
|
||
"docs by counterparty",
|
||
"documents by company",
|
||
"documents by supplier",
|
||
"documents by customer",
|
||
"documents by client",
|
||
"documents by partner",
|
||
"show documents by counterparty",
|
||
"list documents by counterparty",
|
||
"документы по",
|
||
"доступные документы",
|
||
"список документов",
|
||
"документ",
|
||
"доки",
|
||
"доки по",
|
||
"док по",
|
||
"doki",
|
||
"docy",
|
||
"doci",
|
||
"по контрагент"
|
||
];
|
||
const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
|
||
"bank operations by counterparty",
|
||
"bank payments by counterparty",
|
||
"payment orders by counterparty",
|
||
"bank operations by company",
|
||
"bank operations by supplier",
|
||
"bank operations by customer",
|
||
"show bank operations by counterparty",
|
||
"bank ops",
|
||
"bank oper",
|
||
"transactions by counterparty",
|
||
"транзак",
|
||
"банк",
|
||
"банков",
|
||
"по банку",
|
||
"опер",
|
||
"выписк",
|
||
"платеж",
|
||
"платёж",
|
||
"оплат",
|
||
"списан",
|
||
"списани",
|
||
"поступлен",
|
||
"поступлени",
|
||
"движени"
|
||
];
|
||
const DOCUMENTS_BY_CONTRACT_HINTS = [
|
||
"documents by contract",
|
||
"docs by contract",
|
||
"show documents by contract",
|
||
"list documents by contract",
|
||
"документы по договору",
|
||
"доки по договору",
|
||
"док по договору",
|
||
"документы договор",
|
||
"договор",
|
||
"документы по контракту",
|
||
"доки по контракту",
|
||
"контракт"
|
||
];
|
||
const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
|
||
"bank operations by contract",
|
||
"bank payments by contract",
|
||
"payment orders by contract",
|
||
"transactions by contract",
|
||
"bank ops by contract",
|
||
"банковские операции по договору",
|
||
"платежи по договору",
|
||
"выписка по договору",
|
||
"банковские операции по контракту",
|
||
"платежи по контракту",
|
||
"выписка по контракту"
|
||
];
|
||
const BANK_OPERATION_CORE_HINTS = [
|
||
"банк",
|
||
"банков",
|
||
"операц",
|
||
"опер",
|
||
"выписк",
|
||
"платеж",
|
||
"платёж",
|
||
"оплат",
|
||
"списан",
|
||
"поступлен",
|
||
"движени",
|
||
"транзак",
|
||
"bank",
|
||
"payment",
|
||
"payments",
|
||
"transaction",
|
||
"transactions",
|
||
"statement",
|
||
"wire"
|
||
];
|
||
const PERIOD_COVERAGE_PROFILE_HINTS = [
|
||
"за какие годы",
|
||
"за какие года",
|
||
"в базе есть данные",
|
||
"покрытие периодов",
|
||
"диапазон лет",
|
||
"профиль данных",
|
||
"самый активный год",
|
||
"самый активный месяц",
|
||
"самый пассивный год",
|
||
"самый пассивный месяц",
|
||
"наименее активный год",
|
||
"наименее активный месяц",
|
||
"минимум документов по году",
|
||
"минимум операций по месяцу",
|
||
"год с минимальным количеством документов",
|
||
"месяц с минимальным количеством операций",
|
||
"активный год по количеству документов",
|
||
"активный месяц по количеству операций",
|
||
"most active year",
|
||
"most active month",
|
||
"least active year",
|
||
"least active month",
|
||
"year coverage",
|
||
"data coverage"
|
||
];
|
||
const DOCUMENT_TYPE_AND_ACCOUNT_SECTION_PROFILE_HINTS = [
|
||
"типы документов",
|
||
"типы доков",
|
||
"документы чаще всего",
|
||
"документы реже всего",
|
||
"редкие типы документов",
|
||
"наименее используемые типы документов",
|
||
"частые типы документов",
|
||
"сводка по типам документов",
|
||
"доля типов документов",
|
||
"разделы учета",
|
||
"разделы учёта",
|
||
"наиболее заполнены",
|
||
"наименее заполнены",
|
||
"почти не используются",
|
||
"account section",
|
||
"document types usage",
|
||
"document type profile"
|
||
];
|
||
const COUNTERPARTY_POPULATION_AND_ROLES_HINTS = [
|
||
"сколько всего контрагентов",
|
||
"сколько уникальных контрагентов",
|
||
"сколько контрагентов в базе",
|
||
"сколько заказчиков",
|
||
"сколько поставщиков",
|
||
"сколько клиентов",
|
||
"сколько покупателей",
|
||
"скока всего контрагентов",
|
||
"скока уникальных контрагентов",
|
||
"скока контрагентов в базе",
|
||
"скока заказчиков",
|
||
"скока поставщиков",
|
||
"скока клиентов",
|
||
"скока покупателей",
|
||
"скок контрагентов",
|
||
"скок контрагентов в базе",
|
||
"скок заказчиков",
|
||
"скок поставщиков",
|
||
"скок клиентов",
|
||
"скок покупателей",
|
||
"сколько смешанных контрагентов",
|
||
"типы контрагентов",
|
||
"разбей контрагентов",
|
||
"раздели контрагентов",
|
||
"counterparty population",
|
||
"counterparty roles",
|
||
"customer supplier split"
|
||
];
|
||
const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
|
||
"какие заказчики работали",
|
||
"какие заказчики активны",
|
||
"какие клиенты работали",
|
||
"какие клиенты активны",
|
||
"какие контрагенты работали",
|
||
"какие поставщики работали",
|
||
"список заказчиков",
|
||
"список клиентов",
|
||
"список заказчиков за все время",
|
||
"список клиентов за все время",
|
||
"список активных заказчиков",
|
||
"список активных клиентов",
|
||
"новые заказчики",
|
||
"новые клиенты",
|
||
"новые контрагенты",
|
||
"впервые в",
|
||
"кто исчез",
|
||
"кто ушел",
|
||
"кто ушёл",
|
||
"только один раз",
|
||
"дольше всех",
|
||
"долгоживущие контрагенты",
|
||
"регулярные поставщики",
|
||
"эпизодические поставщики",
|
||
"давно не использовались поставщики",
|
||
"всех заков",
|
||
"кто был активен",
|
||
"потом отвалился",
|
||
"ровно один раз",
|
||
"и пропал",
|
||
"самые старые по сотрудничеству",
|
||
"разбей поставщиков на регуляр и разовые",
|
||
"кто новые в этом году",
|
||
"active customers",
|
||
"customer activity list",
|
||
"counterparty lifecycle"
|
||
];
|
||
const CONTRACT_USAGE_OVERVIEW_HINTS = [
|
||
"сколько всего договоров",
|
||
"сколько договоров заведено",
|
||
"сколько договоров в базе",
|
||
"сколько договоров использовались",
|
||
"сколько договоров использовалось",
|
||
"договоры total vs used",
|
||
"обзор договорной базы",
|
||
"договорная база total used",
|
||
"неиспользуемые договоры",
|
||
"давно не использовались договоры",
|
||
"мертвые договоры",
|
||
"мёртвые договоры",
|
||
"stale contracts",
|
||
"unused contracts",
|
||
"contracts total used",
|
||
"contract usage overview"
|
||
];
|
||
const CUSTOMER_REVENUE_AND_PAYMENTS_HINTS = [
|
||
"самые доходные клиенты",
|
||
"самые доходные заказчики",
|
||
"топ клиентов по сумме поступлений",
|
||
"топ заказчиков по сумме поступлений",
|
||
"кто нам больше всего занес",
|
||
"кто нам больше всего занёс",
|
||
"кто нам принес больше всего",
|
||
"кто нам принёс больше всего",
|
||
"кто платит чаще всего",
|
||
"средний чек клиентов",
|
||
"средний чек заказчиков",
|
||
"крупные сделки по поступлениям",
|
||
"маленькие сделки по поступлениям",
|
||
"smallest deals by inflow",
|
||
"largest deals by inflow",
|
||
"top customers by inflow",
|
||
"top customers by revenue"
|
||
];
|
||
const SUPPLIER_PAYOUTS_PROFILE_HINTS = [
|
||
"топ поставщиков по сумме выплат",
|
||
"кому мы больше всего заплатили",
|
||
"кому ушло больше всего денег",
|
||
"кому мы больше всего сгрузили денег",
|
||
"поставщики по выплатам",
|
||
"поставщики по исходящим платежам",
|
||
"поставщики с максимальным числом выплат",
|
||
"крупные разовые выплаты поставщикам",
|
||
"top suppliers by payouts",
|
||
"top suppliers by outgoing payments"
|
||
];
|
||
const CONTRACT_USAGE_AND_VALUE_HINTS = [
|
||
"договоры по обороту",
|
||
"договоры по сумме оборота",
|
||
"топ договоров по обороту",
|
||
"контракты по обороту",
|
||
"контракты по сумме оборота",
|
||
"топ контрактов по обороту",
|
||
"договоры с минимальным бюджетом",
|
||
"договоры с самым маленьким бюджетом",
|
||
"контракты с минимальным бюджетом",
|
||
"контракты с самым маленьким бюджетом",
|
||
"активные договоры по бюджету",
|
||
"активные контракты по бюджету",
|
||
"контрагенты с несколькими договорами",
|
||
"несколько договоров у контрагента",
|
||
"мультидоговорные контрагенты",
|
||
"какие договоры активны",
|
||
"какие контракты активны",
|
||
"рабочие договоры",
|
||
"рабочие контракты",
|
||
"contracts by turnover",
|
||
"contracts by budget"
|
||
];
|
||
const CONTRACT_LIST_BY_COUNTERPARTY_HINTS = [
|
||
"договоры по",
|
||
"договора по",
|
||
"список договоров по",
|
||
"покажи договоры по",
|
||
"выведи договоры по",
|
||
"контракты по",
|
||
"список контрактов по",
|
||
"покажи контракты по",
|
||
"выведи контракты по",
|
||
"contracts by counterparty",
|
||
"list contracts by counterparty",
|
||
"show contracts by counterparty"
|
||
];
|
||
function hasAny(text, patterns) {
|
||
return patterns.some((item) => text.includes(item));
|
||
}
|
||
function tokenizeText(text) {
|
||
return String(text ?? "")
|
||
.toLowerCase()
|
||
.split(/[^a-zа-яё0-9]+/iu)
|
||
.map((token) => token.trim())
|
||
.filter((token) => token.length > 0);
|
||
}
|
||
function trimRussianEnding(token) {
|
||
return token.replace(/(?:иями|ями|ами|ого|ему|ому|ыми|ими|ией|ей|ий|ый|ой|ях|ах|ов|ев|ам|ям|ом|ем|ы|и|а|я|у|ю|е|о)$/u, "");
|
||
}
|
||
function normalizeLexemeToken(rawToken) {
|
||
const token = String(rawToken ?? "").toLowerCase().replace(/[^a-zа-яё0-9]+/gu, "");
|
||
if (!token) {
|
||
return "";
|
||
}
|
||
if (/^[a-z0-9]+$/u.test(token)) {
|
||
return token;
|
||
}
|
||
return trimRussianEnding(token);
|
||
}
|
||
function levenshteinDistance(a, b) {
|
||
if (a === b) {
|
||
return 0;
|
||
}
|
||
if (!a.length) {
|
||
return b.length;
|
||
}
|
||
if (!b.length) {
|
||
return a.length;
|
||
}
|
||
const prev = new Array(b.length + 1);
|
||
const curr = new Array(b.length + 1);
|
||
for (let j = 0; j <= b.length; j += 1) {
|
||
prev[j] = j;
|
||
}
|
||
for (let i = 1; i <= a.length; i += 1) {
|
||
curr[0] = i;
|
||
for (let j = 1; j <= b.length; j += 1) {
|
||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
||
}
|
||
for (let j = 0; j <= b.length; j += 1) {
|
||
prev[j] = curr[j];
|
||
}
|
||
}
|
||
return prev[b.length];
|
||
}
|
||
function hasFuzzyLexeme(text, lexemeRoots) {
|
||
const normalizedRoots = lexemeRoots
|
||
.map((root) => normalizeLexemeToken(root))
|
||
.filter((root) => root.length > 0);
|
||
if (normalizedRoots.length === 0) {
|
||
return false;
|
||
}
|
||
const tokens = tokenizeText(text)
|
||
.map((token) => normalizeLexemeToken(token))
|
||
.filter((token) => token.length >= 4);
|
||
for (const token of tokens) {
|
||
for (const root of normalizedRoots) {
|
||
if (token.includes(root)) {
|
||
return true;
|
||
}
|
||
if (root.includes(token) && token.length >= 5) {
|
||
return true;
|
||
}
|
||
const maxDistance = root.length >= 7 ? 2 : 1;
|
||
if (Math.abs(token.length - root.length) > maxDistance) {
|
||
continue;
|
||
}
|
||
if (levenshteinDistance(token, root) <= maxDistance) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function hasCompactAccountCodeToken(text) {
|
||
// 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) {
|
||
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
|
||
return true;
|
||
}
|
||
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) || hasLooseAccountCodeToken;
|
||
if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) {
|
||
return true;
|
||
}
|
||
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) {
|
||
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) {
|
||
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);
|
||
if (hasAccountLexeme && hasBalanceLexeme) {
|
||
return true;
|
||
}
|
||
const hasAsOfStyleDate = /\b(19|20)\d{2}[./-](0?[1-9]|1[0-2])(?:[./-](0?[1-9]|[12]\d|3[01]))\b/u.test(text) ||
|
||
/(?:на\s+ту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date)/iu.test(text);
|
||
const hasFollowupBalanceVerb = /(?:вернись|вернуться|вернуть|back|return)/iu.test(text);
|
||
return hasAccountLexeme && hasAsOfStyleDate && hasFollowupBalanceVerb;
|
||
}
|
||
function hasPeriodCoverageProfileSignal(text) {
|
||
if (hasAny(text, PERIOD_COVERAGE_PROFILE_HINTS)) {
|
||
return true;
|
||
}
|
||
if (/(?:за\s+какие\s+год[а-яё]*).*(?:баз[аы].*жив|период|данн)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какой\s+год[а-яё]*).*(?:по\s+док|докам|документам)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какой\s+месяц[а-яё]*).*(?:пик|по\s+операц)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:месяц[\s-]*пик).*(?:операц|ops?|operation)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:top\s*year|top\s*month|years?\/top\s*year|years?\s*top\s*year)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:за\s+какие\s+год[а-яё]*\s+в\s+баз[еы]\s+есть\s+данн)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какой\s+год[а-яё]*\s+сам(?:ый|ая|ое)\s+(?:актив|пассив)|какой\s+год[а-яё]*\s+наименее\s+актив|год\s+с\s+минимальн)/iu.test(text) &&
|
||
/(?:документ|doc)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какой\s+месяц[а-яё]*\s+сам(?:ый|ая|ое)\s+(?:актив|пассив)|какой\s+месяц[а-яё]*\s+наименее\s+актив|месяц\s+с\s+минимальн)/iu.test(text) &&
|
||
/(?:операц|operation|ops?)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:профил[ья]\s+данн|покрыт(?:ие|ия)\s+период|диапазон\s+лет)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasDocumentTypeAndAccountSectionProfileSignal(text) {
|
||
if (hasAny(text, DOCUMENT_TYPE_AND_ACCOUNT_SECTION_PROFILE_HINTS)) {
|
||
return true;
|
||
}
|
||
if (/(?:каких?\s+док(?:ов|и)?).*(?:больше\s+всего|чаще\s+всего|крут)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:сводк[ауи].*тип[а-яё]*\s+док(?:умент|ов|и)?).*(?:дол[ья]|объем|объ[её]м)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какие\s+тип[аы]\s+док(?:умент|ов|и)?\s+(?:использ|чаще|больш))/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какие\s+тип[аы]\s+док(?:умент|ов|и)?\s+(?:реже|редк|наименее|миним))/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:типы?\s+док(?:умент|ов|и)?\s+и\s+их\s+дол[ья])/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какие\s+раздел[ыа]\s+уч[её]та\s+(?:наибол|наимен|заполн|почти\s+не))/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:раздел[ыа]\s+уч[её]та).*(?:жирн|мертв|пуст|использ)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasCounterpartyPopulationAndRolesSignal(text) {
|
||
if (hasLifecycleSegmentationSignal(text)) {
|
||
return false;
|
||
}
|
||
if (hasAny(text, COUNTERPARTY_POPULATION_AND_ROLES_HINTS)) {
|
||
return true;
|
||
}
|
||
if (/(?:(?:сколько|скока|скок)\s+(?:всего\s+)?уникальн(?:ых|ые|ого)?\s+контрагент|(?:сколько|скока|скок)\s+(?:всего\s+)?контрагент(?:ов|а)?(?:\s+в\s+баз[еы])?)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?заказчик(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?поставщик(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?клиент(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?покупател(?:ей|я)|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?смешан(?:ных|ые)\s+контрагент(?:ов|а)?|заказчик(?:и|ов)\s*,?\s*поставщик(?:и|ов))/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:разбей|раздели|сформируй\s+сводк).*(?:контрагент|заказчик|поставщик|клиент|покупател)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasLifecycleSegmentationSignal(text) {
|
||
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
|
||
}
|
||
function hasCounterpartyActivityLifecycleSignal(text) {
|
||
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
|
||
return false;
|
||
}
|
||
if (hasAny(text, COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS)) {
|
||
return true;
|
||
}
|
||
if (/(?:сколько|скока|скок)\s+/iu.test(text)) {
|
||
return false;
|
||
}
|
||
const hasCounterpartyLexeme = /(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?|контрагент(?:ов|а|ы)?|поставщик(?:ов|а|и)?|customer(?:s)?|client(?:s)?|counterpart(?:y|ies)|supplier(?:s)?|vendor(?:s)?)/iu.test(text);
|
||
const hasActivityLexeme = /(?:работал(?:и)?|активн(?:ые|ых|а|о)?|сотрудничал(?:и)?|были\s+в\s+работе|active|использ(?:овал(?:и|ось)?|уются|ован(?:ы|о)?))/iu.test(text);
|
||
const hasTimeWindowLexeme = /(?:за\s+вс[её]\s+время|all\s+time|\b(?:19|20)\d{2}\b|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|в\s+конкретн(?:ом|ый)\s+год|за\s+год|в\s+году)/iu.test(text);
|
||
const hasListVerb = /(?:какие|кто|покажи|выведи|список|list|show)/iu.test(text);
|
||
const hasRosterQualifier = /(?:у\s+нас|вообще|в\s+баз[еы]|какие\s+есть|кто\s+есть|who\s+are)/iu.test(text);
|
||
const hasImplicitCounterpartyQuestion = /(?:кто\s+с\s+нами|кто\s+у\s+нас|всех?\s+зак(?:ов|а|и)?|все\s+заки|кто\s+нов(?:ые|ых|ый)\b|кто\s+был\s+активен|самые\s+старые\s+по\s+сотрудничеству)/iu.test(text);
|
||
const hasListWithWindow = hasCounterpartyLexeme && hasListVerb && hasTimeWindowLexeme;
|
||
if (hasListWithWindow) {
|
||
return true;
|
||
}
|
||
if (hasCounterpartyLexeme && hasListVerb && hasRosterQualifier) {
|
||
return true;
|
||
}
|
||
if (hasCounterpartyLexeme && hasLifecycleSegmentationSignal(text)) {
|
||
return true;
|
||
}
|
||
if (hasImplicitCounterpartyQuestion && (hasLifecycleSegmentationSignal(text) || hasTimeWindowLexeme || hasActivityLexeme)) {
|
||
return true;
|
||
}
|
||
if (!hasCounterpartyLexeme && hasListVerb && hasLifecycleSegmentationSignal(text) && /\bкто\b/iu.test(text)) {
|
||
return true;
|
||
}
|
||
return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb);
|
||
}
|
||
function hasContractUsageOverviewSignal(text) {
|
||
if (hasAny(text, CONTRACT_USAGE_OVERVIEW_HINTS)) {
|
||
return true;
|
||
}
|
||
if (/(?:сколько\s+(?:всего\s+)?(?:договор|контракт)(?:ов|а)?(?:\s+заведен[оы])?|(?:договорн(?:ая|ой)|контрактн(?:ая|ой))\s+баз[аы]).*(?:сколько|used|использ)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:сколько\s+из\s+(?:договор|контракт)(?:ов|а)?\s+(?:реально\s+)?использ(?:ован[оы]|овал(?:и|ось)?))/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:договор|контракт|contract)?/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:какие\s+(?:договор|контракт)(?:ы|а)?).*(?:давно\s+не\s+использ|неиспольз|протух|мертв|мёртв|stale|unused)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasCustomerRevenueAndPaymentsSignal(text) {
|
||
if (hasAny(text, CUSTOMER_REVENUE_AND_PAYMENTS_HINTS)) {
|
||
return true;
|
||
}
|
||
if (hasContractAnchorSignal(text)) {
|
||
return false;
|
||
}
|
||
const hasFuzzyCustomerLexeme = hasFuzzyLexeme(text, ["клиент", "заказчик", "покупател", "customer", "client"]);
|
||
const hasFuzzySupplierLexeme = hasFuzzyLexeme(text, ["поставщик", "supplier", "vendor"]);
|
||
const hasCounterpartyLexeme = /(?:контрагент(?:ов|а|ы)?|counterpart(?:y|ies)|компан(?:и|ия|ии|ию)|организац(?:и|ия|ии|ию)|partner(?:s)?)/iu.test(text);
|
||
const hasSpecificCounterpartyAnchor = hasLooseByAnchorMention(text) ||
|
||
hasHeuristicCounterpartyAnchor(text) ||
|
||
/(?:по\s+(?:клиент(?:у|а)?|заказчик(?:у|а)?|покупател(?:ю|я)|customer|client)\s+[a-zа-яё0-9])/iu.test(text);
|
||
const asksWhoPays = /(?:кто\s+(?:нам\s+)?(?:(?:больше|чаще)\s+)?плат(?:ит|ят)?)/iu.test(text);
|
||
const asksCustomerGroup = /(?:клиент(?:ов|а|ы)?|заказчик(?:ов|а|и)?|покупател(?:ей|я|и)?|customer(?:s)?|client(?:s)?)/iu.test(text) ||
|
||
hasFuzzyCustomerLexeme ||
|
||
asksWhoPays;
|
||
const asksCounterpartySource = /(?:с\s+каких|от\s+каких|от\s+кого|from\s+which|from\s+who)/iu.test(text);
|
||
const asksIncomingFlow = /(?:приход|поступлен|входящ|зачислен|inflow|incoming)/iu.test(text);
|
||
const asksDealBudgetRanking = /(?:сделк|deal|бюджет)/iu.test(text) &&
|
||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
|
||
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal)/iu.test(text);
|
||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
|
||
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
|
||
if (asksCountOnly) {
|
||
return false;
|
||
}
|
||
if (hasSpecificCounterpartyAnchor && !asksRankOrTop) {
|
||
return false;
|
||
}
|
||
if (asksCustomerGroup && (asksValue || asksRankOrTop)) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && hasCounterpartyLexeme && asksRankOrTop && (asksValue || asksWhoPays)) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksWhoPays && (asksRankOrTop || hasCounterpartyLexeme)) {
|
||
return true;
|
||
}
|
||
if (asksCounterpartySource && asksValue) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksDealBudgetRanking) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasSupplierPayoutsProfileSignal(text) {
|
||
if (hasAny(text, SUPPLIER_PAYOUTS_PROFILE_HINTS)) {
|
||
return true;
|
||
}
|
||
if (hasContractAnchorSignal(text)) {
|
||
return false;
|
||
}
|
||
const hasFuzzySupplierLexeme = hasFuzzyLexeme(text, ["поставщик", "supplier", "vendor"]);
|
||
const hasSpecificCounterpartyAnchor = hasLooseByAnchorMention(text) ||
|
||
hasHeuristicCounterpartyAnchor(text) ||
|
||
/(?:по\s+(?:поставщик(?:у|а)?|supplier|vendor)\s+[a-zа-яё0-9])/iu.test(text);
|
||
const asksSupplierGroup = /(?:поставщик(?:ов|а|и)?|supplier(?:s)?|vendor(?:s)?|к[ао]му\s+мы)/iu.test(text) ||
|
||
hasFuzzySupplierLexeme ||
|
||
/(?:кому\s+ушло|кому\s+платили|кому\s+заплатили)/iu.test(text);
|
||
const asksPayoutValue = /(?:выплат|исходящ|списан|заплат|ушло|сгрузил|сгрузили|перевел|перевёл|отдали|платеж|платёж|outflow|payout)/iu.test(text);
|
||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|больше\s+всего|чаще\s+всего|максимальн|наибольш)/iu.test(text);
|
||
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksPayoutValue;
|
||
if (asksCountOnly) {
|
||
return false;
|
||
}
|
||
if (hasSpecificCounterpartyAnchor && !asksRankOrTop) {
|
||
return false;
|
||
}
|
||
return asksSupplierGroup && (asksPayoutValue || asksRankOrTop);
|
||
}
|
||
function hasContractUsageAndValueSignal(text) {
|
||
if (hasAny(text, CONTRACT_USAGE_AND_VALUE_HINTS)) {
|
||
return true;
|
||
}
|
||
if (!/(?:договор(?:ов|а|ы)?|контракт(?:ов|а|ы|у|ом|е)?|contract(?:s)?)/iu.test(text)) {
|
||
return false;
|
||
}
|
||
if (hasContractUsageOverviewSignal(text)) {
|
||
return false;
|
||
}
|
||
const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+(?:договор|контракт)|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+(?:договор|контракт)|какие\s+(?:договор|контракт)(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+(?:договор|контракт))/iu.test(text);
|
||
const asksValue = /(?:оборот|бюджет|сумм|стоим|value|turnover|amount|revenue|крупн|мелк|миним|максим)/iu.test(text);
|
||
const asksRank = /(?:топ|top|ранк|rank|сам(?:ый|ая|ое|ые))/iu.test(text);
|
||
return asksStructure || asksValue || asksRank;
|
||
}
|
||
function hasContractListByCounterpartySignal(text) {
|
||
const hasContractLexeme = /(?:договор(?:а|у|ом|е|ы)?|контракт(?:а|у|ом|е|ы)?|contracts?|contract)/iu.test(text);
|
||
if (!hasContractLexeme) {
|
||
return false;
|
||
}
|
||
// If user explicitly asks for documents, keep routing in document-by-contract/counterparty lane.
|
||
if (hasDocumentSignal(text)) {
|
||
return false;
|
||
}
|
||
if (hasContractUsageOverviewSignal(text) || hasOpenContractsListSignal(text)) {
|
||
return false;
|
||
}
|
||
if (hasContractNumberLikeToken(text)) {
|
||
return false;
|
||
}
|
||
if (hasBankOperationSignal(text)) {
|
||
return false;
|
||
}
|
||
const hasListVerb = /(?:покажи|выведи|список|какие|show|list)/iu.test(text);
|
||
const hasAllQualifier = /(?:\ball\b|\bвсе\b|всё)/iu.test(text);
|
||
const hasCounterpartyAnchor = hasPartyAnchorMention(text) ||
|
||
hasLooseByAnchorMention(text) ||
|
||
hasHeuristicCounterpartyAnchor(text);
|
||
if (!hasCounterpartyAnchor) {
|
||
return false;
|
||
}
|
||
return hasListVerb || hasAllQualifier || hasAny(text, CONTRACT_LIST_BY_COUNTERPARTY_HINTS);
|
||
}
|
||
function hasDocumentsByAccountDrilldownSignal(text) {
|
||
const hasAccountLexeme = hasAccountNumberAnchor(text) || hasCompactAccountCodeToken(text);
|
||
const hasDocLexeme = /(?:документ|док(?:и|ам|ах|ов|а)?|docs?|documents?|doki|docy|doci)/iu.test(text);
|
||
const hasDrilldownVerb = /(?:раскрой|раскры|разлож|разверн|документами|по\s+документ)/iu.test(text);
|
||
const hasSameDate = /(?:на\s+ту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date)/iu.test(text);
|
||
return hasAccountLexeme && hasDocLexeme && (hasDrilldownVerb || hasSameDate);
|
||
}
|
||
function hasOpenContractsListSignal(text) {
|
||
const hasContractLexeme = text.includes("договор") || text.includes("контракт") || text.includes("contract") || text.includes("dogovor");
|
||
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
|
||
if (!hasContractLexeme || !hasOpenLexeme) {
|
||
return false;
|
||
}
|
||
// Query about a specific contract should stay in open-items lane.
|
||
if (hasContractNumberLikeToken(text)) {
|
||
return false;
|
||
}
|
||
// Debt/tail wording indicates open-items intent, not contract list.
|
||
if (/(?:долг|задолж|хвост|позиц|open\s+items|unclosed\s+items|взаиморасчет|взаиморасчёт)/iu.test(text)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
function isLikelyCounterpartyToken(rawToken) {
|
||
const token = String(rawToken ?? "").trim().toLowerCase();
|
||
if (!token || token.length < 2) {
|
||
return false;
|
||
}
|
||
if (/^\d+$/.test(token)) {
|
||
return false;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(token)) {
|
||
return false;
|
||
}
|
||
const stopWords = new Set([
|
||
"за",
|
||
"с",
|
||
"по",
|
||
"на",
|
||
"и",
|
||
"или",
|
||
"док",
|
||
"доки",
|
||
"доки?",
|
||
"документ",
|
||
"документы",
|
||
"документов",
|
||
"документами",
|
||
"документу",
|
||
"документе",
|
||
"документа",
|
||
"документах",
|
||
"докам",
|
||
"доками",
|
||
"количество",
|
||
"количеству",
|
||
"количества",
|
||
"количеством",
|
||
"активный",
|
||
"активного",
|
||
"активности",
|
||
"пассивный",
|
||
"пассивного",
|
||
"пассивности",
|
||
"наименее",
|
||
"минимальный",
|
||
"минимум",
|
||
"реже",
|
||
"редкий",
|
||
"банк",
|
||
"банковские",
|
||
"операции",
|
||
"платежи",
|
||
"платеж",
|
||
"платёж",
|
||
"контрагент",
|
||
"контрагенту",
|
||
"контрагента",
|
||
"компания",
|
||
"компании",
|
||
"организация",
|
||
"организации",
|
||
"год",
|
||
"года",
|
||
"г",
|
||
"плс",
|
||
"pls",
|
||
"пж",
|
||
"пжлст",
|
||
"пожалуйста",
|
||
"есть",
|
||
"же",
|
||
"сводные",
|
||
"сводный",
|
||
"сводная",
|
||
"сводную",
|
||
"сводном",
|
||
"сводного",
|
||
"сводному",
|
||
"неуказанному",
|
||
"неуказанный",
|
||
"неуказанная",
|
||
"неуказанное",
|
||
"указанному",
|
||
"указанный",
|
||
"указанная",
|
||
"указанное",
|
||
"объекту",
|
||
"объект",
|
||
"бля",
|
||
"блять",
|
||
"епт",
|
||
"ёпт",
|
||
"епта",
|
||
"нах",
|
||
"нахуй",
|
||
"связанным",
|
||
"связанные",
|
||
"связанных",
|
||
"связанному",
|
||
"related",
|
||
"linked",
|
||
"этомуже",
|
||
"томуже"
|
||
]);
|
||
return !stopWords.has(token);
|
||
}
|
||
function hasPartyAnchorMention(text) {
|
||
return (text.includes("контраг") ||
|
||
text.includes("контра") ||
|
||
text.includes("counterparty") ||
|
||
text.includes("компан") ||
|
||
text.includes("company") ||
|
||
text.includes("организац") ||
|
||
text.includes("supplier") ||
|
||
text.includes("vendor") ||
|
||
text.includes("customer") ||
|
||
text.includes("client") ||
|
||
text.includes("partner") ||
|
||
text.includes("поставщик") ||
|
||
text.includes("клиент") ||
|
||
text.includes("покупател") ||
|
||
text.includes("партнер"));
|
||
}
|
||
function hasContractAnchorMention(text) {
|
||
return (text.includes("договор") ||
|
||
text.includes("контракт") ||
|
||
/\bдог\.?\b/iu.test(text) ||
|
||
text.includes("дог.") ||
|
||
text.includes("contract") ||
|
||
text.includes("dogovor"));
|
||
}
|
||
function hasContractNumberLikeToken(text) {
|
||
if (/(?:^|[\s([{])(?:№|#|n)\s*[a-zа-яё0-9][a-zа-яё0-9./_-]{1,}(?=$|[\s,.;:!?)\]}])/iu.test(text)) {
|
||
return true;
|
||
}
|
||
const rawTokens = text
|
||
.split(/[\s,;:!?()[\]{}"«»]+/u)
|
||
.map((token) => token.replace(/^[^\p{L}\p{N}#№]+|[^\p{L}\p{N}./_-]+$/gu, "").trim())
|
||
.filter((token) => token.length > 0);
|
||
for (const rawToken of rawTokens) {
|
||
const token = String(rawToken ?? "").trim();
|
||
if (!/^\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?$/u.test(token)) {
|
||
continue;
|
||
}
|
||
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;
|
||
}
|
||
if (parts.length === 2) {
|
||
const [a, b] = parts;
|
||
const yearFirst = a >= 1900 && a <= 2099 && b >= 1 && b <= 12;
|
||
const yearSecond = b >= 1900 && b <= 2099 && a >= 1 && a <= 12;
|
||
if (yearFirst || yearSecond) {
|
||
continue;
|
||
}
|
||
return true;
|
||
}
|
||
if (parts.length === 3) {
|
||
const [a, b, c] = parts;
|
||
const ymd = a >= 1900 && a <= 2099 && b >= 1 && b <= 12 && c >= 1 && c <= 31;
|
||
const dmy = c >= 1900 && c <= 2099 && a >= 1 && a <= 31 && b >= 1 && b <= 12;
|
||
if (ymd || dmy) {
|
||
continue;
|
||
}
|
||
return true;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasContractAnchorSignal(text) {
|
||
if (hasContractAnchorMention(text)) {
|
||
return true;
|
||
}
|
||
// Allow short forms like "19/15" for follow-up prompts if document/bank signal exists.
|
||
return hasContractNumberLikeToken(text) && hasDocsOrBankSignal(text);
|
||
}
|
||
function hasLooseByAnchorMention(text) {
|
||
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||
if (!match) {
|
||
return false;
|
||
}
|
||
const token = String(match[1] ?? "").toLowerCase();
|
||
if (!token) {
|
||
return false;
|
||
}
|
||
const stopWords = new Set([
|
||
"контрагенту",
|
||
"контрагента",
|
||
"контре",
|
||
"компании",
|
||
"компанию",
|
||
"организации",
|
||
"организацию",
|
||
"поставщику",
|
||
"поставщика",
|
||
"клиенту",
|
||
"клиента",
|
||
"покупателю",
|
||
"покупателя",
|
||
"партнеру",
|
||
"партнера",
|
||
"договору",
|
||
"договора",
|
||
"счету",
|
||
"счёту",
|
||
"дате",
|
||
"периоду",
|
||
"период",
|
||
"документам",
|
||
"докам",
|
||
"количество",
|
||
"количеству",
|
||
"количества",
|
||
"количеством",
|
||
"активности",
|
||
"пассивности",
|
||
"наименее",
|
||
"минимум"
|
||
]);
|
||
return !stopWords.has(token);
|
||
}
|
||
function hasImplicitCounterpartyAnchorAroundDocs(text) {
|
||
const beforeDocsMatch = text.match(/(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?|doki|docy|doci)(?=[\s,.;:!?)]|$)/iu);
|
||
if (beforeDocsMatch && isLikelyCounterpartyToken(String(beforeDocsMatch[1] ?? ""))) {
|
||
return true;
|
||
}
|
||
const afterDocsMatch = text.match(/(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?|doki|docy|doci)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||
if (afterDocsMatch && isLikelyCounterpartyToken(String(afterDocsMatch[1] ?? ""))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasDocsOrBankSignal(text) {
|
||
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|doki|docy|doci|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
||
}
|
||
function hasBankOperationSignal(text) {
|
||
return hasAny(text, BANK_OPERATION_CORE_HINTS) || hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) || hasAny(text, BANK_OPERATIONS_BY_CONTRACT_HINTS);
|
||
}
|
||
function hasDocumentSignal(text) {
|
||
return (text.includes("док") ||
|
||
text.includes("доки") ||
|
||
text.includes("документ") ||
|
||
text.includes("doki") ||
|
||
text.includes("docy") ||
|
||
text.includes("doci") ||
|
||
text.includes("docs") ||
|
||
text.includes("documents"));
|
||
}
|
||
function hasHeuristicCounterpartyAnchor(text) {
|
||
if (!hasDocsOrBankSignal(text) && !hasBankOperationSignal(text)) {
|
||
return false;
|
||
}
|
||
const tokens = String(text ?? "")
|
||
.split(/[^a-zа-яё0-9._-]+/iu)
|
||
.map((item) => item.trim())
|
||
.filter((item) => item.length > 0);
|
||
for (const token of tokens) {
|
||
const lowered = token.toLowerCase();
|
||
if (!isLikelyCounterpartyToken(lowered)) {
|
||
continue;
|
||
}
|
||
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
|
||
continue;
|
||
}
|
||
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) {
|
||
continue;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasGenericAddressLookupSignal(text) {
|
||
return (/\bесть\b/iu.test(text) ||
|
||
/\bпокажи\b/iu.test(text) ||
|
||
/\bвыведи\b/iu.test(text) ||
|
||
/\bкакие\b/iu.test(text) ||
|
||
/\bчто(?:-|\s)?то\b/iu.test(text) ||
|
||
/за\s+любой\s+период/iu.test(text) ||
|
||
/за\s+вс[её]\s+время/iu.test(text) ||
|
||
/for\s+all\s+time/iu.test(text) ||
|
||
/all\s+time/iu.test(text));
|
||
}
|
||
function hasAccountNumberAnchor(text) {
|
||
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
|
||
}
|
||
function resolveAddressIntent(userMessage) {
|
||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||
if (hasAny(text, RECEIVABLES_STRONG)) {
|
||
return {
|
||
intent: "list_receivables_counterparties",
|
||
confidence: "high",
|
||
reasons: ["receivables_signal_detected"]
|
||
};
|
||
}
|
||
if (hasAny(text, PAYABLES_STRONG)) {
|
||
return {
|
||
intent: "list_payables_counterparties",
|
||
confidence: "high",
|
||
reasons: ["payables_signal_detected"]
|
||
};
|
||
}
|
||
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
|
||
return {
|
||
intent: "documents_forming_balance",
|
||
confidence: "high",
|
||
reasons: ["documents_forming_balance_signal_detected"]
|
||
};
|
||
}
|
||
if (hasDocumentsByAccountDrilldownSignal(text)) {
|
||
return {
|
||
intent: "documents_forming_balance",
|
||
confidence: "medium",
|
||
reasons: ["documents_by_account_drilldown_signal_detected"]
|
||
};
|
||
}
|
||
if (hasOpenContractsListSignal(text)) {
|
||
return {
|
||
intent: "list_open_contracts",
|
||
confidence: "medium",
|
||
reasons: ["open_contract_signal_detected"]
|
||
};
|
||
}
|
||
if (hasAny(text, OPEN_ITEMS_HINTS) &&
|
||
(text.includes("контраг") || 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 (hasPeriodCoverageProfileSignal(text) &&
|
||
!hasPartyAnchorMention(text) &&
|
||
!hasContractAnchorSignal(text) &&
|
||
!hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "period_coverage_profile",
|
||
confidence: "high",
|
||
reasons: ["period_coverage_profile_signal_detected"]
|
||
};
|
||
}
|
||
if (hasDocumentTypeAndAccountSectionProfileSignal(text) &&
|
||
!hasPartyAnchorMention(text) &&
|
||
!hasContractAnchorSignal(text) &&
|
||
!hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "document_type_and_account_section_profile",
|
||
confidence: "high",
|
||
reasons: ["document_type_and_account_section_profile_signal_detected"]
|
||
};
|
||
}
|
||
if (hasCounterpartyPopulationAndRolesSignal(text) &&
|
||
!hasContractAnchorSignal(text) &&
|
||
!hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "counterparty_population_and_roles",
|
||
confidence: "high",
|
||
reasons: ["counterparty_population_and_roles_signal_detected"]
|
||
};
|
||
}
|
||
if (hasCounterpartyActivityLifecycleSignal(text) &&
|
||
!hasContractAnchorSignal(text) &&
|
||
!hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "counterparty_activity_lifecycle",
|
||
confidence: "high",
|
||
reasons: ["counterparty_activity_lifecycle_signal_detected"]
|
||
};
|
||
}
|
||
if (hasContractUsageOverviewSignal(text) &&
|
||
!hasAccountBalanceSignal(text) &&
|
||
!hasOpenContractsListSignal(text)) {
|
||
return {
|
||
intent: "contract_usage_overview",
|
||
confidence: "high",
|
||
reasons: ["contract_usage_overview_signal_detected"]
|
||
};
|
||
}
|
||
if (hasCustomerRevenueAndPaymentsSignal(text) && !hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "customer_revenue_and_payments",
|
||
confidence: "high",
|
||
reasons: ["customer_revenue_and_payments_signal_detected"]
|
||
};
|
||
}
|
||
if (hasSupplierPayoutsProfileSignal(text) && !hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "supplier_payouts_profile",
|
||
confidence: "high",
|
||
reasons: ["supplier_payouts_profile_signal_detected"]
|
||
};
|
||
}
|
||
if (hasContractUsageAndValueSignal(text) &&
|
||
!hasAccountBalanceSignal(text) &&
|
||
!hasOpenContractsListSignal(text)) {
|
||
return {
|
||
intent: "contract_usage_and_value",
|
||
confidence: "high",
|
||
reasons: ["contract_usage_and_value_signal_detected"]
|
||
};
|
||
}
|
||
if (hasContractListByCounterpartySignal(text)) {
|
||
return {
|
||
intent: "list_contracts_by_counterparty",
|
||
confidence: "medium",
|
||
reasons: ["contracts_by_counterparty_signal_detected"]
|
||
};
|
||
}
|
||
if (hasContractAnchorSignal(text) &&
|
||
hasBankOperationSignal(text)) {
|
||
return {
|
||
intent: "bank_operations_by_contract",
|
||
confidence: "medium",
|
||
reasons: ["bank_ops_by_contract_signal_detected"]
|
||
};
|
||
}
|
||
if (hasContractAnchorSignal(text) &&
|
||
(hasAny(text, DOCUMENTS_BY_CONTRACT_HINTS) || hasDocumentSignal(text))) {
|
||
return {
|
||
intent: "list_documents_by_contract",
|
||
confidence: "medium",
|
||
reasons: ["documents_by_contract_signal_detected"]
|
||
};
|
||
}
|
||
if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) &&
|
||
(hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) {
|
||
return {
|
||
intent: "bank_operations_by_counterparty",
|
||
confidence: "medium",
|
||
reasons: ["bank_ops_by_counterparty_signal_detected"]
|
||
};
|
||
}
|
||
if (hasAny(text, DOCUMENTS_BY_COUNTERPARTY_HINTS) &&
|
||
(hasPartyAnchorMention(text) ||
|
||
hasLooseByAnchorMention(text) ||
|
||
hasImplicitCounterpartyAnchorAroundDocs(text) ||
|
||
hasHeuristicCounterpartyAnchor(text))) {
|
||
return {
|
||
intent: "list_documents_by_counterparty",
|
||
confidence: "medium",
|
||
reasons: ["documents_by_counterparty_signal_detected"]
|
||
};
|
||
}
|
||
if (hasAccountBalanceSignal(text)) {
|
||
return {
|
||
intent: "account_balance_snapshot",
|
||
confidence: "high",
|
||
reasons: ["account_balance_signal_detected"]
|
||
};
|
||
}
|
||
if (hasLooseByAnchorMention(text) && hasGenericAddressLookupSignal(text)) {
|
||
return {
|
||
intent: "list_documents_by_counterparty",
|
||
confidence: "low",
|
||
reasons: ["generic_lookup_with_loose_anchor_fallback"]
|
||
};
|
||
}
|
||
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("контракт") || text.includes("contract"))) {
|
||
return {
|
||
intent: "list_open_contracts",
|
||
confidence: "medium",
|
||
reasons: ["open_contract_signal_detected"]
|
||
};
|
||
}
|
||
return {
|
||
intent: "unknown",
|
||
confidence: "low",
|
||
reasons: ["intent_not_supported_in_v1"]
|
||
};
|
||
}
|