АДРЕСНЫЙ РЕЖИМ -Step-5 - feat(assistant): стабилизация свободного LLM-роутинга, прическа маршрутов chat/address, прототип прогноза НДС
This commit is contained in:
@@ -57,7 +57,8 @@ const OPEN_CONTRACTS_HINTS = [
|
||||
"незакрыт",
|
||||
"не закрыт",
|
||||
"открыт",
|
||||
"договор"
|
||||
"договор",
|
||||
"контракт"
|
||||
];
|
||||
|
||||
const OPEN_ITEMS_HINTS = [
|
||||
@@ -130,7 +131,10 @@ const DOCUMENTS_BY_CONTRACT_HINTS = [
|
||||
"доки по договору",
|
||||
"док по договору",
|
||||
"документы договор",
|
||||
"договор"
|
||||
"договор",
|
||||
"документы по контракту",
|
||||
"доки по контракту",
|
||||
"контракт"
|
||||
];
|
||||
const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
|
||||
"bank operations by contract",
|
||||
@@ -140,7 +144,10 @@ const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
|
||||
"bank ops by contract",
|
||||
"банковские операции по договору",
|
||||
"платежи по договору",
|
||||
"выписка по договору"
|
||||
"выписка по договору",
|
||||
"банковские операции по контракту",
|
||||
"платежи по контракту",
|
||||
"выписка по контракту"
|
||||
];
|
||||
|
||||
const BANK_OPERATION_CORE_HINTS = [
|
||||
@@ -337,14 +344,22 @@ const CONTRACT_USAGE_AND_VALUE_HINTS = [
|
||||
"договоры по обороту",
|
||||
"договоры по сумме оборота",
|
||||
"топ договоров по обороту",
|
||||
"контракты по обороту",
|
||||
"контракты по сумме оборота",
|
||||
"топ контрактов по обороту",
|
||||
"договоры с минимальным бюджетом",
|
||||
"договоры с самым маленьким бюджетом",
|
||||
"контракты с минимальным бюджетом",
|
||||
"контракты с самым маленьким бюджетом",
|
||||
"активные договоры по бюджету",
|
||||
"активные контракты по бюджету",
|
||||
"контрагенты с несколькими договорами",
|
||||
"несколько договоров у контрагента",
|
||||
"мультидоговорные контрагенты",
|
||||
"какие договоры активны",
|
||||
"какие контракты активны",
|
||||
"рабочие договоры",
|
||||
"рабочие контракты",
|
||||
"contracts by turnover",
|
||||
"contracts by budget"
|
||||
];
|
||||
@@ -355,6 +370,10 @@ const CONTRACT_LIST_BY_COUNTERPARTY_HINTS = [
|
||||
"список договоров по",
|
||||
"покажи договоры по",
|
||||
"выведи договоры по",
|
||||
"контракты по",
|
||||
"список контрактов по",
|
||||
"покажи контракты по",
|
||||
"выведи контракты по",
|
||||
"contracts by counterparty",
|
||||
"list contracts by counterparty",
|
||||
"show contracts by counterparty"
|
||||
@@ -453,8 +472,25 @@ function hasFuzzyLexeme(text: string, lexemeRoots: string[]): boolean {
|
||||
}
|
||||
|
||||
function hasCompactAccountCodeToken(text: string): boolean {
|
||||
// Match compact account tokens like 60.01 / 62, while avoiding date fragments.
|
||||
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})?(?![\d-])/u.test(text);
|
||||
// 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 {
|
||||
@@ -517,6 +553,18 @@ function hasAccountBalanceSignal(text: string): boolean {
|
||||
return hasAccountLexeme && hasAsOfStyleDate && hasFollowupBalanceVerb;
|
||||
}
|
||||
|
||||
function hasForecastTaxSignal(text: string): boolean {
|
||||
const hasForecastLexeme =
|
||||
/(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
||||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
||||
const hasVatPayableEstimatePattern =
|
||||
/(?:(?:сколько|скока|скок).{0,48}(?:ндс|vat).{0,48}(?:надо|нужно|к\s+уплате|заплатить|уплатить|платеж|платежа|платежей|платежку)|(?:ндс|vat).{0,48}(?:к\s+уплате|надо|нужно|заплатить|уплатить)|(?:сколько|скока|скок).{0,32}(?:надо|нужно).{0,32}(?:заплатить|уплатить).{0,32}(?:ндс|vat))/iu.test(
|
||||
text
|
||||
);
|
||||
return (hasForecastLexeme && hasTaxLexeme) || (hasVatLexeme && hasVatPayableEstimatePattern);
|
||||
}
|
||||
|
||||
function hasPeriodCoverageProfileSignal(text: string): boolean {
|
||||
if (hasAny(text, PERIOD_COVERAGE_PROFILE_HINTS)) {
|
||||
return true;
|
||||
@@ -670,21 +718,21 @@ function hasContractUsageOverviewSignal(text: string): boolean {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/(?:сколько\s+(?:всего\s+)?договор(?:ов|а)?(?:\s+заведен[оы])?|договорн(?:ая|ой)\s+баз[аы]).*(?:сколько|used|использ)/iu.test(
|
||||
/(?:сколько\s+(?:всего\s+)?(?:договор|контракт)(?:ов|а)?(?:\s+заведен[оы])?|(?:договорн(?:ая|ой)|контрактн(?:ая|ой))\s+баз[аы]).*(?:сколько|used|использ)/iu.test(
|
||||
text
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/(?:сколько\s+из\s+договор(?:ов|а)?\s+(?:реально\s+)?использ(?:ован[оы]|овал(?:и|ось)?))/iu.test(text)
|
||||
/(?:сколько\s+из\s+(?:договор|контракт)(?:ов|а)?\s+(?:реально\s+)?использ(?:ован[оы]|овал(?:и|ось)?))/iu.test(text)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:договор|contract)?/iu.test(text)) {
|
||||
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)) {
|
||||
if (/(?:какие\s+(?:договор|контракт)(?:ы|а)?).*(?:давно\s+не\s+использ|неиспольз|протух|мертв|мёртв|stale|unused)/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -699,14 +747,18 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||
}
|
||||
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 ||
|
||||
/(?:кто\s+нам\s+(?:больше|чаще)|кто\s+платит)/iu.test(text);
|
||||
asksWhoPays;
|
||||
const asksCounterpartySource = /(?:с\s+каких|от\s+каких|от\s+кого|from\s+which|from\s+who)/iu.test(text);
|
||||
const asksIncomingFlow = /(?:приход|поступлен|входящ|зачислен|inflow|incoming)/iu.test(text);
|
||||
const asksDealBudgetRanking =
|
||||
@@ -714,8 +766,10 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||
/(?:топ|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 asksValue =
|
||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal)/iu.test(
|
||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(
|
||||
@@ -731,6 +785,15 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||
if (asksCustomerGroup && (asksValue || asksRankOrTop)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && hasCounterpartyLexeme && asksRankOrTop && (asksValue || asksWhoPays)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksWhoPays && (asksRankOrTop || hasCounterpartyLexeme)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && (asksRevenueTotal || asksOverallTurnover)) {
|
||||
return true;
|
||||
}
|
||||
if (asksCounterpartySource && asksValue) {
|
||||
return true;
|
||||
}
|
||||
@@ -780,13 +843,13 @@ function hasContractUsageAndValueSignal(text: string): boolean {
|
||||
if (hasAny(text, CONTRACT_USAGE_AND_VALUE_HINTS)) {
|
||||
return true;
|
||||
}
|
||||
if (!/(?:договор(?:ов|а|ы)?|contract(?:s)?)/iu.test(text)) {
|
||||
if (!/(?:договор(?:ов|а|ы)?|контракт(?:ов|а|ы|у|ом|е)?|contract(?:s)?)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
if (hasContractUsageOverviewSignal(text)) {
|
||||
return false;
|
||||
}
|
||||
const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+договор|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+договор|какие\s+договор(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+договор)/iu.test(
|
||||
const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+(?:договор|контракт)|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+(?:договор|контракт)|какие\s+(?:договор|контракт)(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+(?:договор|контракт))/iu.test(
|
||||
text
|
||||
);
|
||||
const asksValue =
|
||||
@@ -796,7 +859,7 @@ function hasContractUsageAndValueSignal(text: string): boolean {
|
||||
}
|
||||
|
||||
function hasContractListByCounterpartySignal(text: string): boolean {
|
||||
const hasContractLexeme = /(?:договор(?:а|у|ом|е|ы)?|contracts?|contract)/iu.test(text);
|
||||
const hasContractLexeme = /(?:договор(?:а|у|ом|е|ы)?|контракт(?:а|у|ом|е|ы)?|contracts?|contract)/iu.test(text);
|
||||
if (!hasContractLexeme) {
|
||||
return false;
|
||||
}
|
||||
@@ -841,7 +904,7 @@ function hasDocumentsByAccountDrilldownSignal(text: string): boolean {
|
||||
}
|
||||
|
||||
function hasOpenContractsListSignal(text: string): boolean {
|
||||
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
|
||||
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;
|
||||
@@ -1186,6 +1249,14 @@ function hasAccountNumberAnchor(text: string): boolean {
|
||||
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 (hasAny(text, RECEIVABLES_STRONG)) {
|
||||
return {
|
||||
intent: "list_receivables_counterparties",
|
||||
@@ -1228,7 +1299,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
|
||||
if (
|
||||
hasAny(text, OPEN_ITEMS_HINTS) &&
|
||||
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))
|
||||
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))
|
||||
) {
|
||||
return {
|
||||
intent: "open_items_by_counterparty_or_contract",
|
||||
@@ -1398,7 +1469,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
};
|
||||
}
|
||||
|
||||
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) {
|
||||
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("контракт") || text.includes("contract"))) {
|
||||
return {
|
||||
intent: "list_open_contracts",
|
||||
confidence: "medium",
|
||||
|
||||
Reference in New Issue
Block a user