import type { AddressModeDetection } from "../types/addressQuery"; const ADDRESS_ACTION_TOKENS = [ "show", "list", "find", "get", "lookup", "open", "balance", "debt", "owe", "покажи", "покаж", "показ", "список", "найди", "найд", "выведи", "вывед", "кто", "кому", "какой", "какая", "какое", "какую", "какие", "каких", "что по", "че по", "чё по", "остаток", "скока", "сколько", "долг", "задолж", "хвост", "незакрыт" ]; const ADDRESS_ENTITY_TOKENS = [ "counterparty", "counterparties", "company", "organization", "supplier", "vendor", "customer", "client", "partner", "contract", "contracts", "account", "accounts", "document", "documents", "balance", "payable", "payables", "receivable", "receivables", "owe", "owes", "owed", "контрагент", "контра", "компан", "организац", "поставщик", "заказчик", "клиент", "покупател", "партнер", "контракт", "банк", "выписк", "операц", "транзак", "договор", "счет", "счёт", "документ", "доки", "док", "остаток", "дебитор", "кредитор", "аванс", "оплат", "приход", "чек", "доход", "выруч", "сделк", "бюджет", "топ", "самый", "самые", "поступлен", "поступлени", "списан", "списани", "склад", "складе", "складу", "товар", "товары", "товарн", "номенклат", "материал", "долг", "должен", "должны", "должна" ]; const DEEP_REASONING_TOKENS = [ "why", "because", "root cause", "mechanism", "prove", "chain", "почему", "причин", "механизм", "докажи", "цепоч", "разрыв", "ошибк" ]; function hasManagementProfileSignal(text: string): boolean { 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+всего|крут)/iu.test(text)) { return true; } if (/(?:сводк[ауи].*тип[а-яё]*\s+док(?:умент|ов|и)?).*(?:дол[ья]|объем|объ[её]м)/iu.test(text)) { return true; } if (/(?:за\s+какие\s+год[а-яё]*\s+в\s+баз[еы]\s+есть\s+данн)/iu.test(text)) { return true; } if (/(?:какой\s+год[а-яё]*\s+сам(?:ый|ая|ое)\s+актив)/iu.test(text)) { return true; } if (/(?:какой\s+год[а-яё]*\s+сам(?:ый|ая|ое)\s+пассив|какой\s+год[а-яё]*\s+наименее\s+актив|год\s+с\s+минимальн)/iu.test(text)) { return true; } if (/(?:какой\s+месяц[а-яё]*\s+сам(?:ый|ая|ое)\s+актив)/iu.test(text)) { return true; } if (/(?:какой\s+месяц[а-яё]*\s+сам(?:ый|ая|ое)\s+пассив|какой\s+месяц[а-яё]*\s+наименее\s+актив|месяц\s+с\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+(?:реже|редк|наименее|миним))/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; } if (/(?:(?:сколько|скока|скок)\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+контр)/iu.test(text)) { return true; } if (/(?:покажи|выведи|список|какие|кто).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:за\s+вс[её]\s+время|all\s+time|(?:^|[^\d])(19|20)\d{2}(?:[^\d]|$)|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|за\s+год|в\s+году)/iu.test(text)) { return true; } if (/(?:какие|кто|покажи|выведи|список).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:работал(?:и)?|активн(?:ые|ых|а|о)?).*(?:за\s+вс[её]\s+время|(?:19|20)\d{2}|за\s+год|в\s+году)|(?:active\s+customers?|customer\s+activity)/iu.test(text)) { return true; } if ( /(?:сколько\s+(?:всего\s+)?договор(?:ов|а)?(?:\s+заведен[оы])?|договорн(?:ая|ой)\s+баз[аы]|total\s+vs\s+used).*(?:использ|used|договор|contract)?/iu.test( text ) ) { return true; } return false; } function hasLooseByAnchorMention(text: string): boolean { const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu); if (!match) { return false; } const token = String(match[1] ?? "").toLowerCase(); if (!token) { return false; } const stopWords = new Set([ "контрагенту", "контрагента", "контре", "компании", "компанию", "организации", "организацию", "поставщику", "поставщика", "клиенту", "клиента", "покупателю", "покупателя", "партнеру", "партнера", "договору", "договора", "счету", "счёту", "дате", "периоду", "период", "документам", "докам", "взаиморасчетам", "взаиморасчётам", "количество", "количеству", "количества", "активности", "пассивности", "наименее", "минимум", "запрос", "запросу", "запроса", "запросом", "запросе", "вопрос", "вопросу", "вопроса", "вопросом", "вопросе" ]); return !stopWords.has(token); } function hasAddressFollowupSignal(text: string): boolean { if (/(?:за\s+любой\s+период|за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(text)) { return true; } if (/(?:\bесть\s+что(?:-|\s)?то\b|\bесть\s+ли\b|\bчто\s+есть\b)/iu.test(text)) { return true; } return false; } function hasSelectedObjectInventoryFollowupSignal(text: string): boolean { if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) { return false; } return /(?:у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test( text ); } function hasDocsOrBankSignal(text: string): boolean { return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|транзак|transactions?|bank\s+ops|bank\s+operations?)/iu.test( text ); } function hasAccountCodeAnchor(text: string): boolean { return /(? token.trim()) .filter((token) => token.length >= 2); return tokens.some((token) => { const lowered = token.toLowerCase(); if (stopWords.has(lowered)) { return false; } if (/^\d+$/.test(lowered)) { return false; } if (/^(?:19|20)\d{2}$/.test(lowered)) { return false; } return true; }); } function hasAnyToken(text: string, tokens: string[]): boolean { return tokens.some((token) => text.includes(token)); } export function detectAddressQuestionMode(userMessage: string): AddressModeDetection { const text = String(userMessage ?? "").trim().toLowerCase(); if (!text) { return { mode: "unsupported", confidence: "low", reasons: ["empty_message"] }; } const hasAddressAction = hasAnyToken(text, ADDRESS_ACTION_TOKENS); const hasAddressEntity = hasAnyToken(text, ADDRESS_ENTITY_TOKENS); const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS); const hasManagementSignal = hasManagementProfileSignal(text); const hasLooseByAnchor = hasLooseByAnchorMention(text); const hasFollowupSignal = hasAddressFollowupSignal(text); const hasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignal(text); const hasAccountCode = hasAccountCodeAnchor(text); if (hasAddressAction && (hasAddressEntity || hasAccountCode) && !hasDeepReasoning) { return { mode: "address_query", confidence: "high", reasons: ["address_action_detected", "address_entity_detected"] }; } if (hasSelectedObjectInventoryFollowup && !hasDeepReasoning) { return { mode: "address_query", confidence: "medium", reasons: ["selected_object_inventory_followup_detected"] }; } if (hasManagementSignal && !hasDeepReasoning) { return { mode: "address_query", confidence: "medium", reasons: ["management_profile_signal_detected"] }; } if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal || hasAccountCode) && !hasDeepReasoning) { return { mode: "address_query", confidence: "medium", reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])] }; } if (hasAccountCode && !hasDeepReasoning) { return { mode: "address_query", confidence: "medium", reasons: ["account_code_detected"] }; } if (!hasDeepReasoning && hasDocsOrBankSignal(text) && (hasLooseByAnchor || hasLikelyCounterpartyToken(text))) { return { mode: "address_query", confidence: "medium", reasons: ["docs_or_bank_signal_detected", "anchor_like_token_detected"] }; } if (hasDeepReasoning) { return { mode: "deep_analysis", confidence: "high", reasons: ["deep_reasoning_signal_detected"] }; } return { mode: "unsupported", confidence: "low", reasons: ["no_address_or_deep_signal"] }; }