From f1333c457e0f13259198013545c8e7c7b1ccf20c Mon Sep 17 00:00:00 2001 From: dctouch Date: Thu, 16 Apr 2026 09:47:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20?= =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D0=B8=D0=BA=D1=83=20=D0=BE=D1=80=D0=BA=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B8=D0=B7=20assistantService=20=D0=B2?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/assistantLivingModePolicy.js | 333 +++++++ .../dist/services/assistantRoutePolicy.js | 582 +++++++++++ .../backend/dist/services/assistantService.js | 908 ++--------------- .../src/services/assistantLivingModePolicy.ts | 395 ++++++++ .../src/services/assistantRoutePolicy.ts | 616 ++++++++++++ .../backend/src/services/assistantService.ts | 909 ++---------------- .../tests/assistantLivingModePolicy.test.ts | 81 ++ .../tests/assistantRoutePolicy.test.ts | 147 +++ 8 files changed, 2283 insertions(+), 1688 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantLivingModePolicy.js create mode 100644 llm_normalizer/backend/dist/services/assistantRoutePolicy.js create mode 100644 llm_normalizer/backend/src/services/assistantLivingModePolicy.ts create mode 100644 llm_normalizer/backend/src/services/assistantRoutePolicy.ts create mode 100644 llm_normalizer/backend/tests/assistantLivingModePolicy.test.ts create mode 100644 llm_normalizer/backend/tests/assistantRoutePolicy.test.ts diff --git a/llm_normalizer/backend/dist/services/assistantLivingModePolicy.js b/llm_normalizer/backend/dist/services/assistantLivingModePolicy.js new file mode 100644 index 0000000..e8b0cf4 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantLivingModePolicy.js @@ -0,0 +1,333 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createAssistantLivingModePolicy = createAssistantLivingModePolicy; +function createAssistantLivingModePolicy(deps) { + const { featureAssistantLivingChatRouterV1, compactWhitespace, repairAddressMojibake, toNonEmptyString, normalizeOrganizationScopeValue, hasReferentialPointer, hasSmallTalkSignal, hasAssistantCapabilityQuestionSignal, hasOperationalAdminActionRequestSignal } = deps; + function hasStrongDataIntentSignal(text) { + const lower = String(text ?? "").toLowerCase(); + return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower); + } + function hasDataRetrievalRequestSignal(text) { + const lower = compactWhitespace(String(text ?? "").toLowerCase()); + if (!lower) { + return false; + } + const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower); + const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover|warehouse|inventory|stock|item)/iu.test(lower); + if (hasBroadInterrogative && hasBroadBusinessObject) { + return true; + } + const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a|\u043f\u0440\u043e\u0432\u0435\u0440\u044c|\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c)(?:$|[\s,.!?;:])/iu.test(lower); + const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b)/iu.test(lower); + if (hasRussianRetrievalAction && hasRussianRetrievalObject) { + return true; + } + const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower); + const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower); + if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) { + return false; + } + const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower); + if (!hasRetrievalObject) { + return false; + } + if (hasExplicitRetrievalAction) { + return true; + } + const hasMetaCapabilityShape = /(?:мож(?:ем|ешь|ете|но)|уме(?:ешь|ете)|доступ|подключ|чья|как\s+называ(?:ет|ется)|работ(?:ать|аем|аешь|аете)|в\s+тебе|у\s+тебя)/i.test(lower); + return !hasMetaCapabilityShape; + } + function hasOrganizationFactLookupSignal(text) { + const repaired = repairAddressMojibake(String(text ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + const hasFactCue = /(?:возраст|сколько\s+лет|дата\s+регистрац|когда\s+(?:зарегистр|создан|основан)|год\s+регистрац|год\s+основан|с\s+какого\s+года|when\s+was\s+(?:it\s+)?(?:registered|founded|created))/i.test(normalized); + if (!hasFactCue) { + return false; + } + return /(?:организац|компан|контор|фирм|ооо|ао|зао|ип|альтернатив|лайсвуд|райм|organization|company)/i.test(normalized); + } + function findLastAssistantLivingChatDebug(items) { + if (!Array.isArray(items)) { + return null; + } + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (!item || item.role !== "assistant") { + continue; + } + if (item.debug && typeof item.debug === "object") { + return item.debug; + } + } + return null; + } + function hasMetaAnswerFollowupSignal(userMessage) { + const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + const samples = [rawText, repairedText] + .filter((item) => item.length > 0) + .map((item) => item.replace(/ё/g, "е")); + if (samples.length === 0) { + return false; + } + const hasReflectionCue = samples.some((sample) => sample.includes("дума") || + sample.includes("скаж") || + sample.includes("мнение") || + sample.includes("как тебе") || + sample.includes("норм") || + sample.includes("стран") || + sample.includes("логич") || + sample.includes("смуща") || + sample.includes("выгляд")); + const hasTopicPointerCue = samples.some((sample) => sample.includes("на эту тему") || + sample.includes("по этому поводу") || + sample.includes("об этом") || + (sample.includes("это") && hasReferentialPointer(sample))); + const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample)); + if (!((hasReflectionCue || hasEvaluationCue) && + (hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) { + return false; + } + return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || + shouldHandleAsAssistantCapabilityMetaQuery(sample) || + hasDataRetrievalRequestSignal(sample) || + hasStrongDataIntentSignal(sample)); + } + function hasConversationMemoryRecallFollowupSignal(userMessage) { + const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + const samples = [rawText, repairedText] + .filter((item) => item.length > 0) + .map((item) => item.replace(/ё/g, "е")); + if (samples.length === 0) { + return false; + } + const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample)); + const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample)); + if (!hasMemoryCue || !hasDiscussionCue) { + return false; + } + return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || + shouldHandleAsAssistantCapabilityMetaQuery(sample) || + hasDataRetrievalRequestSignal(sample) || + hasStrongDataIntentSignal(sample)); + } + function hasHistoricalCapabilityFollowupSignal(text) { + const repaired = repairAddressMojibake(String(text ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + const hasHistoryCue = /(?:историческ|история|архив|прошл(?:ый|ые|ую|ых)?|раньше|ретро|старые\s+данные)/iu.test(normalized); + if (!hasHistoryCue) { + return false; + } + return /(?:мож(?:ешь|ете|но)|уме(?:ешь|ете)|показ|вывед|дай|раскрой)/iu.test(normalized); + } + function hasOrganizationFactFollowupSignal(userMessage, items) { + const repaired = repairAddressMojibake(String(userMessage ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + if (hasOrganizationFactLookupSignal(normalized)) { + return false; + } + const hasFollowupCue = /(?:^|\s)(?:давай|го|погнали|ок(?:ей)?|хорошо|принято|подтверждаю|запрашивай|запроси|проверь|продолжай|ну\s+давай|да\s+давай)(?=$|[\s,.!?;:])/iu.test(normalized); + if (!hasFollowupCue) { + return false; + } + const lastDebug = findLastAssistantLivingChatDebug(items); + const lastSource = toNonEmptyString(lastDebug?.living_chat_response_source); + const lastGuardReason = toNonEmptyString(lastDebug?.living_chat_grounding_guard_reason); + const inOrganizationFactBoundary = lastSource === "deterministic_organization_fact_boundary" || + lastSource === "deterministic_organization_fact_boundary_followup" || + lastGuardReason === "organization_fact_without_live_source_blocked"; + return inOrganizationFactBoundary; + } + function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) { + const selected = normalizeOrganizationScopeValue(selectedOrganization); + if (!selected) { + return false; + } + const repaired = repairAddressMojibake(String(userMessage ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + if (hasOrganizationFactLookupSignal(normalized) || hasDataRetrievalRequestSignal(normalized) || hasStrongDataIntentSignal(normalized)) { + return false; + } + const hasAnalyticalCue = /(?:какой|какая|какие|когда|сколько|кто|почему|зачем|возраст|дата|регистрац|ндс|налог|контракт|договор|документ|операц|оборот|сумм|остат|сальдо|founded|registered|created)/i.test(normalized); + if (hasAnalyticalCue) { + return false; + } + const hasSelectionCue = /(?:давай|го|погнали|ок(?:ей)?|хорошо|отлично|берем|выберем|выбираем|переключ(?:им|аем|ай)|фиксир|работаем|обсудим|тогда)\b/i.test(normalized); + if (hasSelectionCue) { + return true; + } + const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС…|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|РїРёР·РґР°|РЅРёС…СѓСЏ|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) || + normalized.includes("\u0430\u0445\u0443") || + normalized.includes("\u043e\u0445\u0443") || + normalized.includes("\u043f\u0438\u0437\u0434") || + normalized.includes("\u0431\u043b\u044f"); + if (hasAffectiveReactionCue) { + return false; + } + return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? "")); + } + function hasAssistantDataScopeMetaQuestionSignal(text) { + const repaired = repairAddressMojibake(String(text ?? "")); + const lower = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с"); + if (!normalized) { + return false; + } + const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized); + if (hasDirectSlangScopeLead) { + return true; + } + const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized); + if (hasSlangScopeQuestion) { + return true; + } + const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized); + const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized); + const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized); + const hasNamingCue = /(?:как\s+называ(?:ет|ется)|что\s+за\s+(?:контор|компан|организац|фирм))/i.test(normalized); + const hasWorkabilityCue = /(?:мож(?:ем|ешь|ете|но)\s+работ|работ(?:ать|аем|аешь|аете))/i.test(normalized); + const hasScopeObject = hasBaseOrTenantObject || hasCompanyObject || hasConnectionCue; + if (!hasScopeObject) { + return false; + } + const hasMetaPerspective = /(?:ты|тебе|твой|у\s+тебя|в\s+тебе|мы|нам|наш(?:а|е|и|у|ей)?|сейчас|щас)/i.test(normalized); + const hasScopedInterrogativePair = /(?:^|\s)(?:по\s+какой|с\s+какой|какая|какой|какие)\s+(?:баз|компан|компин|компини|компани|организац|контор|фирм|тенант|контур)/i.test(normalized); + const hasScopeQuestion = /(?:чья|чье|чьи|доступн|подключен|подруб|воткнут|какая\s+баз|какой\s+баз)/i.test(normalized) || + hasNamingCue || + hasWorkabilityCue || + hasScopedInterrogativePair; + const hasInterrogativeScopeLead = /(?:^|\s)(?:по\s+какой|с\s+какой|чья|чье|чьи|which|who|what)/i.test(normalized); + const isQuestionLike = /[?]/.test(String(text ?? "")) || hasInterrogativeScopeLead || hasScopedInterrogativePair; + const hasExplicitScopeContext = hasBaseOrTenantObject || hasConnectionCue || hasWorkabilityCue || hasNamingCue; + const hasRetrievalSignal = hasDataRetrievalRequestSignal(normalized); + const hasContractAnalyticsCue = /(?:договор|контракт|contract).*(?:топ|сам(?:ый|ая|ое|ые)|крупн|жирн|оборот|бюджет|сумм|стоим|value|turnover|all\s+time|всю\s+истори|за\s+вс[её]\s+время)/iu.test(normalized); + if (hasContractAnalyticsCue) { + return false; + } + if (hasRetrievalSignal && !hasExplicitScopeContext) { + return false; + } + const hasEligibleScopeObject = hasBaseOrTenantObject || (hasCompanyObject && (hasConnectionCue || hasWorkabilityCue || hasNamingCue || hasMetaPerspective)); + return hasEligibleScopeObject && hasScopeQuestion && (hasMetaPerspective || isQuestionLike || hasExplicitScopeContext); + } + function shouldHandleAsAssistantCapabilityMetaQuery(text) { + const raw = String(text ?? ""); + const repaired = repairAddressMojibake(raw); + const hasScopeMetaSignal = hasAssistantDataScopeMetaQuestionSignal(raw) || hasAssistantDataScopeMetaQuestionSignal(repaired); + if (hasScopeMetaSignal) { + return true; + } + const hasCapabilitySignal = hasAssistantCapabilityQuestionSignal(raw) || + hasAssistantCapabilityQuestionSignal(repaired) || + hasOperationalAdminActionRequestSignal(raw) || + hasOperationalAdminActionRequestSignal(repaired); + const hasRetrievalSignal = hasDataRetrievalRequestSignal(raw) || hasDataRetrievalRequestSignal(repaired); + return hasCapabilitySignal && !hasRetrievalSignal; + } + function hasLivingChatSignal(text) { + const lower = compactWhitespace(String(text ?? "").toLowerCase()); + if (!lower) { + return false; + } + if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) { + return true; + } + if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) { + return true; + } + if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) { + return true; + } + return hasSmallTalkSignal(lower); + } + function resolveLivingAssistantModeDecision(input) { + const userMessage = String(input?.userMessage ?? ""); + if (input?.addressLaneTriggered) { + return { + mode: "address_data", + reason: "address_lane_triggered" + }; + } + if (!featureAssistantLivingChatRouterV1) { + return { + mode: "deep_analysis", + reason: "living_chat_router_disabled" + }; + } + if (Boolean(input?.useMock)) { + return { + mode: "deep_analysis", + reason: "mock_mode_keeps_deep_pipeline" + }; + } + if (hasAssistantDataScopeMetaQuestionSignal(userMessage)) { + return { + mode: "chat", + reason: "assistant_data_scope_query_detected" + }; + } + if (shouldHandleAsAssistantCapabilityMetaQuery(userMessage)) { + return { + mode: "chat", + reason: "assistant_capability_query_detected" + }; + } + if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) { + return { + mode: "chat", + reason: "organization_fact_lookup_signal_detected" + }; + } + if (hasStrongDataIntentSignal(userMessage)) { + return { + mode: "deep_analysis", + reason: "strong_data_signal_detected" + }; + } + if (hasLivingChatSignal(userMessage)) { + return { + mode: "chat", + reason: "living_chat_signal_detected" + }; + } + const predecomposeMode = toNonEmptyString(input?.predecomposeMode); + const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence); + if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) { + return { + mode: "deep_analysis", + reason: "predecompose_unsupported_mode_fallback_to_deep" + }; + } + return { + mode: "deep_analysis", + reason: "default_deep_pipeline" + }; + } + return { + hasStrongDataIntentSignal, + hasDataRetrievalRequestSignal, + hasOrganizationFactLookupSignal, + hasMetaAnswerFollowupSignal, + hasConversationMemoryRecallFollowupSignal, + hasHistoricalCapabilityFollowupSignal, + hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasLivingChatSignal, + resolveLivingAssistantModeDecision + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js new file mode 100644 index 0000000..e26e21d --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js @@ -0,0 +1,582 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createAssistantRoutePolicy = createAssistantRoutePolicy; +// @ts-nocheck +const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ + "period_coverage_profile", + "document_type_and_account_section_profile", + "counterparty_population_and_roles", + "counterparty_activity_lifecycle", + "customer_revenue_and_payments", + "supplier_payouts_profile", + "open_contracts_confirmed_as_of_date", + "list_open_contracts", + "open_items_by_counterparty_or_contract", + "list_payables_counterparties", + "list_receivables_counterparties", + "inventory_on_hand_as_of_date", + "payables_confirmed_as_of_date", + "receivables_confirmed_as_of_date", + "list_documents_by_contract", + "bank_operations_by_contract", + "list_documents_by_counterparty", + "bank_operations_by_counterparty", + "list_contracts_by_counterparty", + "inventory_purchase_provenance_for_item", + "inventory_purchase_documents_for_item", + "inventory_supplier_stock_overlap_as_of_date", + "inventory_sale_trace_for_item", + "inventory_profitability_for_item", + "inventory_purchase_to_sale_chain", + "inventory_aging_by_purchase_date", + "contract_usage_overview", + "contract_usage_and_value", + "vat_payable_forecast", + "vat_liability_confirmed_for_tax_period", + "vat_payable_confirmed_as_of_date" +]); +const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([ + "inventory_purchase_provenance_for_item", + "inventory_purchase_documents_for_item", + "inventory_sale_trace_for_item", + "inventory_profitability_for_item", + "inventory_purchase_to_sale_chain" +]); +function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) { + return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent)); +} +function createAssistantRoutePolicy(deps) { + const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, hasAssistantDataScopeMetaQuestionSignal, shouldHandleAsAssistantCapabilityMetaQuery, hasDataRetrievalRequestSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, hasHistoricalCapabilityFollowupSignal, isGroundedInventoryContextDebug, hasConversationMemoryRecallFollowupSignal, findLastAddressAssistantItem, hasMetaAnswerFollowupSignal, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision } = deps; + function resolveAssistantOrchestrationDecision(input) { + const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? ""); + const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage); + const repairedRawUserMessage = repairAddressMojibake(rawUserMessage); + const repairedEffectiveAddressUserMessage = repairAddressMojibake(effectiveAddressUserMessage); + const followupContext = input?.followupContext ?? null; + const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null; + const useMock = Boolean(input?.useMock); + const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null; + const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" + ? input.sessionOrganizationScope + : null; + const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems); + const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); + const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) + ? mergeKnownOrganizations([ + ...lastOrganizationClarificationDebug.organization_candidates, + ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) + ? sessionOrganizationScope.knownOrganizations + : [])) + ]) + : []; + const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization); + const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? + resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? + resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? + resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ?? + (organizationClarificationSelectionFromScope && + organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope) + ? organizationClarificationSelectionFromScope + : null); + const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) || + hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) || + hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) || + hasAssistantDataScopeMetaQuestionSignal(repairedEffectiveAddressUserMessage); + const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawUserMessage) || + shouldHandleAsAssistantCapabilityMetaQuery(repairedRawUserMessage) || + shouldHandleAsAssistantCapabilityMetaQuery(effectiveAddressUserMessage) || + shouldHandleAsAssistantCapabilityMetaQuery(repairedEffectiveAddressUserMessage); + const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawUserMessage) || + hasDataRetrievalRequestSignal(repairedRawUserMessage) || + hasDataRetrievalRequestSignal(effectiveAddressUserMessage) || + hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage); + const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) || + hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) || + hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) || + hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage); + const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) || + hasStandaloneAddressTopicSignal(repairedRawUserMessage) || + hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) || + hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage); + const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) || + hasOpenContractsAddressSignal(repairedRawUserMessage) || + hasOpenContractsAddressSignal(effectiveAddressUserMessage) || + hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage); + const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage; + const modeDetection = detectAddressQuestionMode(modeSample); + const modeDetectionRaw = detectAddressQuestionMode(repairedRawUserMessage || rawUserMessage); + const resolvedModeDetection = modeDetection.mode === "address_query" ? modeDetection : modeDetectionRaw; + const intentResolution = resolveAddressIntent(modeSample); + const intentResolutionRaw = resolveAddressIntent(repairedRawUserMessage || rawUserMessage); + const resolvedIntentResolution = intentResolution.intent !== "unknown" ? intentResolution : intentResolutionRaw; + const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); + const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); + const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason && + /(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason)); + const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && + typeof llmPreDecomposeMeta.semanticExtractionContract === "object" + ? llmPreDecomposeMeta.semanticExtractionContract + : null; + const semanticContractValid = semanticExtractionContract?.valid !== false; + const semanticApplyCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; + const semanticReasonCodes = Array.isArray(semanticExtractionContract?.reason_codes) + ? semanticExtractionContract.reason_codes + : []; + const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) || + hasStrictDeepInvestigationCue(repairedRawUserMessage) || + hasStrictDeepInvestigationCue(effectiveAddressUserMessage) || + hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage); + const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(resolvedIntentResolution.intent) || + shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent); + const keepAddressLaneByIntent = semanticApplyCanonicalRecommended && + Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || + (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || + openContractsAddressSignal) && + (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed); + const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) || + hasStrongDataIntentSignal(repairedRawUserMessage) || + hasStrongDataIntentSignal(effectiveAddressUserMessage) || + hasStrongDataIntentSignal(repairedEffectiveAddressUserMessage) || + hasAccountingSignal(rawUserMessage) || + hasAccountingSignal(repairedRawUserMessage) || + hasAccountingSignal(effectiveAddressUserMessage) || + hasAccountingSignal(repairedEffectiveAddressUserMessage) || + hasDataRetrievalRequestSignal(rawUserMessage) || + hasDataRetrievalRequestSignal(repairedRawUserMessage); + const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); + const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown"); + const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" && + (!llmContractIntent || llmContractIntent === "unknown")); + const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) || + hasDangerOrCoercionSignal(repairedRawUserMessage) || + hasDangerOrCoercionSignal(effectiveAddressUserMessage) || + hasDangerOrCoercionSignal(repairedEffectiveAddressUserMessage); + const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) || + hasAddressFollowupContextSignal(repairedRawUserMessage) || + hasAddressFollowupContextSignal(effectiveAddressUserMessage) || + hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(rawUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || + hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage); + const protectedInventoryShortFollowup = Boolean(followupContext && + isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) && + (hasShortInventoryObjectFollowupSignal(rawUserMessage) || + hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || + hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || + hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); + const organizationClarificationContinuationDetected = Boolean(followupContext && + lastOrganizationClarificationDebug && + organizationClarificationSelection && + !dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal); + const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; + const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal && + !effectiveAddressFollowupSignal && + resolvedModeDetection.mode === "unsupported" && + resolvedIntentResolution.intent === "unknown"); + const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && + deterministicNonDomainGuard && + (llmFirstUnsupportedCandidate || llmContractMode === null) && + !protectedInventoryShortFollowup && + !organizationClarificationContinuationDetected); + const contextualHistoricalCapabilityFollowupDetected = Boolean(capabilityMetaQuery && + !dataScopeMetaQuery && + !dataRetrievalSignal && + (hasHistoricalCapabilityFollowupSignal(rawUserMessage) || + hasHistoricalCapabilityFollowupSignal(repairedRawUserMessage) || + hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) || + hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) && + isGroundedInventoryContextDebug(lastGroundedAddressDebug)); + const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal && + !strongDataSignal && + !aggregateBusinessAnalyticsSignal && + (hasConversationMemoryRecallFollowupSignal(rawUserMessage) || + hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) || + hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) || + hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) && + (lastGroundedAddressDebug || + findLastAddressAssistantItem(sessionItems)?.debug)); + const hardMetaMode = dataScopeMetaQuery + ? "data_scope" + : capabilityMetaQuery && !dataRetrievalSignal + ? "capability" + : null; + if (hardMetaMode === "data_scope") { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "assistant_data_scope_query_detected", + livingMode: "chat", + livingReason: "assistant_data_scope_query_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "data_scope", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "assistant_data_scope_query_detected", + living_mode: "chat", + living_reason: "assistant_data_scope_query_detected" + } + } + }; + } + if (hardMetaMode === "capability") { + if (contextualHistoricalCapabilityFollowupDetected) { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "inventory_history_capability_followup_detected", + livingMode: "chat", + livingReason: "inventory_history_capability_followup_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "capability", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "inventory_history_capability_followup_detected", + living_mode: "chat", + living_reason: "inventory_history_capability_followup_detected" + } + } + }; + } + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "assistant_capability_query_detected", + livingMode: "chat", + livingReason: "assistant_capability_query_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "capability", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "assistant_capability_query_detected", + living_mode: "chat", + living_reason: "assistant_capability_query_detected" + } + } + }; + } + if (nonDomainQueryIndexed) { + if (contextualMemoryRecapFollowupDetected) { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "memory_recap_followup_detected", + livingMode: "chat", + livingReason: "memory_recap_followup_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "non_domain", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "memory_recap_followup_detected", + living_mode: "chat", + living_reason: "memory_recap_followup_detected" + } + } + }; + } + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "non_domain_query_indexed", + livingMode: "chat", + livingReason: "non_domain_query_indexed", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "non_domain", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "non_domain_query_indexed", + living_mode: "chat", + living_reason: "non_domain_query_indexed" + } + } + }; + } + const metaAnswerFollowupSignal = hasMetaAnswerFollowupSignal(rawUserMessage) || + hasMetaAnswerFollowupSignal(repairedRawUserMessage) || + hasMetaAnswerFollowupSignal(effectiveAddressUserMessage) || + hasMetaAnswerFollowupSignal(repairedEffectiveAddressUserMessage); + const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); + const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected && + llmPreDecomposeMeta?.applied && + llmContractMode === "address_query") || + hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) || + hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) || + hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) || + hasSameDateAccountFollowupSignalForPredecompose(repairedEffectiveAddressUserMessage) || + hasLooseAllTimeAddressLookupSignal(rawUserMessage) || + hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) || + hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) || + hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) || + hasAddressFollowupContextSignal(rawUserMessage) || + hasAddressFollowupContextSignal(effectiveAddressUserMessage) || + hasAddressFollowupContextSignal(repairedRawUserMessage) || + hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(rawUserMessage) || + hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage)); + const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) && + Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || + (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || + openContractsAddressSignal); + const semanticGuardHints = semanticExtractionContract?.guard_hints && + typeof semanticExtractionContract.guard_hints === "object" + ? semanticExtractionContract.guard_hints + : null; + const semanticExtraction = semanticExtractionContract?.extraction && + typeof semanticExtractionContract.extraction === "object" + ? semanticExtractionContract.extraction + : null; + const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true; + const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" || + semanticExtraction?.aggregation_profile === "management_profile"; + const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true); + const followupSemanticOverrideToDeepAllowed = Boolean(followupContext && + !supportedAddressIntentDetected && + (rootContextOnlyFollowup || + llmContractMode === "unsupported" || + semanticAggregateShapeDetected || + semanticDeepInvestigationHintDetected || + !semanticApplyCanonicalRecommended)); + const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") || + llmContractMode === "unsupported" || + (rootContextOnlyFollowup && + resolvedIntentResolution.intent === "unknown" && + (!llmContractIntent || llmContractIntent === "unknown")); + const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + unsupportedIntentOrMode && + strongDataSignal && + (rootContextOnlyFollowup || + llmContractMode === "deep_analysis" || + !dataRetrievalSignal || + strictDeepInvestigationCueDetected || + semanticDeepInvestigationHintDetected || + aggregateBusinessAnalyticsSignal) && + !preserveAddressLaneSignal && + !keepAddressLaneByIntent && + !supportedAddressIntentDetected && + (!followupContext || followupSemanticOverrideToDeepAllowed)); + const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) || + hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) || + hasDeepAnalysisPreferenceSignal(effectiveAddressUserMessage) || + hasDeepAnalysisPreferenceSignal(repairedEffectiveAddressUserMessage) || + hasDirectDeepAnalysisSignal(rawUserMessage) || + hasDirectDeepAnalysisSignal(repairedRawUserMessage) || + hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) || + hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage)); + const vatExplainFollowupSignal = Boolean(followupContext && + toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && + /(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); + const vatEvaluativeFollowupSignal = Boolean(followupContext && + toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && + /(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); + const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + (deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) && + !keepAddressLaneByIntent && + !supportedAddressIntentDetected && + !vatExplainFollowupSignal && + (!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed)); + const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + aggregateBusinessAnalyticsSignal && + !keepAddressLaneByIntent && + !supportedAddressIntentDetected && + (!followupContext || + llmContractMode === "unsupported" || + semanticAggregateShapeDetected || + !semanticApplyCanonicalRecommended || + standaloneAddressTopicSignal)); + const deepSessionContinuationFallbackToDeep = Boolean(!followupContext && + baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + hasDeepSessionContinuationSignal({ + rawUserMessage, + repairedRawUserMessage, + effectiveAddressUserMessage, + repairedEffectiveAddressUserMessage, + sessionItems + })); + const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); + const metaFollowupOverGroundedAnswer = Boolean(followupContext && + hasPriorAddressAnswerContext && + (metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) && + !dataScopeMetaQuery && + !capabilityMetaQuery && + !aggregateBusinessAnalyticsSignal && + !dataRetrievalSignal && + !strongDataSignal && + resolvedModeDetection.mode !== "address_query" && + resolvedIntentResolution.intent === "unknown" && + (!llmContractIntent || llmContractIntent === "unknown") && + llmContractMode !== "address_query"); + let runAddressLane = Boolean(baseToolGate?.runAddressLane); + let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); + let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); + if (unsupportedAddressIntentFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "address_signal_unsupported_intent_fallback_to_deep"; + } + if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "deep_analysis_signal_fallback_to_deep"; + } + if (aggregateAnalyticsFallbackToDeep && + !unsupportedAddressIntentFallbackToDeep && + !deepAnalysisSignalFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "aggregate_analytics_signal_fallback_to_deep"; + } + if (deepSessionContinuationFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "deep_session_continuation_fallback_to_deep"; + } + if (metaFollowupOverGroundedAnswer) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "meta_followup_over_grounded_answer"; + } + let livingDecision = resolveLivingAssistantModeDecision({ + userMessage: rawUserMessage, + addressLaneTriggered: runAddressLane, + useMock, + predecomposeMode: llmPreDecomposeMeta?.predecomposeContract?.mode ?? null, + predecomposeModeConfidence: llmPreDecomposeMeta?.predecomposeContract?.mode_confidence ?? null + }); + if (unsupportedAddressIntentFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "unsupported_address_intent_fallback_to_deep" + }; + } + if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "deep_analysis_signal_fallback_to_deep" + }; + } + if (aggregateAnalyticsFallbackToDeep && + !unsupportedAddressIntentFallbackToDeep && + !deepAnalysisSignalFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "aggregate_analytics_signal_fallback_to_deep" + }; + } + if (deepSessionContinuationFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "deep_session_continuation_fallback_to_deep" + }; + } + if (metaFollowupOverGroundedAnswer) { + livingDecision = { + mode: "chat", + reason: "meta_followup_over_grounded_answer" + }; + } + return { + runAddressLane, + toolGateDecision, + toolGateReason, + livingMode: livingDecision.mode, + livingReason: livingDecision.reason, + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: null, + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + semantic_contract_valid: semanticContractValid, + semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended, + semantic_reason_codes: semanticReasonCodes, + semantic_route_arbitration: { + supported_address_intent_detected: supportedAddressIntentDetected, + strict_deep_investigation_bypass_allowed: strictDeepInvestigationBypassAllowed, + semantic_deep_investigation_hint_detected: semanticDeepInvestigationHintDetected, + semantic_aggregate_shape_detected: semanticAggregateShapeDetected, + followup_semantic_override_to_deep_allowed: followupSemanticOverrideToDeepAllowed + }, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep, + deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep, + aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep, + deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep, + final_decision: { + run_address_lane: runAddressLane, + tool_gate_decision: toolGateDecision, + tool_gate_reason: toolGateReason, + living_mode: livingDecision.mode, + living_reason: livingDecision.reason + } + } + }; + } + return { + resolveAssistantOrchestrationDecision + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index b80582e..e23fab3 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -67,6 +67,8 @@ const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistan const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding")); const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter")); const assistantBoundaryPolicy_1 = __importStar(require("./assistantBoundaryPolicy")); +const assistantLivingModePolicy_1 = __importStar(require("./assistantLivingModePolicy")); +const assistantRoutePolicy_1 = __importStar(require("./assistantRoutePolicy")); const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter")); const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter")); const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter")); @@ -4252,623 +4254,17 @@ function hasOpenContractsAddressSignal(text) { const hasTemporalCue = hasPeriodLiteral(normalized) || /\b\d{4}[-/.]\d{2}[-/.]\d{2}\b/.test(normalized); return hasRequestCue || hasTemporalCue; } -const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ - "period_coverage_profile", - "document_type_and_account_section_profile", - "counterparty_population_and_roles", - "counterparty_activity_lifecycle", - "customer_revenue_and_payments", - "supplier_payouts_profile", - "open_contracts_confirmed_as_of_date", - "list_open_contracts", - "open_items_by_counterparty_or_contract", - "list_payables_counterparties", - "list_receivables_counterparties", - "inventory_on_hand_as_of_date", - "payables_confirmed_as_of_date", - "receivables_confirmed_as_of_date", - "list_documents_by_contract", - "bank_operations_by_contract", - "list_documents_by_counterparty", - "bank_operations_by_counterparty", - "list_contracts_by_counterparty", - "inventory_purchase_provenance_for_item", - "inventory_purchase_documents_for_item", - "inventory_supplier_stock_overlap_as_of_date", - "inventory_sale_trace_for_item", - "inventory_profitability_for_item", - "inventory_purchase_to_sale_chain", - "inventory_aging_by_purchase_date", - "contract_usage_overview", - "contract_usage_and_value", - "vat_payable_forecast", - "vat_liability_confirmed_for_tax_period", - "vat_payable_confirmed_as_of_date" -]); -const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([ - "inventory_purchase_provenance_for_item", - "inventory_purchase_documents_for_item", - "inventory_sale_trace_for_item", - "inventory_profitability_for_item", - "inventory_purchase_to_sale_chain" -]); -function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) { - return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent)); -} function resolveAssistantOrchestrationDecision(input) { - const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? ""); - const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage); - const repairedRawUserMessage = repairAddressMojibake(rawUserMessage); - const repairedEffectiveAddressUserMessage = repairAddressMojibake(effectiveAddressUserMessage); - const followupContext = input?.followupContext ?? null; - const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null; - const useMock = Boolean(input?.useMock); - const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null; - const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" - ? input.sessionOrganizationScope - : null; - const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems); - const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); - const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) - ? mergeKnownOrganizations([ - ...lastOrganizationClarificationDebug.organization_candidates, - ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) - ? sessionOrganizationScope.knownOrganizations - : [])) - ]) - : []; - const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization); - const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? - resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? - resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? - resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ?? - (organizationClarificationSelectionFromScope && - organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope) - ? organizationClarificationSelectionFromScope - : null); - const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) || - hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) || - hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) || - hasAssistantDataScopeMetaQuestionSignal(repairedEffectiveAddressUserMessage); - const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawUserMessage) || - shouldHandleAsAssistantCapabilityMetaQuery(repairedRawUserMessage) || - shouldHandleAsAssistantCapabilityMetaQuery(effectiveAddressUserMessage) || - shouldHandleAsAssistantCapabilityMetaQuery(repairedEffectiveAddressUserMessage); - const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawUserMessage) || - hasDataRetrievalRequestSignal(repairedRawUserMessage) || - hasDataRetrievalRequestSignal(effectiveAddressUserMessage) || - hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage); - const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) || - hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) || - hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) || - hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage); - const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) || - hasStandaloneAddressTopicSignal(repairedRawUserMessage) || - hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) || - hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage); - const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) || - hasOpenContractsAddressSignal(repairedRawUserMessage) || - hasOpenContractsAddressSignal(effectiveAddressUserMessage) || - hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage); - const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage; - const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample); - const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedRawUserMessage || rawUserMessage); - const resolvedModeDetection = modeDetection.mode === "address_query" ? modeDetection : modeDetectionRaw; - const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample); - const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(repairedRawUserMessage || rawUserMessage); - const resolvedIntentResolution = intentResolution.intent !== "unknown" ? intentResolution : intentResolutionRaw; - const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); - const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); - const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason && - /(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason)); - const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && - typeof llmPreDecomposeMeta.semanticExtractionContract === "object" - ? llmPreDecomposeMeta.semanticExtractionContract - : null; - const semanticContractValid = semanticExtractionContract?.valid !== false; - const semanticApplyCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; - const semanticReasonCodes = Array.isArray(semanticExtractionContract?.reason_codes) - ? semanticExtractionContract.reason_codes - : []; - const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) || - hasStrictDeepInvestigationCue(repairedRawUserMessage) || - hasStrictDeepInvestigationCue(effectiveAddressUserMessage) || - hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage); - const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(resolvedIntentResolution.intent) || - shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent); - const keepAddressLaneByIntent = semanticApplyCanonicalRecommended && - Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || - (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || - openContractsAddressSignal) && - (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed); - const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) || - hasStrongDataIntentSignal(repairedRawUserMessage) || - hasStrongDataIntentSignal(effectiveAddressUserMessage) || - hasStrongDataIntentSignal(repairedEffectiveAddressUserMessage) || - hasAccountingSignal(rawUserMessage) || - hasAccountingSignal(repairedRawUserMessage) || - hasAccountingSignal(effectiveAddressUserMessage) || - hasAccountingSignal(repairedEffectiveAddressUserMessage) || - hasDataRetrievalRequestSignal(rawUserMessage) || - hasDataRetrievalRequestSignal(repairedRawUserMessage); - const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); - const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown"); - const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" && - (!llmContractIntent || llmContractIntent === "unknown")); - const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) || - hasDangerOrCoercionSignal(repairedRawUserMessage) || - hasDangerOrCoercionSignal(effectiveAddressUserMessage) || - hasDangerOrCoercionSignal(repairedEffectiveAddressUserMessage); - const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) || - hasAddressFollowupContextSignal(repairedRawUserMessage) || - hasAddressFollowupContextSignal(effectiveAddressUserMessage) || - hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(rawUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || - hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage); - const protectedInventoryShortFollowup = Boolean(followupContext && - isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) && - (hasShortInventoryObjectFollowupSignal(rawUserMessage) || - hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || - hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || - hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); - const organizationClarificationContinuationDetected = Boolean(followupContext && - lastOrganizationClarificationDebug && - organizationClarificationSelection && - !dataScopeMetaQuery && - !capabilityMetaQuery && - !dataRetrievalSignal); - const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; - const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && - !capabilityMetaQuery && - !dataRetrievalSignal && - !effectiveAddressFollowupSignal && - resolvedModeDetection.mode === "unsupported" && - resolvedIntentResolution.intent === "unknown"); - const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && - deterministicNonDomainGuard && - (llmFirstUnsupportedCandidate || llmContractMode === null) && - !protectedInventoryShortFollowup && - !organizationClarificationContinuationDetected); - const contextualHistoricalCapabilityFollowupDetected = Boolean(capabilityMetaQuery && - !dataScopeMetaQuery && - !dataRetrievalSignal && - (hasHistoricalCapabilityFollowupSignal(rawUserMessage) || - hasHistoricalCapabilityFollowupSignal(repairedRawUserMessage) || - hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) || - hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) && - isGroundedInventoryContextDebug(lastGroundedAddressDebug)); - const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery && - !capabilityMetaQuery && - !dataRetrievalSignal && - !strongDataSignal && - !aggregateBusinessAnalyticsSignal && - (hasConversationMemoryRecallFollowupSignal(rawUserMessage) || - hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) || - hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) || - hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) && - (lastGroundedAddressDebug || - findLastAddressAssistantItem(sessionItems)?.debug)); - const hardMetaMode = dataScopeMetaQuery - ? "data_scope" - : capabilityMetaQuery && !dataRetrievalSignal - ? "capability" - : null; - if (hardMetaMode === "data_scope") { - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "assistant_data_scope_query_detected", - livingMode: "chat", - livingReason: "assistant_data_scope_query_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "data_scope", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "assistant_data_scope_query_detected", - living_mode: "chat", - living_reason: "assistant_data_scope_query_detected" - } - } - }; - } - if (hardMetaMode === "capability") { - if (contextualHistoricalCapabilityFollowupDetected) { - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "inventory_history_capability_followup_detected", - livingMode: "chat", - livingReason: "inventory_history_capability_followup_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "capability", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "inventory_history_capability_followup_detected", - living_mode: "chat", - living_reason: "inventory_history_capability_followup_detected" - } - } - }; - } - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "assistant_capability_query_detected", - livingMode: "chat", - livingReason: "assistant_capability_query_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "capability", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "assistant_capability_query_detected", - living_mode: "chat", - living_reason: "assistant_capability_query_detected" - } - } - }; - } - if (nonDomainQueryIndexed) { - if (contextualMemoryRecapFollowupDetected) { - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "memory_recap_followup_detected", - livingMode: "chat", - livingReason: "memory_recap_followup_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "non_domain", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "memory_recap_followup_detected", - living_mode: "chat", - living_reason: "memory_recap_followup_detected" - } - } - }; - } - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "non_domain_query_indexed", - livingMode: "chat", - livingReason: "non_domain_query_indexed", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "non_domain", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "non_domain_query_indexed", - living_mode: "chat", - living_reason: "non_domain_query_indexed" - } - } - }; - } - const metaAnswerFollowupSignal = hasMetaAnswerFollowupSignal(rawUserMessage) || - hasMetaAnswerFollowupSignal(repairedRawUserMessage) || - hasMetaAnswerFollowupSignal(effectiveAddressUserMessage) || - hasMetaAnswerFollowupSignal(repairedEffectiveAddressUserMessage); - const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); - const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected && - llmPreDecomposeMeta?.applied && - llmContractMode === "address_query") || - hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) || - hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) || - hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) || - hasSameDateAccountFollowupSignalForPredecompose(repairedEffectiveAddressUserMessage) || - hasLooseAllTimeAddressLookupSignal(rawUserMessage) || - hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) || - hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) || - hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) || - hasAddressFollowupContextSignal(rawUserMessage) || - hasAddressFollowupContextSignal(effectiveAddressUserMessage) || - hasAddressFollowupContextSignal(repairedRawUserMessage) || - hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(rawUserMessage) || - hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage)); - const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) && - Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || - (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || - openContractsAddressSignal); - const semanticGuardHints = semanticExtractionContract?.guard_hints && - typeof semanticExtractionContract.guard_hints === "object" - ? semanticExtractionContract.guard_hints - : null; - const semanticExtraction = semanticExtractionContract?.extraction && - typeof semanticExtractionContract.extraction === "object" - ? semanticExtractionContract.extraction - : null; - const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true; - const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" || - semanticExtraction?.aggregation_profile === "management_profile"; - const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true); - const followupSemanticOverrideToDeepAllowed = Boolean(followupContext && - !supportedAddressIntentDetected && - (rootContextOnlyFollowup || - llmContractMode === "unsupported" || - semanticAggregateShapeDetected || - semanticDeepInvestigationHintDetected || - !semanticApplyCanonicalRecommended)); - const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") || - llmContractMode === "unsupported" || - (rootContextOnlyFollowup && - resolvedIntentResolution.intent === "unknown" && - (!llmContractIntent || llmContractIntent === "unknown")); - const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - unsupportedIntentOrMode && - strongDataSignal && - (rootContextOnlyFollowup || - llmContractMode === "deep_analysis" || - !dataRetrievalSignal || - strictDeepInvestigationCueDetected || - semanticDeepInvestigationHintDetected || - aggregateBusinessAnalyticsSignal) && - !preserveAddressLaneSignal && - !keepAddressLaneByIntent && - !supportedAddressIntentDetected && - (!followupContext || followupSemanticOverrideToDeepAllowed)); - const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) || - hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) || - hasDeepAnalysisPreferenceSignal(effectiveAddressUserMessage) || - hasDeepAnalysisPreferenceSignal(repairedEffectiveAddressUserMessage) || - hasDirectDeepAnalysisSignal(rawUserMessage) || - hasDirectDeepAnalysisSignal(repairedRawUserMessage) || - hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) || - hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage)); - const vatExplainFollowupSignal = Boolean(followupContext && - toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && - /(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); - const vatEvaluativeFollowupSignal = Boolean(followupContext && - toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && - /(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); - const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - (deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) && - !keepAddressLaneByIntent && - !supportedAddressIntentDetected && - !vatExplainFollowupSignal && - (!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed)); - const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - aggregateBusinessAnalyticsSignal && - !keepAddressLaneByIntent && - !supportedAddressIntentDetected && - (!followupContext || - llmContractMode === "unsupported" || - semanticAggregateShapeDetected || - !semanticApplyCanonicalRecommended || - standaloneAddressTopicSignal)); - const deepSessionContinuationFallbackToDeep = Boolean(!followupContext && - baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - hasDeepSessionContinuationSignal({ - rawUserMessage, - repairedRawUserMessage, - effectiveAddressUserMessage, - repairedEffectiveAddressUserMessage, - sessionItems - })); - const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); - const metaFollowupOverGroundedAnswer = Boolean(followupContext && - hasPriorAddressAnswerContext && - (metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) && - !dataScopeMetaQuery && - !capabilityMetaQuery && - !aggregateBusinessAnalyticsSignal && - !dataRetrievalSignal && - !strongDataSignal && - resolvedModeDetection.mode !== "address_query" && - resolvedIntentResolution.intent === "unknown" && - (!llmContractIntent || llmContractIntent === "unknown") && - llmContractMode !== "address_query"); - let runAddressLane = Boolean(baseToolGate?.runAddressLane); - let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); - let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); - if (unsupportedAddressIntentFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "address_signal_unsupported_intent_fallback_to_deep"; - } - if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "deep_analysis_signal_fallback_to_deep"; - } - if (aggregateAnalyticsFallbackToDeep && - !unsupportedAddressIntentFallbackToDeep && - !deepAnalysisSignalFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "aggregate_analytics_signal_fallback_to_deep"; - } - if (deepSessionContinuationFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "deep_session_continuation_fallback_to_deep"; - } - if (metaFollowupOverGroundedAnswer) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "meta_followup_over_grounded_answer"; - } - let livingDecision = resolveLivingAssistantModeDecision({ - userMessage: rawUserMessage, - addressLaneTriggered: runAddressLane, - useMock, - predecomposeMode: llmPreDecomposeMeta?.predecomposeContract?.mode ?? null, - predecomposeModeConfidence: llmPreDecomposeMeta?.predecomposeContract?.mode_confidence ?? null - }); - if (unsupportedAddressIntentFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "unsupported_address_intent_fallback_to_deep" - }; - } - if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "deep_analysis_signal_fallback_to_deep" - }; - } - if (aggregateAnalyticsFallbackToDeep && - !unsupportedAddressIntentFallbackToDeep && - !deepAnalysisSignalFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "aggregate_analytics_signal_fallback_to_deep" - }; - } - if (deepSessionContinuationFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "deep_session_continuation_fallback_to_deep" - }; - } - if (metaFollowupOverGroundedAnswer) { - livingDecision = { - mode: "chat", - reason: "meta_followup_over_grounded_answer" - }; - } - return { - runAddressLane, - toolGateDecision, - toolGateReason, - livingMode: livingDecision.mode, - livingReason: livingDecision.reason, - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: null, - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - semantic_contract_valid: semanticContractValid, - semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended, - semantic_reason_codes: semanticReasonCodes, - semantic_route_arbitration: { - supported_address_intent_detected: supportedAddressIntentDetected, - strict_deep_investigation_bypass_allowed: strictDeepInvestigationBypassAllowed, - semantic_deep_investigation_hint_detected: semanticDeepInvestigationHintDetected, - semantic_aggregate_shape_detected: semanticAggregateShapeDetected, - followup_semantic_override_to_deep_allowed: followupSemanticOverrideToDeepAllowed - }, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep, - deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep, - aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep, - deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep, - final_decision: { - run_address_lane: runAddressLane, - tool_gate_decision: toolGateDecision, - tool_gate_reason: toolGateReason, - living_mode: livingDecision.mode, - living_reason: livingDecision.reason - } - } - }; + return assistantRoutePolicy.resolveAssistantOrchestrationDecision(input); } function hasStrongDataIntentSignal(text) { - const lower = String(text ?? "").toLowerCase(); - return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower); + return assistantLivingModePolicy.hasStrongDataIntentSignal(text); } function hasDataRetrievalRequestSignal(text) { - const lower = compactWhitespace(String(text ?? "").toLowerCase()); - if (!lower) { - return false; - } - const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower); - const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover|warehouse|inventory|stock|item)/iu.test(lower); - if (hasBroadInterrogative && hasBroadBusinessObject) { - return true; - } - const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a|\u043f\u0440\u043e\u0432\u0435\u0440\u044c|\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c)(?:$|[\s,.!?;:])/iu.test(lower); - const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b)/iu.test(lower); - if (hasRussianRetrievalAction && hasRussianRetrievalObject) { - return true; - } - const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower); - const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower); - if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) { - return false; - } - const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower); - if (!hasRetrievalObject) { - return false; - } - if (hasExplicitRetrievalAction) { - return true; - } - const hasMetaCapabilityShape = /(?:мож(?:ем|ешь|ете|но)|уме(?:ешь|ете)|доступ|подключ|чья|как\s+называ(?:ет|ется)|работ(?:ать|аем|аешь|аете)|в\s+тебе|у\s+тебя)/i.test(lower); - return !hasMetaCapabilityShape; + return assistantLivingModePolicy.hasDataRetrievalRequestSignal(text); } function hasOrganizationFactLookupSignal(text) { - const repaired = repairAddressMojibake(String(text ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - const hasFactCue = /(?:возраст|сколько\s+лет|дата\s+регистрац|когда\s+(?:зарегистр|создан|основан)|год\s+регистрац|год\s+основан|с\s+какого\s+года|when\s+was\s+(?:it\s+)?(?:registered|founded|created))/i.test(normalized); - if (!hasFactCue) { - return false; - } - return /(?:организац|компан|контор|фирм|ооо|ао|зао|ип|альтернатив|лайсвуд|райм|organization|company)/i.test(normalized); + return assistantLivingModePolicy.hasOrganizationFactLookupSignal(text); } function findLastAssistantLivingChatDebug(items) { if (!Array.isArray(items)) { @@ -4929,67 +4325,13 @@ function findLastOrganizationClarificationAddressDebug(items) { return null; } function hasMetaAnswerFollowupSignal(userMessage) { - const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); - const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); - const samples = [rawText, repairedText] - .filter((item) => item.length > 0) - .map((item) => item.replace(/ё/g, "е")); - if (samples.length === 0) { - return false; - } - const hasReflectionCue = samples.some((sample) => sample.includes("дума") || - sample.includes("скаж") || - sample.includes("мнение") || - sample.includes("как тебе") || - sample.includes("норм") || - sample.includes("стран") || - sample.includes("логич") || - sample.includes("смуща") || - sample.includes("выгляд")); - const hasTopicPointerCue = samples.some((sample) => sample.includes("на эту тему") || - sample.includes("по этому поводу") || - sample.includes("об этом") || - (sample.includes("это") && hasReferentialPointer(sample))); - const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample)); - if (!((hasReflectionCue || hasEvaluationCue) && - (hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) { - return false; - } - return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || - shouldHandleAsAssistantCapabilityMetaQuery(sample) || - hasDataRetrievalRequestSignal(sample) || - hasStrongDataIntentSignal(sample)); + return assistantLivingModePolicy.hasMetaAnswerFollowupSignal(userMessage); } function hasConversationMemoryRecallFollowupSignal(userMessage) { - const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); - const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); - const samples = [rawText, repairedText] - .filter((item) => item.length > 0) - .map((item) => item.replace(/ё/g, "е")); - if (samples.length === 0) { - return false; - } - const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample)); - const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample)); - if (!hasMemoryCue || !hasDiscussionCue) { - return false; - } - return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || - shouldHandleAsAssistantCapabilityMetaQuery(sample) || - hasDataRetrievalRequestSignal(sample) || - hasStrongDataIntentSignal(sample)); + return assistantLivingModePolicy.hasConversationMemoryRecallFollowupSignal(userMessage); } function hasHistoricalCapabilityFollowupSignal(text) { - const repaired = repairAddressMojibake(String(text ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - const hasHistoryCue = /(?:историческ|история|архив|прошл(?:ый|ые|ую|ых)?|раньше|ретро|старые\s+данные)/iu.test(normalized); - if (!hasHistoryCue) { - return false; - } - return /(?:мож(?:ешь|ете|но)|уме(?:ешь|ете)|показ|вывед|дай|раскрой)/iu.test(normalized); + return assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal(text); } function isGroundedInventoryContextDebug(debug) { if (!debug || typeof debug !== "object") { @@ -5006,56 +4348,10 @@ function isGroundedInventoryContextDebug(debug) { rootIntent === "inventory_on_hand_as_of_date"; } function hasOrganizationFactFollowupSignal(userMessage, items) { - const repaired = repairAddressMojibake(String(userMessage ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - if (hasOrganizationFactLookupSignal(normalized)) { - return false; - } - const hasFollowupCue = /(?:^|\s)(?:давай|го|погнали|ок(?:ей)?|хорошо|принято|подтверждаю|запрашивай|запроси|проверь|продолжай|ну\s+давай|да\s+давай)(?=$|[\s,.!?;:])/iu.test(normalized); - if (!hasFollowupCue) { - return false; - } - const lastDebug = findLastAssistantLivingChatDebug(items); - const lastSource = toNonEmptyString(lastDebug?.living_chat_response_source); - const lastGuardReason = toNonEmptyString(lastDebug?.living_chat_grounding_guard_reason); - const inOrganizationFactBoundary = lastSource === "deterministic_organization_fact_boundary" || - lastSource === "deterministic_organization_fact_boundary_followup" || - lastGuardReason === "organization_fact_without_live_source_blocked"; - return inOrganizationFactBoundary; + return assistantLivingModePolicy.hasOrganizationFactFollowupSignal(userMessage, items); } function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) { - const selected = normalizeOrganizationScopeValue(selectedOrganization); - if (!selected) { - return false; - } - const repaired = repairAddressMojibake(String(userMessage ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - if (hasOrganizationFactLookupSignal(normalized) || hasDataRetrievalRequestSignal(normalized) || hasStrongDataIntentSignal(normalized)) { - return false; - } - const hasAnalyticalCue = /(?:какой|какая|какие|когда|сколько|кто|почему|зачем|возраст|дата|регистрац|ндс|налог|контракт|договор|документ|операц|оборот|сумм|остат|сальдо|founded|registered|created)/i.test(normalized); - if (hasAnalyticalCue) { - return false; - } - const hasSelectionCue = /(?:давай|го|погнали|ок(?:ей)?|хорошо|отлично|берем|выберем|выбираем|переключ(?:им|аем|ай)|фиксир|работаем|обсудим|тогда)\b/i.test(normalized); - if (hasSelectionCue) { - return true; - } - const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС…|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|РїРёР·РґР°|РЅРёС…СѓСЏ|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) || - normalized.includes("\u0430\u0445\u0443") || - normalized.includes("\u043e\u0445\u0443") || - normalized.includes("\u043f\u0438\u0437\u0434") || - normalized.includes("\u0431\u043b\u044f"); - if (hasAffectiveReactionCue) { - return false; - } - return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? "")); + return assistantLivingModePolicy.shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization); } function hasOperationalAdminActionRequestSignal(text) { const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е"); @@ -5131,78 +4427,13 @@ function hasAssistantCapabilityQuestionSignal(text) { return false; } function hasAssistantDataScopeMetaQuestionSignal(text) { - const repaired = repairAddressMojibake(String(text ?? "")); - const lower = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с"); - if (!normalized) { - return false; - } - const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized); - if (hasDirectSlangScopeLead) { - return true; - } - const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized); - if (hasSlangScopeQuestion) { - return true; - } - const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized); - const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized); - const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized); - const hasNamingCue = /(?:как\s+называ(?:ет|ется)|что\s+за\s+(?:контор|компан|организац|фирм))/i.test(normalized); - const hasWorkabilityCue = /(?:мож(?:ем|ешь|ете|но)\s+работ|работ(?:ать|аем|аешь|аете))/i.test(normalized); - const hasScopeObject = hasBaseOrTenantObject || hasCompanyObject || hasConnectionCue; - if (!hasScopeObject) { - return false; - } - const hasMetaPerspective = /(?:ты|тебе|твой|у\s+тебя|в\s+тебе|мы|нам|наш(?:а|е|и|у|ей)?|сейчас|щас)/i.test(normalized); - const hasScopedInterrogativePair = /(?:^|\s)(?:по\s+какой|с\s+какой|какая|какой|какие)\s+(?:баз|компан|компин|компини|компани|организац|контор|фирм|тенант|контур)/i.test(normalized); - const hasScopeQuestion = /(?:чья|чье|чьи|доступн|подключен|подруб|воткнут|какая\s+баз|какой\s+баз)/i.test(normalized) || - hasNamingCue || - hasWorkabilityCue || - hasScopedInterrogativePair; - const hasInterrogativeScopeLead = /(?:^|\s)(?:по\s+какой|с\s+какой|чья|чье|чьи|which|who|what)/i.test(normalized); - const isQuestionLike = /[?]/.test(String(text ?? "")) || hasInterrogativeScopeLead || hasScopedInterrogativePair; - const hasExplicitScopeContext = hasBaseOrTenantObject || hasConnectionCue || hasWorkabilityCue || hasNamingCue; - const hasRetrievalSignal = hasDataRetrievalRequestSignal(normalized); - const hasContractAnalyticsCue = /(?:договор|контракт|contract).*(?:топ|сам(?:ый|ая|ое|ые)|крупн|жирн|оборот|бюджет|сумм|стоим|value|turnover|all\s+time|всю\s+истори|за\s+вс[её]\s+время)/iu.test(normalized); - if (hasContractAnalyticsCue) { - return false; - } - if (hasRetrievalSignal && !hasExplicitScopeContext) { - return false; - } - const hasEligibleScopeObject = hasBaseOrTenantObject || (hasCompanyObject && (hasConnectionCue || hasWorkabilityCue || hasNamingCue || hasMetaPerspective)); - return hasEligibleScopeObject && hasScopeQuestion && (hasMetaPerspective || isQuestionLike || hasExplicitScopeContext); + return assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal(text); } function shouldHandleAsAssistantCapabilityMetaQuery(text) { - const raw = String(text ?? ""); - const repaired = repairAddressMojibake(raw); - const hasScopeMetaSignal = hasAssistantDataScopeMetaQuestionSignal(raw) || hasAssistantDataScopeMetaQuestionSignal(repaired); - if (hasScopeMetaSignal) { - return true; - } - const hasCapabilitySignal = hasAssistantCapabilityQuestionSignal(raw) || - hasAssistantCapabilityQuestionSignal(repaired) || - hasOperationalAdminActionRequestSignal(raw) || - hasOperationalAdminActionRequestSignal(repaired); - const hasRetrievalSignal = hasDataRetrievalRequestSignal(raw) || hasDataRetrievalRequestSignal(repaired); - return hasCapabilitySignal && !hasRetrievalSignal; + return assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery(text); } function hasLivingChatSignal(text) { - const lower = compactWhitespace(String(text ?? "").toLowerCase()); - if (!lower) { - return false; - } - if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) { - return true; - } - if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) { - return true; - } - if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) { - return true; - } - return hasSmallTalkSignal(lower); + return assistantLivingModePolicy.hasLivingChatSignal(text); } function buildAssistantCapabilityContractReply() { return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)(); @@ -5306,6 +4537,55 @@ function normalizeOrganizationScopeValue(value) { .trim(); return unwrapped ? unwrapped : null; } +const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({ + featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1, + compactWhitespace, + repairAddressMojibake, + toNonEmptyString, + normalizeOrganizationScopeValue, + hasReferentialPointer, + hasSmallTalkSignal, + hasAssistantCapabilityQuestionSignal, + hasOperationalAdminActionRequestSignal +}); +const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePolicy)({ + repairAddressMojibake, + findLastGroundedAddressAnswerDebug, + findLastOrganizationClarificationAddressDebug, + mergeKnownOrganizations, + normalizeOrganizationScopeValue, + resolveOrganizationSelectionFromMessage, + hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery, + hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal, + hasAggregateBusinessAnalyticsSignal, + hasStandaloneAddressTopicSignal, + hasOpenContractsAddressSignal, + detectAddressQuestionMode: addressQueryClassifier_1.detectAddressQuestionMode, + resolveAddressIntent: addressIntentResolver_1.resolveAddressIntent, + toNonEmptyString, + hasStrictDeepInvestigationCue, + hasStrongDataIntentSignal: assistantLivingModePolicy.hasStrongDataIntentSignal, + hasAccountingSignal, + hasDangerOrCoercionSignal, + hasAddressFollowupContextSignal, + hasShortDebtMirrorFollowupSignal, + isInventorySelectedObjectIntent, + hasShortInventoryObjectFollowupSignal, + hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal, + isGroundedInventoryContextDebug, + hasConversationMemoryRecallFollowupSignal: assistantLivingModePolicy.hasConversationMemoryRecallFollowupSignal, + findLastAddressAssistantItem, + hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal, + resolveAddressToolGateDecision, + hasSameDateAccountFollowupSignalForPredecompose, + hasLooseAllTimeAddressLookupSignal, + hasDeepAnalysisPreferenceSignal, + hasDirectDeepAnalysisSignal, + compactWhitespace, + hasDeepSessionContinuationSignal, + resolveLivingAssistantModeDecision: assistantLivingModePolicy.resolveLivingAssistantModeDecision +}); function normalizeOrganizationScopeSearchText(value) { const source = normalizeScopeKey(value); return source @@ -6105,67 +5385,7 @@ function applyLivingChatGroundingGuard(input) { }; } function resolveLivingAssistantModeDecision(input) { - const userMessage = String(input?.userMessage ?? ""); - if (input?.addressLaneTriggered) { - return { - mode: "address_data", - reason: "address_lane_triggered" - }; - } - if (!config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1) { - return { - mode: "deep_analysis", - reason: "living_chat_router_disabled" - }; - } - if (Boolean(input?.useMock)) { - return { - mode: "deep_analysis", - reason: "mock_mode_keeps_deep_pipeline" - }; - } - if (hasAssistantDataScopeMetaQuestionSignal(userMessage)) { - return { - mode: "chat", - reason: "assistant_data_scope_query_detected" - }; - } - if (shouldHandleAsAssistantCapabilityMetaQuery(userMessage)) { - return { - mode: "chat", - reason: "assistant_capability_query_detected" - }; - } - if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) { - return { - mode: "chat", - reason: "organization_fact_lookup_signal_detected" - }; - } - if (hasStrongDataIntentSignal(userMessage)) { - return { - mode: "deep_analysis", - reason: "strong_data_signal_detected" - }; - } - if (hasLivingChatSignal(userMessage)) { - return { - mode: "chat", - reason: "living_chat_signal_detected" - }; - } - const predecomposeMode = toNonEmptyString(input?.predecomposeMode); - const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence); - if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) { - return { - mode: "deep_analysis", - reason: "predecompose_unsupported_mode_fallback_to_deep" - }; - } - return { - mode: "deep_analysis", - reason: "default_deep_pipeline" - }; + return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input); } class AssistantService { normalizerService; diff --git a/llm_normalizer/backend/src/services/assistantLivingModePolicy.ts b/llm_normalizer/backend/src/services/assistantLivingModePolicy.ts new file mode 100644 index 0000000..397c924 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantLivingModePolicy.ts @@ -0,0 +1,395 @@ +// @ts-nocheck +export interface ResolveLivingAssistantModeDecisionInput { + userMessage?: unknown; + addressLaneTriggered?: boolean; + useMock?: boolean; + predecomposeMode?: unknown; + predecomposeModeConfidence?: unknown; +} + +export interface AssistantLivingModePolicyDeps { + featureAssistantLivingChatRouterV1: boolean; + compactWhitespace: (text: string) => string; + repairAddressMojibake: (text: string) => string; + toNonEmptyString: (value: unknown) => string | null; + normalizeOrganizationScopeValue: (value: unknown) => string | null; + hasReferentialPointer: (text: string) => boolean; + hasSmallTalkSignal: (text: string) => boolean; + hasAssistantCapabilityQuestionSignal: (text: string) => boolean; + hasOperationalAdminActionRequestSignal: (text: string) => boolean; +} + +export interface AssistantLivingModeDecision { + mode: "address_data" | "deep_analysis" | "chat"; + reason: string; +} + +export interface AssistantLivingModePolicy { + hasStrongDataIntentSignal: (text: unknown) => boolean; + hasDataRetrievalRequestSignal: (text: unknown) => boolean; + hasOrganizationFactLookupSignal: (text: unknown) => boolean; + hasMetaAnswerFollowupSignal: (text: unknown) => boolean; + hasConversationMemoryRecallFollowupSignal: (text: unknown) => boolean; + hasHistoricalCapabilityFollowupSignal: (text: unknown) => boolean; + hasOrganizationFactFollowupSignal: (userMessage: unknown, items: unknown[]) => boolean; + shouldEmitOrganizationSelectionReply: (userMessage: unknown, selectedOrganization: unknown) => boolean; + hasAssistantDataScopeMetaQuestionSignal: (text: unknown) => boolean; + shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) => boolean; + hasLivingChatSignal: (text: unknown) => boolean; + resolveLivingAssistantModeDecision: (input: ResolveLivingAssistantModeDecisionInput) => AssistantLivingModeDecision; +} + +export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyDeps): AssistantLivingModePolicy { + const { + featureAssistantLivingChatRouterV1, + compactWhitespace, + repairAddressMojibake, + toNonEmptyString, + normalizeOrganizationScopeValue, + hasReferentialPointer, + hasSmallTalkSignal, + hasAssistantCapabilityQuestionSignal, + hasOperationalAdminActionRequestSignal + } = deps; + + function hasStrongDataIntentSignal(text) { + const lower = String(text ?? "").toLowerCase(); + return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower); + } + + function hasDataRetrievalRequestSignal(text) { + const lower = compactWhitespace(String(text ?? "").toLowerCase()); + if (!lower) { + return false; + } + const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower); + const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover|warehouse|inventory|stock|item)/iu.test(lower); + if (hasBroadInterrogative && hasBroadBusinessObject) { + return true; + } + const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a|\u043f\u0440\u043e\u0432\u0435\u0440\u044c|\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c)(?:$|[\s,.!?;:])/iu.test(lower); + const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b)/iu.test(lower); + if (hasRussianRetrievalAction && hasRussianRetrievalObject) { + return true; + } + const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower); + const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower); + if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) { + return false; + } + const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower); + if (!hasRetrievalObject) { + return false; + } + if (hasExplicitRetrievalAction) { + return true; + } + const hasMetaCapabilityShape = /(?:мож(?:ем|ешь|ете|но)|уме(?:ешь|ете)|доступ|подключ|чья|как\s+называ(?:ет|ется)|работ(?:ать|аем|аешь|аете)|в\s+тебе|у\s+тебя)/i.test(lower); + return !hasMetaCapabilityShape; + } + + function hasOrganizationFactLookupSignal(text) { + const repaired = repairAddressMojibake(String(text ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + const hasFactCue = /(?:возраст|сколько\s+лет|дата\s+регистрац|когда\s+(?:зарегистр|создан|основан)|год\s+регистрац|год\s+основан|с\s+какого\s+года|when\s+was\s+(?:it\s+)?(?:registered|founded|created))/i.test(normalized); + if (!hasFactCue) { + return false; + } + return /(?:организац|компан|контор|фирм|ооо|ао|зао|ип|альтернатив|лайсвуд|райм|organization|company)/i.test(normalized); + } + + function findLastAssistantLivingChatDebug(items) { + if (!Array.isArray(items)) { + return null; + } + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (!item || item.role !== "assistant") { + continue; + } + if (item.debug && typeof item.debug === "object") { + return item.debug; + } + } + return null; + } + + function hasMetaAnswerFollowupSignal(userMessage) { + const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + const samples = [rawText, repairedText] + .filter((item) => item.length > 0) + .map((item) => item.replace(/ё/g, "е")); + if (samples.length === 0) { + return false; + } + const hasReflectionCue = samples.some((sample) => sample.includes("дума") || + sample.includes("скаж") || + sample.includes("мнение") || + sample.includes("как тебе") || + sample.includes("норм") || + sample.includes("стран") || + sample.includes("логич") || + sample.includes("смуща") || + sample.includes("выгляд")); + const hasTopicPointerCue = samples.some((sample) => sample.includes("на эту тему") || + sample.includes("по этому поводу") || + sample.includes("об этом") || + (sample.includes("это") && hasReferentialPointer(sample))); + const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample)); + if (!((hasReflectionCue || hasEvaluationCue) && + (hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) { + return false; + } + return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || + shouldHandleAsAssistantCapabilityMetaQuery(sample) || + hasDataRetrievalRequestSignal(sample) || + hasStrongDataIntentSignal(sample)); + } + + function hasConversationMemoryRecallFollowupSignal(userMessage) { + const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); + const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + const samples = [rawText, repairedText] + .filter((item) => item.length > 0) + .map((item) => item.replace(/ё/g, "е")); + if (samples.length === 0) { + return false; + } + const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample)); + const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample)); + if (!hasMemoryCue || !hasDiscussionCue) { + return false; + } + return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || + shouldHandleAsAssistantCapabilityMetaQuery(sample) || + hasDataRetrievalRequestSignal(sample) || + hasStrongDataIntentSignal(sample)); + } + + function hasHistoricalCapabilityFollowupSignal(text) { + const repaired = repairAddressMojibake(String(text ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + const hasHistoryCue = /(?:историческ|история|архив|прошл(?:ый|ые|ую|ых)?|раньше|ретро|старые\s+данные)/iu.test(normalized); + if (!hasHistoryCue) { + return false; + } + return /(?:мож(?:ешь|ете|но)|уме(?:ешь|ете)|показ|вывед|дай|раскрой)/iu.test(normalized); + } + + function hasOrganizationFactFollowupSignal(userMessage, items) { + const repaired = repairAddressMojibake(String(userMessage ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + if (hasOrganizationFactLookupSignal(normalized)) { + return false; + } + const hasFollowupCue = /(?:^|\s)(?:давай|го|погнали|ок(?:ей)?|хорошо|принято|подтверждаю|запрашивай|запроси|проверь|продолжай|ну\s+давай|да\s+давай)(?=$|[\s,.!?;:])/iu.test(normalized); + if (!hasFollowupCue) { + return false; + } + const lastDebug = findLastAssistantLivingChatDebug(items); + const lastSource = toNonEmptyString(lastDebug?.living_chat_response_source); + const lastGuardReason = toNonEmptyString(lastDebug?.living_chat_grounding_guard_reason); + const inOrganizationFactBoundary = lastSource === "deterministic_organization_fact_boundary" || + lastSource === "deterministic_organization_fact_boundary_followup" || + lastGuardReason === "organization_fact_without_live_source_blocked"; + return inOrganizationFactBoundary; + } + + function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) { + const selected = normalizeOrganizationScopeValue(selectedOrganization); + if (!selected) { + return false; + } + const repaired = repairAddressMojibake(String(userMessage ?? "")); + const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + if (!normalized) { + return false; + } + if (hasOrganizationFactLookupSignal(normalized) || hasDataRetrievalRequestSignal(normalized) || hasStrongDataIntentSignal(normalized)) { + return false; + } + const hasAnalyticalCue = /(?:какой|какая|какие|когда|сколько|кто|почему|зачем|возраст|дата|регистрац|ндс|налог|контракт|договор|документ|операц|оборот|сумм|остат|сальдо|founded|registered|created)/i.test(normalized); + if (hasAnalyticalCue) { + return false; + } + const hasSelectionCue = /(?:давай|го|погнали|ок(?:ей)?|хорошо|отлично|берем|выберем|выбираем|переключ(?:им|аем|ай)|фиксир|работаем|обсудим|тогда)\b/i.test(normalized); + if (hasSelectionCue) { + return true; + } + const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС…|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|РїРёР·РґР°|РЅРёС…СѓСЏ|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) || + normalized.includes("\u0430\u0445\u0443") || + normalized.includes("\u043e\u0445\u0443") || + normalized.includes("\u043f\u0438\u0437\u0434") || + normalized.includes("\u0431\u043b\u044f"); + if (hasAffectiveReactionCue) { + return false; + } + return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? "")); + } + + function hasAssistantDataScopeMetaQuestionSignal(text) { + const repaired = repairAddressMojibake(String(text ?? "")); + const lower = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); + const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с"); + if (!normalized) { + return false; + } + const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized); + if (hasDirectSlangScopeLead) { + return true; + } + const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized); + if (hasSlangScopeQuestion) { + return true; + } + const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized); + const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized); + const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized); + const hasNamingCue = /(?:как\s+называ(?:ет|ется)|что\s+за\s+(?:контор|компан|организац|фирм))/i.test(normalized); + const hasWorkabilityCue = /(?:мож(?:ем|ешь|ете|но)\s+работ|работ(?:ать|аем|аешь|аете))/i.test(normalized); + const hasScopeObject = hasBaseOrTenantObject || hasCompanyObject || hasConnectionCue; + if (!hasScopeObject) { + return false; + } + const hasMetaPerspective = /(?:ты|тебе|твой|у\s+тебя|в\s+тебе|мы|нам|наш(?:а|е|и|у|ей)?|сейчас|щас)/i.test(normalized); + const hasScopedInterrogativePair = /(?:^|\s)(?:по\s+какой|с\s+какой|какая|какой|какие)\s+(?:баз|компан|компин|компини|компани|организац|контор|фирм|тенант|контур)/i.test(normalized); + const hasScopeQuestion = /(?:чья|чье|чьи|доступн|подключен|подруб|воткнут|какая\s+баз|какой\s+баз)/i.test(normalized) || + hasNamingCue || + hasWorkabilityCue || + hasScopedInterrogativePair; + const hasInterrogativeScopeLead = /(?:^|\s)(?:по\s+какой|с\s+какой|чья|чье|чьи|which|who|what)/i.test(normalized); + const isQuestionLike = /[?]/.test(String(text ?? "")) || hasInterrogativeScopeLead || hasScopedInterrogativePair; + const hasExplicitScopeContext = hasBaseOrTenantObject || hasConnectionCue || hasWorkabilityCue || hasNamingCue; + const hasRetrievalSignal = hasDataRetrievalRequestSignal(normalized); + const hasContractAnalyticsCue = /(?:договор|контракт|contract).*(?:топ|сам(?:ый|ая|ое|ые)|крупн|жирн|оборот|бюджет|сумм|стоим|value|turnover|all\s+time|всю\s+истори|за\s+вс[её]\s+время)/iu.test(normalized); + if (hasContractAnalyticsCue) { + return false; + } + if (hasRetrievalSignal && !hasExplicitScopeContext) { + return false; + } + const hasEligibleScopeObject = hasBaseOrTenantObject || (hasCompanyObject && (hasConnectionCue || hasWorkabilityCue || hasNamingCue || hasMetaPerspective)); + return hasEligibleScopeObject && hasScopeQuestion && (hasMetaPerspective || isQuestionLike || hasExplicitScopeContext); + } + + function shouldHandleAsAssistantCapabilityMetaQuery(text) { + const raw = String(text ?? ""); + const repaired = repairAddressMojibake(raw); + const hasScopeMetaSignal = hasAssistantDataScopeMetaQuestionSignal(raw) || hasAssistantDataScopeMetaQuestionSignal(repaired); + if (hasScopeMetaSignal) { + return true; + } + const hasCapabilitySignal = hasAssistantCapabilityQuestionSignal(raw) || + hasAssistantCapabilityQuestionSignal(repaired) || + hasOperationalAdminActionRequestSignal(raw) || + hasOperationalAdminActionRequestSignal(repaired); + const hasRetrievalSignal = hasDataRetrievalRequestSignal(raw) || hasDataRetrievalRequestSignal(repaired); + return hasCapabilitySignal && !hasRetrievalSignal; + } + + function hasLivingChatSignal(text) { + const lower = compactWhitespace(String(text ?? "").toLowerCase()); + if (!lower) { + return false; + } + if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) { + return true; + } + if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) { + return true; + } + if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) { + return true; + } + return hasSmallTalkSignal(lower); + } + + function resolveLivingAssistantModeDecision(input) { + const userMessage = String(input?.userMessage ?? ""); + if (input?.addressLaneTriggered) { + return { + mode: "address_data", + reason: "address_lane_triggered" + }; + } + if (!featureAssistantLivingChatRouterV1) { + return { + mode: "deep_analysis", + reason: "living_chat_router_disabled" + }; + } + if (Boolean(input?.useMock)) { + return { + mode: "deep_analysis", + reason: "mock_mode_keeps_deep_pipeline" + }; + } + if (hasAssistantDataScopeMetaQuestionSignal(userMessage)) { + return { + mode: "chat", + reason: "assistant_data_scope_query_detected" + }; + } + if (shouldHandleAsAssistantCapabilityMetaQuery(userMessage)) { + return { + mode: "chat", + reason: "assistant_capability_query_detected" + }; + } + if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) { + return { + mode: "chat", + reason: "organization_fact_lookup_signal_detected" + }; + } + if (hasStrongDataIntentSignal(userMessage)) { + return { + mode: "deep_analysis", + reason: "strong_data_signal_detected" + }; + } + if (hasLivingChatSignal(userMessage)) { + return { + mode: "chat", + reason: "living_chat_signal_detected" + }; + } + const predecomposeMode = toNonEmptyString(input?.predecomposeMode); + const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence); + if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) { + return { + mode: "deep_analysis", + reason: "predecompose_unsupported_mode_fallback_to_deep" + }; + } + return { + mode: "deep_analysis", + reason: "default_deep_pipeline" + }; + } + + return { + hasStrongDataIntentSignal, + hasDataRetrievalRequestSignal, + hasOrganizationFactLookupSignal, + hasMetaAnswerFollowupSignal, + hasConversationMemoryRecallFollowupSignal, + hasHistoricalCapabilityFollowupSignal, + hasOrganizationFactFollowupSignal, + shouldEmitOrganizationSelectionReply, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasLivingChatSignal, + resolveLivingAssistantModeDecision + }; +} diff --git a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts new file mode 100644 index 0000000..cdca213 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts @@ -0,0 +1,616 @@ +// @ts-nocheck +const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ + "period_coverage_profile", + "document_type_and_account_section_profile", + "counterparty_population_and_roles", + "counterparty_activity_lifecycle", + "customer_revenue_and_payments", + "supplier_payouts_profile", + "open_contracts_confirmed_as_of_date", + "list_open_contracts", + "open_items_by_counterparty_or_contract", + "list_payables_counterparties", + "list_receivables_counterparties", + "inventory_on_hand_as_of_date", + "payables_confirmed_as_of_date", + "receivables_confirmed_as_of_date", + "list_documents_by_contract", + "bank_operations_by_contract", + "list_documents_by_counterparty", + "bank_operations_by_counterparty", + "list_contracts_by_counterparty", + "inventory_purchase_provenance_for_item", + "inventory_purchase_documents_for_item", + "inventory_supplier_stock_overlap_as_of_date", + "inventory_sale_trace_for_item", + "inventory_profitability_for_item", + "inventory_purchase_to_sale_chain", + "inventory_aging_by_purchase_date", + "contract_usage_overview", + "contract_usage_and_value", + "vat_payable_forecast", + "vat_liability_confirmed_for_tax_period", + "vat_payable_confirmed_as_of_date" +]); +const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([ + "inventory_purchase_provenance_for_item", + "inventory_purchase_documents_for_item", + "inventory_sale_trace_for_item", + "inventory_profitability_for_item", + "inventory_purchase_to_sale_chain" +]); +function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) { + return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent)); +} +export function createAssistantRoutePolicy(deps) { + const { + repairAddressMojibake, + findLastGroundedAddressAnswerDebug, + findLastOrganizationClarificationAddressDebug, + mergeKnownOrganizations, + normalizeOrganizationScopeValue, + resolveOrganizationSelectionFromMessage, + hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery, + hasDataRetrievalRequestSignal, + hasAggregateBusinessAnalyticsSignal, + hasStandaloneAddressTopicSignal, + hasOpenContractsAddressSignal, + detectAddressQuestionMode, + resolveAddressIntent, + toNonEmptyString, + hasStrictDeepInvestigationCue, + hasStrongDataIntentSignal, + hasAccountingSignal, + hasDangerOrCoercionSignal, + hasAddressFollowupContextSignal, + hasShortDebtMirrorFollowupSignal, + isInventorySelectedObjectIntent, + hasShortInventoryObjectFollowupSignal, + hasHistoricalCapabilityFollowupSignal, + isGroundedInventoryContextDebug, + hasConversationMemoryRecallFollowupSignal, + findLastAddressAssistantItem, + hasMetaAnswerFollowupSignal, + resolveAddressToolGateDecision, + hasSameDateAccountFollowupSignalForPredecompose, + hasLooseAllTimeAddressLookupSignal, + hasDeepAnalysisPreferenceSignal, + hasDirectDeepAnalysisSignal, + compactWhitespace, + hasDeepSessionContinuationSignal, + resolveLivingAssistantModeDecision + } = deps; + function resolveAssistantOrchestrationDecision(input) { + const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? ""); + const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage); + const repairedRawUserMessage = repairAddressMojibake(rawUserMessage); + const repairedEffectiveAddressUserMessage = repairAddressMojibake(effectiveAddressUserMessage); + const followupContext = input?.followupContext ?? null; + const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null; + const useMock = Boolean(input?.useMock); + const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null; + const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" + ? input.sessionOrganizationScope + : null; + const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems); + const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); + const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) + ? mergeKnownOrganizations([ + ...lastOrganizationClarificationDebug.organization_candidates, + ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) + ? sessionOrganizationScope.knownOrganizations + : [])) + ]) + : []; + const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization); + const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? + resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? + resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? + resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ?? + (organizationClarificationSelectionFromScope && + organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope) + ? organizationClarificationSelectionFromScope + : null); + const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) || + hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) || + hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) || + hasAssistantDataScopeMetaQuestionSignal(repairedEffectiveAddressUserMessage); + const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawUserMessage) || + shouldHandleAsAssistantCapabilityMetaQuery(repairedRawUserMessage) || + shouldHandleAsAssistantCapabilityMetaQuery(effectiveAddressUserMessage) || + shouldHandleAsAssistantCapabilityMetaQuery(repairedEffectiveAddressUserMessage); + const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawUserMessage) || + hasDataRetrievalRequestSignal(repairedRawUserMessage) || + hasDataRetrievalRequestSignal(effectiveAddressUserMessage) || + hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage); + const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) || + hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) || + hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) || + hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage); + const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) || + hasStandaloneAddressTopicSignal(repairedRawUserMessage) || + hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) || + hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage); + const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) || + hasOpenContractsAddressSignal(repairedRawUserMessage) || + hasOpenContractsAddressSignal(effectiveAddressUserMessage) || + hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage); + const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage; + const modeDetection = detectAddressQuestionMode(modeSample); + const modeDetectionRaw = detectAddressQuestionMode(repairedRawUserMessage || rawUserMessage); + const resolvedModeDetection = modeDetection.mode === "address_query" ? modeDetection : modeDetectionRaw; + const intentResolution = resolveAddressIntent(modeSample); + const intentResolutionRaw = resolveAddressIntent(repairedRawUserMessage || rawUserMessage); + const resolvedIntentResolution = intentResolution.intent !== "unknown" ? intentResolution : intentResolutionRaw; + const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); + const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); + const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason && + /(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason)); + const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && + typeof llmPreDecomposeMeta.semanticExtractionContract === "object" + ? llmPreDecomposeMeta.semanticExtractionContract + : null; + const semanticContractValid = semanticExtractionContract?.valid !== false; + const semanticApplyCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; + const semanticReasonCodes = Array.isArray(semanticExtractionContract?.reason_codes) + ? semanticExtractionContract.reason_codes + : []; + const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) || + hasStrictDeepInvestigationCue(repairedRawUserMessage) || + hasStrictDeepInvestigationCue(effectiveAddressUserMessage) || + hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage); + const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(resolvedIntentResolution.intent) || + shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent); + const keepAddressLaneByIntent = semanticApplyCanonicalRecommended && + Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || + (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || + openContractsAddressSignal) && + (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed); + const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) || + hasStrongDataIntentSignal(repairedRawUserMessage) || + hasStrongDataIntentSignal(effectiveAddressUserMessage) || + hasStrongDataIntentSignal(repairedEffectiveAddressUserMessage) || + hasAccountingSignal(rawUserMessage) || + hasAccountingSignal(repairedRawUserMessage) || + hasAccountingSignal(effectiveAddressUserMessage) || + hasAccountingSignal(repairedEffectiveAddressUserMessage) || + hasDataRetrievalRequestSignal(rawUserMessage) || + hasDataRetrievalRequestSignal(repairedRawUserMessage); + const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); + const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown"); + const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" && + (!llmContractIntent || llmContractIntent === "unknown")); + const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) || + hasDangerOrCoercionSignal(repairedRawUserMessage) || + hasDangerOrCoercionSignal(effectiveAddressUserMessage) || + hasDangerOrCoercionSignal(repairedEffectiveAddressUserMessage); + const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) || + hasAddressFollowupContextSignal(repairedRawUserMessage) || + hasAddressFollowupContextSignal(effectiveAddressUserMessage) || + hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(rawUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || + hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage); + const protectedInventoryShortFollowup = Boolean(followupContext && + isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) && + (hasShortInventoryObjectFollowupSignal(rawUserMessage) || + hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || + hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || + hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); + const organizationClarificationContinuationDetected = Boolean(followupContext && + lastOrganizationClarificationDebug && + organizationClarificationSelection && + !dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal); + const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; + const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal && + !effectiveAddressFollowupSignal && + resolvedModeDetection.mode === "unsupported" && + resolvedIntentResolution.intent === "unknown"); + const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && + deterministicNonDomainGuard && + (llmFirstUnsupportedCandidate || llmContractMode === null) && + !protectedInventoryShortFollowup && + !organizationClarificationContinuationDetected); + const contextualHistoricalCapabilityFollowupDetected = Boolean(capabilityMetaQuery && + !dataScopeMetaQuery && + !dataRetrievalSignal && + (hasHistoricalCapabilityFollowupSignal(rawUserMessage) || + hasHistoricalCapabilityFollowupSignal(repairedRawUserMessage) || + hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) || + hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) && + isGroundedInventoryContextDebug(lastGroundedAddressDebug)); + const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery && + !capabilityMetaQuery && + !dataRetrievalSignal && + !strongDataSignal && + !aggregateBusinessAnalyticsSignal && + (hasConversationMemoryRecallFollowupSignal(rawUserMessage) || + hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) || + hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) || + hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) && + (lastGroundedAddressDebug || + findLastAddressAssistantItem(sessionItems)?.debug)); + const hardMetaMode = dataScopeMetaQuery + ? "data_scope" + : capabilityMetaQuery && !dataRetrievalSignal + ? "capability" + : null; + if (hardMetaMode === "data_scope") { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "assistant_data_scope_query_detected", + livingMode: "chat", + livingReason: "assistant_data_scope_query_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "data_scope", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "assistant_data_scope_query_detected", + living_mode: "chat", + living_reason: "assistant_data_scope_query_detected" + } + } + }; + } + if (hardMetaMode === "capability") { + if (contextualHistoricalCapabilityFollowupDetected) { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "inventory_history_capability_followup_detected", + livingMode: "chat", + livingReason: "inventory_history_capability_followup_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "capability", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "inventory_history_capability_followup_detected", + living_mode: "chat", + living_reason: "inventory_history_capability_followup_detected" + } + } + }; + } + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "assistant_capability_query_detected", + livingMode: "chat", + livingReason: "assistant_capability_query_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "capability", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "assistant_capability_query_detected", + living_mode: "chat", + living_reason: "assistant_capability_query_detected" + } + } + }; + } + if (nonDomainQueryIndexed) { + if (contextualMemoryRecapFollowupDetected) { + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "memory_recap_followup_detected", + livingMode: "chat", + livingReason: "memory_recap_followup_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "non_domain", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "memory_recap_followup_detected", + living_mode: "chat", + living_reason: "memory_recap_followup_detected" + } + } + }; + } + return { + runAddressLane: false, + toolGateDecision: "skip_address_lane", + toolGateReason: "non_domain_query_indexed", + livingMode: "chat", + livingReason: "non_domain_query_indexed", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: "non_domain", + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: false, + final_decision: { + run_address_lane: false, + tool_gate_decision: "skip_address_lane", + tool_gate_reason: "non_domain_query_indexed", + living_mode: "chat", + living_reason: "non_domain_query_indexed" + } + } + }; + } + const metaAnswerFollowupSignal = hasMetaAnswerFollowupSignal(rawUserMessage) || + hasMetaAnswerFollowupSignal(repairedRawUserMessage) || + hasMetaAnswerFollowupSignal(effectiveAddressUserMessage) || + hasMetaAnswerFollowupSignal(repairedEffectiveAddressUserMessage); + const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); + const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected && + llmPreDecomposeMeta?.applied && + llmContractMode === "address_query") || + hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) || + hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) || + hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) || + hasSameDateAccountFollowupSignalForPredecompose(repairedEffectiveAddressUserMessage) || + hasLooseAllTimeAddressLookupSignal(rawUserMessage) || + hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) || + hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) || + hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) || + hasAddressFollowupContextSignal(rawUserMessage) || + hasAddressFollowupContextSignal(effectiveAddressUserMessage) || + hasAddressFollowupContextSignal(repairedRawUserMessage) || + hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(rawUserMessage) || + hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || + hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage)); + const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) && + Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || + (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || + openContractsAddressSignal); + const semanticGuardHints = semanticExtractionContract?.guard_hints && + typeof semanticExtractionContract.guard_hints === "object" + ? semanticExtractionContract.guard_hints + : null; + const semanticExtraction = semanticExtractionContract?.extraction && + typeof semanticExtractionContract.extraction === "object" + ? semanticExtractionContract.extraction + : null; + const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true; + const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" || + semanticExtraction?.aggregation_profile === "management_profile"; + const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true); + const followupSemanticOverrideToDeepAllowed = Boolean(followupContext && + !supportedAddressIntentDetected && + (rootContextOnlyFollowup || + llmContractMode === "unsupported" || + semanticAggregateShapeDetected || + semanticDeepInvestigationHintDetected || + !semanticApplyCanonicalRecommended)); + const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") || + llmContractMode === "unsupported" || + (rootContextOnlyFollowup && + resolvedIntentResolution.intent === "unknown" && + (!llmContractIntent || llmContractIntent === "unknown")); + const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + unsupportedIntentOrMode && + strongDataSignal && + (rootContextOnlyFollowup || + llmContractMode === "deep_analysis" || + !dataRetrievalSignal || + strictDeepInvestigationCueDetected || + semanticDeepInvestigationHintDetected || + aggregateBusinessAnalyticsSignal) && + !preserveAddressLaneSignal && + !keepAddressLaneByIntent && + !supportedAddressIntentDetected && + (!followupContext || followupSemanticOverrideToDeepAllowed)); + const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) || + hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) || + hasDeepAnalysisPreferenceSignal(effectiveAddressUserMessage) || + hasDeepAnalysisPreferenceSignal(repairedEffectiveAddressUserMessage) || + hasDirectDeepAnalysisSignal(rawUserMessage) || + hasDirectDeepAnalysisSignal(repairedRawUserMessage) || + hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) || + hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage)); + const vatExplainFollowupSignal = Boolean(followupContext && + toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && + /(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); + const vatEvaluativeFollowupSignal = Boolean(followupContext && + toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && + /(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); + const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + (deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) && + !keepAddressLaneByIntent && + !supportedAddressIntentDetected && + !vatExplainFollowupSignal && + (!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed)); + const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + aggregateBusinessAnalyticsSignal && + !keepAddressLaneByIntent && + !supportedAddressIntentDetected && + (!followupContext || + llmContractMode === "unsupported" || + semanticAggregateShapeDetected || + !semanticApplyCanonicalRecommended || + standaloneAddressTopicSignal)); + const deepSessionContinuationFallbackToDeep = Boolean(!followupContext && + baseToolGate?.runAddressLane && + !llmRuntimeUnavailableDetected && + hasDeepSessionContinuationSignal({ + rawUserMessage, + repairedRawUserMessage, + effectiveAddressUserMessage, + repairedEffectiveAddressUserMessage, + sessionItems + })); + const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); + const metaFollowupOverGroundedAnswer = Boolean(followupContext && + hasPriorAddressAnswerContext && + (metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) && + !dataScopeMetaQuery && + !capabilityMetaQuery && + !aggregateBusinessAnalyticsSignal && + !dataRetrievalSignal && + !strongDataSignal && + resolvedModeDetection.mode !== "address_query" && + resolvedIntentResolution.intent === "unknown" && + (!llmContractIntent || llmContractIntent === "unknown") && + llmContractMode !== "address_query"); + let runAddressLane = Boolean(baseToolGate?.runAddressLane); + let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); + let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); + if (unsupportedAddressIntentFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "address_signal_unsupported_intent_fallback_to_deep"; + } + if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "deep_analysis_signal_fallback_to_deep"; + } + if (aggregateAnalyticsFallbackToDeep && + !unsupportedAddressIntentFallbackToDeep && + !deepAnalysisSignalFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "aggregate_analytics_signal_fallback_to_deep"; + } + if (deepSessionContinuationFallbackToDeep) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "deep_session_continuation_fallback_to_deep"; + } + if (metaFollowupOverGroundedAnswer) { + runAddressLane = false; + toolGateDecision = "skip_address_lane"; + toolGateReason = "meta_followup_over_grounded_answer"; + } + let livingDecision = resolveLivingAssistantModeDecision({ + userMessage: rawUserMessage, + addressLaneTriggered: runAddressLane, + useMock, + predecomposeMode: llmPreDecomposeMeta?.predecomposeContract?.mode ?? null, + predecomposeModeConfidence: llmPreDecomposeMeta?.predecomposeContract?.mode_confidence ?? null + }); + if (unsupportedAddressIntentFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "unsupported_address_intent_fallback_to_deep" + }; + } + if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "deep_analysis_signal_fallback_to_deep" + }; + } + if (aggregateAnalyticsFallbackToDeep && + !unsupportedAddressIntentFallbackToDeep && + !deepAnalysisSignalFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "aggregate_analytics_signal_fallback_to_deep" + }; + } + if (deepSessionContinuationFallbackToDeep) { + livingDecision = { + mode: "deep_analysis", + reason: "deep_session_continuation_fallback_to_deep" + }; + } + if (metaFollowupOverGroundedAnswer) { + livingDecision = { + mode: "chat", + reason: "meta_followup_over_grounded_answer" + }; + } + return { + runAddressLane, + toolGateDecision, + toolGateReason, + livingMode: livingDecision.mode, + livingReason: livingDecision.reason, + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + hard_meta_mode: null, + address_mode: resolvedModeDetection.mode, + address_mode_confidence: resolvedModeDetection.confidence, + address_intent: resolvedIntentResolution.intent, + address_intent_confidence: resolvedIntentResolution.confidence, + strong_data_signal_detected: strongDataSignal, + data_retrieval_signal_detected: dataRetrievalSignal, + semantic_contract_valid: semanticContractValid, + semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended, + semantic_reason_codes: semanticReasonCodes, + semantic_route_arbitration: { + supported_address_intent_detected: supportedAddressIntentDetected, + strict_deep_investigation_bypass_allowed: strictDeepInvestigationBypassAllowed, + semantic_deep_investigation_hint_detected: semanticDeepInvestigationHintDetected, + semantic_aggregate_shape_detected: semanticAggregateShapeDetected, + followup_semantic_override_to_deep_allowed: followupSemanticOverrideToDeepAllowed + }, + followup_context_detected: Boolean(followupContext), + unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep, + deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep, + aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep, + deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep, + final_decision: { + run_address_lane: runAddressLane, + tool_gate_decision: toolGateDecision, + tool_gate_reason: toolGateReason, + living_mode: livingDecision.mode, + living_reason: livingDecision.reason + } + } + }; + } + return { + resolveAssistantOrchestrationDecision + }; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index a71839e..43c2057 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -21,6 +21,8 @@ import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAtte import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter"; import * as assistantBoundaryPolicy_1 from "./assistantBoundaryPolicy"; +import * as assistantLivingModePolicy_1 from "./assistantLivingModePolicy"; +import * as assistantRoutePolicy_1 from "./assistantRoutePolicy"; import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter"; import * as assistantOrganizationMatcher_1 from "./assistantOrganizationMatcher"; import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter"; @@ -4211,623 +4213,18 @@ function hasOpenContractsAddressSignal(text) { const hasTemporalCue = hasPeriodLiteral(normalized) || /\b\d{4}[-/.]\d{2}[-/.]\d{2}\b/.test(normalized); return hasRequestCue || hasTemporalCue; } -const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ - "period_coverage_profile", - "document_type_and_account_section_profile", - "counterparty_population_and_roles", - "counterparty_activity_lifecycle", - "customer_revenue_and_payments", - "supplier_payouts_profile", - "open_contracts_confirmed_as_of_date", - "list_open_contracts", - "open_items_by_counterparty_or_contract", - "list_payables_counterparties", - "list_receivables_counterparties", - "inventory_on_hand_as_of_date", - "payables_confirmed_as_of_date", - "receivables_confirmed_as_of_date", - "list_documents_by_contract", - "bank_operations_by_contract", - "list_documents_by_counterparty", - "bank_operations_by_counterparty", - "list_contracts_by_counterparty", - "inventory_purchase_provenance_for_item", - "inventory_purchase_documents_for_item", - "inventory_supplier_stock_overlap_as_of_date", - "inventory_sale_trace_for_item", - "inventory_profitability_for_item", - "inventory_purchase_to_sale_chain", - "inventory_aging_by_purchase_date", - "contract_usage_overview", - "contract_usage_and_value", - "vat_payable_forecast", - "vat_liability_confirmed_for_tax_period", - "vat_payable_confirmed_as_of_date" -]); -const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([ - "inventory_purchase_provenance_for_item", - "inventory_purchase_documents_for_item", - "inventory_sale_trace_for_item", - "inventory_profitability_for_item", - "inventory_purchase_to_sale_chain" -]); -function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) { - return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent)); -} export function resolveAssistantOrchestrationDecision(input) { - const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? ""); - const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage); - const repairedRawUserMessage = repairAddressMojibake(rawUserMessage); - const repairedEffectiveAddressUserMessage = repairAddressMojibake(effectiveAddressUserMessage); - const followupContext = input?.followupContext ?? null; - const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null; - const useMock = Boolean(input?.useMock); - const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null; - const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" - ? input.sessionOrganizationScope - : null; - const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems); - const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); - const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) - ? mergeKnownOrganizations([ - ...lastOrganizationClarificationDebug.organization_candidates, - ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) - ? sessionOrganizationScope.knownOrganizations - : [])) - ]) - : []; - const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization); - const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? - resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? - resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? - resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ?? - (organizationClarificationSelectionFromScope && - organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope) - ? organizationClarificationSelectionFromScope - : null); - const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) || - hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) || - hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) || - hasAssistantDataScopeMetaQuestionSignal(repairedEffectiveAddressUserMessage); - const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawUserMessage) || - shouldHandleAsAssistantCapabilityMetaQuery(repairedRawUserMessage) || - shouldHandleAsAssistantCapabilityMetaQuery(effectiveAddressUserMessage) || - shouldHandleAsAssistantCapabilityMetaQuery(repairedEffectiveAddressUserMessage); - const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawUserMessage) || - hasDataRetrievalRequestSignal(repairedRawUserMessage) || - hasDataRetrievalRequestSignal(effectiveAddressUserMessage) || - hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage); - const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) || - hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) || - hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) || - hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage); - const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) || - hasStandaloneAddressTopicSignal(repairedRawUserMessage) || - hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) || - hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage); - const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) || - hasOpenContractsAddressSignal(repairedRawUserMessage) || - hasOpenContractsAddressSignal(effectiveAddressUserMessage) || - hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage); - const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage; - const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample); - const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedRawUserMessage || rawUserMessage); - const resolvedModeDetection = modeDetection.mode === "address_query" ? modeDetection : modeDetectionRaw; - const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample); - const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(repairedRawUserMessage || rawUserMessage); - const resolvedIntentResolution = intentResolution.intent !== "unknown" ? intentResolution : intentResolutionRaw; - const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); - const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); - const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason && - /(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason)); - const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract && - typeof llmPreDecomposeMeta.semanticExtractionContract === "object" - ? llmPreDecomposeMeta.semanticExtractionContract - : null; - const semanticContractValid = semanticExtractionContract?.valid !== false; - const semanticApplyCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false; - const semanticReasonCodes = Array.isArray(semanticExtractionContract?.reason_codes) - ? semanticExtractionContract.reason_codes - : []; - const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) || - hasStrictDeepInvestigationCue(repairedRawUserMessage) || - hasStrictDeepInvestigationCue(effectiveAddressUserMessage) || - hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage); - const strictDeepInvestigationBypassAllowed = shouldBypassStrictDeepInvestigationCueForAddressIntent(resolvedIntentResolution.intent) || - shouldBypassStrictDeepInvestigationCueForAddressIntent(llmContractIntent); - const keepAddressLaneByIntent = semanticApplyCanonicalRecommended && - Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || - (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || - openContractsAddressSignal) && - (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed); - const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) || - hasStrongDataIntentSignal(repairedRawUserMessage) || - hasStrongDataIntentSignal(effectiveAddressUserMessage) || - hasStrongDataIntentSignal(repairedEffectiveAddressUserMessage) || - hasAccountingSignal(rawUserMessage) || - hasAccountingSignal(repairedRawUserMessage) || - hasAccountingSignal(effectiveAddressUserMessage) || - hasAccountingSignal(repairedEffectiveAddressUserMessage) || - hasDataRetrievalRequestSignal(rawUserMessage) || - hasDataRetrievalRequestSignal(repairedRawUserMessage); - const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); - const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown"); - const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" && - (!llmContractIntent || llmContractIntent === "unknown")); - const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) || - hasDangerOrCoercionSignal(repairedRawUserMessage) || - hasDangerOrCoercionSignal(effectiveAddressUserMessage) || - hasDangerOrCoercionSignal(repairedEffectiveAddressUserMessage); - const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) || - hasAddressFollowupContextSignal(repairedRawUserMessage) || - hasAddressFollowupContextSignal(effectiveAddressUserMessage) || - hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(rawUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || - hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage); - const protectedInventoryShortFollowup = Boolean(followupContext && - isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) && - (hasShortInventoryObjectFollowupSignal(rawUserMessage) || - hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || - hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || - hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); - const organizationClarificationContinuationDetected = Boolean(followupContext && - lastOrganizationClarificationDebug && - organizationClarificationSelection && - !dataScopeMetaQuery && - !capabilityMetaQuery && - !dataRetrievalSignal); - const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal; - const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery && - !capabilityMetaQuery && - !dataRetrievalSignal && - !effectiveAddressFollowupSignal && - resolvedModeDetection.mode === "unsupported" && - resolvedIntentResolution.intent === "unknown"); - const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && - deterministicNonDomainGuard && - (llmFirstUnsupportedCandidate || llmContractMode === null) && - !protectedInventoryShortFollowup && - !organizationClarificationContinuationDetected); - const contextualHistoricalCapabilityFollowupDetected = Boolean(capabilityMetaQuery && - !dataScopeMetaQuery && - !dataRetrievalSignal && - (hasHistoricalCapabilityFollowupSignal(rawUserMessage) || - hasHistoricalCapabilityFollowupSignal(repairedRawUserMessage) || - hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) || - hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) && - isGroundedInventoryContextDebug(lastGroundedAddressDebug)); - const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery && - !capabilityMetaQuery && - !dataRetrievalSignal && - !strongDataSignal && - !aggregateBusinessAnalyticsSignal && - (hasConversationMemoryRecallFollowupSignal(rawUserMessage) || - hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) || - hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) || - hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) && - (lastGroundedAddressDebug || - findLastAddressAssistantItem(sessionItems)?.debug)); - const hardMetaMode = dataScopeMetaQuery - ? "data_scope" - : capabilityMetaQuery && !dataRetrievalSignal - ? "capability" - : null; - if (hardMetaMode === "data_scope") { - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "assistant_data_scope_query_detected", - livingMode: "chat", - livingReason: "assistant_data_scope_query_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "data_scope", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "assistant_data_scope_query_detected", - living_mode: "chat", - living_reason: "assistant_data_scope_query_detected" - } - } - }; - } - if (hardMetaMode === "capability") { - if (contextualHistoricalCapabilityFollowupDetected) { - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "inventory_history_capability_followup_detected", - livingMode: "chat", - livingReason: "inventory_history_capability_followup_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "capability", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "inventory_history_capability_followup_detected", - living_mode: "chat", - living_reason: "inventory_history_capability_followup_detected" - } - } - }; - } - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "assistant_capability_query_detected", - livingMode: "chat", - livingReason: "assistant_capability_query_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "capability", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "assistant_capability_query_detected", - living_mode: "chat", - living_reason: "assistant_capability_query_detected" - } - } - }; - } - if (nonDomainQueryIndexed) { - if (contextualMemoryRecapFollowupDetected) { - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "memory_recap_followup_detected", - livingMode: "chat", - livingReason: "memory_recap_followup_detected", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "non_domain", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "memory_recap_followup_detected", - living_mode: "chat", - living_reason: "memory_recap_followup_detected" - } - } - }; - } - return { - runAddressLane: false, - toolGateDecision: "skip_address_lane", - toolGateReason: "non_domain_query_indexed", - livingMode: "chat", - livingReason: "non_domain_query_indexed", - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: "non_domain", - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: false, - final_decision: { - run_address_lane: false, - tool_gate_decision: "skip_address_lane", - tool_gate_reason: "non_domain_query_indexed", - living_mode: "chat", - living_reason: "non_domain_query_indexed" - } - } - }; - } - const metaAnswerFollowupSignal = hasMetaAnswerFollowupSignal(rawUserMessage) || - hasMetaAnswerFollowupSignal(repairedRawUserMessage) || - hasMetaAnswerFollowupSignal(effectiveAddressUserMessage) || - hasMetaAnswerFollowupSignal(repairedEffectiveAddressUserMessage); - const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage); - const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected && - llmPreDecomposeMeta?.applied && - llmContractMode === "address_query") || - hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) || - hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) || - hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) || - hasSameDateAccountFollowupSignalForPredecompose(repairedEffectiveAddressUserMessage) || - hasLooseAllTimeAddressLookupSignal(rawUserMessage) || - hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) || - hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) || - hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) || - hasAddressFollowupContextSignal(rawUserMessage) || - hasAddressFollowupContextSignal(effectiveAddressUserMessage) || - hasAddressFollowupContextSignal(repairedRawUserMessage) || - hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(rawUserMessage) || - hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) || - hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage)); - const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) && - Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) || - (llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) || - openContractsAddressSignal); - const semanticGuardHints = semanticExtractionContract?.guard_hints && - typeof semanticExtractionContract.guard_hints === "object" - ? semanticExtractionContract.guard_hints - : null; - const semanticExtraction = semanticExtractionContract?.extraction && - typeof semanticExtractionContract.extraction === "object" - ? semanticExtractionContract.extraction - : null; - const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true; - const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" || - semanticExtraction?.aggregation_profile === "management_profile"; - const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true); - const followupSemanticOverrideToDeepAllowed = Boolean(followupContext && - !supportedAddressIntentDetected && - (rootContextOnlyFollowup || - llmContractMode === "unsupported" || - semanticAggregateShapeDetected || - semanticDeepInvestigationHintDetected || - !semanticApplyCanonicalRecommended)); - const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") || - llmContractMode === "unsupported" || - (rootContextOnlyFollowup && - resolvedIntentResolution.intent === "unknown" && - (!llmContractIntent || llmContractIntent === "unknown")); - const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - unsupportedIntentOrMode && - strongDataSignal && - (rootContextOnlyFollowup || - llmContractMode === "deep_analysis" || - !dataRetrievalSignal || - strictDeepInvestigationCueDetected || - semanticDeepInvestigationHintDetected || - aggregateBusinessAnalyticsSignal) && - !preserveAddressLaneSignal && - !keepAddressLaneByIntent && - !supportedAddressIntentDetected && - (!followupContext || followupSemanticOverrideToDeepAllowed)); - const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) || - hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) || - hasDeepAnalysisPreferenceSignal(effectiveAddressUserMessage) || - hasDeepAnalysisPreferenceSignal(repairedEffectiveAddressUserMessage) || - hasDirectDeepAnalysisSignal(rawUserMessage) || - hasDirectDeepAnalysisSignal(repairedRawUserMessage) || - hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) || - hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage)); - const vatExplainFollowupSignal = Boolean(followupContext && - toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && - /(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); - const vatEvaluativeFollowupSignal = Boolean(followupContext && - toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && - /(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`))); - const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - (deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) && - !keepAddressLaneByIntent && - !supportedAddressIntentDetected && - !vatExplainFollowupSignal && - (!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed)); - const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - aggregateBusinessAnalyticsSignal && - !keepAddressLaneByIntent && - !supportedAddressIntentDetected && - (!followupContext || - llmContractMode === "unsupported" || - semanticAggregateShapeDetected || - !semanticApplyCanonicalRecommended || - standaloneAddressTopicSignal)); - const deepSessionContinuationFallbackToDeep = Boolean(!followupContext && - baseToolGate?.runAddressLane && - !llmRuntimeUnavailableDetected && - hasDeepSessionContinuationSignal({ - rawUserMessage, - repairedRawUserMessage, - effectiveAddressUserMessage, - repairedEffectiveAddressUserMessage, - sessionItems - })); - const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); - const metaFollowupOverGroundedAnswer = Boolean(followupContext && - hasPriorAddressAnswerContext && - (metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) && - !dataScopeMetaQuery && - !capabilityMetaQuery && - !aggregateBusinessAnalyticsSignal && - !dataRetrievalSignal && - !strongDataSignal && - resolvedModeDetection.mode !== "address_query" && - resolvedIntentResolution.intent === "unknown" && - (!llmContractIntent || llmContractIntent === "unknown") && - llmContractMode !== "address_query"); - let runAddressLane = Boolean(baseToolGate?.runAddressLane); - let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); - let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); - if (unsupportedAddressIntentFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "address_signal_unsupported_intent_fallback_to_deep"; - } - if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "deep_analysis_signal_fallback_to_deep"; - } - if (aggregateAnalyticsFallbackToDeep && - !unsupportedAddressIntentFallbackToDeep && - !deepAnalysisSignalFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "aggregate_analytics_signal_fallback_to_deep"; - } - if (deepSessionContinuationFallbackToDeep) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "deep_session_continuation_fallback_to_deep"; - } - if (metaFollowupOverGroundedAnswer) { - runAddressLane = false; - toolGateDecision = "skip_address_lane"; - toolGateReason = "meta_followup_over_grounded_answer"; - } - let livingDecision = resolveLivingAssistantModeDecision({ - userMessage: rawUserMessage, - addressLaneTriggered: runAddressLane, - useMock, - predecomposeMode: llmPreDecomposeMeta?.predecomposeContract?.mode ?? null, - predecomposeModeConfidence: llmPreDecomposeMeta?.predecomposeContract?.mode_confidence ?? null - }); - if (unsupportedAddressIntentFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "unsupported_address_intent_fallback_to_deep" - }; - } - if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "deep_analysis_signal_fallback_to_deep" - }; - } - if (aggregateAnalyticsFallbackToDeep && - !unsupportedAddressIntentFallbackToDeep && - !deepAnalysisSignalFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "aggregate_analytics_signal_fallback_to_deep" - }; - } - if (deepSessionContinuationFallbackToDeep) { - livingDecision = { - mode: "deep_analysis", - reason: "deep_session_continuation_fallback_to_deep" - }; - } - if (metaFollowupOverGroundedAnswer) { - livingDecision = { - mode: "chat", - reason: "meta_followup_over_grounded_answer" - }; - } - return { - runAddressLane, - toolGateDecision, - toolGateReason, - livingMode: livingDecision.mode, - livingReason: livingDecision.reason, - orchestrationContract: { - schema_version: "assistant_orchestration_contract_v1", - hard_meta_mode: null, - address_mode: resolvedModeDetection.mode, - address_mode_confidence: resolvedModeDetection.confidence, - address_intent: resolvedIntentResolution.intent, - address_intent_confidence: resolvedIntentResolution.confidence, - strong_data_signal_detected: strongDataSignal, - data_retrieval_signal_detected: dataRetrievalSignal, - semantic_contract_valid: semanticContractValid, - semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended, - semantic_reason_codes: semanticReasonCodes, - semantic_route_arbitration: { - supported_address_intent_detected: supportedAddressIntentDetected, - strict_deep_investigation_bypass_allowed: strictDeepInvestigationBypassAllowed, - semantic_deep_investigation_hint_detected: semanticDeepInvestigationHintDetected, - semantic_aggregate_shape_detected: semanticAggregateShapeDetected, - followup_semantic_override_to_deep_allowed: followupSemanticOverrideToDeepAllowed - }, - followup_context_detected: Boolean(followupContext), - unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep, - deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep, - aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep, - deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep, - final_decision: { - run_address_lane: runAddressLane, - tool_gate_decision: toolGateDecision, - tool_gate_reason: toolGateReason, - living_mode: livingDecision.mode, - living_reason: livingDecision.reason - } - } - }; + return assistantRoutePolicy.resolveAssistantOrchestrationDecision(input); } + function hasStrongDataIntentSignal(text) { - const lower = String(text ?? "").toLowerCase(); - return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower); + return assistantLivingModePolicy.hasStrongDataIntentSignal(text); } function hasDataRetrievalRequestSignal(text) { - const lower = compactWhitespace(String(text ?? "").toLowerCase()); - if (!lower) { - return false; - } - const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower); - const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover|warehouse|inventory|stock|item)/iu.test(lower); - if (hasBroadInterrogative && hasBroadBusinessObject) { - return true; - } - const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a|\u043f\u0440\u043e\u0432\u0435\u0440\u044c|\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c)(?:$|[\s,.!?;:])/iu.test(lower); - const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b)/iu.test(lower); - if (hasRussianRetrievalAction && hasRussianRetrievalObject) { - return true; - } - const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower); - const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower); - if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) { - return false; - } - const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower); - if (!hasRetrievalObject) { - return false; - } - if (hasExplicitRetrievalAction) { - return true; - } - const hasMetaCapabilityShape = /(?:мож(?:ем|ешь|ете|но)|уме(?:ешь|ете)|доступ|подключ|чья|как\s+называ(?:ет|ется)|работ(?:ать|аем|аешь|аете)|в\s+тебе|у\s+тебя)/i.test(lower); - return !hasMetaCapabilityShape; + return assistantLivingModePolicy.hasDataRetrievalRequestSignal(text); } function hasOrganizationFactLookupSignal(text) { - const repaired = repairAddressMojibake(String(text ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - const hasFactCue = /(?:возраст|сколько\s+лет|дата\s+регистрац|когда\s+(?:зарегистр|создан|основан)|год\s+регистрац|год\s+основан|с\s+какого\s+года|when\s+was\s+(?:it\s+)?(?:registered|founded|created))/i.test(normalized); - if (!hasFactCue) { - return false; - } - return /(?:организац|компан|контор|фирм|ооо|ао|зао|ип|альтернатив|лайсвуд|райм|organization|company)/i.test(normalized); + return assistantLivingModePolicy.hasOrganizationFactLookupSignal(text); } function findLastAssistantLivingChatDebug(items) { if (!Array.isArray(items)) { @@ -4888,67 +4285,13 @@ function findLastOrganizationClarificationAddressDebug(items) { return null; } function hasMetaAnswerFollowupSignal(userMessage) { - const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); - const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); - const samples = [rawText, repairedText] - .filter((item) => item.length > 0) - .map((item) => item.replace(/ё/g, "е")); - if (samples.length === 0) { - return false; - } - const hasReflectionCue = samples.some((sample) => sample.includes("дума") || - sample.includes("скаж") || - sample.includes("мнение") || - sample.includes("как тебе") || - sample.includes("норм") || - sample.includes("стран") || - sample.includes("логич") || - sample.includes("смуща") || - sample.includes("выгляд")); - const hasTopicPointerCue = samples.some((sample) => sample.includes("на эту тему") || - sample.includes("по этому поводу") || - sample.includes("об этом") || - (sample.includes("это") && hasReferentialPointer(sample))); - const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample)); - if (!((hasReflectionCue || hasEvaluationCue) && - (hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) { - return false; - } - return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || - shouldHandleAsAssistantCapabilityMetaQuery(sample) || - hasDataRetrievalRequestSignal(sample) || - hasStrongDataIntentSignal(sample)); + return assistantLivingModePolicy.hasMetaAnswerFollowupSignal(userMessage); } function hasConversationMemoryRecallFollowupSignal(userMessage) { - const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase()); - const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); - const samples = [rawText, repairedText] - .filter((item) => item.length > 0) - .map((item) => item.replace(/ё/g, "е")); - if (samples.length === 0) { - return false; - } - const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample)); - const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample)); - if (!hasMemoryCue || !hasDiscussionCue) { - return false; - } - return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) || - shouldHandleAsAssistantCapabilityMetaQuery(sample) || - hasDataRetrievalRequestSignal(sample) || - hasStrongDataIntentSignal(sample)); + return assistantLivingModePolicy.hasConversationMemoryRecallFollowupSignal(userMessage); } function hasHistoricalCapabilityFollowupSignal(text) { - const repaired = repairAddressMojibake(String(text ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - const hasHistoryCue = /(?:историческ|история|архив|прошл(?:ый|ые|ую|ых)?|раньше|ретро|старые\s+данные)/iu.test(normalized); - if (!hasHistoryCue) { - return false; - } - return /(?:мож(?:ешь|ете|но)|уме(?:ешь|ете)|показ|вывед|дай|раскрой)/iu.test(normalized); + return assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal(text); } function isGroundedInventoryContextDebug(debug) { if (!debug || typeof debug !== "object") { @@ -4965,56 +4308,10 @@ function isGroundedInventoryContextDebug(debug) { rootIntent === "inventory_on_hand_as_of_date"; } function hasOrganizationFactFollowupSignal(userMessage, items) { - const repaired = repairAddressMojibake(String(userMessage ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - if (hasOrganizationFactLookupSignal(normalized)) { - return false; - } - const hasFollowupCue = /(?:^|\s)(?:давай|го|погнали|ок(?:ей)?|хорошо|принято|подтверждаю|запрашивай|запроси|проверь|продолжай|ну\s+давай|да\s+давай)(?=$|[\s,.!?;:])/iu.test(normalized); - if (!hasFollowupCue) { - return false; - } - const lastDebug = findLastAssistantLivingChatDebug(items); - const lastSource = toNonEmptyString(lastDebug?.living_chat_response_source); - const lastGuardReason = toNonEmptyString(lastDebug?.living_chat_grounding_guard_reason); - const inOrganizationFactBoundary = lastSource === "deterministic_organization_fact_boundary" || - lastSource === "deterministic_organization_fact_boundary_followup" || - lastGuardReason === "organization_fact_without_live_source_blocked"; - return inOrganizationFactBoundary; + return assistantLivingModePolicy.hasOrganizationFactFollowupSignal(userMessage, items); } function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) { - const selected = normalizeOrganizationScopeValue(selectedOrganization); - if (!selected) { - return false; - } - const repaired = repairAddressMojibake(String(userMessage ?? "")); - const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - if (!normalized) { - return false; - } - if (hasOrganizationFactLookupSignal(normalized) || hasDataRetrievalRequestSignal(normalized) || hasStrongDataIntentSignal(normalized)) { - return false; - } - const hasAnalyticalCue = /(?:какой|какая|какие|когда|сколько|кто|почему|зачем|возраст|дата|регистрац|ндс|налог|контракт|договор|документ|операц|оборот|сумм|остат|сальдо|founded|registered|created)/i.test(normalized); - if (hasAnalyticalCue) { - return false; - } - const hasSelectionCue = /(?:давай|го|погнали|ок(?:ей)?|хорошо|отлично|берем|выберем|выбираем|переключ(?:им|аем|ай)|фиксир|работаем|обсудим|тогда)\b/i.test(normalized); - if (hasSelectionCue) { - return true; - } - const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС…|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|РїРёР·РґР°|РЅРёС…СѓСЏ|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) || - normalized.includes("\u0430\u0445\u0443") || - normalized.includes("\u043e\u0445\u0443") || - normalized.includes("\u043f\u0438\u0437\u0434") || - normalized.includes("\u0431\u043b\u044f"); - if (hasAffectiveReactionCue) { - return false; - } - return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? "")); + return assistantLivingModePolicy.shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization); } function hasOperationalAdminActionRequestSignal(text) { const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е"); @@ -5090,78 +4387,13 @@ function hasAssistantCapabilityQuestionSignal(text) { return false; } function hasAssistantDataScopeMetaQuestionSignal(text) { - const repaired = repairAddressMojibake(String(text ?? "")); - const lower = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е"); - const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с"); - if (!normalized) { - return false; - } - const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized); - if (hasDirectSlangScopeLead) { - return true; - } - const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized); - if (hasSlangScopeQuestion) { - return true; - } - const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized); - const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized); - const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized); - const hasNamingCue = /(?:как\s+называ(?:ет|ется)|что\s+за\s+(?:контор|компан|организац|фирм))/i.test(normalized); - const hasWorkabilityCue = /(?:мож(?:ем|ешь|ете|но)\s+работ|работ(?:ать|аем|аешь|аете))/i.test(normalized); - const hasScopeObject = hasBaseOrTenantObject || hasCompanyObject || hasConnectionCue; - if (!hasScopeObject) { - return false; - } - const hasMetaPerspective = /(?:ты|тебе|твой|у\s+тебя|в\s+тебе|мы|нам|наш(?:а|е|и|у|ей)?|сейчас|щас)/i.test(normalized); - const hasScopedInterrogativePair = /(?:^|\s)(?:по\s+какой|с\s+какой|какая|какой|какие)\s+(?:баз|компан|компин|компини|компани|организац|контор|фирм|тенант|контур)/i.test(normalized); - const hasScopeQuestion = /(?:чья|чье|чьи|доступн|подключен|подруб|воткнут|какая\s+баз|какой\s+баз)/i.test(normalized) || - hasNamingCue || - hasWorkabilityCue || - hasScopedInterrogativePair; - const hasInterrogativeScopeLead = /(?:^|\s)(?:по\s+какой|с\s+какой|чья|чье|чьи|which|who|what)/i.test(normalized); - const isQuestionLike = /[?]/.test(String(text ?? "")) || hasInterrogativeScopeLead || hasScopedInterrogativePair; - const hasExplicitScopeContext = hasBaseOrTenantObject || hasConnectionCue || hasWorkabilityCue || hasNamingCue; - const hasRetrievalSignal = hasDataRetrievalRequestSignal(normalized); - const hasContractAnalyticsCue = /(?:договор|контракт|contract).*(?:топ|сам(?:ый|ая|ое|ые)|крупн|жирн|оборот|бюджет|сумм|стоим|value|turnover|all\s+time|всю\s+истори|за\s+вс[её]\s+время)/iu.test(normalized); - if (hasContractAnalyticsCue) { - return false; - } - if (hasRetrievalSignal && !hasExplicitScopeContext) { - return false; - } - const hasEligibleScopeObject = hasBaseOrTenantObject || (hasCompanyObject && (hasConnectionCue || hasWorkabilityCue || hasNamingCue || hasMetaPerspective)); - return hasEligibleScopeObject && hasScopeQuestion && (hasMetaPerspective || isQuestionLike || hasExplicitScopeContext); + return assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal(text); } function shouldHandleAsAssistantCapabilityMetaQuery(text) { - const raw = String(text ?? ""); - const repaired = repairAddressMojibake(raw); - const hasScopeMetaSignal = hasAssistantDataScopeMetaQuestionSignal(raw) || hasAssistantDataScopeMetaQuestionSignal(repaired); - if (hasScopeMetaSignal) { - return true; - } - const hasCapabilitySignal = hasAssistantCapabilityQuestionSignal(raw) || - hasAssistantCapabilityQuestionSignal(repaired) || - hasOperationalAdminActionRequestSignal(raw) || - hasOperationalAdminActionRequestSignal(repaired); - const hasRetrievalSignal = hasDataRetrievalRequestSignal(raw) || hasDataRetrievalRequestSignal(repaired); - return hasCapabilitySignal && !hasRetrievalSignal; + return assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery(text); } function hasLivingChatSignal(text) { - const lower = compactWhitespace(String(text ?? "").toLowerCase()); - if (!lower) { - return false; - } - if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) { - return true; - } - if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) { - return true; - } - if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) { - return true; - } - return hasSmallTalkSignal(lower); + return assistantLivingModePolicy.hasLivingChatSignal(text); } function buildAssistantCapabilityContractReply() { return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)(); @@ -5265,6 +4497,55 @@ function normalizeOrganizationScopeValue(value) { .trim(); return unwrapped ? unwrapped : null; } +const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({ + featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1, + compactWhitespace, + repairAddressMojibake, + toNonEmptyString, + normalizeOrganizationScopeValue, + hasReferentialPointer, + hasSmallTalkSignal, + hasAssistantCapabilityQuestionSignal, + hasOperationalAdminActionRequestSignal +}); +const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePolicy)({ + repairAddressMojibake, + findLastGroundedAddressAnswerDebug, + findLastOrganizationClarificationAddressDebug, + mergeKnownOrganizations, + normalizeOrganizationScopeValue, + resolveOrganizationSelectionFromMessage, + hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal, + shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery, + hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal, + hasAggregateBusinessAnalyticsSignal, + hasStandaloneAddressTopicSignal, + hasOpenContractsAddressSignal, + detectAddressQuestionMode: addressQueryClassifier_1.detectAddressQuestionMode, + resolveAddressIntent: addressIntentResolver_1.resolveAddressIntent, + toNonEmptyString, + hasStrictDeepInvestigationCue, + hasStrongDataIntentSignal: assistantLivingModePolicy.hasStrongDataIntentSignal, + hasAccountingSignal, + hasDangerOrCoercionSignal, + hasAddressFollowupContextSignal, + hasShortDebtMirrorFollowupSignal, + isInventorySelectedObjectIntent, + hasShortInventoryObjectFollowupSignal, + hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal, + isGroundedInventoryContextDebug, + hasConversationMemoryRecallFollowupSignal: assistantLivingModePolicy.hasConversationMemoryRecallFollowupSignal, + findLastAddressAssistantItem, + hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal, + resolveAddressToolGateDecision, + hasSameDateAccountFollowupSignalForPredecompose, + hasLooseAllTimeAddressLookupSignal, + hasDeepAnalysisPreferenceSignal, + hasDirectDeepAnalysisSignal, + compactWhitespace, + hasDeepSessionContinuationSignal, + resolveLivingAssistantModeDecision: assistantLivingModePolicy.resolveLivingAssistantModeDecision +}); function normalizeOrganizationScopeSearchText(value) { const source = normalizeScopeKey(value); return source @@ -6063,67 +5344,7 @@ function applyLivingChatGroundingGuard(input) { }; } export function resolveLivingAssistantModeDecision(input) { - const userMessage = String(input?.userMessage ?? ""); - if (input?.addressLaneTriggered) { - return { - mode: "address_data", - reason: "address_lane_triggered" - }; - } - if (!config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1) { - return { - mode: "deep_analysis", - reason: "living_chat_router_disabled" - }; - } - if (Boolean(input?.useMock)) { - return { - mode: "deep_analysis", - reason: "mock_mode_keeps_deep_pipeline" - }; - } - if (hasAssistantDataScopeMetaQuestionSignal(userMessage)) { - return { - mode: "chat", - reason: "assistant_data_scope_query_detected" - }; - } - if (shouldHandleAsAssistantCapabilityMetaQuery(userMessage)) { - return { - mode: "chat", - reason: "assistant_capability_query_detected" - }; - } - if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) { - return { - mode: "chat", - reason: "organization_fact_lookup_signal_detected" - }; - } - if (hasStrongDataIntentSignal(userMessage)) { - return { - mode: "deep_analysis", - reason: "strong_data_signal_detected" - }; - } - if (hasLivingChatSignal(userMessage)) { - return { - mode: "chat", - reason: "living_chat_signal_detected" - }; - } - const predecomposeMode = toNonEmptyString(input?.predecomposeMode); - const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence); - if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) { - return { - mode: "deep_analysis", - reason: "predecompose_unsupported_mode_fallback_to_deep" - }; - } - return { - mode: "deep_analysis", - reason: "default_deep_pipeline" - }; + return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input); } export class AssistantService { normalizerService; diff --git a/llm_normalizer/backend/tests/assistantLivingModePolicy.test.ts b/llm_normalizer/backend/tests/assistantLivingModePolicy.test.ts new file mode 100644 index 0000000..abd1a89 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantLivingModePolicy.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import { createAssistantLivingModePolicy } from "../src/services/assistantLivingModePolicy"; + +function buildPolicy() { + return createAssistantLivingModePolicy({ + featureAssistantLivingChatRouterV1: true, + compactWhitespace: (text: string) => text.replace(/\s+/g, " ").trim(), + repairAddressMojibake: (text: string) => text, + toNonEmptyString: (value: unknown) => { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; + }, + normalizeOrganizationScopeValue: (value: unknown) => { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim().replace(/^"+|"+$/g, "").replace(/^'+|'+$/g, ""); + return text.length > 0 ? text : null; + }, + hasReferentialPointer: (text: string) => + /(по этому|по тому|это же|этой|этим|этому|этого|этот|эту|этом|это|эти|этих|из этого|из них|из этих|из тех|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test( + text.toLowerCase() + ), + hasSmallTalkSignal: (text: string) => /(привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase()), + hasAssistantCapabilityQuestionSignal: (text: string) => + /(?:кто ты|что ты можешь|какие фичи|полный список возможностей|чем ты можешь помочь|что ты умеешь)/i.test(text), + hasOperationalAdminActionRequestSignal: (text: string) => + /(?:настро|установ|подключ|обнов|почин|исправ|удал|снеси|delete\s+database|drop\s+database)/i.test(text) + }); +} + +describe("assistantLivingModePolicy", () => { + it("routes data-scope question to chat mode", () => { + const policy = buildPolicy(); + + const decision = policy.resolveLivingAssistantModeDecision({ + userMessage: "по какой компании мы можем работать?", + addressLaneTriggered: false, + useMock: false, + predecomposeMode: "unsupported", + predecomposeModeConfidence: "low" + }); + + expect(decision.mode).toBe("chat"); + expect(decision.reason).toBe("assistant_data_scope_query_detected"); + }); + + it("keeps explicit accounting question in deep mode", () => { + const policy = buildPolicy(); + + const decision = policy.resolveLivingAssistantModeDecision({ + userMessage: "покажи документы по сверке за 2020", + addressLaneTriggered: false, + useMock: false, + predecomposeMode: "unsupported", + predecomposeModeConfidence: "low" + }); + + expect(decision.mode).toBe("deep_analysis"); + expect(decision.reason).toBe("strong_data_signal_detected"); + }); + + it("detects organization fact follow-up after prior boundary reply", () => { + const policy = buildPolicy(); + + const detected = policy.hasOrganizationFactFollowupSignal("давай", [ + { + role: "assistant", + debug: { + living_chat_response_source: "deterministic_organization_fact_boundary", + living_chat_grounding_guard_reason: null + } + } + ]); + + expect(detected).toBe(true); + }); +}); diff --git a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts new file mode 100644 index 0000000..edc23cf --- /dev/null +++ b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from "vitest"; +import { createAssistantRoutePolicy } from "../src/services/assistantRoutePolicy"; + +function toNonEmptyString(value: unknown): string | null { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} + +function normalizeOrganizationScopeValue(value: unknown): string | null { + const text = toNonEmptyString(value); + if (!text) { + return null; + } + return text.replace(/^"+|"+$/g, "").replace(/^'+|'+$/g, ""); +} + +function buildPolicy(overrides: Record = {}) { + return createAssistantRoutePolicy({ + repairAddressMojibake: (text: string) => text, + findLastGroundedAddressAnswerDebug: () => null, + findLastOrganizationClarificationAddressDebug: () => null, + mergeKnownOrganizations: (values: unknown[]) => + Array.from( + new Set( + (Array.isArray(values) ? values : []) + .map((item) => normalizeOrganizationScopeValue(item)) + .filter((item): item is string => Boolean(item)) + ) + ), + normalizeOrganizationScopeValue, + resolveOrganizationSelectionFromMessage: () => null, + hasAssistantDataScopeMetaQuestionSignal: (text: string) => /по какой компании|какая база|по каким конторам/i.test(text), + shouldHandleAsAssistantCapabilityMetaQuery: (text: string) => /что ты можешь|что ты умеешь/i.test(text), + hasDataRetrievalRequestSignal: () => false, + hasAggregateBusinessAnalyticsSignal: () => false, + hasStandaloneAddressTopicSignal: () => false, + hasOpenContractsAddressSignal: () => false, + detectAddressQuestionMode: () => ({ mode: "unsupported", confidence: "low" }), + resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" }), + toNonEmptyString, + hasStrictDeepInvestigationCue: () => false, + hasStrongDataIntentSignal: () => false, + hasAccountingSignal: () => false, + hasDangerOrCoercionSignal: () => false, + hasAddressFollowupContextSignal: () => false, + hasShortDebtMirrorFollowupSignal: () => false, + isInventorySelectedObjectIntent: (intent: unknown) => /inventory/i.test(String(intent ?? "")), + hasShortInventoryObjectFollowupSignal: () => false, + hasHistoricalCapabilityFollowupSignal: () => false, + isGroundedInventoryContextDebug: (debug: unknown) => Boolean(debug), + hasConversationMemoryRecallFollowupSignal: () => false, + findLastAddressAssistantItem: () => null, + hasMetaAnswerFollowupSignal: () => false, + resolveAddressToolGateDecision: () => ({ + runAddressLane: false, + decision: "skip_address_lane", + reason: "no_address_signal_after_l0" + }), + hasSameDateAccountFollowupSignalForPredecompose: () => false, + hasLooseAllTimeAddressLookupSignal: () => false, + hasDeepAnalysisPreferenceSignal: () => false, + hasDirectDeepAnalysisSignal: () => false, + compactWhitespace: (text: string) => String(text ?? "").replace(/\s+/g, " ").trim(), + hasDeepSessionContinuationSignal: () => false, + resolveLivingAssistantModeDecision: (input: { addressLaneTriggered?: boolean }) => + input.addressLaneTriggered + ? { mode: "address_data", reason: "address_lane_triggered" } + : { mode: "chat", reason: "living_chat_signal_detected" }, + ...overrides + }); +} + +describe("assistantRoutePolicy", () => { + it("routes data-scope meta question to chat contract", () => { + const policy = buildPolicy(); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "по какой компании мы можем работать?", + effectiveAddressUserMessage: "по какой компании мы можем работать?", + followupContext: null, + llmPreDecomposeMeta: null, + useMock: false + }); + + expect(decision.runAddressLane).toBe(false); + expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected"); + expect(decision.livingMode).toBe("chat"); + expect(decision.orchestrationContract?.hard_meta_mode).toBe("data_scope"); + }); + + it("keeps supported address intent in address lane", () => { + const policy = buildPolicy({ + detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }), + resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }), + resolveAddressToolGateDecision: () => ({ + runAddressLane: true, + decision: "run_address_lane", + reason: "address_mode_classifier_detected" + }) + }); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "какие товары сейчас лежат на складе", + effectiveAddressUserMessage: "какие товары сейчас лежат на складе", + followupContext: null, + llmPreDecomposeMeta: null, + useMock: false + }); + + expect(decision.runAddressLane).toBe(true); + expect(decision.toolGateReason).toBe("address_mode_classifier_detected"); + expect(decision.livingMode).toBe("address_data"); + expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true); + }); + + it("routes memory recap follow-up over grounded answer to chat", () => { + const policy = buildPolicy({ + hasConversationMemoryRecallFollowupSignal: () => true, + findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" }) + }); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "а ты помнишь что мы обсуждали?", + effectiveAddressUserMessage: "а ты помнишь что мы обсуждали?", + followupContext: null, + llmPreDecomposeMeta: { + applied: false, + reason: "normalized_fragment_rejected_semantic_guard", + predecomposeContract: { + mode: "unsupported", + mode_confidence: "low", + intent: "unknown", + intent_confidence: "low" + } + }, + useMock: false + }); + + expect(decision.runAddressLane).toBe(false); + expect(decision.toolGateReason).toBe("memory_recap_followup_detected"); + expect(decision.livingMode).toBe("chat"); + expect(decision.livingReason).toBe("memory_recap_followup_detected"); + }); +});