diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index cc35ef9..d5f5625 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1952,6 +1952,10 @@ function resolveUnicodeAddressIntentBridge(text) { const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized); const hasVatMonthPeriodCue = /(?:за|на|в)\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(normalized) && !/\b\d{1,2}\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(normalized); + if ((hasTaxPeriodCue || hasVatMonthPeriodCue) && + /(?:сгруз|какой\s+ндс\s+(?:мы\s+)?должн|ндс\s+необходимо)/iu.test(normalized)) { + return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_liability_colloquial_bridge_signal_detected"); + } if ((hasTaxPeriodCue || (hasVatMonthPeriodCue && !hasVatDebtCue)) && /(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)) { return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_liability_confirmed_tax_period_signal_detected"); diff --git a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js index 8322d27..fb97b1b 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js @@ -3886,7 +3886,6 @@ function composeFactualReplyBody(intent, rows, options = {}) { (String(right.period ?? "").localeCompare(String(left.period ?? ""), "ru"))) .slice(0, Math.min(rows.length, 5)); const semanticSummary = summarizeBankOperationSemantics(rows); - const compactRoleAnswer = /(?:клиент|поставщик|финансов|роль|коротко)/iu.test(String(options.userMessage ?? "")); const compactEvidenceRows = visibleRows.map((row, index) => { const direction = bankOperationDirectionLabel(bankOperationDirection(row)); const amount = formatMoneyRub(row.amount ?? 0); @@ -3904,11 +3903,9 @@ function composeFactualReplyBody(intent, rows, options = {}) { roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.", ...(semanticSummary ? [semanticSummary] : []) ]; - if (!compactRoleAnswer) { - lines.push(bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)), "Примеры строк 1С:", ...compactEvidenceRows); - } + lines.push(bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)), "Примеры строк 1С:", ...compactEvidenceRows); lines.push("Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого."); - if (!compactRoleAnswer && rows.length > visibleRows.length) { + if (rows.length > visibleRows.length) { lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`); } return { diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js index 3b2361d..60d328c 100644 --- a/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js @@ -42,11 +42,17 @@ async function tryHandleAssistantLivingChatRuntime(input) { if (!runtime.handled || !runtime.debug) { return null; } + const runtimeReplySource = typeof runtime.debug.living_chat_response_source === "string" + ? runtime.debug.living_chat_response_source + : null; + const replyType = runtimeReplySource === "deterministic_unsupported_current_turn_boundary" + ? "clarification_required" + : "factual_with_explanation"; const finalization = finalizeLivingChatTurnSafe({ sessionId: input.sessionId, userMessage: input.userMessage, assistantReply: runtime.chatText, - replyType: "factual_with_explanation", + replyType, debug: runtime.debug, modeDecision: input.modeDecision, appendItem: input.appendItem, diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js index b048186..2c16df3 100644 --- a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js @@ -377,6 +377,8 @@ async function runAssistantLivingChatRuntime(input) { address_llm_predecompose_contract: predecomposeContract, address_semantic_extraction_contract: semanticExtractionContract, orchestration_contract_v1: addressRuntimeMeta.orchestrationContract ?? null, + address_tool_gate_decision: addressRuntimeMeta.toolGateDecision ?? null, + address_tool_gate_reason: addressRuntimeMeta.toolGateReason ?? null, tool_gate_decision: addressRuntimeMeta.toolGateDecision ?? null, tool_gate_reason: addressRuntimeMeta.toolGateReason ?? null, normalized: null, diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js index 322555b..fa337fc 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js @@ -350,6 +350,29 @@ function hasExactBankOperationsAddressReply(input, entryPoint) { routeMode === "exact" || hasFullConfirmedTruth(input)); } +function hasExactDocumentListAddressReply(input, entryPoint) { + if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { + return false; + } + if (!hasEffectivelyFactualAddressReply(input)) { + return false; + } + const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase(); + if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") { + return false; + } + const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); + const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe); + const isDocumentIntent = detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract"; + const isDocumentRecipe = selectedRecipe === "address_documents_by_counterparty_v1" || + selectedRecipe === "address_documents_by_contract_v1"; + if (!isDocumentIntent || !isDocumentRecipe) { + return false; + } + const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status); + const responseType = toNonEmptyString(input.addressRuntimeMeta?.response_type); + return Boolean(mcpCallStatus === "matched_non_empty" || responseType === "FACTUAL_LIST" || hasFullConfirmedTruth(input)); +} function hasInventoryMarginRankingAddressReply(input, entryPoint) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { return false; @@ -643,6 +666,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint); const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint); const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint); + const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint); const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint); const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint); const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint); @@ -711,6 +735,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { if (exactBankOperationsProtectsCurrent) { pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply"); } + if (exactDocumentListAddressReply) { + pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_document_list_address_reply"); + } if (inventoryMarginRankingAddressReply) { pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply"); } @@ -744,6 +771,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { !staleMetadataDiscoveryFallbackAgainstExactAddressReply && !exactValueFlowReplyForBusinessOverviewDirectMoneyNeed && !exactBankOperationsProtectsCurrent && + !exactDocumentListAddressReply && !inventoryMarginRankingAddressReply && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js index e2009c0..4cd7f2b 100644 --- a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js @@ -48,6 +48,21 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([ function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) { return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent)); } +function hasTerseUnsupportedLookupBoundarySignal(text) { + const normalized = String(text ?? "").toLowerCase().replace(/\s+/gu, " ").trim(); + if (!normalized) { + return false; + } + if (!/\b(?:gib|give|list|lst)\b/iu.test(normalized)) { + return false; + } + const tokens = normalized.split(/[^\p{L}0-9._-]+/u).filter(Boolean); + if (tokens.length < 2 || tokens.length > 5) { + return false; + } + const commandTokens = new Set(["gib", "give", "list", "lst", "show", "find"]); + return tokens.some((token) => token.length >= 2 && !commandTokens.has(token)); +} function resolveAddressFallbackToDeepArbitration(input) { const { baseToolGateRunAddressLane, llmRuntimeUnavailableDetected, unsupportedIntentOrMode, strongDataSignal, rootContextOnlyFollowup, llmContractMode, strictDeepInvestigationCueDetected, semanticDeepInvestigationHintDetected, aggregateBusinessAnalyticsSignal, preserveAddressLaneSignal, supportedAddressRouteCandidateDetected, followupContext, followupSemanticOverrideToDeepAllowed, deepAnalysisPreferenceDetected, protectAddressLaneFromFallback, dataRetrievalSignal, vatExplainFollowupSignal, semanticAggregateShapeDetected, semanticApplyCanonicalRecommended, standaloneAddressTopicSignal, hasDeepSessionContinuationSignalDetected } = input; const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGateRunAddressLane && @@ -79,11 +94,10 @@ function resolveAddressFallbackToDeepArbitration(input) { semanticAggregateShapeDetected || !semanticApplyCanonicalRecommended || standaloneAddressTopicSignal)); - const deepSessionContinuationFallbackToDeep = Boolean(!followupContext && - baseToolGateRunAddressLane && + const deepSessionContinuationFallbackToDeep = Boolean(baseToolGateRunAddressLane && !llmRuntimeUnavailableDetected && - !protectAddressLaneFromFallback && - hasDeepSessionContinuationSignalDetected); + hasDeepSessionContinuationSignalDetected && + (!protectAddressLaneFromFallback || deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected)); return { unsupportedAddressIntentFallbackToDeep, deepAnalysisSignalFallbackToDeep, @@ -444,6 +458,7 @@ function createAssistantRoutePolicy(deps) { } : baseResolvedIntentResolution; const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); + const diagnosticRewriteRejected = llmPreDecomposeReason === "normalized_fragment_rejected_diagnostic_rewrite"; const providerExecution = resolveProviderExecutionState({ useMock, llmPreDecomposeReason @@ -590,11 +605,13 @@ function createAssistantRoutePolicy(deps) { ].includes(String(baseToolGate?.reason ?? ""))) || Boolean(baseToolGate?.runAddressLane && String(baseToolGate?.reason ?? "") === "followup_context_detected" && - effectiveGroundedValueFlowFollowupContextDetected); + effectiveGroundedValueFlowFollowupContextDetected) || + diagnosticRewriteRejected; const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && deterministicNonDomainGuard && (llmFirstUnsupportedCandidate || llmContractMode === null) && !baseToolGatePreservesAddressLane && + !strictDeepInvestigationCueDetected && !effectiveGroundedValueFlowFollowupContextDetected && !protectedInventoryShortFollowup && !protectedInventoryMarginRankingFollowup && @@ -644,6 +661,13 @@ function createAssistantRoutePolicy(deps) { !dangerOrCoercionSignal && !effectiveGroundedValueFlowFollowupContextDetected && !organizationClarificationContinuationDetected); + const nonDomainUnsupportedBoundary = Boolean(assistantTurnMeaning?.unsupported_but_understood_family && + assistantTurnMeaning?.stale_replay_forbidden === true && + !turnMeaningIntentCandidate) || + hasTerseUnsupportedLookupBoundarySignal(rawUserMessage) || + hasTerseUnsupportedLookupBoundarySignal(repairedRawUserMessage) || + hasTerseUnsupportedLookupBoundarySignal(effectiveAddressUserMessage) || + hasTerseUnsupportedLookupBoundarySignal(repairedEffectiveAddressUserMessage); const hardMetaMode = resolveHardMetaMode({ dataScopeMetaQuery, capabilityMetaQuery, @@ -1044,11 +1068,14 @@ function createAssistantRoutePolicy(deps) { toolGateDecision: "skip_address_lane", toolGateReason: "non_domain_query_indexed", livingMode: "chat", - livingReason: "non_domain_query_indexed", + livingReason: nonDomainUnsupportedBoundary + ? "unsupported_current_turn_meaning_boundary" + : "non_domain_query_indexed", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1", hard_meta_mode: "non_domain", provider_execution: providerExecution, + assistant_turn_meaning: assistantTurnMeaning, address_mode: resolvedModeDetection.mode, address_mode_confidence: resolvedModeDetection.confidence, address_intent: resolvedIntentResolution.intent, @@ -1056,13 +1083,19 @@ function createAssistantRoutePolicy(deps) { strong_data_signal_detected: strongDataSignal, data_retrieval_signal_detected: dataRetrievalSignal, followup_context_detected: Boolean(followupContext), + unsupported_current_turn_meaning_boundary: nonDomainUnsupportedBoundary, + unsupported_current_turn_family: nonDomainUnsupportedBoundary + ? assistantTurnMeaning?.unsupported_but_understood_family ?? "terse_unsupported_lookup" + : null, 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" + living_reason: nonDomainUnsupportedBoundary + ? "unsupported_current_turn_meaning_boundary" + : "non_domain_query_indexed" } } }; @@ -1146,7 +1179,8 @@ function createAssistantRoutePolicy(deps) { const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint; const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal || - protectedInventoryMarginRankingFollowup); + protectedInventoryMarginRankingFollowup || + diagnosticRewriteRejected); 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}`))); @@ -1226,7 +1260,7 @@ function createAssistantRoutePolicy(deps) { let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); const semanticAddressLaneRecovery = Boolean(!runAddressLane && - (supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) && + (supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup || diagnosticRewriteRejected) && !deepAnalysisPreferenceDetected && !unsupportedAddressIntentFallbackToDeep && !deepAnalysisSignalFallbackToDeep && diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 906017d..dfbdb6c 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -1605,6 +1605,7 @@ const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|(?: const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i; const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu; const ADDRESS_MANAGEMENT_PROFILE_PATTERN = /(?:за\s+какие\s+год[а-яё]*|сам(?:ый|ая|ое)\s+(?:актив|пассив)|наименее\s+актив|минимальн|покрыт(?:ие|ия)\s+период|диапазон\s+лет|тип[аы]\s+док(?:умент|ов|и)?|раздел[ыа]\s+уч[её]та|по\s+количеств[аоуе]|редк|реже|(?:сколько|скока|скок)\s+(?:всего\s+)?(?:уникальн(?:ых|ые|ого)?\s+)?контрагент(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?(?:заказчик(?:ов|а)?|поставщик(?:ов|а)?|клиент(?:ов|а)?|покупател(?:ей|я)|смешан(?:ных|ые)\s+контрагент(?:ов|а)?)|(?:покажи|выведи|список|какие|кто).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:за\s+вс[её]\s+время|all\s+time|(?:^|[^\d])(19|20)\d{2}(?:[^\d]|$)|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|за\s+год|в\s+году)|(?:какие|кто|покажи|выведи|список).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:работал(?:и)?|активн(?:ые|ых|а|о)?).*(?:за\s+вс[её]\s+время|(?:19|20)\d{2}|за\s+год|в\s+году)|договорн(?:ая|ой)\s+баз[аы]|total\s+vs\s+used)/iu; +const ADDRESS_DEEP_ANALYSIS_FALLBACK_BLOCK_PATTERN = /(?:\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u043e\u0431\u0449\p{L}*\s+\u043a\u0430\u0440\u0442\u0438\u043d|\u0442\u043e\u043f\s+\u0440\u0438\u0441\u043a|\u0447\u0442\u043e\s+\u043d\u0435\s+\u0442\u0430\u043a|\u043e\u0441\u043d\u043e\u0432\p{L}*\s+\u0441\u0440\u0435\u0434\u0441\u0442)/iu; function normalizeAddressMonthAliasToken(token) { const source = String(token ?? "").trim().toLowerCase(); if (!source) { @@ -1814,6 +1815,9 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) if (ADDRESS_MANAGEMENT_PROFILE_PATTERN.test(source)) { return null; } + if (ADDRESS_DEEP_ANALYSIS_FALLBACK_BLOCK_PATTERN.test(source)) { + return null; + } const monthYear = extractAddressFallbackMonthYear(source); const year = extractAddressFallbackYear(source); const allTime = ADDRESS_ALL_TIME_PATTERN.test(source); @@ -3319,6 +3323,13 @@ function hasPredecomposeDiagnosticUncertaintyLead(text) { } return /^(?:неясно|не\s+ясно|непонятно|не\s+понятно|unclear|not\s+clear|ambiguous|unknown)(?=$|[\s,.;:!?])/iu.test(normalized); } +function hasPredecomposeDebtSnapshotIntentSignal(text) { + const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/\u0451/gu, "\u0435"); + if (!normalized) { + return false; + } + return /(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+(?:\u043a\u0442\u043e(?:-\u0442\u043e)?\s+)?\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|receivables?)/iu.test(normalized); +} function attachAddressPredecomposeContract(meta, sourceMessage) { const sourceMeta = meta && typeof meta === "object" ? meta : {}; const { providerExecutionInput, providerExecutionContract: providerExecutionContractInput, ...restMeta } = sourceMeta; @@ -3439,9 +3450,11 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage const repairedSourceMessage = repairAddressMojibake(userMessage); const sourceIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedSourceMessage || userMessage); const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate); - const sourceIntentKnown = sourceIntentResolution.intent !== "unknown"; + const sourceIntentKnown = sourceIntentResolution.intent !== "unknown" || + hasPredecomposeDebtSnapshotIntentSignal(repairedSourceMessage || userMessage); const candidateIntentKnown = candidateIntentResolution.intent !== "unknown"; - const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate); + const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate) || + /^(?:\u043d\u0435\u044f\u0441\u043d\u043e|\u043d\u0435\s+\u044f\u0441\u043d\u043e|\u043d\u0435\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u043d\u0435\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e)(?=$|[\s,.;:!?])/iu.test(compactWhitespace(repairAddressMojibake(String(candidate ?? "")).toLowerCase())); if (candidateStartsWithDiagnosticUncertainty && sourceIntentKnown) { return attachAddressPredecomposeContract({ ...baseMeta, @@ -3463,6 +3476,13 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage const rejectCandidateForIntentSafety = intentDroppedByCandidate || (intentConflict && (sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high")); + const sourceAnchorQualityForIntentSafety = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage); + const candidateAnchorQualityForIntentSafety = evaluateAddressAnchorQuality(candidate); + const counterpartyAnchorSubstitutedDuringIntentDrop = sourceAnchorQualityForIntentSafety.anchorType === "counterparty" && + sourceAnchorQualityForIntentSafety.quality >= 2 && + Boolean(sourceAnchorQualityForIntentSafety.anchorValue) && + candidateAnchorQualityForIntentSafety.quality < sourceAnchorQualityForIntentSafety.quality && + hasCounterpartyAnchorSubstitution(sourceAnchorQualityForIntentSafety.anchorValue ?? "", candidate); if (rejectCandidateForIntentSafety) { return attachAddressPredecomposeContract({ ...baseMeta, @@ -3471,9 +3491,11 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: userMessage, - reason: intentDroppedByCandidate - ? "normalized_fragment_rejected_intent_drop" - : "normalized_fragment_rejected_intent_conflict", + reason: counterpartyAnchorSubstitutedDuringIntentDrop + ? "normalized_fragment_rejected_anchor_substitution" + : intentDroppedByCandidate + ? "normalized_fragment_rejected_intent_drop" + : "normalized_fragment_rejected_intent_conflict", fallbackRuleHit: null, sanitizedUserMessage, semanticHints: candidateMeta?.semanticHints ?? null @@ -3482,7 +3504,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage); const candidateHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(candidate); const sourceLooksLikeSameDateAccountFollowup = hasSameDateAccountFollowupSignalForPredecompose(repairedSourceMessage || userMessage); - const candidateInjectsDrilldownIntent = candidateIntentResolution.intent === "documents_forming_balance"; + const candidateInjectsDrilldownIntent = candidateIntentResolution.intent === "documents_forming_balance" || + (candidateHasExplicitDrilldownSignal && /(?:document|posting|docs?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u043f\u0440\u043e\u0432\u043e\u0434\u043a)/iu.test(candidate)); if (sourceLooksLikeSameDateAccountFollowup && !sourceHasExplicitDrilldownSignal && candidateHasExplicitDrilldownSignal && @@ -3801,8 +3824,8 @@ function hasDeepSessionContinuationSignal(input) { return candidateTexts.some((text) => { const hasContinuationCue = /^(?:\u0438|\u0430|\u0442\u0430\u043a\u0436\u0435|\u0435\u0449[\u0435\u0451]|\u0434\u043e\u0431\u0430\u0432\u044c|\u0434\u043e\u043f\u043e\u043b\u043d\u0438|\u0443\u0442\u043e\u0447\u043d\u0438|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438|\u0442\u0435\u043f\u0435\u0440\u044c|then|also|and)\b/iu.test(text) || /(?:\u043f\u043e\s+\u0442\u043e\u043c\u0443\s+\u0436\u0435|\u043f\u043e\s+\u044d\u0442\u043e\u043c\u0443|\u0432\s+\u044d\u0442\u043e\u043c\s+\u0436\u0435|\u0438\s+\u043f\u043e\s+\u043f\u0435\u0440\u0438\u043e\u0434\u0443|\u0434\u043e\u0431\u0430\u0432\u044c\s+\u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u043c|\u0430\s+\u0435\u0441\u043b\u0438|\u0435\u0441\u043b\u0438\s+\u0442\u043e\u043b\u044c\u043a\u043e|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e)/iu.test(text); - const hasAccountOrPeriodCue = /(?:\u0441\u0447[\u0435\u0451]\u0442|account|\b\d{2}(?:[.,]\d{1,2})?\b|\b20\d{2}(?:[-/.]\d{1,2})?\b|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(text); - const hasDeepRebindCue = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|fixed\s*asset|\u043e\u0441\b|\u043d\u0434\u0441|vat|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447\u043a|\u0430\u043d\u043e\u043c\u0430\u043b|lifecycle|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447)/iu.test(text); + const hasAccountOrPeriodCue = /(?:\u0441\u0447[\u0435\u0451]\u0442|account|\b\d{2}(?:[.,]\d{1,2})?\b|\b20\d{2}(?:[-/.]\d{1,2})?\b|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|contract)/iu.test(text); + const hasDeepRebindCue = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|fixed\s*asset|\u043e\u0441\b|\u043d\u0434\u0441|vat|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447\u043a|\u0430\u043d\u043e\u043c\u0430\u043b|lifecycle|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442|\u0445\u0432\u043e\u0441\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|contract)/iu.test(text); if (hasContinuationCue && (hasAccountOrPeriodCue || hasDeepRebindCue)) { return true; } @@ -3822,6 +3845,7 @@ function hasDeepAnalysisPreferenceSignal(text) { if (openContractsListQuestionSignal) { return false; } + const broadOverviewRiskSignal = /(?:\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u043e\u0431\u0449\p{L}*\s+\u043a\u0430\u0440\u0442\u0438\u043d|\u0442\u043e\u043f\s+\u0440\u0438\u0441\u043a|\u0447\u0442\u043e\s+\u043d\u0435\s+\u0442\u0430\u043a)/iu.test(lower); const riskOrAnomalySignal = /(?:\u0440\u0438\u0441\u043a|risk|\u0430\u043d\u043e\u043c\u0430\u043b|anomal|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|conflict|deviation|\u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d|\u043d\u0435\u0441\u044b\u043a\u043e\u0432\u043a|\u043d\u0435\u0441\u0445\u043e\u0434|\u043e\u0448\u0438\u0431|error|issue|\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower); const chainSignal = /(?:\u0446\u0435\u043f\u043e\u0447\u043a|chain|trace\s*chain|lifecycle|\u0436\u0438\u0437\u043d\u0435\u043d\u043d[\u0430-\u044f]+\s+\u0446\u0438\u043a\u043b|state\s+transition|\u0440\u0430\u0437\u0440\u044b\u0432[\u0430-\u044f]*)/iu.test(lower); const diagnosticsKeywordSignal = /(?:\u0440\u0430\u0437\u043b\u043e\u0436\u0438|\u0434\u0435\u043a\u043e\u043c\u043f\u043e\u0437|\u0440\u0430\u0437\u0431\u0435\u0440\u0438|\u043f\u043e\u0447\u0435\u043c\u0443|why|audit|scan|\u043a\u043e\u0440\u043d\u0435\u0432[\u0430-\u044f]+\s+\u043f\u0440\u0438\u0447\u0438\u043d|root\s*cause|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c[\u0430-\u044f]*|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower); @@ -3829,17 +3853,20 @@ function hasDeepAnalysisPreferenceSignal(text) { const diagnosticsSignal = diagnosticsKeywordSignal || diagnosticsCheckVerbSignal; const closureSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|\u043d\u0435\s+\u0437\u0430\u043a\u0440\u044b\u043b[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)/iu.test(lower); const closureIntentSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|period\s*close|close\s+period)/iu.test(lower); + const closureStateQuestionSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)[^\n]{0,80}(?:\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434[\u0430-\u044f]*|\u0436\u0438\u0432[\u0430-\u044f]*|\u043e\u0441\u0442\u0430[\u043b-\u044f]*|\u0435\u0441\u0442\u044c)|(?:\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434[\u0430-\u044f]*|\u0436\u0438\u0432[\u0430-\u044f]*|\u043e\u0441\u0442\u0430[\u043b-\u044f]*|\u0435\u0441\u0442\u044c)[^\n]{0,80}(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)/iu.test(lower); const closureDiagnosticPhraseSignal = /(?:\u0447\u0442\u043e(?:\s+\S+){0,8}\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower); const signalVsNoiseDiagnostic = /(?:\u043d\u0435\s+\u043f\u0440\u043e\u0441\u0442\u043e\s+(?:\u043d\u0430\s+)?\u0448\u0443\u043c|\u043f\u043e\u0445\u043e\u0436[\u0438\u0435]\s+(?:\u0438\u043c\u0435\u043d\u043d\u043e\s+)?\u043d\u0430\s+\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower); const lifecycleMismatchSignal = /(?:\u043d\u0435\s+\u0442\u0435\u043c\s+\u0442\u0438\u043f(?:\u043e\u043c)?\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|wrong\s+closing\s+document|expected\s+transition)/iu.test(lower); const lifecycleTransitionGapSignal = /(?:\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u0441\u0442\u0430\u0434\u0438[\u0438\u044f\u0435]\s+.*\u043f\u0440\u043e\u0439\u0434\u0435\u043d.*\u043f\u0435\u0440\u0435\u0445\u043e\u0434)/iu.test(lower); const expectedActualMismatchSignal = /(?:\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a[\u0430-\u044f]+\s+\u0441\u043e\u0441\u0442\u043e\u044f\u043d[\u0438\u0435\u044f]+\s+.*\u0440\u0430\u0441\u0445\u043e\u0434[\u0430-\u044f]*\s+\u0441\s+\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d[\u0430-\u044f]*\s+\u0441\u043f\u0438\u0441\u0430\u043d)/iu.test(lower); - return lifecycleMismatchSignal || + return broadOverviewRiskSignal || + lifecycleMismatchSignal || (chainSignal && lifecycleTransitionGapSignal) || expectedActualMismatchSignal || (chainSignal && diagnosticsSignal) || (riskOrAnomalySignal && (chainSignal || diagnosticsSignal || lifecycleTransitionGapSignal)) || (diagnosticsSignal && (closureSignal || closureIntentSignal)) || + closureStateQuestionSignal || closureDiagnosticPhraseSignal || signalVsNoiseDiagnostic; } @@ -3867,7 +3894,7 @@ function hasStrictDeepInvestigationCue(text) { if (!hasInvestigativeVerb) { return false; } - return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|lifecycle|state\s+transition)/iu.test(normalized); + return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u043d\u043e\u0432\p{L}*\s+\u0441\u0440\u0435\u0434\u0441\u0442|fixed\s*asset|\b\u043e\u0441\b|lifecycle|state\s+transition)/iu.test(normalized); } function hasAggregateBusinessAnalyticsSignal(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()); diff --git a/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js b/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js index bc397f5..e712f98 100644 --- a/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTurnMeaningPolicy.js @@ -67,7 +67,7 @@ function detectSupportedIntent(text, deps) { reason: "address_intent_resolver_current_turn_signal" }; } - if (/(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a|\breceivables?\b)/iu.test(text)) { + if (/(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u043a\u0442\u043e(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a|\breceivables?\b)/iu.test(text)) { return { intent: "receivables_confirmed_as_of_date", confidence: "high", diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index c4a9e2b..b7323a9 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -2811,6 +2811,16 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio !/\b\d{1,2}\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test( normalized ); + if ( + (hasTaxPeriodCue || hasVatMonthPeriodCue) && + /(?:сгруз|какой\s+ндс\s+(?:мы\s+)?должн|ндс\s+необходимо)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "vat_liability_confirmed_for_tax_period", + "high", + "vat_liability_colloquial_bridge_signal_detected" + ); + } if ( (hasTaxPeriodCue || (hasVatMonthPeriodCue && !hasVatDebtCue)) && /(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized) diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts index cb30fc8..0c78b36 100644 --- a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -4940,7 +4940,6 @@ function composeFactualReplyBody( ) .slice(0, Math.min(rows.length, 5)); const semanticSummary = summarizeBankOperationSemantics(rows); - const compactRoleAnswer = /(?:клиент|поставщик|финансов|роль|коротко)/iu.test(String(options.userMessage ?? "")); const compactEvidenceRows = visibleRows.map((row, index) => { const direction = bankOperationDirectionLabel(bankOperationDirection(row)); const amount = formatMoneyRub(row.amount ?? 0); @@ -4958,15 +4957,13 @@ function composeFactualReplyBody( roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.", ...(semanticSummary ? [semanticSummary] : []) ]; - if (!compactRoleAnswer) { - lines.push( - bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)), - "Примеры строк 1С:", - ...compactEvidenceRows - ); - } + lines.push( + bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)), + "Примеры строк 1С:", + ...compactEvidenceRows + ); lines.push("Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого."); - if (!compactRoleAnswer && rows.length > visibleRows.length) { + if (rows.length > visibleRows.length) { lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`); } return { diff --git a/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts index 77c6c81..0747969 100644 --- a/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantLivingChatHandlerRuntimeAdapter.ts @@ -98,11 +98,19 @@ export async function tryHandleAssistantLivingChatRuntime 5) { + return false; + } + const commandTokens = new Set(["gib", "give", "list", "lst", "show", "find"]); + return tokens.some((token) => token.length >= 2 && !commandTokens.has(token)); +} + function resolveAddressFallbackToDeepArbitration(input) { const { baseToolGateRunAddressLane, @@ -103,11 +119,10 @@ function resolveAddressFallbackToDeepArbitration(input) { semanticAggregateShapeDetected || !semanticApplyCanonicalRecommended || standaloneAddressTopicSignal)); - const deepSessionContinuationFallbackToDeep = Boolean(!followupContext && - baseToolGateRunAddressLane && + const deepSessionContinuationFallbackToDeep = Boolean(baseToolGateRunAddressLane && !llmRuntimeUnavailableDetected && - !protectAddressLaneFromFallback && - hasDeepSessionContinuationSignalDetected); + hasDeepSessionContinuationSignalDetected && + (!protectAddressLaneFromFallback || deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected)); return { unsupportedAddressIntentFallbackToDeep, deepAnalysisSignalFallbackToDeep, @@ -531,6 +546,7 @@ export function createAssistantRoutePolicy(deps) { } : baseResolvedIntentResolution; const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason); + const diagnosticRewriteRejected = llmPreDecomposeReason === "normalized_fragment_rejected_diagnostic_rewrite"; const providerExecution = resolveProviderExecutionState({ useMock, llmPreDecomposeReason @@ -678,11 +694,13 @@ export function createAssistantRoutePolicy(deps) { ].includes(String(baseToolGate?.reason ?? ""))) || Boolean(baseToolGate?.runAddressLane && String(baseToolGate?.reason ?? "") === "followup_context_detected" && - effectiveGroundedValueFlowFollowupContextDetected); + effectiveGroundedValueFlowFollowupContextDetected) || + diagnosticRewriteRejected; const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && deterministicNonDomainGuard && (llmFirstUnsupportedCandidate || llmContractMode === null) && !baseToolGatePreservesAddressLane && + !strictDeepInvestigationCueDetected && !effectiveGroundedValueFlowFollowupContextDetected && !protectedInventoryShortFollowup && !protectedInventoryMarginRankingFollowup && @@ -733,6 +751,14 @@ export function createAssistantRoutePolicy(deps) { !dangerOrCoercionSignal && !effectiveGroundedValueFlowFollowupContextDetected && !organizationClarificationContinuationDetected); + const nonDomainUnsupportedBoundary = Boolean( + assistantTurnMeaning?.unsupported_but_understood_family && + assistantTurnMeaning?.stale_replay_forbidden === true && + !turnMeaningIntentCandidate) || + hasTerseUnsupportedLookupBoundarySignal(rawUserMessage) || + hasTerseUnsupportedLookupBoundarySignal(repairedRawUserMessage) || + hasTerseUnsupportedLookupBoundarySignal(effectiveAddressUserMessage) || + hasTerseUnsupportedLookupBoundarySignal(repairedEffectiveAddressUserMessage); const hardMetaMode = resolveHardMetaMode({ dataScopeMetaQuery, capabilityMetaQuery, @@ -1134,11 +1160,14 @@ export function createAssistantRoutePolicy(deps) { toolGateDecision: "skip_address_lane", toolGateReason: "non_domain_query_indexed", livingMode: "chat", - livingReason: "non_domain_query_indexed", + livingReason: nonDomainUnsupportedBoundary + ? "unsupported_current_turn_meaning_boundary" + : "non_domain_query_indexed", orchestrationContract: { schema_version: "assistant_orchestration_contract_v1", hard_meta_mode: "non_domain", provider_execution: providerExecution, + assistant_turn_meaning: assistantTurnMeaning, address_mode: resolvedModeDetection.mode, address_mode_confidence: resolvedModeDetection.confidence, address_intent: resolvedIntentResolution.intent, @@ -1146,13 +1175,19 @@ export function createAssistantRoutePolicy(deps) { strong_data_signal_detected: strongDataSignal, data_retrieval_signal_detected: dataRetrievalSignal, followup_context_detected: Boolean(followupContext), + unsupported_current_turn_meaning_boundary: nonDomainUnsupportedBoundary, + unsupported_current_turn_family: nonDomainUnsupportedBoundary + ? assistantTurnMeaning?.unsupported_but_understood_family ?? "terse_unsupported_lookup" + : null, 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" + living_reason: nonDomainUnsupportedBoundary + ? "unsupported_current_turn_meaning_boundary" + : "non_domain_query_indexed" } } }; @@ -1237,7 +1272,8 @@ export function createAssistantRoutePolicy(deps) { const protectAddressLaneFromFallback = Boolean( laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal || - protectedInventoryMarginRankingFollowup + protectedInventoryMarginRankingFollowup || + diagnosticRewriteRejected ); const vatExplainFollowupSignal = Boolean(followupContext && toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" && @@ -1318,7 +1354,7 @@ export function createAssistantRoutePolicy(deps) { let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane"); let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0"); const semanticAddressLaneRecovery = Boolean(!runAddressLane && - (supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) && + (supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup || diagnosticRewriteRejected) && !deepAnalysisPreferenceDetected && !unsupportedAddressIntentFallbackToDeep && !deepAnalysisSignalFallbackToDeep && diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 5574e76..955c0fa 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -1561,6 +1561,8 @@ const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс| const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu; const ADDRESS_MANAGEMENT_PROFILE_PATTERN = /(?:за\s+какие\s+год[а-яё]*|сам(?:ый|ая|ое)\s+(?:актив|пассив)|наименее\s+актив|минимальн|покрыт(?:ие|ия)\s+период|диапазон\s+лет|тип[аы]\s+док(?:умент|ов|и)?|раздел[ыа]\s+уч[её]та|по\s+количеств[аоуе]|редк|реже|(?:сколько|скока|скок)\s+(?:всего\s+)?(?:уникальн(?:ых|ые|ого)?\s+)?контрагент(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?(?:заказчик(?:ов|а)?|поставщик(?:ов|а)?|клиент(?:ов|а)?|покупател(?:ей|я)|смешан(?:ных|ые)\s+контрагент(?:ов|а)?)|(?:покажи|выведи|список|какие|кто).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:за\s+вс[её]\s+время|all\s+time|(?:^|[^\d])(19|20)\d{2}(?:[^\d]|$)|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|за\s+год|в\s+году)|(?:какие|кто|покажи|выведи|список).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:работал(?:и)?|активн(?:ые|ых|а|о)?).*(?:за\s+вс[её]\s+время|(?:19|20)\d{2}|за\s+год|в\s+году)|договорн(?:ая|ой)\s+баз[аы]|total\s+vs\s+used)/iu; +const ADDRESS_DEEP_ANALYSIS_FALLBACK_BLOCK_PATTERN = + /(?:\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u043e\u0431\u0449\p{L}*\s+\u043a\u0430\u0440\u0442\u0438\u043d|\u0442\u043e\u043f\s+\u0440\u0438\u0441\u043a|\u0447\u0442\u043e\s+\u043d\u0435\s+\u0442\u0430\u043a|\u043e\u0441\u043d\u043e\u0432\p{L}*\s+\u0441\u0440\u0435\u0434\u0441\u0442)/iu; function normalizeAddressMonthAliasToken(token) { const source = String(token ?? "").trim().toLowerCase(); if (!source) { @@ -1770,6 +1772,9 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) if (ADDRESS_MANAGEMENT_PROFILE_PATTERN.test(source)) { return null; } + if (ADDRESS_DEEP_ANALYSIS_FALLBACK_BLOCK_PATTERN.test(source)) { + return null; + } const monthYear = extractAddressFallbackMonthYear(source); const year = extractAddressFallbackYear(source); const allTime = ADDRESS_ALL_TIME_PATTERN.test(source); @@ -3275,6 +3280,13 @@ function hasPredecomposeDiagnosticUncertaintyLead(text) { } return /^(?:неясно|не\s+ясно|непонятно|не\s+понятно|unclear|not\s+clear|ambiguous|unknown)(?=$|[\s,.;:!?])/iu.test(normalized); } +function hasPredecomposeDebtSnapshotIntentSignal(text) { + const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/\u0451/gu, "\u0435"); + if (!normalized) { + return false; + } + return /(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+(?:\u043a\u0442\u043e(?:-\u0442\u043e)?\s+)?\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|receivables?)/iu.test(normalized); +} function attachAddressPredecomposeContract(meta, sourceMessage) { const sourceMeta = meta && typeof meta === "object" ? meta : {}; const { providerExecutionInput, providerExecutionContract: providerExecutionContractInput, ...restMeta } = sourceMeta; @@ -3395,9 +3407,11 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage const repairedSourceMessage = repairAddressMojibake(userMessage); const sourceIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedSourceMessage || userMessage); const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate); - const sourceIntentKnown = sourceIntentResolution.intent !== "unknown"; + const sourceIntentKnown = sourceIntentResolution.intent !== "unknown" || + hasPredecomposeDebtSnapshotIntentSignal(repairedSourceMessage || userMessage); const candidateIntentKnown = candidateIntentResolution.intent !== "unknown"; - const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate); + const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate) || + /^(?:\u043d\u0435\u044f\u0441\u043d\u043e|\u043d\u0435\s+\u044f\u0441\u043d\u043e|\u043d\u0435\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u043d\u0435\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e)(?=$|[\s,.;:!?])/iu.test(compactWhitespace(repairAddressMojibake(String(candidate ?? "")).toLowerCase())); if (candidateStartsWithDiagnosticUncertainty && sourceIntentKnown) { return attachAddressPredecomposeContract({ ...baseMeta, @@ -3419,6 +3433,13 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage const rejectCandidateForIntentSafety = intentDroppedByCandidate || (intentConflict && (sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high")); + const sourceAnchorQualityForIntentSafety = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage); + const candidateAnchorQualityForIntentSafety = evaluateAddressAnchorQuality(candidate); + const counterpartyAnchorSubstitutedDuringIntentDrop = sourceAnchorQualityForIntentSafety.anchorType === "counterparty" && + sourceAnchorQualityForIntentSafety.quality >= 2 && + Boolean(sourceAnchorQualityForIntentSafety.anchorValue) && + candidateAnchorQualityForIntentSafety.quality < sourceAnchorQualityForIntentSafety.quality && + hasCounterpartyAnchorSubstitution(sourceAnchorQualityForIntentSafety.anchorValue ?? "", candidate); if (rejectCandidateForIntentSafety) { return attachAddressPredecomposeContract({ ...baseMeta, @@ -3427,7 +3448,9 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: userMessage, - reason: intentDroppedByCandidate + reason: counterpartyAnchorSubstitutedDuringIntentDrop + ? "normalized_fragment_rejected_anchor_substitution" + : intentDroppedByCandidate ? "normalized_fragment_rejected_intent_drop" : "normalized_fragment_rejected_intent_conflict", fallbackRuleHit: null, @@ -3438,7 +3461,8 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage); const candidateHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(candidate); const sourceLooksLikeSameDateAccountFollowup = hasSameDateAccountFollowupSignalForPredecompose(repairedSourceMessage || userMessage); - const candidateInjectsDrilldownIntent = candidateIntentResolution.intent === "documents_forming_balance"; + const candidateInjectsDrilldownIntent = candidateIntentResolution.intent === "documents_forming_balance" || + (candidateHasExplicitDrilldownSignal && /(?:document|posting|docs?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u043f\u0440\u043e\u0432\u043e\u0434\u043a)/iu.test(candidate)); if (sourceLooksLikeSameDateAccountFollowup && !sourceHasExplicitDrilldownSignal && candidateHasExplicitDrilldownSignal && @@ -3757,8 +3781,8 @@ function hasDeepSessionContinuationSignal(input) { return candidateTexts.some((text) => { const hasContinuationCue = /^(?:\u0438|\u0430|\u0442\u0430\u043a\u0436\u0435|\u0435\u0449[\u0435\u0451]|\u0434\u043e\u0431\u0430\u0432\u044c|\u0434\u043e\u043f\u043e\u043b\u043d\u0438|\u0443\u0442\u043e\u0447\u043d\u0438|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438|\u0442\u0435\u043f\u0435\u0440\u044c|then|also|and)\b/iu.test(text) || /(?:\u043f\u043e\s+\u0442\u043e\u043c\u0443\s+\u0436\u0435|\u043f\u043e\s+\u044d\u0442\u043e\u043c\u0443|\u0432\s+\u044d\u0442\u043e\u043c\s+\u0436\u0435|\u0438\s+\u043f\u043e\s+\u043f\u0435\u0440\u0438\u043e\u0434\u0443|\u0434\u043e\u0431\u0430\u0432\u044c\s+\u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u043c|\u0430\s+\u0435\u0441\u043b\u0438|\u0435\u0441\u043b\u0438\s+\u0442\u043e\u043b\u044c\u043a\u043e|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e)/iu.test(text); - const hasAccountOrPeriodCue = /(?:\u0441\u0447[\u0435\u0451]\u0442|account|\b\d{2}(?:[.,]\d{1,2})?\b|\b20\d{2}(?:[-/.]\d{1,2})?\b|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(text); - const hasDeepRebindCue = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|fixed\s*asset|\u043e\u0441\b|\u043d\u0434\u0441|vat|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447\u043a|\u0430\u043d\u043e\u043c\u0430\u043b|lifecycle|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447)/iu.test(text); + const hasAccountOrPeriodCue = /(?:\u0441\u0447[\u0435\u0451]\u0442|account|\b\d{2}(?:[.,]\d{1,2})?\b|\b20\d{2}(?:[-/.]\d{1,2})?\b|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|contract)/iu.test(text); + const hasDeepRebindCue = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|fixed\s*asset|\u043e\u0441\b|\u043d\u0434\u0441|vat|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447\u043a|\u0430\u043d\u043e\u043c\u0430\u043b|lifecycle|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442|\u0445\u0432\u043e\u0441\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|contract)/iu.test(text); if (hasContinuationCue && (hasAccountOrPeriodCue || hasDeepRebindCue)) { return true; } @@ -3779,6 +3803,8 @@ function hasDeepAnalysisPreferenceSignal(text) { if (openContractsListQuestionSignal) { return false; } + const broadOverviewRiskSignal = + /(?:\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u043e\u0431\u0449\p{L}*\s+\u043a\u0430\u0440\u0442\u0438\u043d|\u0442\u043e\u043f\s+\u0440\u0438\u0441\u043a|\u0447\u0442\u043e\s+\u043d\u0435\s+\u0442\u0430\u043a)/iu.test(lower); const riskOrAnomalySignal = /(?:\u0440\u0438\u0441\u043a|risk|\u0430\u043d\u043e\u043c\u0430\u043b|anomal|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|conflict|deviation|\u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d|\u043d\u0435\u0441\u044b\u043a\u043e\u0432\u043a|\u043d\u0435\u0441\u0445\u043e\u0434|\u043e\u0448\u0438\u0431|error|issue|\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower); const chainSignal = /(?:\u0446\u0435\u043f\u043e\u0447\u043a|chain|trace\s*chain|lifecycle|\u0436\u0438\u0437\u043d\u0435\u043d\u043d[\u0430-\u044f]+\s+\u0446\u0438\u043a\u043b|state\s+transition|\u0440\u0430\u0437\u0440\u044b\u0432[\u0430-\u044f]*)/iu.test(lower); const diagnosticsKeywordSignal = /(?:\u0440\u0430\u0437\u043b\u043e\u0436\u0438|\u0434\u0435\u043a\u043e\u043c\u043f\u043e\u0437|\u0440\u0430\u0437\u0431\u0435\u0440\u0438|\u043f\u043e\u0447\u0435\u043c\u0443|why|audit|scan|\u043a\u043e\u0440\u043d\u0435\u0432[\u0430-\u044f]+\s+\u043f\u0440\u0438\u0447\u0438\u043d|root\s*cause|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c[\u0430-\u044f]*|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower); @@ -3786,17 +3812,20 @@ function hasDeepAnalysisPreferenceSignal(text) { const diagnosticsSignal = diagnosticsKeywordSignal || diagnosticsCheckVerbSignal; const closureSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|\u043d\u0435\s+\u0437\u0430\u043a\u0440\u044b\u043b[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)/iu.test(lower); const closureIntentSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|period\s*close|close\s+period)/iu.test(lower); + const closureStateQuestionSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)[^\n]{0,80}(?:\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434[\u0430-\u044f]*|\u0436\u0438\u0432[\u0430-\u044f]*|\u043e\u0441\u0442\u0430[\u043b-\u044f]*|\u0435\u0441\u0442\u044c)|(?:\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434[\u0430-\u044f]*|\u0436\u0438\u0432[\u0430-\u044f]*|\u043e\u0441\u0442\u0430[\u043b-\u044f]*|\u0435\u0441\u0442\u044c)[^\n]{0,80}(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)/iu.test(lower); const closureDiagnosticPhraseSignal = /(?:\u0447\u0442\u043e(?:\s+\S+){0,8}\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower); const signalVsNoiseDiagnostic = /(?:\u043d\u0435\s+\u043f\u0440\u043e\u0441\u0442\u043e\s+(?:\u043d\u0430\s+)?\u0448\u0443\u043c|\u043f\u043e\u0445\u043e\u0436[\u0438\u0435]\s+(?:\u0438\u043c\u0435\u043d\u043d\u043e\s+)?\u043d\u0430\s+\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower); const lifecycleMismatchSignal = /(?:\u043d\u0435\s+\u0442\u0435\u043c\s+\u0442\u0438\u043f(?:\u043e\u043c)?\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|wrong\s+closing\s+document|expected\s+transition)/iu.test(lower); const lifecycleTransitionGapSignal = /(?:\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u0441\u0442\u0430\u0434\u0438[\u0438\u044f\u0435]\s+.*\u043f\u0440\u043e\u0439\u0434\u0435\u043d.*\u043f\u0435\u0440\u0435\u0445\u043e\u0434)/iu.test(lower); const expectedActualMismatchSignal = /(?:\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a[\u0430-\u044f]+\s+\u0441\u043e\u0441\u0442\u043e\u044f\u043d[\u0438\u0435\u044f]+\s+.*\u0440\u0430\u0441\u0445\u043e\u0434[\u0430-\u044f]*\s+\u0441\s+\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d[\u0430-\u044f]*\s+\u0441\u043f\u0438\u0441\u0430\u043d)/iu.test(lower); - return lifecycleMismatchSignal || + return broadOverviewRiskSignal || + lifecycleMismatchSignal || (chainSignal && lifecycleTransitionGapSignal) || expectedActualMismatchSignal || (chainSignal && diagnosticsSignal) || (riskOrAnomalySignal && (chainSignal || diagnosticsSignal || lifecycleTransitionGapSignal)) || (diagnosticsSignal && (closureSignal || closureIntentSignal)) || + closureStateQuestionSignal || closureDiagnosticPhraseSignal || signalVsNoiseDiagnostic; } @@ -3827,7 +3856,7 @@ function hasStrictDeepInvestigationCue(text) { if (!hasInvestigativeVerb) { return false; } - return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|lifecycle|state\s+transition)/iu.test(normalized); + return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u043d\u043e\u0432\p{L}*\s+\u0441\u0440\u0435\u0434\u0441\u0442|fixed\s*asset|\b\u043e\u0441\b|lifecycle|state\s+transition)/iu.test(normalized); } function hasAggregateBusinessAnalyticsSignal(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()); diff --git a/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts b/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts index 9289669..f55555c 100644 --- a/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTurnMeaningPolicy.ts @@ -70,7 +70,7 @@ function detectSupportedIntent(text, deps) { reason: "address_intent_resolver_current_turn_signal" }; } - if (/(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a|\breceivables?\b)/iu.test(text)) { + if (/(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u043a\u0442\u043e(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a|\breceivables?\b)/iu.test(text)) { return { intent: "receivables_confirmed_as_of_date", confidence: "high", diff --git a/llm_normalizer/backend/tests/addressInventoryRootFrameFollowup.test.ts b/llm_normalizer/backend/tests/addressInventoryRootFrameFollowup.test.ts index 4768f32..c93dba6 100644 --- a/llm_normalizer/backend/tests/addressInventoryRootFrameFollowup.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryRootFrameFollowup.test.ts @@ -34,7 +34,8 @@ describe("inventory root frame follow-up", () => { expect(result?.baseReasons).toContain("intent_restored_to_inventory_root_frame"); expect(result?.filters.extracted_filters.item).toBeUndefined(); expect(result?.filters.extracted_filters.organization).toBe("альтернатива"); - expect(result?.filters.extracted_filters.counterparty).toBe("альтернатива"); + expect(result?.filters.extracted_filters.counterparty).toBeUndefined(); + expect(result?.baseReasons).toContain("counterparty_cleared_as_organization_scope_alias"); expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31"); expect(result?.filters.extracted_filters.as_of_date).toBe("2020-05-31"); diff --git a/llm_normalizer/backend/tests/addressVatConfirmedRoute.test.ts b/llm_normalizer/backend/tests/addressVatConfirmedRoute.test.ts index 088ad50..2558a73 100644 --- a/llm_normalizer/backend/tests/addressVatConfirmedRoute.test.ts +++ b/llm_normalizer/backend/tests/addressVatConfirmedRoute.test.ts @@ -86,5 +86,5 @@ describe("vat payable confirmed as-of route", () => { expect(result?.debug.requested_result_mode).toBe("confirmed_balance"); expect(result?.debug.route_expectation_status).toBe("matched"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); - }, 15000); + }, 30000); });