1810 lines
66 KiB
TypeScript
1810 lines
66 KiB
TypeScript
import type { AddressIntentResolution } from "../types/addressQuery";
|
||
|
||
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: string, patterns: string[]): boolean {
|
||
return patterns.some((item) => text.includes(item));
|
||
}
|
||
|
||
function hasFlexibleReceivablesDebtSignal(text: string): boolean {
|
||
const normalized = String(text ?? "");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return (
|
||
/(?:кто(?:\s+\S+){0,4}\s+нам(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
||
/(?:нам(?:\s+\S+){0,4}\s+кто(?:\s+\S+){0,4}\s+долж)/iu.test(normalized)
|
||
);
|
||
}
|
||
|
||
function hasFlexiblePayablesDebtSignal(text: string): boolean {
|
||
const normalized = String(text ?? "");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return (
|
||
/(?:кому(?:\s+\S+){0,4}\s+мы(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
||
/(?:мы(?:\s+\S+){0,4}\s+кому(?:\s+\S+){0,4}\s+долж)/iu.test(normalized)
|
||
);
|
||
}
|
||
|
||
function tokenizeText(text: string): string[] {
|
||
return String(text ?? "")
|
||
.toLowerCase()
|
||
.split(/[^a-zа-яё0-9]+/iu)
|
||
.map((token) => token.trim())
|
||
.filter((token) => token.length > 0);
|
||
}
|
||
|
||
function trimRussianEnding(token: string): string {
|
||
return token.replace(
|
||
/(?:иями|ями|ами|ого|ему|ому|ыми|ими|ией|ей|ий|ый|ой|ях|ах|ов|ев|ам|ям|ом|ем|ы|и|а|я|у|ю|е|о)$/u,
|
||
""
|
||
);
|
||
}
|
||
|
||
function normalizeLexemeToken(rawToken: string): string {
|
||
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: string, b: string): number {
|
||
if (a === b) {
|
||
return 0;
|
||
}
|
||
if (!a.length) {
|
||
return b.length;
|
||
}
|
||
if (!b.length) {
|
||
return a.length;
|
||
}
|
||
const prev = new Array<number>(b.length + 1);
|
||
const curr = new Array<number>(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: string, lexemeRoots: string[]): boolean {
|
||
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: string): boolean {
|
||
// Match compact account tokens while reducing false positives on short-year literals like "22 год".
|
||
const source = String(text ?? "");
|
||
if (!source) {
|
||
return false;
|
||
}
|
||
// Safe compact form: 60.01 / 62.1
|
||
if (/(?<![\d-])\d{2}[.,]\d{1,2}(?![\d-])/u.test(source)) {
|
||
return true;
|
||
}
|
||
// Plain two-digit code is accepted only in explicit account context.
|
||
if (/(?:сч[её]т|account)\D{0,12}\d{2}(?![\d-])/iu.test(source)) {
|
||
return true;
|
||
}
|
||
if (/(?:^|\s)по\s+\d{2}(?=$|[\s,.;:!?])/iu.test(source)) {
|
||
if (!/(?:^|\s)(?:за|в)\s+\d{2}\s*(?:г(?:од|ода)?|year)\b/iu.test(source)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function hasDocumentsFormingBalanceSignal(text: string): boolean {
|
||
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: 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);
|
||
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 hasForecastTaxSignal(text: string): boolean {
|
||
const hasForecastLexeme =
|
||
/(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
||
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
||
return hasForecastLexeme && hasTaxLexeme;
|
||
}
|
||
|
||
function hasVatPayableConfirmedSignal(text: string): boolean {
|
||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||
if (!hasVatLexeme) {
|
||
return false;
|
||
}
|
||
const hasPaymentCue =
|
||
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(
|
||
text
|
||
);
|
||
if (!hasPaymentCue) {
|
||
return false;
|
||
}
|
||
const hasDateOrPeriodCue =
|
||
/(?:на\s+дат|по\s+состоянию|на\s+конец|за\s+(?:\d{4}|январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)|квартал|месяц|год|период|\b\d{4}[./-]\d{2}[./-]\d{2}\b)/iu.test(
|
||
text
|
||
);
|
||
return hasDateOrPeriodCue || /(?:сколько|скока|скок)/iu.test(text);
|
||
}
|
||
|
||
function hasPeriodCoverageProfileSignal(text: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|дольше\s+всего|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(
|
||
text
|
||
);
|
||
}
|
||
|
||
function hasCounterpartyDebtLongevitySignal(text: string): boolean {
|
||
const hasCounterpartyLexeme =
|
||
/(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?|контрагент(?:ов|а|ы)?|customer(?:s)?|client(?:s)?|counterpart(?:y|ies)|buyer(?:s)?)/iu.test(
|
||
text
|
||
);
|
||
const hasDebtLexeme = /(?:долг(?:и|ов|а|у)?|задолж(?:енность|енности|енностям|ал|али)?|просроч|хвост)/iu.test(text);
|
||
const hasLongevityCue =
|
||
/(?:долгожив|долгожител|несколько\s+месяц|по\s+годам|дольше|лет|год(?:ам|а|у|ы)?|на\s+этот\s+момент|длительн)/iu.test(
|
||
text
|
||
);
|
||
return hasCounterpartyLexeme && hasDebtLexeme && hasLongevityCue;
|
||
}
|
||
|
||
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
|
||
const hasPaymentRiskLexeme =
|
||
/(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|задолж|\bдолг(?:и|ов|а|у)?\b)/iu.test(
|
||
text
|
||
);
|
||
if (hasPaymentRiskLexeme) {
|
||
return false;
|
||
}
|
||
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
|
||
return false;
|
||
}
|
||
if (hasAny(text, COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS)) {
|
||
return true;
|
||
}
|
||
if (/(?:сколько|скока|скок)\s+/iu.test(text) && !hasLifecycleSegmentationSignal(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: string): boolean {
|
||
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: string): boolean {
|
||
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 asksWhoBringsMostMoney =
|
||
/(?:кто\s+(?:нам\s+)?(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш(?:ий|ая|ее|ие))\s+(?:прин[её]с|зан[её]с).*(?:деньг|денег))/iu.test(
|
||
text
|
||
);
|
||
const asksWhoBringsMoneyLoose =
|
||
/(?:кто\s+(?:нам\s+)?(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с))/iu.test(
|
||
text
|
||
) ||
|
||
/(?:кто\s+(?:нам\s+)?(?:прин[её]с|зан[её]с).*(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк))/iu.test(
|
||
text
|
||
);
|
||
const asksLiquidityRanking =
|
||
/(?:ликвидн|liquid)/iu.test(text) &&
|
||
(asksCustomerGroup || hasCounterpartyLexeme || /(?:клиент|заказчик|контрагент|customer|client|counterpart)/iu.test(text));
|
||
const asksProfitableYears =
|
||
/(?:доходн|выручк|оборот|прибыл|revenue|turnover).*(?:год|года|годы|year|years)/iu.test(text) &&
|
||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|max|best|наибольш|больше)/iu.test(text);
|
||
const asksDealBudgetRanking =
|
||
/(?:сделк|deal|бюджет)/iu.test(text) &&
|
||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(
|
||
text
|
||
);
|
||
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
||
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
||
const asksMajorShare =
|
||
/(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(
|
||
text
|
||
);
|
||
const asksValue =
|
||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|ликвидн|revenue|inflow|deal|turnover|liquid)/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 (!hasFuzzySupplierLexeme && asksWhoBringsMostMoney) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksWhoBringsMoneyLoose) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksLiquidityRanking) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksProfitableYears) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && (asksRevenueTotal || asksOverallTurnover)) {
|
||
return true;
|
||
}
|
||
if (asksCounterpartySource && asksValue) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
|
||
return true;
|
||
}
|
||
if (!hasFuzzySupplierLexeme && asksDealBudgetRanking) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function hasSupplierPayoutsProfileSignal(text: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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 hasSupplierTailRiskSignal(text: string): boolean {
|
||
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
|
||
const hasTail = /(?:хвост|висят|незакрыт|не\s+закрыв|задолж|долг|просроч|сч[её]т)/iu.test(text);
|
||
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
|
||
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц|больше\s+месяц)/iu.test(text);
|
||
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
|
||
}
|
||
|
||
function hasPayablesDebtLifecycleSignal(text: string): boolean {
|
||
const hasOweSignal =
|
||
/(?:кому\s+мы\s+долж(?:ен|ны|эны|эна|эно)?|мы\s+долж(?:ен|ны|эны|эна|эно)?|кому\s+долж(?:ен|ны|эны|эна|эно)?|долж[нэ](?:ы|а|о)?\s+(?:заплат|оплат|перечис)|к\s+оплате|на\s+оплату|who\s+we\s+owe|owe\s+to|payables?|кредитор(?:[а-яё]{0,6})?)/iu.test(
|
||
text
|
||
);
|
||
if (!hasOweSignal) {
|
||
return false;
|
||
}
|
||
const hasPastPaymentSignal = /(?:заплатил(?:и)?|платил(?:и)?|кому\s+ушло|выплатил(?:и)?|списан|outflow|payout)/iu.test(text);
|
||
const hasTopRankingSignal = /(?:топ|top|больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|максимальн)/iu.test(text);
|
||
if (hasPastPaymentSignal && hasTopRankingSignal) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function hasReceivablesDebtLifecycleSignal(text: string): boolean {
|
||
const hasOweUsSignal =
|
||
/(?:кто\s+нам\s+долж(?:ен|ны|эны|эна|эно)?|кто\s+долж(?:ен|ны|эны|эна|эно)?\s+нам|нам\s+долж(?:ен|ны|эны|эна|эно)?|должник(?:[а-яё]{0,6})?|дебитор(?:[а-яё]{0,6})?|дебиторск(?:[а-яё]{0,6})?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(
|
||
text
|
||
);
|
||
if (!hasOweUsSignal) {
|
||
return false;
|
||
}
|
||
const hasPastInflowSignal = /(?:прин[её]с|зан[её]с|поступил|приход|inflow|paid\s+us|already\s+paid)/iu.test(text);
|
||
const hasTopRankingSignal = /(?:топ|top|больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|максимальн)/iu.test(text);
|
||
if (hasPastInflowSignal && hasTopRankingSignal) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function hasReceivablesLatencyRiskSignal(text: string): boolean {
|
||
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
|
||
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
|
||
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
|
||
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
|
||
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
|
||
const hasOverdueDeadlineCue =
|
||
/(?:срок(?:и|а)?(?:\s+оплат[ыы]?)?[\s\S]{0,24}(?:прош|выш|истек|истёк)|срок(?:и|а)?\s+давно\s+прошл|давно\s+пора\s+оплат|давно\s+не\s+оплач)/iu.test(
|
||
text
|
||
);
|
||
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|не\s+оплат|не\s+оплач|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
|
||
const hasPaymentShipmentImbalance =
|
||
/(?:оплач(?:ено|ен[аоы]?|ивать|ивать)?\s+меньше[\s\S]{0,36}отгруж|недоплат[\s\S]{0,36}отгруж|отгруж[\s\S]{0,36}оплач(?:ено|ено\s+меньше))/iu.test(
|
||
text
|
||
);
|
||
const hasNegativeSaldoRisk = /(?:сальд[оа]\s+(?:уже\s+)?отрицат|минусов(?:ое|ой)\s+сальдо|сальдо\s+в\s+минус)/iu.test(text);
|
||
const hasPeriodOrRiskCue =
|
||
/(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн|несколько\s+месяц|больше\s+месяц)/iu.test(text) ||
|
||
hasOverdueDeadlineCue ||
|
||
hasNegativeSaldoRisk;
|
||
const hasBetweenShipmentAndPayment =
|
||
/между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
|
||
if (
|
||
hasBuyer &&
|
||
hasPayment &&
|
||
((hasShipment && (hasDelay || hasOverdueDeadlineCue)) || hasBetweenShipmentAndPayment || hasPaymentShipmentImbalance)
|
||
) {
|
||
return true;
|
||
}
|
||
if ((hasBuyer || hasCounterparty) && hasPaymentShipmentImbalance) {
|
||
return true;
|
||
}
|
||
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
|
||
}
|
||
|
||
function hasSettlementGapSignal(text: string): boolean {
|
||
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
|
||
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
|
||
const hasShipment = /(?:отгруз|реализ|shipment|delivery|товар|услуг)/iu.test(text);
|
||
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
|
||
const hasClosureLexeme = /(?:закрыти|взаиморасч|акт|сч[её]т(?:ов|а|ы)?)/iu.test(text);
|
||
const hasNoDocumentForClosing =
|
||
/(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
|
||
hasClosureLexeme;
|
||
const hasNoDocumentForClosingReversed =
|
||
/(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
|
||
hasClosureLexeme;
|
||
const hasNoPayments =
|
||
/(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
|
||
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
|
||
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
|
||
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
|
||
const hasPaymentsWithoutSettlementClosure =
|
||
hasPayment &&
|
||
/(?:без|нет)\s+закрыти(?:я|й)?(?:\s+взаиморасч[её]тов)?/iu.test(text) &&
|
||
hasClosureLexeme;
|
||
const hasShipmentWithoutClosingDocs =
|
||
hasShipment &&
|
||
(hasNoDocumentForClosing ||
|
||
hasNoDocumentForClosingReversed ||
|
||
/(?:без|нет)\s+док(?:и|умент(?:ов|ы|а)?)\s+(?:для\s+)?(?:их\s+)?закрыти/u.test(text));
|
||
const hasClosingWithoutSupportingDocs =
|
||
hasClosureLexeme &&
|
||
/(?:без|нет)\s+подтверждающ(?:их|его|ие)?\s+док(?:и|умент(?:ов|ы|а)?)/iu.test(text);
|
||
const hasAdvanceStuckRisk =
|
||
/(?:зависш(?:ий|ие|ая|ие\s+аванс)|давно\s+пора\s+закрыть|пора\s+закрывать|перепривяз(?:ать|к)|списыв(?:ать|ани|ан)|нереальн)/iu.test(
|
||
text
|
||
);
|
||
const hasUnclosedAdvanceGap =
|
||
hasAdvance &&
|
||
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт|давно\s+пора\s+закрыть)/iu.test(text) ||
|
||
hasAdvanceStuckRisk ||
|
||
hasNoDocumentForClosing ||
|
||
hasNoDocumentForClosingReversed);
|
||
return (
|
||
hasPaymentsWithoutClosingDocs ||
|
||
hasPaymentsWithoutSettlementClosure ||
|
||
hasDocsWithoutPayments ||
|
||
hasShipmentWithoutClosingDocs ||
|
||
hasClosingWithoutSupportingDocs ||
|
||
hasUnclosedAdvanceGap
|
||
);
|
||
}
|
||
|
||
function hasReconciliationMismatchSignal(text: string): boolean {
|
||
const hasCounterparty =
|
||
/(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
|
||
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
|
||
const hasMismatchLexeme =
|
||
/(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
|
||
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
|
||
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
|
||
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
|
||
return (
|
||
hasCounterparty &&
|
||
hasReconciliationLexeme &&
|
||
hasMismatchLexeme &&
|
||
hasBalanceLexeme &&
|
||
(hasLookupVerb || hasInterrogativeLookup)
|
||
);
|
||
}
|
||
|
||
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
return (
|
||
text.includes("договор") ||
|
||
text.includes("контракт") ||
|
||
/\bдог\.?\b/iu.test(text) ||
|
||
text.includes("дог.") ||
|
||
text.includes("contract") ||
|
||
text.includes("dogovor")
|
||
);
|
||
}
|
||
|
||
function hasContractNumberLikeToken(text: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|doki|docy|doci|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(
|
||
text
|
||
);
|
||
}
|
||
|
||
function hasBankOperationSignal(text: string): boolean {
|
||
return hasAny(text, BANK_OPERATION_CORE_HINTS) || hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) || hasAny(text, BANK_OPERATIONS_BY_CONTRACT_HINTS);
|
||
}
|
||
|
||
function hasDocumentSignal(text: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
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: string): boolean {
|
||
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
|
||
}
|
||
|
||
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||
|
||
if (hasForecastTaxSignal(text)) {
|
||
return {
|
||
intent: "vat_payable_forecast",
|
||
confidence: "high",
|
||
reasons: ["forecast_tax_signal_detected"]
|
||
};
|
||
}
|
||
|
||
if (hasVatPayableConfirmedSignal(text)) {
|
||
return {
|
||
intent: "vat_payable_confirmed_as_of_date",
|
||
confidence: "high",
|
||
reasons: ["vat_payable_confirmed_signal_detected"]
|
||
};
|
||
}
|
||
|
||
if (hasAny(text, RECEIVABLES_STRONG) || hasFlexibleReceivablesDebtSignal(text)) {
|
||
const receivablesDebtLifecycleSignal =
|
||
hasReceivablesDebtLifecycleSignal(text) || hasFlexibleReceivablesDebtSignal(text);
|
||
const reasons = ["receivables_signal_detected"];
|
||
if (receivablesDebtLifecycleSignal) {
|
||
reasons.push("receivables_debt_lifecycle_signal_detected");
|
||
if (hasFlexibleReceivablesDebtSignal(text)) {
|
||
reasons.push("receivables_signal_detected_flexible_phrase");
|
||
}
|
||
}
|
||
return {
|
||
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
|
||
confidence: "high",
|
||
reasons
|
||
};
|
||
}
|
||
|
||
if (hasAny(text, PAYABLES_STRONG) || hasFlexiblePayablesDebtSignal(text)) {
|
||
const reasons = ["payables_signal_detected"];
|
||
const payablesDebtLifecycleSignal =
|
||
hasPayablesDebtLifecycleSignal(text) || hasFlexiblePayablesDebtSignal(text);
|
||
if (payablesDebtLifecycleSignal) {
|
||
reasons.push("payables_debt_lifecycle_signal_detected");
|
||
if (hasFlexiblePayablesDebtSignal(text)) {
|
||
reasons.push("payables_signal_detected_flexible_phrase");
|
||
}
|
||
}
|
||
return {
|
||
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
|
||
confidence: "high",
|
||
reasons
|
||
};
|
||
}
|
||
|
||
if (hasSettlementGapSignal(text)) {
|
||
return {
|
||
intent: "list_open_contracts",
|
||
confidence: "medium",
|
||
reasons: ["settlement_gap_signal_detected"]
|
||
};
|
||
}
|
||
|
||
if (hasReconciliationMismatchSignal(text)) {
|
||
return {
|
||
intent: "list_open_contracts",
|
||
confidence: "medium",
|
||
reasons: ["reconciliation_mismatch_signal_detected"]
|
||
};
|
||
}
|
||
|
||
if (hasReceivablesLatencyRiskSignal(text)) {
|
||
return {
|
||
intent: "list_receivables_counterparties",
|
||
confidence: "medium",
|
||
reasons: ["receivables_payment_lag_signal_detected"]
|
||
};
|
||
}
|
||
|
||
if (hasCounterpartyDebtLongevitySignal(text)) {
|
||
return {
|
||
intent: "list_receivables_counterparties",
|
||
confidence: "medium",
|
||
reasons: ["receivables_debt_lifecycle_signal_detected"]
|
||
};
|
||
}
|
||
|
||
if (hasSupplierTailRiskSignal(text)) {
|
||
return {
|
||
intent: "list_payables_counterparties",
|
||
confidence: "medium",
|
||
reasons: ["supplier_tail_risk_signal_detected", "payables_debt_lifecycle_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) &&
|
||
!hasCounterpartyDebtLongevitySignal(text) &&
|
||
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(
|
||
text
|
||
)
|
||
) {
|
||
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"]
|
||
};
|
||
}
|
||
|