NODEDC_1C/llm_normalizer/backend/dist/services/addressIntentResolver.js

1745 lines
84 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 hasFlexibleReceivablesDebtSignal(text) {
const normalized = String(text ?? "");
if (!normalized) {
return false;
}
const hasFlexibleWhoOwesUs = /(?:\u043a\u0442\u043e(?:\s+\S+){0,4}\s+\u043d\u0430\u043c(?:\s+\S+){0,4}\s+\u0434\u043e\u043b\u0436)/iu.test(normalized) ||
/(?:\u043d\u0430\u043c(?:\s+\S+){0,4}\s+\u043a\u0442\u043e(?:\s+\S+){0,4}\s+\u0434\u043e\u043b\u0436)/iu.test(normalized);
const hasTorchatToUsSignal = /(?:\u043d\u0430\u043c(?:\s+\S+){0,3}\s+\u0442\u043e\u0440\u0447(?:\u0430\u0442|\u0438\u0442)|\u0442\u043e\u0440\u0447(?:\u0430\u0442|\u0438\u0442)(?:\s+\S+){0,3}\s+\u043d\u0430\u043c)/iu.test(normalized);
return hasFlexibleWhoOwesUs || hasTorchatToUsSignal;
}
function hasFlexiblePayablesDebtSignal(text) {
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) {
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 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) {
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 hasForecastTaxSignal(text) {
const hasForecastLexeme = /(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
return hasForecastLexeme && hasTaxLexeme;
}
function hasVatLiabilityConfirmedTaxPeriodSignal(text) {
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
if (!hasVatLexeme) {
return false;
}
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить|мы\s+должн[аы]?|должн[аы]?\s+мы)/iu.test(text);
if (!hasPaymentCue) {
return false;
}
const hasAsOfCue = /(?:на\s+дат|по\s+состоянию|на\s+конец|as\s+of)/iu.test(text);
if (hasAsOfCue) {
return false;
}
const hasTaxAuthorityCue = /(?:в\s+налогов|в\s+бюджет|декларац|налогов(?:ый|ую)\s+период)/iu.test(text);
const hasQuarterCue = /(?:\b[1-4]\s*(?:квартал|кв\.?)\b|квартал|кв\.?)/iu.test(text);
const hasZaPeriodCue = /(?:за\s+(?:\d{4}|январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр|квартал|кв\.?|месяц|год|период))/iu.test(text);
const hasExplicitDayDate = /\b(?:\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})|(?:19|20)\d{2}[./-]\d{1,2}[./-]\d{1,2})\b/u.test(text);
const hasMonthYearNaCue = /(?:на\s+(?:январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*\s+(?:19|20)\d{2})/iu.test(text);
const hasHowMuchCue = /(?:сколько|скока|скок)/iu.test(text);
// "На март 2020" и конкретная дата без налогового контекста чаще означают as-of срез.
if (!hasTaxAuthorityCue && !hasZaPeriodCue && !hasQuarterCue && (hasMonthYearNaCue || hasExplicitDayDate)) {
return false;
}
return hasTaxAuthorityCue || hasZaPeriodCue || hasQuarterCue || (hasHowMuchCue && hasTaxAuthorityCue);
}
function hasVatPayableConfirmedSignal(text) {
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
if (!hasVatLexeme) {
return false;
}
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\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) {
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+использ|неиспольз|потом\s+перестал)/iu.test(text);
}
function hasCounterpartyDebtLongevitySignal(text) {
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) {
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) {
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 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) {
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 hasSupplierTailRiskSignal(text) {
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) {
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) {
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) {
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) {
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) {
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) {
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 hasInventoryAccount41Anchor(text) {
return /(?:сч[её]т(?:а|е|у)?|счет(?:а|е|у)?)\D{0,12}41(?:[.,]0?1)?/iu.test(text) || /41(?:[.,]0?1)?\D{0,12}(?:сч[её]т(?:а|е|у)?|счет(?:а|е|у)?)/iu.test(text);
}
function hasInventoryAsOfCue(text) {
return /(?:сейчас|текущ|на\s+дату|по\s+состоянию|срез|на\s+конец|date|as\s+of|current|now|today)/iu.test(text);
}
function hasInventoryOnHandSignal(text) {
const hasColloquialStockSnapshotCue = /(?:что|ч[её])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом)(?=$|[\s,.;:!?])/iu.test(text);
const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
const hasStockLexeme = /(?:склад(?:е|у|ом|ы|ов)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme && !hasAccount41Anchor) {
return false;
}
if (hasInventoryProvenanceSignalV2(text) ||
hasInventoryPurchaseDocumentsSignalV2(text) ||
hasInventorySaleTraceSignalV2(text) ||
hasInventoryAgingSignal(text) ||
hasInventoryPurchaseToSaleChainSignal(text)) {
return false;
}
const hasGoodsLexeme = /(?:товар(?:ы|ов|ом|а|ные)?|номенклатур|материал(?:ы|ов|а|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme = /(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|today|now|current|as\s+of)/iu.test(text);
const hasRequestCue = /(?:покажи|показать|выведи|дай|какие|что|какой|сколько|show|list|which|what)/iu.test(text);
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true;
}
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue);
}
function hasInventoryProvenanceSignal(text) {
return /(?:поставщик|закупк|происхожд|откуда|когда был куплен|активная закупк|purchase provenance|purchase date|supplier provenance|stock overlap)/iu.test(text);
}
function hasInventoryPurchaseDocumentsSignal(text) {
return /(?:по каким документам|документы закупки|purchase documents|documents of purchase|through which documents|chain of documents)/iu.test(text);
}
function hasInventorySaleTraceSignal(text) {
return /(?:продаж|покупател|buyer|sale trace|purchase[\s-]?to[\s-]?sale|purchase -> warehouse -> sale|закупка.*продаж)/iu.test(text);
}
function hasInventoryProvenanceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = /(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|поставщик|supplier|vendor)/iu.test(text);
const hasPurchaseCue = /(?:куплен(?:ы|а)?|закупк|происхождени|откуда|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+provenance|purchase\s+date)/iu.test(text);
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryPurchaseDateSignal(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasPurchaseDateCue = /(?:когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text);
return hasItemCue && hasPurchaseDateCue;
}
function hasInventoryPurchaseDocumentsSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasPurchaseDocCue = /(?:по\s+каким\s+документам\s+был\s+куплен|по\s+каким\s+документам\s+куплен|какими\s+документами\s+был\s+куплен|документ(?:ам|ы)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text);
return hasItemCue && hasPurchaseDocCue;
}
function hasInventorySaleTraceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasTraceCue = /(?:кому\s+был\s+продан|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text);
return hasItemCue && hasTraceCue;
}
function hasInventorySupplierStockOverlapSignal(text) {
const hasDirectSingleItemSupplierQuestion = /(?:от\s+какого\s+поставщика\s+куплен\s+(?:товар|номенклатур(?:а|у|ы)|позици(?:я|ю|и))|от\s+кого\s+куплен\s+(?:товар|номенклатур(?:а|у|ы)|позици(?:я|ю|и)))/iu.test(text);
if (hasDirectSingleItemSupplierQuestion) {
return false;
}
const hasSupplierCue = /(?:поставщик|supplier|vendor|от\s+поставщика|у\s+поставщика)/iu.test(text);
const hasStockCue = /(?:склад|остат(?:ок|ке|ков)|лежат|лежит|сейчас\s+еще|сейчас\s+ещ[её]|на\s+дату|по\s+состоянию\s+на\s+дату|current\s+stock|stock\s+overlap|что\s+сейчас\s+лежит)/iu.test(text);
return hasSupplierCue && hasStockCue;
}
function hasInventoryAgingSignal(text) {
const hasResidueCue = /(?:остат(?:ок|ки)|в\s+остатке|среди\s+текущих\s+остатков|на\s+складе|stock\s+residue|stock\s+balance)/iu.test(text);
const hasAgingCue = /(?:стар(?:ые|ым|ых)\s+закупк|стары(?:м|х)\s+закупк(?:ам|и|ах)|относит(?:ся|ся\s+ли)?\s+.*\s+к\s+старым\s+закупк|закупал(?:ись|ся)\s+очень\s+давно|очень\s+давно|давно\s+куплен|давно\s+приобретен|куплен\s+задолго\s+до(?:\s+даты)?|закуплен(?:ы|а)?\s+давно|приобретен\s+давно|задолго\s+до(?:\s+даты)?|возраст\s+остатк|возраст\s+закупк|aged?\s+stock|old\s+purchase|old\s+purchases|old\s+stock|bought\s+long\s+ago|purchased\s+long\s+ago|aging\s+by\s+purchase\s+date|very\s+old\s+stock|very\s+old\s+purchase|old\s+procurement|older\s+purchases|aged\s+items|old\s+goods)/iu.test(text);
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+приобретен|задолго\s+до)/iu.test(text));
}
function hasInventoryPurchaseToSaleChainSignal(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(text) || text.includes("->");
return hasItemCue && hasChainCue;
}
function hasInventorySupplierToBuyerChainSignal(text) {
const hasSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(text);
const hasBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(text);
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasChainCue = /(?:документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*buyer|supplier\s*->\s*item\s*->\s*customer|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|supplier\s+to\s+item\s+to\s+buyer|поставщик\s*->\s*товар\s*->\s*покупател|поставщик\s*->\s*товар\s*->\s*клиент|поставщик\s*->\s*товар\s*->\s*покупатель|поставщик\s+к\s+покупател|поставщик\s+к\s+клиент|поставщик\s+к\s+товару\s+и\s+покупателю)/iu.test(text) || text.includes("->");
return hasSupplierCue && hasBuyerCue && hasItemCue && hasChainCue;
}
function resolveAddressIntent(userMessage) {
const text = String(userMessage ?? "").trim().toLowerCase();
if (hasVatLiabilityConfirmedTaxPeriodSignal(text)) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "high",
reasons: ["vat_liability_confirmed_tax_period_signal_detected"]
};
}
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 (/(?:старым\s+закупк(?:ам|и|ах)|относится\s+ли\s+.*\s+к\s+старым\s+закупк(?:ам|и|ах)|очень\s+давно|давно\s+куплен|давно\s+приобретен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(text)) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "high",
reasons: ["inventory_aging_signal_detected_strong"]
};
}
if (hasInventoryAccount41Anchor(text) && hasInventoryAsOfCue(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_account_41_as_of_date_signal_detected"]
};
}
if (/(?:без\s+понятн(?:ой|ого)\s+привязк(?:и|а)\s+к\s+поставщик|без\s+привязк(?:и|а)\s+к\s+поставщик|unresolved\s+supplier\s+link)/iu.test(text)) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "medium",
reasons: ["inventory_unresolved_provenance_signal_detected"]
};
}
if (hasInventorySupplierStockOverlapSignal(text)) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "medium",
reasons: ["inventory_supplier_stock_overlap_signal_detected"]
};
}
if (/(?:supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|поставщик\s+к\s+покупателю|поставщик\s*->\s*товар\s*->\s*покупател|документально\s+подтвержденн\w+\s+цепочк)/iu.test(text) &&
/(?:поставщик|supplier|vendor)/iu.test(text) &&
/(?:покупател|buyer|customer|client)/iu.test(text) &&
/(?:товар|номенклатур|sku|item|product)/iu.test(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "high",
reasons: ["inventory_supplier_to_buyer_chain_signal_detected_strong"]
};
}
if (hasInventoryPurchaseToSaleChainSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_purchase_to_sale_chain_signal_detected"]
};
}
if (hasInventoryAgingSignal(text)) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "medium",
reasons: ["inventory_aging_signal_detected"]
};
}
if (hasInventoryProvenanceSignalV2(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_provenance_signal_detected"]
};
}
if (hasInventoryPurchaseDateSignal(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_purchase_date_signal_detected"]
};
}
if (hasInventoryPurchaseDocumentsSignalV2(text)) {
return {
intent: "inventory_purchase_documents_for_item",
confidence: "medium",
reasons: ["inventory_purchase_documents_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySupplierToBuyerChainSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_supplier_to_buyer_chain_signal_detected"]
};
}
if (hasInventoryOnHandSignal(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_on_hand_signal_detected"]
};
}
if (hasOpenContractsListSignal(text)) {
return {
intent: "open_contracts_confirmed_as_of_date",
confidence: "medium",
reasons: ["open_contract_signal_detected"]
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) &&
!hasCounterpartyDebtLongevitySignal(text) &&
!hasInventoryAgingSignal(text) &&
!hasInventoryProvenanceSignalV2(text) &&
!hasInventoryPurchaseDocumentsSignalV2(text) &&
!hasInventorySaleTraceSignalV2(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: "open_contracts_confirmed_as_of_date",
confidence: "medium",
reasons: ["open_contract_signal_detected"]
};
}
return {
intent: "unknown",
confidence: "low",
reasons: ["intent_not_supported_in_v1"]
};
}