From f5409bbcbcfaed2f57a6961ec95d972b0c9fe003 Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 24 Apr 2026 13:43:34 +0300 Subject: [PATCH] =?UTF-8?q?Post-F:=20=D1=83=D0=BA=D1=80=D0=B5=D0=BF=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Unicode=20arbitration=20address-=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/services/addressIntentResolver.js | 277 +++++++ .../src/services/addressIntentResolver.ts | 689 +++++++++++++++++- 2 files changed, 965 insertions(+), 1 deletion(-) diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index 90ae436..000562d 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1551,6 +1551,279 @@ function repairLikelyUtf8Mojibake(text) { return raw; } } +function unicodeBridgeResolution(intent, confidence, reason) { + return { intent, confidence, reasons: [reason] }; +} +function resolveUnicodeAddressIntentBridge(text) { + const normalized = String(text ?? "").trim().toLowerCase(); + if (!normalized) { + return null; + } + const hasAccountAnchor = /(?:\b(?:60|62|76)(?:[.,]\d{2})?\b|сч(?:е|ё)т(?:а|у|ом|е|ов)?|account)/iu.test(normalized); + const hasDocumentCue = /(?:док(?:умент(?:ы|ов|а|ам|ами|ах)?|и|ам|ами|ах|ов|а)?|docs?|documents?)/iu.test(normalized); + const hasBankCue = /(?:банк|банковск|плат[её]ж|оплат|транзакц|51|bank|payment|transaction)/iu.test(normalized); + const hasContractCue = /(?:договор|дог(?:\s|$)|контракт|contract|dogovor)/iu.test(normalized); + const hasSpecificContractCue = /(?:\b\d{1,4}\/\d{1,4}\b|этому\s+же\s+договор)/iu.test(normalized); + const hasCounterpartyCue = /(?:контрагент|компани|организац|клиент|покупател|заказчик|поставщик|свк|альфа|жуковк|альтернатива|counterpart|company|supplier|customer|client|buyer)/iu.test(normalized); + const byAnchorMatch = normalized.match(/(?:^|[\s,.;:!?])(?:по|для)\s+([\p{L}\d._-]{2,})/iu); + const byAnchorToken = String(byAnchorMatch?.[1] ?? "").toLowerCase(); + const hasLooseCounterpartyByAnchor = !!byAnchorToken && + !new Set([ + "количеству", + "документам", + "докам", + "договору", + "договорам", + "счету", + "счёту", + "остатку", + "операциям", + "оплате", + "платежам", + "сальдо", + "дате", + "периоду", + "складу", + "товару", + "этому", + "этой", + "нему", + "ней" + ]).has(byAnchorToken); + const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(normalized); + const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(normalized); + if (/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|месяц[\s-]*пик|profile\s+period|top\s*year|top\s*month)/iu.test(normalized)) { + return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected"); + } + if (/(?:тип(?:ы|ов)?\s+док|док(?:умент|ов).*?(?:чаще|редк|больше\s+всего|меньше\s+всего|крутит)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test(normalized)) { + return unicodeBridgeResolution("document_type_and_account_section_profile", "high", "unicode_document_type_profile_bridge_signal_detected"); + } + if (hasAccountAnchor && + hasDocumentCue && + /(?:формирующ.*остат|раскрой\s+остат|остат.*по\s+документ|по\s+докам.*(?:60|62|76)|(?:60|62|76)(?:[.,]\d{2})?.*(?:по\s+докам|по\s+документ))/iu.test(normalized)) { + return unicodeBridgeResolution("documents_forming_balance", "high", "unicode_documents_forming_balance_bridge_signal_detected"); + } + if (/(?:договор[а-я]*.*(?:все|список).*по\s+[\p{L}\d]|(?:покажи|показать).*договор[а-я]*.*по\s+[\p{L}\d])/iu.test(normalized)) { + return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected"); + } + if (/(?:проконтрол|акты\s+без\s+приход|без\s+приходок|засорять\s+бухгалтер)/iu.test(normalized)) { + return unicodeBridgeResolution("unknown", "low", "unsupported_supplier_control_signal_detected"); + } + if (/(?:кроме\s+этого\s+документ.*(?:есть\s+еще\s+что|есть\s+ещ[её]\s+что|что[-\s]?то))/iu.test(normalized)) { + return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "generic_document_followup_with_previous_counterparty"); + } + if (/(?:плат[её]ж|оплат|отгрузк|документ|аванс|взаиморасчет|закрыт)/iu.test(normalized) && + /(?:без\s+(?:закрыт|документ|оплат)|нет\s+(?:документ|оплат)|не\s+закрыт|оплат[а-я]*\s+нет|документ[а-я]*\s+есть|требует\s+ручн)/iu.test(normalized)) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contract_gap_bridge_signal_detected"); + } + if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) && + /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) { + return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected"); + } + const inventoryBridgeIntent = (0, addressInventoryIntentSignals_1.resolveInventoryAddressIntent)(normalized); + if (inventoryBridgeIntent) { + if (inventoryBridgeIntent.intent === "inventory_aging_by_purchase_date") { + return { ...inventoryBridgeIntent, confidence: "high" }; + } + return inventoryBridgeIntent; + } + if (/(?:поставщик|vendor|supplier)/iu.test(normalized) && + /(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) { + return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected"); + } + if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) { + return unicodeBridgeResolution("bank_operations_by_counterparty", "high", "unicode_bank_ops_by_counterparty_bridge_signal_detected"); + } + if (/(?:есть\s+что[-\s]?то|что[-\s]?то)/iu.test(normalized) && + (hasLooseCounterpartyByAnchor || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized))) { + return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "generic_lookup_with_loose_anchor_fallback"); + } + if (hasDocumentCue && + (hasLooseCounterpartyByAnchor || hasCounterpartyCue || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized)) && + !hasContractCue && + !/(?:купил|куплен|закуп|товар|позици|номенклатур)/iu.test(normalized)) { + return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "unicode_documents_by_counterparty_bridge_signal_detected"); + } + if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) && + /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) { + return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected"); + } + if (/\b41(?:[.,]\d{2})?\b/iu.test(normalized) && /(?:товар|склад|остат|состоит|номенклатур)/iu.test(normalized)) { + return unicodeBridgeResolution("inventory_on_hand_as_of_date", "high", "unicode_inventory_on_hand_bridge_signal_detected"); + } + if (/(?:год.*(?:док|операц).*(?:актив|пик|жив|много|движов)|год.*движов.*(?:док|операц)|(?:док|операц).*год.*(?:актив|пик|жив|много|движов)|месяц[\s-]*пик)/iu.test(normalized)) { + return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected"); + } + if (!hasContractCue && + /(?:скольк|скока).*(?:деньг|денег|выручк|доход|оборот)|(?:деньг|денег|выручк|доход|оборот).*(?:прин[её]с|зан[её]с|плат)/iu.test(normalized)) { + return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected"); + } + if (!hasContractCue && + /(?:кто|какие|выведи|покажи|список|самые)/iu.test(normalized) && + /(?:список\s+(?:заказчик|клиент|покупател).*за\s+\d{2,4}\s*год|актив.*отвал|ровно\s+один\s+раз|один\s+раз.*пропал|стар(?:ые|ые)?\s+по\s+сотруднич|сотрудничеству\s+кто)/iu.test(normalized)) { + return unicodeBridgeResolution("counterparty_activity_lifecycle", "high", "unicode_counterparty_lifecycle_bridge_signal_detected"); + } + if (!hasContractCue && + /(?:кто|какие|выведи|покажи|список|разбей|раздели|самые)/iu.test(normalized) && + /(?:заказчик|клиент|покупател|поставщик|контрагент|зак(?!рыт))/iu.test(normalized) && + /(?:работал|работают|актив|все\s+время|вообще|регулярн|эпизодич|частот|давно\s+не\s+использ|операционн|разов|один\s+раз|пропал|отвал|сотруднич)/iu.test(normalized)) { + return unicodeBridgeResolution("counterparty_activity_lifecycle", "high", "unicode_counterparty_lifecycle_bridge_signal_detected"); + } + if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) && + !/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) && + (hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))) { + return unicodeBridgeResolution(/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile", "high", /(?:хвост|задержк|проблем)/iu.test(normalized) + ? "supplier_tail_risk_signal_detected" + : "unicode_supplier_payouts_bridge_signal_detected"); + } + if (!hasContractCue && + (/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) && + (hasMoneyCue || /поступлен|приход|входящ|сделк|бюджет|inflow/iu.test(normalized))) { + return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected"); + } + if (!hasContractCue && + /(?:кто.*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с|плат)|(?:жирн|ликвидн).*контрагент.*(?:деньг|денег))/iu.test(normalized)) { + return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected"); + } + if (/(?:общие\s+обороты|общая\s+выручк|оборот.*за\s+все\s+время|выручк.*за\s+все\s+время)/iu.test(normalized)) { + return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected"); + } + if (/(?:открыт(?:ые|ая|ый)?\s+задолж|открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(normalized) && + (hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized))) { + return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "high", "unicode_open_items_bridge_signal_detected"); + } + if (hasContractCue && + /(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test(normalized)) { + return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected"); + } + if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) { + return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected"); + } + if (/(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test(normalized)) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected"); + } + if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) && + /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) { + return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected"); + } + if (/(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test(normalized)) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected"); + } + if (/(?:открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(normalized) && + (hasContractCue || hasCounterpartyCue || hasAccountAnchor)) { + return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "high", "unicode_open_items_bridge_signal_detected"); + } + if (hasAccountAnchor && + hasDocumentCue && + /(?:формир|под\s+остат|раскр(?:ой|ыть|ывай)|остат(?:ок|ком)?\s+по\s+док|documents?\s+forming|docs?\s+forming)/iu.test(normalized)) { + return unicodeBridgeResolution("documents_forming_balance", "high", "unicode_documents_forming_balance_bridge_signal_detected"); + } + if (hasContractCue && hasSpecificContractCue && hasBankCue) { + return unicodeBridgeResolution("bank_operations_by_contract", "high", "unicode_bank_ops_by_contract_bridge_signal_detected"); + } + if (hasContractCue && hasSpecificContractCue && hasDocumentCue) { + return unicodeBridgeResolution("list_documents_by_contract", "high", "unicode_documents_by_contract_bridge_signal_detected"); + } + if (hasAccountAnchor && + !hasDocumentCue && + /(?:баланс|остат(?:ок)?|сальдо|что\s+на\s+сч(?:е|ё)те|по\s+сч(?:е|ё)ту|скольк|скока|account\s+balance|balance\s+account|as\s+of)/iu.test(normalized)) { + return unicodeBridgeResolution("account_balance_snapshot", "high", "unicode_account_balance_bridge_signal_detected"); + } + if (/(?:ндс|vat)/iu.test(normalized)) { + const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized); + if (/(?:прогноз|прикин|план)/iu.test(normalized) || + (!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))) { + return unicodeBridgeResolution("vat_payable_forecast", "high", "forecast_tax_signal_detected"); + } + if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) { + return unicodeBridgeResolution(/(?:налогов|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized) + ? "vat_liability_confirmed_for_tax_period" + : "vat_payable_confirmed_as_of_date", "high", "vat_payable_confirmed_signal_detected"); + } + } + if (/(?:незакрыт|открыт).*договор/iu.test(normalized) && + !/(?:долг|задолж|хвост|висит|расчет|расчёт)/iu.test(normalized)) { + return unicodeBridgeResolution("open_contracts_confirmed_as_of_date", "high", "unicode_open_contracts_snapshot_bridge_signal_detected"); + } + if (/(?:долг|задолж|хвост|висит|открыт(?:ые|ая|ый)?\s+задолж|open\s+items?)/iu.test(normalized) && + (hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized))) { + return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "high", "unicode_open_items_bridge_signal_detected"); + } + if (hasContractCue && + /(?:без\s+(?:закрыт|оплат|плат[её]ж|док)|не\s+закрыт|аванс|отгрузк|плат[её]ж.*без|док.*без|расхожд|mismatch)/iu.test(normalized)) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected"); + } + if (hasContractCue && + /(?:скольк.*(?:всего\s+)?договор|договор.*(?:заведен|использовал|реально\s+использ)|сколько\s+из\s+них)/iu.test(normalized)) { + return unicodeBridgeResolution("contract_usage_overview", "high", "unicode_contract_usage_overview_bridge_signal_detected"); + } + if (hasContractCue && + /(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test(normalized)) { + return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected"); + } + if (hasContractCue && + /(?:все|покажи|показать|какие|список|list|show)/iu.test(normalized) && + !hasSpecificContractCue && + !hasDocumentCue && + !hasBankCue && + (hasCounterpartyCue || hasLooseCounterpartyByAnchor)) { + return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected"); + } + if (hasContractCue && !hasSpecificContractCue && !hasDocumentCue && !hasBankCue && hasCounterpartyCue) { + return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected"); + } + if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) { + return unicodeBridgeResolution("bank_operations_by_counterparty", "high", "unicode_bank_ops_by_counterparty_bridge_signal_detected"); + } + if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue && !hasAccountAnchor) { + return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected"); + } + if (/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|profile\s+period|top\s*year|top\s*month)/iu.test(normalized)) { + return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected"); + } + if (/(?:тип(?:ы|ов)?\s+док|документ.*(?:чаще|редк|больше\s+всего|меньше\s+всего)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test(normalized)) { + return unicodeBridgeResolution("document_type_and_account_section_profile", "high", "unicode_document_type_profile_bridge_signal_detected"); + } + if (/(?:скольк|скока|число|количеств|разбей|раздели|сформируй\s+сводк)/iu.test(normalized) && + /(?:контрагент|поставщик|клиент|покупател|заказчик|рол)/iu.test(normalized) && + !/(?:активн|давно|нов(?:ые|ых)|однораз|уш[её]л|исчез|регулярн|эпизодич|частот|разов|churn|lifecycle)/iu.test(normalized)) { + return unicodeBridgeResolution("counterparty_population_and_roles", "high", "unicode_counterparty_population_bridge_signal_detected"); + } + if (/(?:скок|скока|сколько)\s+(?:клиент|покупател|заказчик)/iu.test(normalized)) { + return unicodeBridgeResolution("counterparty_population_and_roles", "high", "unicode_counterparty_population_bridge_signal_detected"); + } + if (/(?:активн(?:ые|ость)?\s+(?:клиент|покупател|поставщик|контрагент)|все\s+время|однораз|давно\s+(?:не\s+)?(?:покупал|платил|актив)|уш[её]л|исчез|нов(?:ые|ых)\s+(?:клиент|контрагент)|регулярн|разов(?:ый|ые)|stale\s+supplier|churn|lifecycle)/iu.test(normalized)) { + return unicodeBridgeResolution("counterparty_activity_lifecycle", "high", "unicode_counterparty_lifecycle_bridge_signal_detected"); + } + if (hasContractCue && /(?:давно\s+не\s+использ|не\s+использ|stale|inactive)/iu.test(normalized)) { + return unicodeBridgeResolution("contract_usage_overview", "high", "unicode_contract_usage_overview_bridge_signal_detected"); + } + if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) { + return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected"); + } + if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) && + (hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))) { + return unicodeBridgeResolution("supplier_payouts_profile", "high", "unicode_supplier_payouts_bridge_signal_detected"); + } + if ((/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) && + (hasMoneyCue || /поступлен|приход|входящ|inflow/iu.test(normalized))) { + return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected"); + } + if (/(?:к[оа]му\s+мы\s+должны|мы\s+должны\s+к[оа]му|кредитор|payables?)/iu.test(normalized)) { + return unicodeBridgeResolution("payables_confirmed_as_of_date", "high", "payables_debt_lifecycle_signal_detected"); + } + if (/(?:кто\s+нам\s+должен|нам\s+должны|дебитор|receivables?)/iu.test(normalized)) { + return unicodeBridgeResolution("receivables_confirmed_as_of_date", "high", "unicode_receivables_snapshot_bridge_signal_detected"); + } + if (/(?:покупател|клиент).*(?:не\s+плат|просроч|долго\s+долж|долг.*давн)|(?:долг|задолж).*(?:покупател|клиент)/iu.test(normalized)) { + return unicodeBridgeResolution("list_receivables_counterparties", "high", "unicode_receivables_list_bridge_signal_detected"); + } + if (/(?:что|че|чё|какие|покажи|показать|список).*(?:склад|остат|товар)|(?:склад|остат).*(?:сейчас|лежит|есть|на\s+дату|на\s+конец|what|show|list)/iu.test(normalized) && + !/(?:поставщик|продаж|реализ|цепоч|документал|давно|стар(?:ые|ый|ым|ых)|закуп)/iu.test(normalized)) { + return unicodeBridgeResolution("inventory_on_hand_as_of_date", "high", "unicode_inventory_on_hand_bridge_signal_detected"); + } + return null; +} function resolveAddressIntent(userMessage) { const text = String(userMessage ?? "").trim().toLowerCase(); const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase(); @@ -1559,6 +1832,10 @@ function resolveAddressIntent(userMessage) { .replace(/(^|[^\p{L}0-9_])\u043d\u0430\u043c\u0441(?=$|[^\p{L}0-9_])/giu, "$1\u043d\u0430\u043c") .replace(/(^|[^\p{L}0-9_])\u043a\u0430\u043a\u0438\u0435\u043a(?=$|[^\p{L}0-9_])/giu, "$1\u043a\u0430\u043a\u0438\u0435"); const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText; + const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText); + if (unicodeAddressIntent) { + return unicodeAddressIntent; + } const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) && /(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) && /(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\u0432\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text); diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 7280b31..1195da2 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -1,4 +1,4 @@ -import type { AddressIntentResolution } from "../types/addressQuery"; +import type { AddressIntent, AddressIntentResolution } from "../types/addressQuery"; import { resolveCounterpartyAddressIntent } from "./addressCounterpartyIntentSignals"; import { resolveInventoryAddressIntent } from "./addressInventoryIntentSignals"; import { @@ -1946,6 +1946,688 @@ function repairLikelyUtf8Mojibake(text: string): string { } } +function unicodeBridgeResolution( + intent: AddressIntent, + confidence: AddressIntentResolution["confidence"], + reason: string +): AddressIntentResolution { + return { intent, confidence, reasons: [reason] }; +} + +function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null { + const normalized = String(text ?? "").trim().toLowerCase(); + if (!normalized) { + return null; + } + + const hasAccountAnchor = /(?:\b(?:60|62|76)(?:[.,]\d{2})?\b|сч(?:е|ё)т(?:а|у|ом|е|ов)?|account)/iu.test( + normalized + ); + const hasDocumentCue = /(?:док(?:умент(?:ы|ов|а|ам|ами|ах)?|и|ам|ами|ах|ов|а)?|docs?|documents?)/iu.test( + normalized + ); + const hasBankCue = /(?:банк|банковск|плат[её]ж|оплат|транзакц|51|bank|payment|transaction)/iu.test(normalized); + const hasContractCue = /(?:договор|дог(?:\s|$)|контракт|contract|dogovor)/iu.test(normalized); + const hasSpecificContractCue = /(?:\b\d{1,4}\/\d{1,4}\b|этому\s+же\s+договор)/iu.test(normalized); + const hasCounterpartyCue = + /(?:контрагент|компани|организац|клиент|покупател|заказчик|поставщик|свк|альфа|жуковк|альтернатива|counterpart|company|supplier|customer|client|buyer)/iu.test( + normalized + ); + const byAnchorMatch = normalized.match(/(?:^|[\s,.;:!?])(?:по|для)\s+([\p{L}\d._-]{2,})/iu); + const byAnchorToken = String(byAnchorMatch?.[1] ?? "").toLowerCase(); + const hasLooseCounterpartyByAnchor = + !!byAnchorToken && + !new Set([ + "количеству", + "документам", + "докам", + "договору", + "договорам", + "счету", + "счёту", + "остатку", + "операциям", + "оплате", + "платежам", + "сальдо", + "дате", + "периоду", + "складу", + "товару", + "этому", + "этой", + "нему", + "ней" + ]).has(byAnchorToken); + const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test( + normalized + ); + const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test( + normalized + ); + + if ( + /(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|месяц[\s-]*пик|profile\s+period|top\s*year|top\s*month)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "period_coverage_profile", + "high", + "unicode_period_coverage_bridge_signal_detected" + ); + } + + if ( + /(?:тип(?:ы|ов)?\s+док|док(?:умент|ов).*?(?:чаще|редк|больше\s+всего|меньше\s+всего|крутит)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "document_type_and_account_section_profile", + "high", + "unicode_document_type_profile_bridge_signal_detected" + ); + } + + if ( + hasAccountAnchor && + hasDocumentCue && + /(?:формирующ.*остат|раскрой\s+остат|остат.*по\s+документ|по\s+докам.*(?:60|62|76)|(?:60|62|76)(?:[.,]\d{2})?.*(?:по\s+докам|по\s+документ))/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "documents_forming_balance", + "high", + "unicode_documents_forming_balance_bridge_signal_detected" + ); + } + + if (/(?:договор[а-я]*.*(?:все|список).*по\s+[\p{L}\d]|(?:покажи|показать).*договор[а-я]*.*по\s+[\p{L}\d])/iu.test(normalized)) { + return unicodeBridgeResolution( + "list_contracts_by_counterparty", + "high", + "unicode_contracts_by_counterparty_bridge_signal_detected" + ); + } + + if (/(?:проконтрол|акты\s+без\s+приход|без\s+приходок|засорять\s+бухгалтер)/iu.test(normalized)) { + return unicodeBridgeResolution("unknown", "low", "unsupported_supplier_control_signal_detected"); + } + + if (/(?:кроме\s+этого\s+документ.*(?:есть\s+еще\s+что|есть\s+ещ[её]\s+что|что[-\s]?то))/iu.test(normalized)) { + return unicodeBridgeResolution( + "list_documents_by_counterparty", + "medium", + "generic_document_followup_with_previous_counterparty" + ); + } + + if ( + /(?:плат[её]ж|оплат|отгрузк|документ|аванс|взаиморасчет|закрыт)/iu.test(normalized) && + /(?:без\s+(?:закрыт|документ|оплат)|нет\s+(?:документ|оплат)|не\s+закрыт|оплат[а-я]*\s+нет|документ[а-я]*\s+есть|требует\s+ручн)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "list_open_contracts", + "high", + "unicode_open_contract_gap_bridge_signal_detected" + ); + } + + if ( + /(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test( + normalized + ) && + /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "list_receivables_counterparties", + "high", + "receivables_debt_lifecycle_signal_detected" + ); + } + + const inventoryBridgeIntent = resolveInventoryAddressIntent(normalized); + if (inventoryBridgeIntent) { + if (inventoryBridgeIntent.intent === "inventory_aging_by_purchase_date") { + return { ...inventoryBridgeIntent, confidence: "high" }; + } + return inventoryBridgeIntent; + } + + if ( + /(?:поставщик|vendor|supplier)/iu.test(normalized) && + /(?:хвост|задержк|проблем|систематическ)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "list_payables_counterparties", + "high", + "supplier_tail_risk_signal_detected" + ); + } + + if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) { + return unicodeBridgeResolution( + "bank_operations_by_counterparty", + "high", + "unicode_bank_ops_by_counterparty_bridge_signal_detected" + ); + } + + if ( + /(?:есть\s+что[-\s]?то|что[-\s]?то)/iu.test(normalized) && + (hasLooseCounterpartyByAnchor || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + "list_documents_by_counterparty", + "medium", + "generic_lookup_with_loose_anchor_fallback" + ); + } + + if ( + hasDocumentCue && + (hasLooseCounterpartyByAnchor || hasCounterpartyCue || /по\s+(?:ней|нему|этой|этому)/iu.test(normalized)) && + !hasContractCue && + !/(?:купил|куплен|закуп|товар|позици|номенклатур)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "list_documents_by_counterparty", + "medium", + "unicode_documents_by_counterparty_bridge_signal_detected" + ); + } + + if ( + /(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test( + normalized + ) && + /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "list_receivables_counterparties", + "high", + "receivables_debt_lifecycle_signal_detected" + ); + } + + if (/\b41(?:[.,]\d{2})?\b/iu.test(normalized) && /(?:товар|склад|остат|состоит|номенклатур)/iu.test(normalized)) { + return unicodeBridgeResolution( + "inventory_on_hand_as_of_date", + "high", + "unicode_inventory_on_hand_bridge_signal_detected" + ); + } + + if ( + /(?:год.*(?:док|операц).*(?:актив|пик|жив|много|движов)|год.*движов.*(?:док|операц)|(?:док|операц).*год.*(?:актив|пик|жив|много|движов)|месяц[\s-]*пик)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "period_coverage_profile", + "high", + "unicode_period_coverage_bridge_signal_detected" + ); + } + + if ( + !hasContractCue && + /(?:скольк|скока).*(?:деньг|денег|выручк|доход|оборот)|(?:деньг|денег|выручк|доход|оборот).*(?:прин[её]с|зан[её]с|плат)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "customer_revenue_and_payments", + "high", + "unicode_customer_revenue_bridge_signal_detected" + ); + } + + if ( + !hasContractCue && + /(?:кто|какие|выведи|покажи|список|самые)/iu.test(normalized) && + /(?:список\s+(?:заказчик|клиент|покупател).*за\s+\d{2,4}\s*год|актив.*отвал|ровно\s+один\s+раз|один\s+раз.*пропал|стар(?:ые|ые)?\s+по\s+сотруднич|сотрудничеству\s+кто)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "counterparty_activity_lifecycle", + "high", + "unicode_counterparty_lifecycle_bridge_signal_detected" + ); + } + + if ( + !hasContractCue && + /(?:кто|какие|выведи|покажи|список|разбей|раздели|самые)/iu.test(normalized) && + /(?:заказчик|клиент|покупател|поставщик|контрагент|зак(?!рыт))/iu.test(normalized) && + /(?:работал|работают|актив|все\s+время|вообще|регулярн|эпизодич|частот|давно\s+не\s+использ|операционн|разов|один\s+раз|пропал|отвал|сотруднич)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "counterparty_activity_lifecycle", + "high", + "unicode_counterparty_lifecycle_bridge_signal_detected" + ); + } + + if ( + /(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) && + !/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) && + (hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + /(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile", + "high", + /(?:хвост|задержк|проблем)/iu.test(normalized) + ? "supplier_tail_risk_signal_detected" + : "unicode_supplier_payouts_bridge_signal_detected" + ); + } + + if ( + !hasContractCue && + (/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) && + (hasMoneyCue || /поступлен|приход|входящ|сделк|бюджет|inflow/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + "customer_revenue_and_payments", + "high", + "unicode_customer_revenue_bridge_signal_detected" + ); + } + + if ( + !hasContractCue && + /(?:кто.*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с|плат)|(?:жирн|ликвидн).*контрагент.*(?:деньг|денег))/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "customer_revenue_and_payments", + "high", + "unicode_customer_revenue_bridge_signal_detected" + ); + } + + if ( + /(?:общие\s+обороты|общая\s+выручк|оборот.*за\s+все\s+время|выручк.*за\s+все\s+время)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "customer_revenue_and_payments", + "high", + "unicode_customer_revenue_bridge_signal_detected" + ); + } + + if ( + /(?:открыт(?:ые|ая|ый)?\s+задолж|открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test( + normalized + ) && + (hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + "open_items_by_counterparty_or_contract", + "high", + "unicode_open_items_bridge_signal_detected" + ); + } + + if ( + hasContractCue && + /(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "contract_usage_and_value", + "high", + "unicode_contract_usage_value_bridge_signal_detected" + ); + } + + if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) { + return unicodeBridgeResolution( + "contract_usage_and_value", + "high", + "unicode_contract_usage_value_bridge_signal_detected" + ); + } + + if ( + /(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected"); + } + + if ( + /(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test( + normalized + ) && + /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "list_receivables_counterparties", + "high", + "receivables_debt_lifecycle_signal_detected" + ); + } + + if ( + /(?:сальдо.*(?:расход|не\s+совпад)|расход.*сальдо|акт(?:ом|ах)?\s+сверк|плат[её]ж[и]?,?\s+но\s+нет\s+док|документ(?:ы)?\s+есть,?\s+а\s+оплат\s+нет|(?:оплат|плат[её]ж|отгрузк|закрыти[ея]\s+счет)[\p{L}\s,]*\s+без\s+(?:закрыт|документ|подтвержд)|аванс.*давно\s+не\s+закрыт)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected"); + } + + if ( + /(?:открыт(?:ые|ая|ый)?\s+позици|позици.*по\s+договор|open\s+items?)/iu.test(normalized) && + (hasContractCue || hasCounterpartyCue || hasAccountAnchor) + ) { + return unicodeBridgeResolution( + "open_items_by_counterparty_or_contract", + "high", + "unicode_open_items_bridge_signal_detected" + ); + } + + if ( + hasAccountAnchor && + hasDocumentCue && + /(?:формир|под\s+остат|раскр(?:ой|ыть|ывай)|остат(?:ок|ком)?\s+по\s+док|documents?\s+forming|docs?\s+forming)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "documents_forming_balance", + "high", + "unicode_documents_forming_balance_bridge_signal_detected" + ); + } + + if (hasContractCue && hasSpecificContractCue && hasBankCue) { + return unicodeBridgeResolution( + "bank_operations_by_contract", + "high", + "unicode_bank_ops_by_contract_bridge_signal_detected" + ); + } + + if (hasContractCue && hasSpecificContractCue && hasDocumentCue) { + return unicodeBridgeResolution( + "list_documents_by_contract", + "high", + "unicode_documents_by_contract_bridge_signal_detected" + ); + } + + if ( + hasAccountAnchor && + !hasDocumentCue && + /(?:баланс|остат(?:ок)?|сальдо|что\s+на\s+сч(?:е|ё)те|по\s+сч(?:е|ё)ту|скольк|скока|account\s+balance|balance\s+account|as\s+of)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution("account_balance_snapshot", "high", "unicode_account_balance_bridge_signal_detected"); + } + + if (/(?:ндс|vat)/iu.test(normalized)) { + const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized); + if ( + /(?:прогноз|прикин|план)/iu.test(normalized) || + (!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized)) + ) { + return unicodeBridgeResolution("vat_payable_forecast", "high", "forecast_tax_signal_detected"); + } + if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) { + return unicodeBridgeResolution( + /(?:налогов|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized) + ? "vat_liability_confirmed_for_tax_period" + : "vat_payable_confirmed_as_of_date", + "high", + "vat_payable_confirmed_signal_detected" + ); + } + } + + if ( + /(?:незакрыт|открыт).*договор/iu.test(normalized) && + !/(?:долг|задолж|хвост|висит|расчет|расчёт)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "open_contracts_confirmed_as_of_date", + "high", + "unicode_open_contracts_snapshot_bridge_signal_detected" + ); + } + + if ( + /(?:долг|задолж|хвост|висит|открыт(?:ые|ая|ый)?\s+задолж|open\s+items?)/iu.test(normalized) && + (hasContractCue || hasCounterpartyCue || hasAccountAnchor || /покупател|клиент/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + "open_items_by_counterparty_or_contract", + "high", + "unicode_open_items_bridge_signal_detected" + ); + } + + if ( + hasContractCue && + /(?:без\s+(?:закрыт|оплат|плат[её]ж|док)|не\s+закрыт|аванс|отгрузк|плат[её]ж.*без|док.*без|расхожд|mismatch)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contracts_list_bridge_signal_detected"); + } + + if ( + hasContractCue && + /(?:скольк.*(?:всего\s+)?договор|договор.*(?:заведен|использовал|реально\s+использ)|сколько\s+из\s+них)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "contract_usage_overview", + "high", + "unicode_contract_usage_overview_bridge_signal_detected" + ); + } + + if ( + hasContractCue && + /(?:нескольк(?:ими|о)?\s+договор|контрагент.*нескольк.*договор|какие\s+из\s+договор.*актив)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "contract_usage_and_value", + "high", + "unicode_contract_usage_value_bridge_signal_detected" + ); + } + + if ( + hasContractCue && + /(?:все|покажи|показать|какие|список|list|show)/iu.test(normalized) && + !hasSpecificContractCue && + !hasDocumentCue && + !hasBankCue && + (hasCounterpartyCue || hasLooseCounterpartyByAnchor) + ) { + return unicodeBridgeResolution( + "list_contracts_by_counterparty", + "high", + "unicode_contracts_by_counterparty_bridge_signal_detected" + ); + } + + if (hasContractCue && !hasSpecificContractCue && !hasDocumentCue && !hasBankCue && hasCounterpartyCue) { + return unicodeBridgeResolution( + "list_contracts_by_counterparty", + "high", + "unicode_contracts_by_counterparty_bridge_signal_detected" + ); + } + + if (hasBankCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) { + return unicodeBridgeResolution( + "bank_operations_by_counterparty", + "high", + "unicode_bank_ops_by_counterparty_bridge_signal_detected" + ); + } + + if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue && !hasAccountAnchor) { + return unicodeBridgeResolution( + "list_documents_by_counterparty", + "high", + "unicode_documents_by_counterparty_bridge_signal_detected" + ); + } + + if ( + /(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|profile\s+period|top\s*year|top\s*month)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "period_coverage_profile", + "high", + "unicode_period_coverage_bridge_signal_detected" + ); + } + + if ( + /(?:тип(?:ы|ов)?\s+док|документ.*(?:чаще|редк|больше\s+всего|меньше\s+всего)|раздел(?:ы|ов)?\s+уч[её]та|сводк.*тип.*док|document\s+type|account\s+section)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "document_type_and_account_section_profile", + "high", + "unicode_document_type_profile_bridge_signal_detected" + ); + } + + if ( + /(?:скольк|скока|число|количеств|разбей|раздели|сформируй\s+сводк)/iu.test(normalized) && + /(?:контрагент|поставщик|клиент|покупател|заказчик|рол)/iu.test(normalized) && + !/(?:активн|давно|нов(?:ые|ых)|однораз|уш[её]л|исчез|регулярн|эпизодич|частот|разов|churn|lifecycle)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "counterparty_population_and_roles", + "high", + "unicode_counterparty_population_bridge_signal_detected" + ); + } + + if (/(?:скок|скока|сколько)\s+(?:клиент|покупател|заказчик)/iu.test(normalized)) { + return unicodeBridgeResolution( + "counterparty_population_and_roles", + "high", + "unicode_counterparty_population_bridge_signal_detected" + ); + } + + if ( + /(?:активн(?:ые|ость)?\s+(?:клиент|покупател|поставщик|контрагент)|все\s+время|однораз|давно\s+(?:не\s+)?(?:покупал|платил|актив)|уш[её]л|исчез|нов(?:ые|ых)\s+(?:клиент|контрагент)|регулярн|разов(?:ый|ые)|stale\s+supplier|churn|lifecycle)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "counterparty_activity_lifecycle", + "high", + "unicode_counterparty_lifecycle_bridge_signal_detected" + ); + } + + if (hasContractCue && /(?:давно\s+не\s+использ|не\s+использ|stale|inactive)/iu.test(normalized)) { + return unicodeBridgeResolution( + "contract_usage_overview", + "high", + "unicode_contract_usage_overview_bridge_signal_detected" + ); + } + + if (hasContractCue && (hasMoneyCue || hasRankingCue || /оборот|бюджет|сумм|стоим|value|amount/iu.test(normalized))) { + return unicodeBridgeResolution( + "contract_usage_and_value", + "high", + "unicode_contract_usage_value_bridge_signal_detected" + ); + } + + if ( + /(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) && + (hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + "supplier_payouts_profile", + "high", + "unicode_supplier_payouts_bridge_signal_detected" + ); + } + + if ( + (/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) && + (hasMoneyCue || /поступлен|приход|входящ|inflow/iu.test(normalized)) + ) { + return unicodeBridgeResolution( + "customer_revenue_and_payments", + "high", + "unicode_customer_revenue_bridge_signal_detected" + ); + } + + if (/(?:к[оа]му\s+мы\s+должны|мы\s+должны\s+к[оа]му|кредитор|payables?)/iu.test(normalized)) { + return unicodeBridgeResolution( + "payables_confirmed_as_of_date", + "high", + "payables_debt_lifecycle_signal_detected" + ); + } + + if (/(?:кто\s+нам\s+должен|нам\s+должны|дебитор|receivables?)/iu.test(normalized)) { + return unicodeBridgeResolution( + "receivables_confirmed_as_of_date", + "high", + "unicode_receivables_snapshot_bridge_signal_detected" + ); + } + + if ( + /(?:покупател|клиент).*(?:не\s+плат|просроч|долго\s+долж|долг.*давн)|(?:долг|задолж).*(?:покупател|клиент)/iu.test( + normalized + ) + ) { + return unicodeBridgeResolution( + "list_receivables_counterparties", + "high", + "unicode_receivables_list_bridge_signal_detected" + ); + } + + if ( + /(?:что|че|чё|какие|покажи|показать|список).*(?:склад|остат|товар)|(?:склад|остат).*(?:сейчас|лежит|есть|на\s+дату|на\s+конец|what|show|list)/iu.test( + normalized + ) && + !/(?:поставщик|продаж|реализ|цепоч|документал|давно|стар(?:ые|ый|ым|ых)|закуп)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "inventory_on_hand_as_of_date", + "high", + "unicode_inventory_on_hand_bridge_signal_detected" + ); + } + + return null; +} + export function resolveAddressIntent(userMessage: string): AddressIntentResolution { const text = String(userMessage ?? "").trim().toLowerCase(); const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase(); @@ -1956,6 +2638,11 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText; + const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText); + if (unicodeAddressIntent) { + return unicodeAddressIntent; + } + const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) && /(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(