"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AssistantService = void 0; exports.evaluateCoverageForTests = evaluateCoverageForTests; exports.extractSubjectTokensForTests = extractSubjectTokensForTests; // @ts-nocheck const nanoid_1 = __importStar(require("nanoid")); const stage1Contracts_1 = __importStar(require("../types/stage1Contracts")); const config_1 = __importStar(require("../config")); const log_1 = __importStar(require("../utils/log")); const answerComposer_1 = __importStar(require("./answerComposer")); const assistantDataLayer_1 = __importStar(require("./assistantDataLayer")); const assistantSessionLogger_1 = __importStar(require("./assistantSessionLogger")); const investigationState_1 = __importStar(require("./investigationState")); const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer")); const questionTypeResolver_1 = __importStar(require("./questionTypeResolver")); const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver")); const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards")); const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence")); const addressQueryService_1 = __importStar(require("./addressQueryService")); const addressQueryClassifier_1 = __importStar(require("./addressQueryClassifier")); const addressIntentResolver_1 = __importStar(require("./addressIntentResolver")); const addressFilterExtractor_1 = __importStar(require("./addressFilterExtractor")); const predecomposeContract_1 = __importStar(require("./address_runtime/predecomposeContract")); const iconv_lite_1 = __importDefault(require("iconv-lite")); function retrievalSummaryForRoute(route) { if (route === "store_canonical") return "Canonical accounting data path selected."; if (route === "store_feature_risk") return "Risk/control profile path selected."; if (route === "hybrid_store_plus_live") return "Hybrid chain analysis path selected."; if (route === "live_mcp_drilldown") return "Live drilldown path selected."; if (route === "batch_refresh_then_store") return "Heavy analytical batch path selected."; return "Route selected."; } function mapNoRouteReason(reason) { if (reason === "out_of_scope") return "Fragment out of scope."; if (reason === "insufficient_specificity") return "Needs clarification."; if (reason === "missing_mapping") return "Route mapping is missing."; if (reason === "unsupported_fragment_type") return "Fragment type unsupported."; return "No-route decision."; } function extractFragments(normalized) { if (!normalized || typeof normalized !== "object") { return []; } const source = normalized; return Array.isArray(source.fragments) ? source.fragments : []; } function hasExplicitPeriodAnchorFromNormalized(normalized) { const fragments = extractFragments(normalized); const explicitPeriodPattern = /(?:\b20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?(?:[-./](?:0?[1-9]|[12]\d|3[01]))?\b|\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b|\b(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]|июл[ьяе]|август[ае]?|сентябр[ьяе]|октябр[ьяе]|ноябр[ьяе]|декабр[ьяе]|january|february|march|april|may|june|july|august|september|october|november|december)\b)/i; for (const item of fragments) { if (!item || typeof item !== "object") { continue; } const fragment = item; const timeScope = fragment.time_scope && typeof fragment.time_scope === "object" ? fragment.time_scope : null; if (timeScope) { const type = String(timeScope.type ?? "").trim().toLowerCase(); const value = String(timeScope.value ?? "").trim(); const confidence = String(timeScope.confidence ?? "").trim().toLowerCase(); if ((type === "explicit" || type === "range") && value.length > 0 && confidence !== "low") { return true; } } const rawText = `${typeof fragment.raw_fragment_text === "string" ? fragment.raw_fragment_text : ""} ${typeof fragment.normalized_fragment_text === "string" ? fragment.normalized_fragment_text : ""}`; if (explicitPeriodPattern.test(rawText)) { return true; } } return false; } function extractExecutionState(normalized) { const fragments = extractFragments(normalized); return fragments.map((item) => { if (!item || typeof item !== "object") { return {}; } const fragment = item; return { fragment_id: fragment.fragment_id ?? null, execution_readiness: fragment.execution_readiness ?? null, route_status: fragment.route_status ?? null, no_route_reason: fragment.no_route_reason ?? null }; }); } function collectBusinessScopesFromNormalized(normalized) { const scopes = []; for (const item of extractFragments(normalized)) { if (!item || typeof item !== "object") { continue; } const scope = String(item.business_scope ?? "").trim(); if (scope) { scopes.push(scope); } } return Array.from(new Set(scopes)); } function hasJuly2020SnapshotSignal(userMessage, companyAnchors) { const lower = String(userMessage ?? "").toLowerCase(); if (/(?:\b2020[-/.]0?7\b|\bиюл[ьяе]?\b(?:\s+20\d{2})?|\bjuly\b(?:\s+20\d{2})?)/i.test(lower)) { return true; } const periods = Array.isArray(companyAnchors?.periods) ? companyAnchors.periods : []; const dates = Array.isArray(companyAnchors?.dates) ? companyAnchors.dates : []; return [...periods, ...dates].some((item) => /2020[-/.]0?7|июл|july/i.test(String(item ?? "").toLowerCase())); } function hasP0DomainSignal(userMessage, companyAnchors) { if (inferP0DomainFromMessage(userMessage)) { return true; } const accounts = Array.isArray(companyAnchors?.accounts) ? companyAnchors.accounts : []; if (accounts.some((item) => /^(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.|$)/.test(String(item ?? "").trim()))) { return true; } return /(?:ндс|vat|рбп|deferred|амортиз|supplier|customer|settlement|month\s*close|закрыти[ея]\s+месяц|поставщ|покупат)/i.test(String(userMessage ?? "").toLowerCase()); } function resolveBusinessScopeAlignment(input) { const rawScopes = collectBusinessScopesFromNormalized(input.normalized); const needsCompanyGrounding = hasJuly2020SnapshotSignal(input.userMessage, input.companyAnchors) && hasP0DomainSignal(input.userMessage, input.companyAnchors); const reasons = []; if (needsCompanyGrounding) { reasons.push("july_2020_snapshot_p0_signal"); } if (!input.routeSummary || input.routeSummary.mode !== "deterministic_v2" || !needsCompanyGrounding) { return { business_scope_raw: rawScopes, business_scope_resolved: rawScopes, company_grounding_applied: false, scope_resolution_reason: reasons, route_summary_resolved: input.routeSummary }; } let changed = false; const decisions = input.routeSummary.decisions.map((decision) => { const scopeValue = String(decision.business_scope ?? "").trim(); if (scopeValue !== "generic_accounting" && scopeValue !== "unclear") { return decision; } changed = true; return { ...decision, business_scope: "company_specific_accounting" }; }); const resolvedSummary = changed ? { ...input.routeSummary, decisions } : input.routeSummary; const resolvedScopes = changed ? Array.from(new Set(decisions.map((decision) => String(decision.business_scope ?? "").trim()).filter(Boolean))) : rawScopes; return { business_scope_raw: rawScopes, business_scope_resolved: resolvedScopes, company_grounding_applied: changed, scope_resolution_reason: changed ? [...reasons, "generic_or_unclear_to_company_specific_override"] : reasons, route_summary_resolved: resolvedSummary }; } function isJuly2020TemporalResolved(temporalGuard) { if (!temporalGuard || typeof temporalGuard !== "object") { return false; } const resolvedAnchor = String(temporalGuard.resolved_time_anchor ?? "").trim(); if (/^2020-07(?:-\d{2})?$/.test(resolvedAnchor)) { return true; } const effective = temporalGuard.effective_primary_period && typeof temporalGuard.effective_primary_period === "object" ? temporalGuard.effective_primary_period : null; if (effective) { const from = String(effective.from ?? "").trim(); const to = String(effective.to ?? "").trim(); if (/^2020-07-\d{2}$/.test(from) && /^2020-07-\d{2}$/.test(to)) { return true; } } const resolvedPrimary = temporalGuard.resolved_primary_period && typeof temporalGuard.resolved_primary_period === "object" ? temporalGuard.resolved_primary_period : null; if (!resolvedPrimary) { return false; } const from = String(resolvedPrimary.from ?? "").trim(); const to = String(resolvedPrimary.to ?? "").trim(); return /^2020-07-\d{2}$/.test(from) && /^2020-07-\d{2}$/.test(to); } function hasP0ClaimSignal(claimType, focusDomainHint) { const claim = String(claimType ?? "").trim(); if (claim === "prove_settlement_closure_state" || claim === "prove_advance_offset_state" || claim === "prove_vat_chain_completeness" || claim === "prove_month_close_state" || claim === "prove_rbp_tail_state" || claim === "prove_fixed_asset_amortization_coverage") { return true; } return (focusDomainHint === "settlements_60_62" || focusDomainHint === "vat_document_register_book" || focusDomainHint === "month_close_costs_20_44" || focusDomainHint === "fixed_asset_amortization"); } function hasSettlementScopeSignal(input) { const claim = String(input.claimType ?? "").trim(); const domain = String(input.focusDomainHint ?? "").trim(); if (claim === "prove_settlement_closure_state" || claim === "prove_advance_offset_state" || domain === "settlements_60_62") { return true; } if (Boolean(input.followupApplied) && domain === "settlements_60_62") { return true; } const lower = String(input.userMessage ?? "").toLowerCase(); if (/(?:60(?:\\.\\d{2})?|62(?:\\.\\d{2})?|76(?:\\.\\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|хвост|supplier|customer|settlement|payable|receivable|поставщ|покупат)/i.test(lower)) { return true; } const accounts = Array.isArray(input.companyAnchors?.accounts) ? input.companyAnchors.accounts : []; return accounts.some((item) => /^(?:51|60|62|76)(?:\\.|$)/.test(String(item ?? "").trim())); } function resolveBusinessScopeFromLiveContext(input) { const current = input.current; const routeSummary = current?.route_summary_resolved; const julyResolved = isJuly2020TemporalResolved(input.temporalGuard); const p0Signal = hasP0ClaimSignal(input.claimType, input.focusDomainHint); const settlementScopeSignal = hasSettlementScopeSignal({ userMessage: input.userMessage, companyAnchors: input.companyAnchors, claimType: input.claimType, focusDomainHint: input.focusDomainHint, followupApplied: input.followupApplied }); const shouldRecoverScope = p0Signal && (julyResolved || settlementScopeSignal); if (!shouldRecoverScope) { return current; } const reasons = Array.isArray(current.scope_resolution_reason) ? [...current.scope_resolution_reason] : []; if (julyResolved && !reasons.includes("temporal_claim_bound_company_scope_recovery")) { reasons.push("temporal_claim_bound_company_scope_recovery"); } if (settlementScopeSignal && !reasons.includes("settlement_claim_company_scope_recovery")) { reasons.push("settlement_claim_company_scope_recovery"); } const currentScopes = Array.isArray(current.business_scope_resolved) ? current.business_scope_resolved : []; let changed = false; const normalizedScopes = currentScopes .map((item) => String(item ?? "").trim()) .filter(Boolean) .map((item) => { if (item === "generic_accounting" || item === "unclear") { changed = true; return "company_specific_accounting"; } return item; }); if (!normalizedScopes.includes("company_specific_accounting")) { normalizedScopes.push("company_specific_accounting"); changed = true; } let routeSummaryResolved = routeSummary; if (routeSummary && routeSummary.mode === "deterministic_v2" && Array.isArray(routeSummary.decisions)) { const decisions = routeSummary.decisions.map((decision) => { const scopeValue = String(decision.business_scope ?? "").trim(); if (scopeValue !== "generic_accounting" && scopeValue !== "unclear") { return decision; } changed = true; return { ...decision, business_scope: "company_specific_accounting" }; }); routeSummaryResolved = changed ? { ...routeSummary, decisions } : routeSummary; } return { ...current, business_scope_resolved: Array.from(new Set(normalizedScopes)), company_grounding_applied: current.company_grounding_applied || changed, scope_resolution_reason: reasons, route_summary_resolved: routeSummaryResolved }; } function escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function enrichFragmentTextWithHints(fragment, text) { const baseText = String(text ?? "").trim(); const accountHints = Array.isArray(fragment.account_hints) ? Array.from(new Set(fragment.account_hints .map((item) => String(item ?? "").trim()) .filter((item) => item.length > 0))) : []; if (accountHints.length === 0) { return baseText; } const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText)); if (hasAccountInText) { return baseText; } return `${baseText}, по счету ${accountHints.join(", ")}`; } function fragmentTextById(normalized) { const result = new Map(); for (const item of extractFragments(normalized)) { if (!item || typeof item !== "object") { continue; } const fragment = item; const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : ""; if (!fragmentId) { continue; } const text = (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) || (typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) || ""; result.set(fragmentId, enrichFragmentTextWithHints(fragment, text)); } return result; } function extractDiscardedIntentSegments(normalized) { if (!normalized || typeof normalized !== "object") { return []; } const source = normalized; if (!Array.isArray(source.discarded_fragments)) { return []; } return source.discarded_fragments .map((item) => { if (!item || typeof item !== "object") { return null; } const value = item; const text = typeof value.raw_fragment_text === "string" ? value.raw_fragment_text.trim() : ""; return text || null; }) .filter((item) => Boolean(item)); } function collectDateSpans(text) { const spans = []; const datePatterns = [ /\b20\d{2}(?:[-/.](?:0?[1-9]|1[0-2]))(?:[-/.](?:0?[1-9]|[12]\d|3[01]))?\b/g, /\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g, /\b(?:0?[1-9]|[12]\d|3[01])\s+(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+20\d{2})?\b/giu ]; for (const datePattern of datePatterns) { let match = null; while ((match = datePattern.exec(text)) !== null) { spans.push({ start: match.index, end: match.index + match[0].length }); } } return spans; } function collectContractSpans(text) { const spans = []; const contractPatterns = [ /(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu, /(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu, /\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu ]; for (const contractPattern of contractPatterns) { let match = null; while ((match = contractPattern.exec(text)) !== null) { spans.push({ start: match.index, end: match.index + match[0].length }); } } return spans; } function collectAmountSpans(text) { const spans = []; const amountPatterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g]; for (const amountPattern of amountPatterns) { let match = null; while ((match = amountPattern.exec(text)) !== null) { spans.push({ start: match.index, end: match.index + match[0].length }); } } return spans; } function collectPercentSpans(text) { const spans = []; const percentPattern = /\b\d{1,3}(?:[.,]\d+)?\s*%/g; let match = null; while ((match = percentPattern.exec(text)) !== null) { spans.push({ start: match.index, end: match.index + match[0].length }); } return spans; } function intersectsAnySpan(start, end, spans) { return spans.some((span) => start < span.end && end > span.start); } function hasAccountContextAround(text, start, end) { const left = text.slice(Math.max(0, start - 28), start); const right = text.slice(end, Math.min(text.length, end + 28)); return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`); } function extractAccountTokens(text) { const lower = String(text ?? "").toLowerCase(); const explicitAccounts = new Set(); const knownAccountPrefixes = new Set([ "01", "02", "07", "08", "10", "13", "19", "20", "21", "23", "25", "26", "28", "29", "41", "43", "44", "45", "50", "51", "52", "55", "57", "58", "60", "62", "66", "67", "68", "69", "70", "71", "73", "76", "90", "91", "94", "96", "97" ]); const contextualPattern = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu; let contextual = null; while ((contextual = contextualPattern.exec(lower)) !== null) { if (contextual[1]) { const token = String(contextual[1]).trim(); const prefix = token.match(/^(\d{2})/)?.[1]; if (prefix && knownAccountPrefixes.has(prefix)) { explicitAccounts.add(token); } } } const pairPattern = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/g; let pairMatch = null; while ((pairMatch = pairPattern.exec(lower)) !== null) { const left = String(pairMatch[1] ?? "").trim(); const right = String(pairMatch[2] ?? "").trim(); const leftPrefix = left.match(/^(\d{2})/)?.[1]; const rightPrefix = right.match(/^(\d{2})/)?.[1]; if (leftPrefix && knownAccountPrefixes.has(leftPrefix)) { explicitAccounts.add(left); } if (rightPrefix && knownAccountPrefixes.has(rightPrefix)) { explicitAccounts.add(right); } } if (explicitAccounts.size > 0) { return Array.from(explicitAccounts); } const contractSpans = collectContractSpans(lower); const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...contractSpans]; const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower); if (!hasAccountingLexeme) { return []; } const accountList = []; const genericPattern = /\b\d{2}(?:\.\d{2})?\b/g; let generic = null; while ((generic = genericPattern.exec(lower)) !== null) { const value = generic[0]; const start = generic.index; const end = start + value.length; if (intersectsAnySpan(start, end, spans)) { continue; } if (!hasAccountContextAround(lower, start, end)) { continue; } const prefix = value.match(/^(\d{2})/)?.[1]; if (!prefix || !knownAccountPrefixes.has(prefix)) { continue; } accountList.push(value); } return Array.from(new Set(accountList)); } function extractSubjectTokens(text) { const lower = text.toLowerCase(); const tokens = []; const push = (token, match) => { if (match) tokens.push(token); }; push("nds", /\b(?:\u043d\u0434\u0441|vat)\b/iu.test(lower)); push("os", /(?:\b\u043e\u0441\b|\u043e\u0441\u043d\u043e\u0432\u043d(?:\u044b\u0435|\u044b\u0445)\s+\u0441\u0440\u0435\u0434|osnovn(?:ye|yh)\s+sred)/iu.test(lower)); push("counterparty", /(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|supplier|buyer|counterparty|kontragent|postavshch|pokupatel)/iu.test(lower)); push("document", /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|document|invoice|posting|dokument|realiz|postuplen|vypisk|platezh)/iu.test(lower)); push("saldo", /(?:\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0441\u0442\u0430\u0442\u043a|saldo|balance)/iu.test(lower)); push("anomaly", /(?:\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a|\u043f\u043e\u0434\u043e\u0437\u0440|\u0445\u0432\u043e\u0441\u0442|anomaly|risk|anomali|hvost|tail)/iu.test(lower)); push("chain", /(?:\u0446\u0435\u043f\u043e\u0447|\u0440\u0430\u0437\u043b\u043e\u0436|\u0441\u0432\u044f\u0437\u043a|\u0440\u0430\u0437\u0440\u044b\u0432|chain|razlozh|svyaz|razryv)/iu.test(lower)); const accountMatches = extractAccountTokens(lower); for (const account of accountMatches) { tokens.push(`account_${account}`); } return Array.from(new Set(tokens)); } function extractRequirements(routeSummary, normalized, userMessage) { const fragmentText = fragmentTextById(normalized); const byFragment = new Map(); const requirements = []; const pushRequirement = (input) => { const subjectTokens = extractSubjectTokens(input.requirement_text); requirements.push({ requirement_id: input.requirement_id, source_fragment_id: input.source_fragment_id, requirement_text: input.requirement_text, subject_tokens: subjectTokens, status: input.status, route: input.route }); if (input.source_fragment_id) { const current = byFragment.get(input.source_fragment_id) ?? []; current.push(input.requirement_id); byFragment.set(input.source_fragment_id, current); } }; if (!routeSummary) { pushRequirement({ requirement_id: "R1", source_fragment_id: null, requirement_text: userMessage, status: "clarification_needed", route: null }); return { requirements, byFragment }; } if (routeSummary.mode === "legacy_v1") { pushRequirement({ requirement_id: "R1", source_fragment_id: "F1", requirement_text: userMessage, status: "covered", route: routeSummary.route_hint }); return { requirements, byFragment }; } routeSummary.decisions.forEach((decision, index) => { const requirementId = `R${index + 1}`; const text = fragmentText.get(decision.fragment_id) ?? userMessage; let status = "covered"; if (decision.route === "no_route") { if (decision.no_route_reason === "out_of_scope") { status = "out_of_scope"; } else if (decision.no_route_reason === "insufficient_specificity") { status = "clarification_needed"; } else { status = "uncovered"; } } pushRequirement({ requirement_id: requirementId, source_fragment_id: decision.fragment_id, requirement_text: text, status, route: decision.route === "no_route" ? null : decision.route }); }); return { requirements, byFragment }; } function toExecutionPlan(routeSummary, normalized, userMessage, requirementByFragment) { if (!routeSummary) { return []; } const fragmentText = fragmentTextById(normalized); if (routeSummary.mode === "legacy_v1") { return [ { fragment_id: "F1", requirement_ids: requirementByFragment.get("F1") ?? ["R1"], route: routeSummary.route_hint, should_execute: true, fragment_text: userMessage, no_route_reason: null, clarification_reason: null } ]; } return routeSummary.decisions.map((decision) => { const text = fragmentText.get(decision.fragment_id) ?? userMessage; if (decision.route === "no_route") { return { fragment_id: decision.fragment_id, requirement_ids: requirementByFragment.get(decision.fragment_id) ?? [], route: "no_route", should_execute: false, fragment_text: text, no_route_reason: decision.no_route_reason ?? null, clarification_reason: decision.clarification_reason ?? null }; } return { fragment_id: decision.fragment_id, requirement_ids: requirementByFragment.get(decision.fragment_id) ?? [], route: decision.route, should_execute: true, fragment_text: text, no_route_reason: null, clarification_reason: decision.clarification_reason ?? null }; }); } function enrichRbpFragmentForLive(fragmentText, temporalGuard) { const base = compactWhitespace(String(fragmentText ?? "")); const hints = ["Списание РБП", "объект РБП", "остаток на конец периода", "счет 97"]; const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null; if (effective && effective.from && effective.to) { hints.push(`период ${effective.from}..${effective.to}`); } const hintText = hints.filter(Boolean).join(", "); if (!base) { return hintText; } if (/списани[ея]\s+рбп|счет\s*97|account\s*97|остат/i.test(base)) { return base; } return `${base}; ${hintText}`; } function enforceRbpLiveRoutePlan(input) { if (input.claimType !== "prove_rbp_tail_state") { return { executionPlan: input.executionPlan, audit: null }; } const requiredLiveCalls = [ "find_rbp_writeoff_documents_in_period", "find_rbp_object_movements_account_97", "find_month_close_entries_linked_to_rbp", "compute_end_period_residual_by_rbp_object" ]; let routeAdjusted = 0; let rescuedNoRoute = 0; const replacedRoutes = []; const adjustedPlan = input.executionPlan.map((item) => { if (!item || typeof item !== "object") { return item; } if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") { rescuedNoRoute += 1; routeAdjusted += 1; return { ...item, route: "live_mcp_drilldown", should_execute: true, no_route_reason: null, clarification_reason: null, fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard) }; } if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") { routeAdjusted += 1; if (item.route && item.route !== "no_route") { replacedRoutes.push(String(item.route)); } return { ...item, route: "hybrid_store_plus_live", fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard) }; } if (item.should_execute === true) { return { ...item, fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard) }; } return item; }); return { executionPlan: adjustedPlan, audit: { claim_type: "prove_rbp_tail_state", required_live_calls: requiredLiveCalls, route_adjustments_applied: routeAdjusted, rescued_no_route_fragments: rescuedNoRoute, replaced_routes: Array.from(new Set(replacedRoutes)), route_gap_reason: routeAdjusted > 0 ? "rbp_claim_bound_live_route_override_applied" : null } }; } function collectRbpLiveRouteAudit(input) { if (input.claimType !== "prove_rbp_tail_state") { return null; } const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []); const executed = []; const missing = new Set(); const routeGaps = []; let matchedRowsTotal = 0; let returnedRowsTotal = 0; let fetchedRowsTotal = 0; for (const result of input.retrievalResults) { if (!result || typeof result !== "object") { continue; } const summary = result.summary && typeof result.summary === "object" ? result.summary : null; const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null; if (!live) { continue; } const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : []; for (const callId of requiredCalls) { required.add(String(callId ?? "").trim()); } const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : []; for (const call of executedCalls) { if (!call || typeof call !== "object") { continue; } executed.push(call); } const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : []; for (const callId of missingCalls) { const token = String(callId ?? "").trim(); if (token) { missing.add(token); } } const routeGapReason = String(live.route_gap_reason ?? "").trim(); if (routeGapReason) { routeGaps.push(routeGapReason); } fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0; matchedRowsTotal += Number(live.matched_rows ?? 0) || 0; returnedRowsTotal += Number(live.returned_rows ?? 0) || 0; } const requiredList = Array.from(required).filter(Boolean); const executedList = executed; const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId)); for (const callId of missingFromExecuted) { missing.add(callId); } const missingList = Array.from(missing); const routeGapReason = missingList.length > 0 ? "required_live_calls_not_executed" : matchedRowsTotal <= 0 ? "claim_live_calls_executed_but_zero_matches" : routeGaps[0] ?? null; const executionRate = requiredList.length > 0 ? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4)) : 1; return { claim_type: "prove_rbp_tail_state", required_live_calls: requiredList, executed_live_calls: executedList, missing_live_calls: missingList, route_gap_reason: routeGapReason, live_route_execution_rate: executionRate, fetched_rows_total: fetchedRowsTotal, matched_rows_total: matchedRowsTotal, returned_rows_total: returnedRowsTotal, plan_override: input.planAudit ?? null }; } function enrichFaFragmentForLive(fragmentText, temporalGuard) { const base = compactWhitespace(String(fragmentText ?? "")); const hints = [ "Начисление амортизации", "объект ОС", "expected set ОС", "счет 01/02" ]; const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null; if (effective && effective.from && effective.to) { hints.push(`период ${effective.from}..${effective.to}`); } const hintText = hints.filter(Boolean).join(", "); if (!base) { return hintText; } if (/амортиз|основн(?:ые|ых)\s+сред|fixed\s*asset|depreciat|счет\s*0[12]|account\s*0[12]/i.test(base)) { return base; } return `${base}; ${hintText}`; } function enforceFaLiveRoutePlan(input) { if (input.claimType !== "prove_fixed_asset_amortization_coverage") { return { executionPlan: input.executionPlan, audit: null }; } const requiredLiveCalls = [ "find_amortization_documents_in_period", "find_fixed_asset_movements_accounts_01_02", "find_fixed_asset_cards_expected_for_period", "match_expected_vs_actual_fa_coverage" ]; let routeAdjusted = 0; let rescuedNoRoute = 0; const replacedRoutes = []; const adjustedPlan = input.executionPlan.map((item) => { if (!item || typeof item !== "object") { return item; } if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") { rescuedNoRoute += 1; routeAdjusted += 1; return { ...item, route: "live_mcp_drilldown", should_execute: true, no_route_reason: null, clarification_reason: null, fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard) }; } if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") { routeAdjusted += 1; if (item.route && item.route !== "no_route") { replacedRoutes.push(String(item.route)); } return { ...item, route: "hybrid_store_plus_live", fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard) }; } if (item.should_execute === true) { return { ...item, fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard) }; } return item; }); return { executionPlan: adjustedPlan, audit: { claim_type: "prove_fixed_asset_amortization_coverage", required_live_calls: requiredLiveCalls, route_adjustments_applied: routeAdjusted, rescued_no_route_fragments: rescuedNoRoute, replaced_routes: Array.from(new Set(replacedRoutes)), route_gap_reason: routeAdjusted > 0 ? "fa_claim_bound_live_route_override_applied" : null } }; } function collectFaLiveRouteAudit(input) { if (input.claimType !== "prove_fixed_asset_amortization_coverage") { return null; } const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []); const executed = []; const missing = new Set(); const routeGaps = []; let matchedRowsTotal = 0; let returnedRowsTotal = 0; let fetchedRowsTotal = 0; for (const result of input.retrievalResults) { if (!result || typeof result !== "object") { continue; } const summary = result.summary && typeof result.summary === "object" ? result.summary : null; const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null; if (!live) { continue; } const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : []; for (const callId of requiredCalls) { required.add(String(callId ?? "").trim()); } const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : []; for (const call of executedCalls) { if (!call || typeof call !== "object") { continue; } executed.push(call); } const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : []; for (const callId of missingCalls) { const token = String(callId ?? "").trim(); if (token) { missing.add(token); } } const routeGapReason = String(live.route_gap_reason ?? "").trim(); if (routeGapReason) { routeGaps.push(routeGapReason); } fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0; matchedRowsTotal += Number(live.matched_rows ?? 0) || 0; returnedRowsTotal += Number(live.returned_rows ?? 0) || 0; } const requiredList = Array.from(required).filter(Boolean); const executedList = executed; const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId)); for (const callId of missingFromExecuted) { missing.add(callId); } const missingList = Array.from(missing); const routeGapReason = missingList.length > 0 ? "required_live_calls_not_executed" : matchedRowsTotal <= 0 ? "claim_live_calls_executed_but_zero_matches" : routeGaps[0] ?? null; const executionRate = requiredList.length > 0 ? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4)) : 1; return { claim_type: "prove_fixed_asset_amortization_coverage", required_live_calls: requiredList, executed_live_calls: executedList, missing_live_calls: missingList, route_gap_reason: routeGapReason, live_route_execution_rate: executionRate, fetched_rows_total: fetchedRowsTotal, matched_rows_total: matchedRowsTotal, returned_rows_total: returnedRowsTotal, plan_override: input.planAudit ?? null }; } function toDebugRoutes(routeSummary) { if (!routeSummary) { return []; } if (routeSummary.mode === "legacy_v1") { return [ { fragment_id: "F1", route: routeSummary.route_hint, reason: retrievalSummaryForRoute(routeSummary.route_hint), confidence: routeSummary.confidence, intent_class: routeSummary.intent_class } ]; } return routeSummary.decisions.map((decision) => ({ fragment_id: decision.fragment_id, route: decision.route, reason: decision.reason, route_status: decision.route_status ?? null, no_route_reason: decision.no_route_reason ?? null, clarification_reason: decision.clarification_reason ?? null, execution_readiness: decision.execution_readiness ?? null })); } function buildSkippedResult(item) { return (0, retrievalResultNormalizer_1.normalizeRetrievalResult)(item.fragment_id, item.requirement_ids, "no_route", { status: "empty", result_type: "summary", items: [], summary: { skipped: true, reason: mapNoRouteReason(item.no_route_reason), no_route_reason: item.no_route_reason, clarification_reason: item.clarification_reason }, evidence: [], why_included: [], selection_reason: [mapNoRouteReason(item.no_route_reason)], risk_factors: [], business_interpretation: ["Данный фрагмент не был выполнен из-за no-route решения."], confidence: "low", limitations: ["Фрагмент требует уточнения или отсутствует поддерживаемый маршрут."], errors: [] }); } function summarizeUnique(values, limit = 6) { return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit); } const SUBJECT_TOKEN_RULES = { nds: { critical: true, patterns: [ "vat", "accumulationregister", "\u043d\u0434\u0441", "\u043a\u043d\u0438\u0433\u0438\u043f\u043e\u043a\u0443\u043f\u043e\u043a", "\u043a\u043d\u0438\u0433\u0438\u043f\u0440\u043e\u0434\u0430\u0436", "\u043d\u0430\u043b\u043e\u0433\u043d\u0430\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043d\u0443\u044e\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c" ] }, os: { critical: true, patterns: ["fixed_asset", "fixedasset", "\u043e\u0441\u043d\u043e\u0432\u043d", "\u0430\u043c\u043e\u0440\u0442\u0438\u0437"] }, saldo: { critical: true, patterns: ["balance", "saldo", "\u0441\u0430\u043b\u044c\u0434\u043e", "\u043e\u0441\u0442\u0430\u0442"] }, counterparty: { critical: false, patterns: [ "counterparty", "supplier", "buyer", "counterparty_id", "journal_counterparty", "document_has_counterparty", "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442", "\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a", "\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b" ], routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"] }, document: { critical: false, patterns: [ "document", "recorder", "journal", "document_refs_count", "recorded_by_document", "journal_refers_to_document", "\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442" ], routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"] }, anomaly: { critical: false, patterns: [ "risk", "risk_score", "unknown_link_count", "zero_guid", "navigation_links", "missing_counterparty_link", "\u0430\u043d\u043e\u043c\u0430\u043b", "\u0440\u0438\u0441\u043a" ], routes: ["store_feature_risk", "batch_refresh_then_store"] }, chain: { critical: false, patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "\u0446\u0435\u043f\u043e\u0447"], routes: ["hybrid_store_plus_live"] } }; function hasRegexMatch(corpus, pattern) { try { return pattern.test(corpus); } catch { return false; } } function evaluateSubjectTokenMatch(token, corpus, executedRoutes) { if (token.startsWith("account_")) { const account = token.slice("account_".length).trim(); if (!account) { return { matched: false, critical: true }; } const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i"); return { matched: hasRegexMatch(corpus, accountPattern), critical: true }; } const rule = SUBJECT_TOKEN_RULES[token]; if (rule) { const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern)); const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false; return { matched: byPattern || byRoute, critical: rule.critical }; } return { matched: corpus.includes(token), critical: false }; } function evidenceCountForRequirement(requirementId, result) { const evidence = Array.isArray(result.evidence) ? result.evidence : []; if (evidence.length === 0) { return 0; } const tagged = evidence.filter((item) => { const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : ""; return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`; }).length; if (tagged > 0) { return tagged; } if (Array.isArray(result.requirement_ids) && result.requirement_ids.length === 1 && result.requirement_ids[0] === requirementId) { return evidence.length; } return 0; } function hasSubstantiveCoverageForRequirement(requirementId, result) { const evidenceCount = evidenceCountForRequirement(requirementId, result); if (evidenceCount > 0) { return true; } const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0; const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0; if (problemUnitsCount > 0 || candidateEvidenceCount > 0) { if (Array.isArray(result.requirement_ids) && result.requirement_ids.length === 1 && result.requirement_ids[0] === requirementId) { return true; } } return false; } function evaluateCoverage(requirements, retrievalResults) { const statusByRequirement = new Map(); for (const result of retrievalResults) { for (const requirementId of result.requirement_ids) { const list = statusByRequirement.get(requirementId) ?? []; list.push({ status: result.status, substantive: hasSubstantiveCoverageForRequirement(requirementId, result) }); statusByRequirement.set(requirementId, list); } } const resolvedRequirements = requirements.map((requirement) => { if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") { return requirement; } const states = statusByRequirement.get(requirement.requirement_id) ?? []; if (states.length === 0) { return { ...requirement, status: "uncovered" }; } const hasAnySubstantive = states.some((item) => item.substantive); if (!hasAnySubstantive) { return { ...requirement, status: "uncovered" }; } const hasOk = states.some((item) => item.status === "ok"); const hasPartial = states.some((item) => item.status === "partial"); const hasEmpty = states.some((item) => item.status === "empty"); const hasError = states.some((item) => item.status === "error"); const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive); const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive); const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive); if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) { return { ...requirement, status: "covered" }; } if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) { return { ...requirement, status: "partially_covered" }; } return { ...requirement, status: "uncovered" }; }); const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length; const requirementsUncovered = resolvedRequirements .filter((item) => item.status === "uncovered") .map((item) => item.requirement_id); const requirementsPartiallyCovered = resolvedRequirements .filter((item) => item.status === "partially_covered") .map((item) => item.requirement_id); const clarificationNeededFor = resolvedRequirements .filter((item) => item.status === "clarification_needed") .map((item) => item.requirement_id); const outOfScopeRequirements = resolvedRequirements .filter((item) => item.status === "out_of_scope") .map((item) => item.requirement_id); return { requirements: resolvedRequirements, coverage: { requirements_total: resolvedRequirements.length, requirements_covered: requirementsCovered, requirements_uncovered: requirementsUncovered, requirements_partially_covered: requirementsPartiallyCovered, clarification_needed_for: clarificationNeededFor, out_of_scope_requirements: outOfScopeRequirements } }; } function evaluateCoverageForTests(requirements, retrievalResults) { return evaluateCoverage(requirements, retrievalResults); } function extractSubjectTokensForTests(text) { return extractSubjectTokens(text); } function checkGrounding(userMessage, requirements, coverage, retrievalResults) { const whyIncludedSummary = summarizeUnique(retrievalResults.flatMap((item) => item.why_included)); const selectionReasonSummary = summarizeUnique(retrievalResults.flatMap((item) => item.selection_reason)); const hasMaterialResults = retrievalResults.some((item) => item.status === "ok" || item.status === "partial"); const subjectTokens = extractSubjectTokens(userMessage); const executedRoutes = new Set(retrievalResults .filter((item) => item.status !== "error") .map((item) => item.route) .filter(Boolean)); const retrievalCorpus = JSON.stringify(retrievalResults.map((item) => ({ route: item.route, result_type: item.result_type, summary: item.summary, items: item.items, evidence: item.evidence, why_included: item.why_included, selection_reason: item.selection_reason, risk_factors: item.risk_factors, business_interpretation: item.business_interpretation }))).toLowerCase(); const missingSubjectTokens = []; const missingCriticalTokens = []; for (const token of subjectTokens) { const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes); if (!match.matched) { missingSubjectTokens.push(token); if (match.critical) { missingCriticalTokens.push(token); } } } const onlyAccountCriticalMissing = missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_")); const accountOnlyMismatchRecoverable = hasMaterialResults && coverage.requirements_covered > 0 && onlyAccountCriticalMissing && (whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0); const routeSubjectMatch = !hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable; let status = "grounded"; const reasons = []; if (!routeSubjectMatch) { status = "route_mismatch_blocked"; reasons.push(`Не подтверждены критичные предметные токены запроса: ${missingCriticalTokens.join(", ")}`); } else if (accountOnlyMismatchRecoverable) { status = "partial"; reasons.push(`Счет-токены не подтверждены напрямую (${missingCriticalTokens.join(", ")}), но есть релевантная опора для ограниченного вывода.`); } else if (coverage.requirements_covered === 0) { status = "no_grounded_answer"; reasons.push("Ни одно требование не получило подтвержденного покрытия."); } else if (coverage.requirements_uncovered.length > 0 || coverage.requirements_partially_covered.length > 0 || coverage.clarification_needed_for.length > 0 || coverage.out_of_scope_requirements.length > 0) { status = "partial"; reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования."); } if (whyIncludedSummary.length === 0) { reasons.push("Нет explainable-сигналов why_included в результатах выборки."); } if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) { reasons.push(`Часть контекстных токенов не подтверждена напрямую: ${missingSubjectTokens.join(", ")}`); } const missingRequirements = [ ...coverage.requirements_uncovered, ...coverage.requirements_partially_covered, ...coverage.clarification_needed_for, ...coverage.out_of_scope_requirements ]; return { status, route_subject_match: routeSubjectMatch, missing_requirements: missingRequirements, reasons, why_included_summary: whyIncludedSummary, selection_reason_summary: selectionReasonSummary }; } function firstNonEmptyLine(text) { const line = text .split("\n") .map((item) => item.trim()) .find((item) => item.length > 0); return (line ?? text).slice(0, 220); } function buildClaimEvidenceLinks(retrievalResults) { const byClaim = new Map(); for (const result of retrievalResults) { for (const evidence of result.evidence) { const claimRef = String(evidence.claim_ref ?? "").trim(); if (!claimRef) { continue; } const evidenceId = String(evidence.evidence_id ?? "").trim(); if (!evidenceId) { continue; } const current = byClaim.get(claimRef) ?? []; current.push(evidenceId); byClaim.set(claimRef, current); } } return Array.from(byClaim.entries()) .slice(0, 10) .map(([claimRef, evidenceIds]) => ({ claim_ref: claimRef, evidence_ids: summarizeUnique(evidenceIds, 10) })); } function buildAnswerStructureV11(input) { const evidenceIds = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)), 10); const mechanismNotes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence .map((evidence) => evidence.mechanism_note) .filter((note) => typeof note === "string" && note.trim().length > 0)), 6); const sourceRefs = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence .map((evidence) => evidence.source_ref?.canonical_ref) .filter((value) => typeof value === "string" && value.trim().length > 0)), 8); const limitationReasonCodes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence .flatMap((evidence) => { const code = evidence.limitation?.reason_code; return typeof code === "string" && code.trim().length > 0 ? [code] : []; })), 8); const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults); const limitations = summarizeUnique([...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons], 8); const clarificationQuestions = input.coverageReport.clarification_needed_for.map((item) => `Уточните требование ${item}.`); const recommendedActions = summarizeUnique([ ...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`), ...input.coverageReport.requirements_partially_covered.map((item) => `Доуточнить частично покрытое требование ${item}.`) ], 6); const mechanismStatus = mechanismNotes.length === 0 ? "unresolved" : limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference") ? "limited" : "grounded"; return { schema_version: stage1Contracts_1.ANSWER_STRUCTURE_SCHEMA_VERSION, answer_summary: firstNonEmptyLine(input.assistantReply), direct_answer: input.assistantReply, mechanism_block: { status: mechanismStatus, mechanism_notes: mechanismNotes, limitation_reason_codes: limitationReasonCodes }, evidence_block: { evidence_ids: evidenceIds, source_refs: sourceRefs, mechanism_notes: mechanismNotes, coverage_note: input.coverageReport.requirements_total === input.coverageReport.requirements_covered ? "coverage_full_or_near_full" : "coverage_partial_or_limited", ...(config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 && claimEvidenceLinks.length > 0 ? { claim_evidence_links: claimEvidenceLinks } : {}) }, uncertainty_block: { open_uncertainties: input.groundingCheck.missing_requirements, limitations }, next_step_block: { recommended_actions: recommendedActions, clarification_questions: clarificationQuestions } }; } const FOLLOWUP_ROUTE_HINTS = new Set(["store_canonical", "store_feature_risk", "hybrid_store_plus_live", "live_mcp_drilldown", "batch_refresh_then_store"]); const FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP = { settlements_60_62: "hybrid_store_plus_live", vat_document_register_book: "hybrid_store_plus_live", month_close_costs_20_44: "hybrid_store_plus_live", fixed_asset_amortization: "hybrid_store_plus_live" }; const FOLLOWUP_BUSINESS_CONTEXT_MAX = 320; const FOLLOWUP_SUBJECT_MAX = 160; const FOLLOWUP_QUESTION_APPEND_MAX = 260; function compactWhitespace(value) { return value.replace(/\s+/g, " ").trim(); } function hasAccountingSignal(text) { const lower = text.toLowerCase(); if (/(?:^|[\s,;:])\d{2}(?:\.\d{2})?(?=$|[\s,.;:])/i.test(lower)) { return true; } return /(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|счёт|ндс|амортиз|рбп|ос|контрагент|поставщик|покупател|оплат|банк|выписк|склад|товар|материал|закрыти|период|postavshchik|kontragent|schet|schetu|period|counterparty|supplier|invoice|posting|ledger|account|anomaly|risk)/i.test(lower); } function hasFollowupMarker(text) { const compact = compactWhitespace(text.toLowerCase()); return /^(и|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|а если|а теперь|теперь|plus|also|dobav|utochn|prodolzh|then|now)/i.test(compact); } function hasReferentialPointer(text) { return /(по этому|по тому|это же|этой|этим|этому|из этого|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase()); } function hasSmallTalkSignal(text) { return /(привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase()); } function countTokens(text) { return compactWhitespace(text) .split(" ") .filter(Boolean).length; } function hasPeriodLiteral(text) { return /\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/.test(text); } function extractNormalizedPeriodLiteral(text) { const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/); if (monthly) { return `${monthly[1]}-${monthly[2]}`; } const yearly = text.match(/\b(20\d{2})\b/); if (yearly) { return yearly[1]; } return null; } function extractFollowupAccountAnchorsLoose(text) { const lower = String(text ?? "").toLowerCase(); const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)]; const anchors = []; const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g; let match = null; while ((match = followupAccountPattern.exec(lower)) !== null) { const value = String(match[0] ?? "").trim(); const start = match.index; const end = start + value.length; if (intersectsAnySpan(start, end, spans)) { continue; } anchors.push(value); } return Array.from(new Set(anchors)); } function accountPrefixToken(value) { const token = String(value ?? "").trim(); const match = token.match(/^(\d{2})/); return match ? match[1] : null; } function hasCrossScopeConflictWithState(userMessage, state) { const explicitPeriod = extractNormalizedPeriodLiteral(userMessage); const statePeriod = compactWhitespace(state.focus.period ?? ""); if (explicitPeriod && statePeriod && explicitPeriod !== statePeriod) { return true; } const inferredDomain = inferP0DomainFromMessage(userMessage); const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? ""); if (inferredDomain && stateDomain && inferredDomain !== stateDomain) { return true; } const explicitAccounts = extractAccountTokens(userMessage); const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage); const knownAccounts = Array.isArray(state.focus.primary_accounts) ? state.focus.primary_accounts : []; if (fallbackAccounts.length > 0 && knownAccounts.length > 0) { const knownPrefixes = new Set(knownAccounts.map((item) => accountPrefixToken(item)).filter(Boolean)); const newPrefixes = new Set(fallbackAccounts.map((item) => accountPrefixToken(item)).filter(Boolean)); if (newPrefixes.size > 0 && knownPrefixes.size > 0) { let intersects = false; for (const prefix of newPrefixes) { if (knownPrefixes.has(prefix)) { intersects = true; break; } } if (!intersects) { return true; } } } return false; } function inferP0DomainFromMessage(text) { if (typeof investigationState_1.inferP0DomainFromMessage === "function") { return investigationState_1.inferP0DomainFromMessage(text); } return null; } function hasStrongFollowupAnchors(userMessage, state) { const explicitPeriod = extractNormalizedPeriodLiteral(userMessage); if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) { const periodLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) || hasReferentialPointer(userMessage); if (!periodLooksLikeFollowupRefinement) { return true; } } const inferredDomain = inferP0DomainFromMessage(userMessage); const activeDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? ""); if (inferredDomain && activeDomain && inferredDomain !== activeDomain) { const domainLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) && hasReferentialPointer(userMessage); if (!domainLooksLikeFollowupRefinement) { return true; } } const explicitAccounts = extractAccountTokens(userMessage); const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage); if (followupAccounts.length > 0) { const knownAccounts = new Set(state.focus.primary_accounts.map((item) => item.trim())); if (knownAccounts.size === 0) { return true; } if (followupAccounts.some((item) => !knownAccounts.has(item))) { return true; } } return false; } function routeFromInvestigationState(state) { const rawDomain = compactWhitespace(state.focus.domain ?? ""); if (!rawDomain) { const mappedFromFollowup = state.followup_context?.active_domain ? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null : null; return mappedFromFollowup; } if (FOLLOWUP_ROUTE_HINTS.has(rawDomain)) { return rawDomain; } for (const candidate of rawDomain.split(",").map((item) => compactWhitespace(item))) { if (FOLLOWUP_ROUTE_HINTS.has(candidate)) { return candidate; } if (Object.prototype.hasOwnProperty.call(FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP, candidate)) { return FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[candidate]; } } const mappedFromFollowup = state.followup_context?.active_domain ? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null : null; if (mappedFromFollowup) { return mappedFromFollowup; } return null; } function withCappedLength(value, max) { return value.length <= max ? value : value.slice(0, max); } function mergeBusinessContext(existing, patchParts) { const existingText = compactWhitespace(existing ?? ""); const patchText = compactWhitespace(patchParts.filter(Boolean).join("; ")); if (!existingText && !patchText) { return undefined; } const merged = existingText && patchText ? `${existingText}; ${patchText}` : existingText || patchText; return withCappedLength(merged, FOLLOWUP_BUSINESS_CONTEXT_MAX); } function buildFollowupStateBinding(input) { const userMessage = String(input.userMessage ?? "").trim(); if (!userMessage || input.investigationState.status !== "active" || input.investigationState.turn_index <= 0) { return { normalizedQuestion: userMessage, mergedContext: input.payloadContext, usage: null }; } const strongSignal = hasAccountingSignal(userMessage); const followupMarker = hasFollowupMarker(userMessage); const referentialPointer = hasReferentialPointer(userMessage); const shortPrompt = countTokens(userMessage) <= 10; const smallTalkSignal = hasSmallTalkSignal(userMessage); const problemState = input.investigationState.problem_unit_state; const problemContinuityAvailable = config_1.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 && Boolean(problemState) && ((problemState?.active_problem_units.length ?? 0) > 0 || (problemState?.focus_problem_types.length ?? 0) > 0); const strongNewAnchorDetected = hasStrongFollowupAnchors(userMessage, input.investigationState); const scopeConflictDetected = hasCrossScopeConflictWithState(userMessage, input.investigationState); const periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable; const shouldBind = !smallTalkSignal && !strongNewAnchorDetected && !scopeConflictDetected && (followupMarker || referentialPointer || periodRefinementFollowup || (!strongSignal && shortPrompt)); if (!shouldBind) { return { normalizedQuestion: userMessage, mergedContext: input.payloadContext, usage: null }; } const context = { ...(input.payloadContext ?? {}) }; const hasExplicitExpectedRoute = Boolean(input.payloadContext?.expected_route); const expectedRouteFromState = !context?.expected_route ? routeFromInvestigationState(input.investigationState) : null; const periodHintFromState = !context?.period_hint ? input.investigationState.focus.period : null; const followupContext = input.investigationState.followup_context; if (expectedRouteFromState) { context.expected_route = expectedRouteFromState; } if (periodHintFromState) { context.period_hint = periodHintFromState; } const subject = withCappedLength(compactWhitespace(input.investigationState.focus.active_query_subject ?? ""), FOLLOWUP_SUBJECT_MAX); const businessContextPatch = ["followup_state_binding_v1"]; let problemContinuityApplied = false; let problemContinuitySkippedReason = null; if (input.investigationState.focus.period) { businessContextPatch.push("active_period"); } if (subject) { businessContextPatch.push(`focus_subject:${subject}`); } if (input.investigationState.focus.primary_accounts.length > 0) { businessContextPatch.push(`focus_accounts:${input.investigationState.focus.primary_accounts.join(",")}`); } if (followupContext?.active_domain) { businessContextPatch.push(`focus_domain:${followupContext.active_domain}`); } if ((followupContext?.active_requirement_ids?.length ?? 0) > 0) { businessContextPatch.push(`active_requirements:${followupContext.active_requirement_ids.slice(0, 4).join(",")}`); } if ((followupContext?.uncovered_requirement_ids?.length ?? 0) > 0) { businessContextPatch.push(`uncovered_requirements:${followupContext.uncovered_requirement_ids.slice(0, 4).join(",")}`); } if (followupContext?.last_problem_unit_id) { businessContextPatch.push(`last_problem_unit:${followupContext.last_problem_unit_id}`); } if ((followupContext?.evidence_summary?.length ?? 0) > 0) { businessContextPatch.push(`evidence_state:${followupContext.evidence_summary.slice(0, 3).join("|")}`); } if ((followupContext?.settlement_next_actions?.length ?? 0) > 0) { businessContextPatch.push("settlement_focus_retained_v1"); } if (problemContinuityAvailable) { if (hasExplicitExpectedRoute) { problemContinuitySkippedReason = "explicit_expected_route"; } else { const focusTypes = (problemState?.focus_problem_types ?? []).slice(0, 3); const activeCount = problemState?.active_problem_units.length ?? 0; businessContextPatch.push("problem_unit_continuity_v1"); if (focusTypes.length > 0) { businessContextPatch.push(`problem_focus_types:${focusTypes.join(",")}`); } businessContextPatch.push(`problem_active_count:${activeCount}`); problemContinuityApplied = true; } } const mergedBusinessContext = mergeBusinessContext(context?.business_context, businessContextPatch); if (mergedBusinessContext) { context.business_context = mergedBusinessContext; } const shouldAugmentQuestion = Boolean(subject) && (followupMarker || referentialPointer || !strongSignal); let normalizedQuestion = userMessage; if (shouldAugmentQuestion) { const appendParts = [`Фокус текущего разбора: ${subject}`]; if (input.investigationState.focus.primary_accounts.length > 0 && !/\b\d{2}(?:\.\d{2})?\b/.test(userMessage)) { appendParts.push(`Счета фокуса: ${input.investigationState.focus.primary_accounts.join(", ")}`); } if (periodHintFromState && !hasPeriodLiteral(userMessage)) { appendParts.push(`Период фокуса: ${periodHintFromState}`); } const appendBlock = withCappedLength(compactWhitespace(appendParts.join("; ")), FOLLOWUP_QUESTION_APPEND_MAX); normalizedQuestion = `${userMessage}\n${appendBlock}`.trim(); } const reason = followupMarker ? "followup_marker" : referentialPointer ? "referential_pointer" : "underspecified_short_followup"; return { normalizedQuestion, mergedContext: context, usage: { applied: true, reason, state_turn_index: input.investigationState.turn_index, context_patch: { period_hint_from_state: Boolean(periodHintFromState), expected_route_from_state: Boolean(expectedRouteFromState), business_context_from_state: Boolean(mergedBusinessContext), question_augmented: shouldAugmentQuestion, problem_continuity_available: problemContinuityAvailable, problem_continuity_applied: problemContinuityApplied, problem_continuity_skipped_reason: problemContinuityApplied ? null : problemContinuitySkippedReason, strong_new_anchor_detected: strongNewAnchorDetected, scope_isolation_applied: true, scope_carryover_allowed: !scopeConflictDetected, scope_reset_reason: scopeConflictDetected ? "cross_scope_conflict" : null } } }; } function cloneItems(items) { return items.map((item) => ({ ...item, debug: item.debug ? { ...item.debug } : null })); } function buildAddressCoverageReport() { return { requirements_total: 0, requirements_covered: 0, requirements_uncovered: [], requirements_partially_covered: [], clarification_needed_for: [], out_of_scope_requirements: [] }; } function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) { const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded"; const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null; return { trace_id: `address-${(0, nanoid_1.nanoid)(10)}`, prompt_version: "address_query_runtime_v1", schema_version: "address_query_runtime_v1", fallback_type: addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "none", route_summary: null, fragments: [], requirements_extracted: [], coverage_report: buildAddressCoverageReport(), routes: [], retrieval_status: [], retrieval_results: [], answer_grounding_check: { status: grounded, route_subject_match: true, missing_requirements: [], reasons: addressDebug.reasons ?? [], why_included_summary: [], selection_reason_summary: [] }, dropped_intent_segments: [], detected_mode: addressDebug.detected_mode, detected_mode_confidence: addressDebug.detected_mode_confidence, query_shape: addressDebug.query_shape, query_shape_confidence: addressDebug.query_shape_confidence, detected_intent: addressDebug.detected_intent, detected_intent_confidence: addressDebug.detected_intent_confidence, extracted_filters: addressDebug.extracted_filters, missing_required_filters: addressDebug.missing_required_filters, selected_recipe: addressDebug.selected_recipe, mcp_call_status_legacy: addressDebug.mcp_call_status_legacy, account_scope_mode: addressDebug.account_scope_mode, account_scope_fallback_applied: addressDebug.account_scope_fallback_applied, anchor_type: addressDebug.anchor_type, anchor_value_raw: addressDebug.anchor_value_raw, anchor_value_resolved: addressDebug.anchor_value_resolved, resolver_confidence: addressDebug.resolver_confidence, ambiguity_count: addressDebug.ambiguity_count, match_failure_stage: addressDebug.match_failure_stage, match_failure_reason: addressDebug.match_failure_reason, mcp_call_status: addressDebug.mcp_call_status, rows_fetched: addressDebug.rows_fetched, raw_rows_received: addressDebug.raw_rows_received, rows_after_account_scope: addressDebug.rows_after_account_scope, rows_after_recipe_filter: addressDebug.rows_after_recipe_filter, rows_materialized: addressDebug.rows_materialized, rows_matched: addressDebug.rows_matched, raw_row_keys_sample: addressDebug.raw_row_keys_sample, materialization_drop_reason: addressDebug.materialization_drop_reason, account_token_raw: addressDebug.account_token_raw, account_token_normalized: addressDebug.account_token_normalized, account_scope_fields_checked: addressDebug.account_scope_fields_checked, account_scope_match_strategy: addressDebug.account_scope_match_strategy, account_scope_drop_reason: addressDebug.account_scope_drop_reason, runtime_readiness: addressDebug.runtime_readiness, limited_reason_category: addressDebug.limited_reason_category, response_type: addressDebug.response_type, execution_lane: "address_query", llm_decomposition_applied: Boolean(llmMeta?.applied), llm_decomposition_attempted: Boolean(llmMeta?.attempted), llm_provider_used: llmMeta?.provider ?? null, llm_decomposition_trace_id: llmMeta?.traceId ?? null, llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null, llm_decomposition_reason: llmMeta?.reason ?? null, llm_canonical_candidate_detected: Boolean(llmMeta?.llmCanonicalCandidateDetected), llm_predecompose_contract: llmMeta?.predecomposeContract ?? null, fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null, sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null, tool_gate_decision: llmMeta?.toolGateDecision ?? null, tool_gate_reason: llmMeta?.toolGateReason ?? null, answer_structure_v11: null, investigation_state_snapshot: null, normalized: null, normalizer_output: llmMeta?.traceId ? { trace_id: llmMeta.traceId, prompt_version: "normalizer_v2_0_2", applied: Boolean(llmMeta?.applied), effective_message: llmMeta?.effectiveMessage ?? null } : null }; } function toNonEmptyString(value) { if (value === null || value === undefined) { return null; } const text = String(value).trim(); return text.length > 0 ? text : null; } const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([ "за", "с", "по", "на", "и", "или", "док", "доки", "docs", "documents", "doki", "dokument", "dokumenty", "документ", "документы", "документов", "банк", "банковские", "операции", "платеж", "платёж", "платежи", "контрагент", "контрагенту", "контрагента", "год", "года", "г", "year", "god", "плс", "pls", "пж", "пжлст", "пожалуйста", "please", "покеж", "покажи", "скажи", "показать", "show", "list", "skazhi", "выведи", "что", "чо", "которые", "какие", "какой", "активный", "активная", "активное", "активности", "месяц", "месяца", "месяцев", "количество", "количеству", "количества", "были", "был", "была", "было", "ли", "списания", "списание", "поступления", "поступление", "расчетного", "расчётного", "счета", "счёта", "есть", "est", "kakie", "kakoi", "vse", "all", "blya", "blyat", "епт", "ёпт", "бля" ]); const ADDRESS_FALLBACK_STRIP_TOKENS = new Set([ "бля", "блять", "blya", "blyat", "епт", "ёпт", "епта", "нах", "нахуй", "плс", "pls", "пж", "пжлст", "пожалуйста", "please" ]); const ADDRESS_MONTH_ALIAS_MAP = { янв: "01", январ: "01", january: "01", jan: "01", фев: "02", феврал: "02", february: "02", feb: "02", мар: "03", март: "03", march: "03", apr: "04", апр: "04", апрел: "04", april: "04", май: "05", ма: "05", may: "05", июн: "06", июнь: "06", june: "06", jun: "06", июл: "07", июль: "07", july: "07", jul: "07", авг: "08", август: "08", august: "08", aug: "08", сен: "09", сент: "09", сентябр: "09", september: "09", sep: "09", окт: "10", октябр: "10", october: "10", oct: "10", ноя: "11", ноябр: "11", november: "11", nov: "11", дек: "12", декабр: "12", december: "12", dec: "12" }; const ADDRESS_DOCS_SIGNAL_PATTERN = /(?:док|доки|документ|документы|документов|docs?|documents?|doki|docy|doci|bank|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|transaction)/i; const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|расчетн|транзак)/i; const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|(?:^|[^\p{L}\p{N}_])(?:дог\.?|[dд][oо][gг]\.?|dog\.?)(?=$|[^\p{L}\p{N}_])|contract|dogovor)/iu; 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; function normalizeAddressMonthAliasToken(token) { const source = String(token ?? "").trim().toLowerCase(); if (!source) { return null; } const direct = ADDRESS_MONTH_ALIAS_MAP[source]; if (direct) { return direct; } for (const [key, value] of Object.entries(ADDRESS_MONTH_ALIAS_MAP)) { if (source.startsWith(key)) { return value; } } return null; } function normalizeAddressShortYearMentions(text) { return String(text ?? "").replace(/(^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/giu, (_full, prefix, shortYear) => { const normalized = Number(shortYear); if (!Number.isFinite(normalized) || normalized < 0 || normalized > 99) { return _full; } return `${prefix}${String(2000 + normalized)} год`; }); } function sanitizeAddressMessageForFallback(userMessage) { const repaired = compactWhitespace(repairAddressMojibake(String(userMessage ?? ""))); if (!repaired) { return ""; } let sanitized = repaired.toLowerCase(); sanitized = sanitized .replace(/\bpokezh\b/giu, "покажи") .replace(/\bpokazh(?:i)?\b/giu, "покажи") .replace(/\bpokaji\b/giu, "покажи") .replace(/\bop(?:er|ers?)\b/giu, "операции") .replace(/(^|[^\p{L}\p{N}_])опер(?:аци[яиюе]|ы|)?(?=$|[^\p{L}\p{N}_])/giu, "$1операции") .replace(/(^|[^\p{L}\p{N}_])дог\.?(?=$|[^\p{L}\p{N}_])/giu, "$1договор") .replace(/(^|[^\p{L}\p{N}_])dog\.?(?=$|[^\p{L}\p{N}_])/giu, "$1contract") .replace(/\bdoc(?:y|i)\b/giu, "доки") .replace(/\bdok(?:i|y)?\b/giu, "доки") .replace(/\bdocuments?\b/giu, "документы") .replace(/\bdocs?\b/giu, "документы") .replace(/\bschet(?:u)?\b/giu, "счет") .replace(/\bsaldo\b/giu, "сальдо") .replace(/\bgod\b/giu, "год"); sanitized = normalizeAddressShortYearMentions(sanitized); const tokens = sanitized .split(/\s+/) .map((item) => item.trim()) .filter(Boolean); const filteredTokens = tokens.filter((token) => { const normalizedToken = token.replace(/^[^a-zа-яё0-9]+|[^a-zа-яё0-9]+$/giu, ""); if (!normalizedToken) { return true; } return !ADDRESS_FALLBACK_STRIP_TOKENS.has(normalizedToken); }); const compact = compactWhitespace(filteredTokens.join(" ")); return compact || compactWhitespace(repaired.toLowerCase()); } function extractAddressFallbackYear(text) { const source = String(text ?? ""); const fullYearMatch = source.match(/\b(20\d{2})\b/); if (fullYearMatch) { return fullYearMatch[1]; } const shortYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/iu); if (shortYearMatch) { const shortYear = Number(shortYearMatch[1]); if (Number.isFinite(shortYear) && shortYear >= 0 && shortYear <= 99) { return String(2000 + shortYear); } } const shortOrdinalYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:[-\s]?(?:й|ый|ой|th))(?=$|[^a-zа-яё0-9])/iu); if (!shortOrdinalYearMatch) { return null; } const shortOrdinalYear = Number(shortOrdinalYearMatch[1]); if (!Number.isFinite(shortOrdinalYear) || shortOrdinalYear < 0 || shortOrdinalYear > 99) { return null; } return String(2000 + shortOrdinalYear); } function extractAddressFallbackMonthYear(text) { const source = String(text ?? ""); const numericYearMonth = source.match(/\b(20\d{2})[./-](0?[1-9]|1[0-2])\b/); if (numericYearMonth) { const year = numericYearMonth[1]; const month = String(Number(numericYearMonth[2])).padStart(2, "0"); return `${year}-${month}`; } const numericMonthYear = source.match(/\b(0?[1-9]|1[0-2])[./-](20\d{2})\b/); if (numericMonthYear) { const month = String(Number(numericMonthYear[1])).padStart(2, "0"); const year = numericMonthYear[2]; return `${year}-${month}`; } const namedMonthYear = source.match(/(?:^|[^a-zа-яё0-9])([a-zа-яё]+)\s+(20\d{2})(?=$|[^a-zа-яё0-9])/iu); if (namedMonthYear) { const month = normalizeAddressMonthAliasToken(namedMonthYear[1]); if (month) { return `${namedMonthYear[2]}-${month}`; } } const yearNamedMonth = source.match(/(?:^|[^a-zа-яё0-9])(20\d{2})\s+([a-zа-яё]+)(?=$|[^a-zа-яё0-9])/iu); if (yearNamedMonth) { const month = normalizeAddressMonthAliasToken(yearNamedMonth[2]); if (month) { return `${yearNamedMonth[1]}-${month}`; } } return null; } function extractAddressFallbackAccountToken(text) { const source = String(text ?? ""); const explicitMatch = source.match(/(?:сч[её]т(?:а|у|ом|е)?|account)\D{0,12}(\d{2}(?:[.,]\d{1,2})?)/iu); if (explicitMatch && explicitMatch[1]) { return String(explicitMatch[1]).replace(",", "."); } const tokenPattern = /\b(\d{2}(?:[.,]\d{1,2})?)\b/giu; let match = tokenPattern.exec(source); while (match) { const raw = String(match[1] ?? ""); const start = match.index; const end = start + raw.length; const prev = start > 0 ? source[start - 1] : " "; const next = end < source.length ? source[end] : " "; if (!/[./-]/.test(prev) && !/[./-]/.test(next) && !/\d/.test(prev) && !/\d/.test(next)) { return raw.replace(",", "."); } match = tokenPattern.exec(source); } return null; } function pickAddressFallbackCounterpartyToken(text) { const source = String(text ?? ""); const byAnchor = source.match(/(?:^|[\s,.;:!?()\-])(?:по|от)\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=$|[\s,.;:!?()\-])/iu); if (byAnchor && byAnchor[1]) { const byToken = String(byAnchor[1]).trim(); const normalizedByToken = byToken.toLowerCase(); if (byToken && !ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(normalizedByToken) && !/^\d{2}(?:\.\d{1,2})?$/.test(normalizedByToken) && !/^(?:19|20)\d{2}$/.test(normalizedByToken) && !/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalizedByToken) && !/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalizedByToken)) { return byToken; } } const candidates = extractAddressAnchorTokens(text); for (const token of candidates) { const normalized = String(token ?? "").toLowerCase(); if (!normalized || /^\d{2}(?:\.\d{1,2})?$/.test(normalized)) { continue; } if (/^(?:19|20)\d{2}$/.test(normalized)) { continue; } if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalized)) { continue; } if (/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalized)) { continue; } return token; } return null; } function extractAddressFallbackContractToken(text) { const source = String(text ?? ""); const patterns = [ /(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)\s*(?:№|#|n|no\.?)?\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})/iu, /(?:№|#|n|no\.?)\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})\s*(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)/iu ]; for (const pattern of patterns) { const match = pattern.exec(source); if (!match || !match[1]) { continue; } const candidate = String(match[1]).replace(/^[^a-zа-я0-9]+|[^a-zа-я0-9/_-]+$/giu, ""); if (!candidate || candidate.length < 2) { continue; } if (/^(?:19|20)\d{2}$/.test(candidate)) { continue; } if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])(?:[./-](?:0?[1-9]|[12]\d|3[01]))?$/.test(candidate)) { continue; } return candidate; } if (ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source) || /(?:^|[^\p{L}\p{N}_])(?:[dд][oо][gг]|dogovor)(?=$|[^\p{L}\p{N}_])/iu.test(source)) { const generic = source.match(/\b([a-zа-я0-9]{1,10}[/-][a-zа-я0-9]{1,10}(?:[/-][a-zа-я0-9]{1,10})?)\b/iu); if (generic && generic[1]) { return generic[1]; } } return null; } function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) { const sourceRaw = compactWhitespace(repairAddressMojibake(String(userMessage ?? ""))); const source = compactWhitespace(String(sanitizedUserMessage ?? sourceRaw).toLowerCase()); if (!source) { return null; } if (ADDRESS_MANAGEMENT_PROFILE_PATTERN.test(source)) { return null; } const monthYear = extractAddressFallbackMonthYear(source); const year = extractAddressFallbackYear(source); const allTime = ADDRESS_ALL_TIME_PATTERN.test(source); const account = extractAddressFallbackAccountToken(source); const docsSignal = ADDRESS_DOCS_SIGNAL_PATTERN.test(source); const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source); const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source); const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source); if (balanceSignal && account) { let periodClause = ""; let rule = "balance_account_rewrite"; if (monthYear) { periodClause = ` на ${monthYear}`; rule = "balance_month_period_rewrite"; } else if (year) { periodClause = ` на ${year}-12-31`; rule = "balance_year_period_rewrite"; } const candidate = compactWhitespace(`остаток по счету ${account}${periodClause}`); if (candidate && candidate !== sourceRaw.toLowerCase()) { return { candidate, rule }; } } if (!docsSignal && !contractSignal && !balanceSignal) { const counterparty = pickAddressFallbackCounterpartyToken(source); const genericLookupSignal = /(?:\bесть\b|\bпокажи\b|\bвыведи\b|\bч[её]\b|\bчто\b)/iu.test(source); if (counterparty && (allTime || monthYear || year) && genericLookupSignal) { let periodClause = ""; let rule = "documents_counterparty_rewrite_from_generic_lookup"; if (allTime) { periodClause = " за все время"; rule = "documents_counterparty_all_time_rewrite_from_generic_lookup"; } else if (monthYear) { periodClause = ` за ${monthYear}`; rule = "documents_counterparty_month_rewrite_from_generic_lookup"; } else if (year) { periodClause = ` за ${year} год`; rule = "documents_counterparty_year_rewrite_from_generic_lookup"; } const candidate = compactWhitespace(`документы по контрагенту ${counterparty}${periodClause}`); if (candidate && candidate !== sourceRaw.toLowerCase()) { return { candidate, rule }; } } } if (docsSignal) { const contract = extractAddressFallbackContractToken(sourceRaw || source); if (contractSignal || contract) { if (contract) { let periodClause = ""; let rule = bankSignal ? "bank_operations_contract_rewrite" : "documents_contract_rewrite"; if (allTime) { periodClause = " за все время"; rule = bankSignal ? "bank_operations_contract_all_time_rewrite" : "documents_contract_all_time_rewrite"; } else if (monthYear) { periodClause = ` за ${monthYear}`; rule = bankSignal ? "bank_operations_contract_month_rewrite" : "documents_contract_month_rewrite"; } else if (year) { periodClause = ` за ${year} год`; rule = bankSignal ? "bank_operations_contract_year_rewrite" : "documents_contract_year_rewrite"; } const subject = bankSignal ? "банковские операции" : "документы"; const candidate = compactWhitespace(`${subject} по договору ${contract}${periodClause}`); if (candidate && candidate !== sourceRaw.toLowerCase()) { return { candidate, rule }; } } } else { const counterparty = pickAddressFallbackCounterpartyToken(source); if (counterparty) { let periodClause = ""; const subject = bankSignal ? "банковские операции" : "документы"; const rulePrefix = bankSignal ? "bank_operations_counterparty" : "documents_counterparty"; let rule = `${rulePrefix}_rewrite`; if (allTime) { periodClause = " за все время"; rule = `${rulePrefix}_all_time_rewrite`; } else if (monthYear) { periodClause = ` за ${monthYear}`; rule = `${rulePrefix}_month_rewrite`; } else if (year) { periodClause = ` за ${year} год`; rule = `${rulePrefix}_year_rewrite`; } const candidate = compactWhitespace(`${subject} по контрагенту ${counterparty}${periodClause}`); if (candidate && candidate !== sourceRaw.toLowerCase()) { return { candidate, rule }; } } } } if (source !== sourceRaw.toLowerCase() && isAddressLlmPreDecomposeCandidate(source)) { return { candidate: source, rule: "noise_cleanup" }; } return null; } function textMojibakeScoreForAddress(value) { const source = String(value ?? ""); const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length; const latin = (source.match(/[A-Za-z]/g) ?? []).length; const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length; const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length; return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2; } function looksLikeMojibakeForAddress(value) { const source = String(value ?? ""); if (!source.trim()) { return false; } if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) { return true; } return (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2; } function repairAddressMojibake(value) { const source = String(value ?? ""); if (!looksLikeMojibakeForAddress(source)) { return source; } let candidate = source; try { const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8"); if (textMojibakeScoreForAddress(fromWin1251) > textMojibakeScoreForAddress(candidate)) { candidate = fromWin1251; } } catch (_error) { } try { const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8"); if (textMojibakeScoreForAddress(fromLatin1) > textMojibakeScoreForAddress(candidate)) { candidate = fromLatin1; } } catch (_error) { } return candidate; } function extractAddressAnchorTokens(value) { const source = repairAddressMojibake(compactWhitespace(String(value ?? "").toLowerCase())); if (!source) { return []; } const tokens = source .split(/[^a-zа-яё0-9._-]+/iu) .map((item) => item.trim()) .filter((item) => item.length >= 2); const filtered = []; for (const token of tokens) { if (/^\d+$/.test(token)) { continue; } if (/^(?:19|20)\d{2}$/.test(token)) { continue; } if (/^(?:0?[1-9]|1[0-2])[./-](?:19|20)\d{2}$/.test(token) || /^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])$/.test(token)) { continue; } if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(token)) { continue; } if (ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(token)) { continue; } filtered.push(token); } return Array.from(new Set(filtered)); } function selectPreferredAddressFragmentCandidate(rawText, normalizedText) { const normalizedCandidate = compactWhitespace(repairAddressMojibake(normalizedText ?? "")); const rawCandidate = compactWhitespace(repairAddressMojibake(rawText ?? "")); if (!normalizedCandidate && !rawCandidate) { return null; } if (!normalizedCandidate) { return rawCandidate; } if (!rawCandidate) { return normalizedCandidate; } const normalizedAnchors = extractAddressAnchorTokens(normalizedCandidate); const rawAnchors = extractAddressAnchorTokens(rawCandidate); if (rawAnchors.length > 0 && normalizedAnchors.length === 0) { return rawCandidate; } return normalizedCandidate; } function readAddressFilterString(addressDebug, key) { const filters = addressDebug?.extracted_filters; if (!filters || typeof filters !== "object") { return null; } return toNonEmptyString(filters[key]); } function findLastAddressAssistantDebug(items) { for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (!item || item.role !== "assistant" || !item.debug) { continue; } const debug = item.debug; if (debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1") { return debug; } } return null; } function findRecentAddressFilterValue(items, key) { for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (!item || item.role !== "assistant" || !item.debug) { continue; } const debug = item.debug; if (!(debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1")) { continue; } const directFilterValue = readAddressFilterString(debug, key); if (directFilterValue) { return directFilterValue; } if (key === "contract" && String(debug.anchor_type ?? "").trim() === "contract") { const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw); if (anchorValue) { return anchorValue; } } if (key === "counterparty" && String(debug.anchor_type ?? "").trim() === "counterparty") { const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw); if (anchorValue) { return anchorValue; } } } return null; } function hasAddressFollowupContextSignal(userMessage) { const repaired = repairAddressMojibake(String(userMessage ?? "")); const text = compactWhitespace(repaired.toLowerCase()); if (!text) { return false; } if (/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu.test(text)) { return true; } if (hasReferentialPointer(text)) { return true; } if (/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu.test(text)) { return true; } const shortFollowup = countTokens(text) <= 8; if (shortFollowup && hasFollowupMarker(text)) { return true; } if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu.test(text)) { return true; } if (shortFollowup && /(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) && !/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) { return true; } if (shortFollowup && hasPeriodLiteral(text)) { return true; } return false; } function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null) { const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage); const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage) ? hasAddressFollowupContextSignal(alternateMessage) : false; if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal) { return null; } const previousAddressDebug = findLastAddressAssistantDebug(items); if (!previousAddressDebug) { return null; } const previousIntent = toNonEmptyString(previousAddressDebug.detected_intent); const previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type); const previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ?? toNonEmptyString(previousAddressDebug.anchor_value_raw) ?? readAddressFilterString(previousAddressDebug, "counterparty") ?? readAddressFilterString(previousAddressDebug, "account") ?? readAddressFilterString(previousAddressDebug, "contract"); const previousFiltersRaw = previousAddressDebug.extracted_filters; const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" ? { ...previousFiltersRaw } : {}; if (!toNonEmptyString(previousFilters.contract)) { const historicalContract = findRecentAddressFilterValue(items, "contract"); if (historicalContract) { previousFilters.contract = historicalContract; } } if (!toNonEmptyString(previousFilters.counterparty)) { const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty"); if (historicalCounterparty) { previousFilters.counterparty = historicalCounterparty; } } if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) { return null; } return { followupContext: { previous_intent: previousIntent ?? undefined, previous_filters: previousFilters, previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_value: previousAnchor }, previousAddressIntent: previousIntent, previousAddressAnchor: previousAnchor }; } function isAddressLlmPreDecomposeCandidate(userMessage) { const repaired = repairAddressMojibake(String(userMessage ?? "")); const text = compactWhitespace(repaired.toLowerCase()); if (!text) { return false; } return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|сальдо|банк|выписк|платеж|оплат|поступлен|поступлени|списан|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?|doki|dokument(?:y|ov|am|a)?|platezh|oplata|schet|saldo)/i.test(text); } function extractAddressQuestionFromNormalized(normalized) { if (!normalized || typeof normalized !== "object") { return null; } const source = normalized; const fragments = Array.isArray(source.fragments) ? source.fragments : []; for (const item of fragments) { if (!item || typeof item !== "object") { continue; } const fragment = item; const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase(); if (domainRelevance === "out_of_scope") { continue; } const normalizedText = toNonEmptyString(fragment.normalized_fragment_text); const rawText = toNonEmptyString(fragment.raw_fragment_text); const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? ""); if (!candidate) { continue; } if (candidate.length >= 3 && candidate.length <= 500) { return candidate; } } return null; } function stripMarkdownJsonFence(text) { return String(text ?? "") .trim() .replace(/^```json\s*/i, "") .replace(/^```\s*/i, "") .replace(/```$/i, "") .trim(); } function safeParseLooseJson(text) { const fenced = stripMarkdownJsonFence(text); if (!fenced) { return null; } try { return JSON.parse(fenced); } catch (_error) { // Local OpenAI-compatible models often wrap JSON with extra text. // Try extracting the first top-level JSON object defensively. const start = fenced.indexOf("{"); const end = fenced.lastIndexOf("}"); if (start < 0 || end < 0 || end <= start) { return null; } const candidate = fenced.slice(start, end + 1).trim(); try { return JSON.parse(candidate); } catch (_nestedError) { return null; } } } function extractOutputTextFromRawNormalizerOutput(raw) { if (!raw || typeof raw !== "object") { return null; } const source = raw; if (typeof source.output_text === "string" && source.output_text.trim().length > 0) { return source.output_text; } if (Array.isArray(source.output)) { for (const item of source.output) { if (!item || typeof item !== "object") { continue; } const content = item.content; if (!Array.isArray(content)) { continue; } for (const block of content) { if (!block || typeof block !== "object") { continue; } if (typeof block.text === "string" && block.text.trim().length > 0) { return block.text; } } } } if (source.response && typeof source.response === "object") { const nested = source.response; if (typeof nested.output_text === "string" && nested.output_text.trim().length > 0) { return nested.output_text; } } if (Array.isArray(source.choices) && source.choices.length > 0) { const first = source.choices[0]; if (first && typeof first === "object" && first.message && typeof first.message === "object") { const message = first.message; if (typeof message.content === "string" && message.content.trim().length > 0) { return message.content; } } } return null; } function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) { const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput); if (!outputText) { return null; } const parsed = safeParseLooseJson(outputText); if (!parsed || typeof parsed !== "object") { return null; } const source = parsed; const fragments = Array.isArray(source.fragments) ? source.fragments : []; for (const item of fragments) { if (!item || typeof item !== "object") { continue; } const fragment = item; const domainRelevance = fragment.domain_relevance; if (typeof domainRelevance === "string" && domainRelevance.trim().toLowerCase() === "out_of_scope") { continue; } if (domainRelevance === false) { continue; } const normalizedText = toNonEmptyString(fragment.normalized_fragment_text); const rawText = toNonEmptyString(fragment.raw_fragment_text); const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? ""); if (!candidate) { continue; } if (candidate.length >= 3 && candidate.length <= 500) { return candidate; } } return null; } const ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([ "есть", "же", "что", "все", "всё", "год", "года", "году", "контрагентам", "предоставьте", "получить", "скажи", "skazhi", "покажи", "выведи", "сверка", "теперь", "сейчас", "этому", "этомуже", "тому", "томуже", "нему", "ней", "ним", "неуказанному", "неуказанный", "неуказанная", "неуказанное", "указанному", "указанный", "указанная", "указанное", "объект", "объекту", "период", "периоду", "сводные", "сводный", "сводная", "сводную", "сводном", "сводного", "сводному" ]); const ADDRESS_PREDECOMPOSE_LOW_QUALITY_CONTRACT_TOKENS = new Set([ "за", "же", "это", "указанный", "указанному", "период", "периоду", "тот", "тотже", "этот", "этому", "этомуже", "договор", "договору", "номер" ]); function normalizePredecomposeAnchorTokens(value) { return String(value ?? "") .trim() .toLowerCase() .replace(/ё/g, "е") .split(/[^a-zа-я0-9]+/iu) .map((token) => token.trim()) .filter(Boolean); } function isLowQualityPredecomposeCounterpartyAnchor(value) { const tokens = normalizePredecomposeAnchorTokens(value); if (tokens.length === 0) { return true; } const meaningful = tokens.filter((token) => { if (token.length < 2) { return false; } if (/^(?:19|20)\d{2}$/.test(token)) { return false; } return !ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS.has(token); }); return meaningful.length === 0; } function normalizePredecomposeCounterpartyAnchorTokensForMatch(value) { return normalizePredecomposeAnchorTokens(value).filter((token) => { if (token.length < 2) { return false; } if (/^(?:19|20)\d{2}$/.test(token)) { return false; } return !ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS.has(token); }); } function hasCounterpartyAnchorSubstitution(sourceValue, candidateValue) { const sourceNormalized = String(sourceValue ?? "").trim().toLowerCase().replace(/ё/g, "е"); const candidateNormalized = String(candidateValue ?? "").trim().toLowerCase().replace(/ё/g, "е"); if (!sourceNormalized || !candidateNormalized) { return false; } if (sourceNormalized === candidateNormalized) { return false; } if (sourceNormalized.includes(candidateNormalized) || candidateNormalized.includes(sourceNormalized)) { return false; } const sourceTokens = new Set(normalizePredecomposeCounterpartyAnchorTokensForMatch(sourceNormalized)); const candidateTokens = normalizePredecomposeCounterpartyAnchorTokensForMatch(candidateNormalized); if (sourceTokens.size === 0 || candidateTokens.length === 0) { return false; } for (const token of candidateTokens) { if (sourceTokens.has(token)) { return false; } } return true; } function isLowQualityPredecomposeContractAnchor(value) { const normalized = String(value ?? "").trim().toLowerCase().replace(/ё/g, "е"); if (!normalized) { return true; } if (/\b[a-zа-я0-9]{1,20}[\/_-][a-zа-я0-9]{1,20}(?:[\/_-][a-zа-я0-9]{1,20})?\b/iu.test(normalized)) { return false; } if (!/\d/.test(normalized)) { return true; } const tokens = normalizePredecomposeAnchorTokens(normalized); if (tokens.length === 0) { return true; } const meaningful = tokens.filter((token) => !ADDRESS_PREDECOMPOSE_LOW_QUALITY_CONTRACT_TOKENS.has(token)); return meaningful.length === 0; } function resolveRequiredAnchorTypeForIntent(intent) { if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty" || intent === "list_contracts_by_counterparty") { return "counterparty"; } if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") { return "contract"; } return null; } function evaluateAddressAnchorQuality(message) { const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(String(message ?? "")); const intent = intentResolution.intent; const anchorType = resolveRequiredAnchorTypeForIntent(intent); if (!anchorType) { return { intent, anchorType: null, anchorValue: null, quality: 0 }; } const extracted = (0, addressFilterExtractor_1.extractAddressFilters)(String(message ?? ""), intent); const anchorValue = anchorType === "counterparty" ? toNonEmptyString(extracted?.extracted_filters?.counterparty) : toNonEmptyString(extracted?.extracted_filters?.contract); if (!anchorValue) { return { intent, anchorType, anchorValue: null, quality: 0 }; } const lowQuality = anchorType === "counterparty" ? isLowQualityPredecomposeCounterpartyAnchor(anchorValue) : isLowQualityPredecomposeContractAnchor(anchorValue); return { intent, anchorType, anchorValue, quality: lowQuality ? 1 : 2 }; } function hasPredecomposeExplicitDrilldownSignal(text) { const source = String(text ?? ""); return ADDRESS_DOCS_SIGNAL_PATTERN.test(source) || ADDRESS_BANK_SIGNAL_PATTERN.test(source) || ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source); } function hasSameDateAccountFollowupSignalForPredecompose(text) { const source = String(text ?? ""); const hasSameDate = /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|та\s+же\s+дата|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu.test(source); if (!hasSameDate) { return false; } return (/(?:сч[её]т|счет|account)\D{0,12}\d{2}(?:[.,]\d{1,2})?/iu.test(source) || /(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) || /\b\d{2}(?:[.,]\d{1,2})\b/u.test(source)); } function attachAddressPredecomposeContract(meta, sourceMessage) { const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? ""); return { ...meta, predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({ sourceMessage: String(sourceMessage ?? ""), canonicalMessage }) }; } async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) { const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null; const sanitizedUserMessage = sanitizeAddressMessageForFallback(userMessage); const fallbackCandidate = resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage); const baseMeta = { attempted: false, applied: false, provider, traceId: null, effectiveMessage: userMessage, reason: "not_attempted", llmCanonicalCandidateDetected: false, fallbackRuleHit: null, sanitizedUserMessage, toolGateDecision: null, toolGateReason: null }; if (Boolean(payload?.useMock)) { if (fallbackCandidate) { const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase()); const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact; if (fallbackApplied) { return attachAddressPredecomposeContract({ ...baseMeta, applied: true, effectiveMessage: fallbackCandidate.candidate, reason: "fallback_rule_applied_without_llm", fallbackRuleHit: fallbackCandidate.rule }, userMessage); } } return attachAddressPredecomposeContract({ ...baseMeta, reason: "skipped_in_mock" }, userMessage); } const normalizePayload = { llmProvider: payload?.llmProvider, apiKey: payload?.apiKey, model: payload?.model, baseUrl: payload?.baseUrl, temperature: 0, maxOutputTokens: payload?.maxOutputTokens, promptVersion: "normalizer_v2_0_2", userQuestion: userMessage, context: payload?.context, useMock: Boolean(payload?.useMock), retryPolicy: "single-pass-strict" }; try { const normalized = await normalizerService.normalize(normalizePayload); const candidateFromNormalized = extractAddressQuestionFromNormalized(normalized?.normalized); const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output); const candidate = candidateFromNormalized ?? candidateFromRaw; if (!candidate) { if (fallbackCandidate) { const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase()); const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact; if (fallbackApplied) { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: true, traceId: normalized?.trace_id ?? null, effectiveMessage: fallbackCandidate.candidate, reason: "fallback_rule_applied_after_llm", fallbackRuleHit: fallbackCandidate.rule }, userMessage); } } return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, traceId: normalized?.trace_id ?? null, reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed" }, 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 candidateIntentKnown = candidateIntentResolution.intent !== "unknown"; const intentConflict = sourceIntentKnown && candidateIntentKnown && sourceIntentResolution.intent !== candidateIntentResolution.intent; const intentDroppedByCandidate = sourceIntentKnown && !candidateIntentKnown; const rejectCandidateForIntentSafety = intentDroppedByCandidate || (intentConflict && (sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high")); if (rejectCandidateForIntentSafety) { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: false, traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: userMessage, reason: intentDroppedByCandidate ? "normalized_fragment_rejected_intent_drop" : "normalized_fragment_rejected_intent_conflict", fallbackRuleHit: null, sanitizedUserMessage }, userMessage); } const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage); const candidateHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(candidate); const sourceLooksLikeSameDateAccountFollowup = hasSameDateAccountFollowupSignalForPredecompose(repairedSourceMessage || userMessage); const candidateInjectsDrilldownIntent = candidateIntentResolution.intent === "documents_forming_balance"; if (sourceLooksLikeSameDateAccountFollowup && !sourceHasExplicitDrilldownSignal && candidateHasExplicitDrilldownSignal && candidateInjectsDrilldownIntent && sourceIntentResolution.intent !== "documents_forming_balance") { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: false, traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: userMessage, reason: "normalized_fragment_rejected_followup_intent_injection", fallbackRuleHit: null, sanitizedUserMessage }, userMessage); } const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage); const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate); const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent; const anchorDegradedByCandidate = sameIntentForAnchorSafety && sourceAnchorQuality.anchorType && sourceAnchorQuality.quality >= 2 && candidateAnchorQuality.quality < sourceAnchorQuality.quality; if (anchorDegradedByCandidate) { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: false, traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: userMessage, reason: "normalized_fragment_rejected_anchor_degradation", fallbackRuleHit: null, sanitizedUserMessage }, userMessage); } const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety && sourceAnchorQuality.anchorType === "counterparty" && candidateAnchorQuality.anchorType === "counterparty" && sourceAnchorQuality.quality >= 2 && candidateAnchorQuality.quality >= 2 && Boolean(sourceAnchorQuality.anchorValue) && Boolean(candidateAnchorQuality.anchorValue) && hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? ""); if (counterpartyAnchorSubstitutedByCandidate) { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: false, traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: userMessage, reason: "normalized_fragment_rejected_anchor_substitution", fallbackRuleHit: null, sanitizedUserMessage }, userMessage); } if (fallbackCandidate) { const fallbackAnchorQuality = evaluateAddressAnchorQuality(String(fallbackCandidate.candidate ?? "")); const fallbackPreferredForAnchorSafety = sameIntentForAnchorSafety && fallbackAnchorQuality.intent === sourceAnchorQuality.intent && fallbackAnchorQuality.quality >= 2 && fallbackAnchorQuality.quality > candidateAnchorQuality.quality; if (fallbackPreferredForAnchorSafety) { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: true, traceId: normalized?.trace_id ?? null, llmCanonicalCandidateDetected: true, effectiveMessage: fallbackCandidate.candidate, reason: "fallback_rule_preferred_over_llm_candidate_anchor_quality", fallbackRuleHit: fallbackCandidate.rule, sanitizedUserMessage }, userMessage); } } const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const candidateCompact = compactWhitespace(candidate.toLowerCase()); const applied = sourceCompact !== candidateCompact; const candidateSource = candidateFromNormalized ? "normalized" : "raw"; const reason = candidateSource === "normalized" ? applied ? "normalized_fragment_applied" : "normalized_fragment_same" : normalized?.ok ? applied ? "raw_fragment_applied" : "raw_fragment_same" : applied ? "raw_fragment_applied_after_normalize_failed" : "raw_fragment_same_after_normalize_failed"; return attachAddressPredecomposeContract({ attempted: true, applied, provider, traceId: normalized?.trace_id ?? null, effectiveMessage: applied ? candidate : userMessage, reason, llmCanonicalCandidateDetected: true, fallbackRuleHit: null, sanitizedUserMessage }, userMessage); } catch (error) { if (fallbackCandidate) { const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase()); const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase()); const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact; if (fallbackApplied) { return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, applied: true, effectiveMessage: fallbackCandidate.candidate, reason: "fallback_rule_applied_after_llm_error", fallbackRuleHit: fallbackCandidate.rule }, userMessage); } } return attachAddressPredecomposeContract({ ...baseMeta, attempted: true, reason: `error:${error instanceof Error ? error.message : String(error)}` }, userMessage); } } function resolveAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null) { const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? "")); const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage); const hasClassifierSignal = modeDetection.mode === "address_query"; const hasLlmCanonicalSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected); const hasMessageSignal = hasClassifierSignal || hasLlmCanonicalSignal || isAddressLlmPreDecomposeCandidate(addressInputMessage) || isAddressLlmPreDecomposeCandidate(repairedInputMessage) || hasAccountingSignal(addressInputMessage) || hasAccountingSignal(repairedInputMessage); if (hasMessageSignal) { return { runAddressLane: true, decision: "run_address_lane", reason: hasClassifierSignal ? "address_mode_classifier_detected" : hasLlmCanonicalSignal ? "llm_canonical_candidate_detected" : "address_signal_detected" }; } if (followupContext) { return { runAddressLane: true, decision: "run_address_lane", reason: "followup_context_detected" }; } return { runAddressLane: false, decision: "skip_address_lane", reason: "no_address_signal_after_l0" }; } class AssistantService { normalizerService; sessions; dataLayer; sessionLogger; addressQueryService; constructor(normalizerService, sessions, dataLayer = new assistantDataLayer_1.AssistantDataLayer(), sessionLogger = new assistantSessionLogger_1.AssistantSessionLogger(), addressQueryService = new addressQueryService_1.AddressQueryService()) { this.normalizerService = normalizerService; this.sessions = sessions; this.dataLayer = dataLayer; this.sessionLogger = sessionLogger; this.addressQueryService = addressQueryService; } getSession(sessionId) { return this.sessions.getSession(sessionId); } async handleMessage(payload) { const session = this.sessions.ensureSession(payload.session_id); const sessionId = session.session_id; const userMessage = String(payload.user_message ?? payload.message ?? "").trim(); const userItem = { message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, session_id: sessionId, role: "user", text: userMessage, reply_type: null, created_at: new Date().toISOString(), trace_id: null, debug: null }; this.sessions.appendItem(sessionId, userItem); const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? ""); const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta); const assistantItem = { message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, session_id: sessionId, role: "assistant", text: safeAddressReply, reply_type: addressLane.reply_type, created_at: new Date().toISOString(), trace_id: debug.trace_id, debug }; this.sessions.appendItem(sessionId, assistantItem); const current = this.sessions.getSession(sessionId); if (current) { this.sessionLogger.persistSession(current); } const conversation = cloneItems(current?.items ?? []); (0, log_1.logJson)({ timestamp: new Date().toISOString(), level: "info", service: "assistant_loop", message: "assistant_message_processed", sessionId, eventType: "assistant_message_address", details: { session_id: sessionId, message_id: assistantItem.message_id, user_message: userMessage, effective_address_user_message: effectiveAddressUserMessage, address_followup_context_applied: Boolean(carryoverMeta), address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null, address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null, address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted), address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied), address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null, address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null, address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null, address_fallback_rule_hit: llmPreDecomposeMeta?.fallbackRuleHit ?? null, address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null, address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null, address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null, address_llm_predecompose_contract_intent: llmPreDecomposeMeta?.predecomposeContract?.intent ?? null, address_llm_predecompose_contract_aggregation_profile: llmPreDecomposeMeta?.predecomposeContract?.aggregation_profile ?? null, address_llm_predecompose_contract_period_scope: llmPreDecomposeMeta?.predecomposeContract?.period?.scope ?? null, detected_mode: addressLane.debug.detected_mode, query_shape: addressLane.debug.query_shape, detected_intent: addressLane.debug.detected_intent, extracted_filters: addressLane.debug.extracted_filters, selected_recipe: addressLane.debug.selected_recipe, mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy, account_scope_mode: addressLane.debug.account_scope_mode, account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied, anchor_type: addressLane.debug.anchor_type, resolver_confidence: addressLane.debug.resolver_confidence, match_failure_stage: addressLane.debug.match_failure_stage, match_failure_reason: addressLane.debug.match_failure_reason, mcp_call_status: addressLane.debug.mcp_call_status, rows_fetched: addressLane.debug.rows_fetched, raw_rows_received: addressLane.debug.raw_rows_received, rows_after_account_scope: addressLane.debug.rows_after_account_scope, rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter, rows_materialized: addressLane.debug.rows_materialized, rows_matched: addressLane.debug.rows_matched, materialization_drop_reason: addressLane.debug.materialization_drop_reason, account_token_raw: addressLane.debug.account_token_raw, account_token_normalized: addressLane.debug.account_token_normalized, account_scope_fields_checked: addressLane.debug.account_scope_fields_checked, account_scope_match_strategy: addressLane.debug.account_scope_match_strategy, account_scope_drop_reason: addressLane.debug.account_scope_drop_reason, runtime_readiness: addressLane.debug.runtime_readiness, limited_reason_category: addressLane.debug.limited_reason_category, response_type: addressLane.debug.response_type, limitations: addressLane.debug.limitations, assistant_reply: assistantItem.text, reply_type: assistantItem.reply_type, trace_id: assistantItem.trace_id } }); return { ok: true, session_id: sessionId, assistant_reply: assistantItem.text, reply_type: assistantItem.reply_type, conversation_item: assistantItem, debug, conversation }; }; let addressRuntimeMetaForDeep = null; if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 ? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage) : { attempted: false, applied: false, provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null, traceId: null, effectiveMessage: userMessage, reason: "disabled_by_feature_flag", llmCanonicalCandidateDetected: false, predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({ sourceMessage: userMessage, canonicalMessage: userMessage }), fallbackRuleHit: null, sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage), toolGateDecision: null, toolGateReason: null }; const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage; const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items, addressInputMessage); const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null, addressPreDecompose); const addressRuntimeMeta = { ...addressPreDecompose, toolGateDecision: toolGate.decision, toolGateReason: toolGate.reason }; addressRuntimeMetaForDeep = addressRuntimeMeta; if (!toolGate.runAddressLane) { (0, log_1.logJson)({ timestamp: new Date().toISOString(), level: "info", service: "assistant_loop", message: "assistant_address_tool_gate_skip", sessionId, details: { session_id: sessionId, user_message: userMessage, effective_address_user_message: addressInputMessage, address_llm_predecompose_attempted: Boolean(addressRuntimeMeta?.attempted), address_llm_predecompose_applied: Boolean(addressRuntimeMeta?.applied), address_llm_predecompose_reason: addressRuntimeMeta?.reason ?? null, address_fallback_rule_hit: addressRuntimeMeta?.fallbackRuleHit ?? null, address_sanitized_user_message: addressRuntimeMeta?.sanitizedUserMessage ?? null, address_tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null, address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null, address_llm_predecompose_contract_intent: addressRuntimeMeta?.predecomposeContract?.intent ?? null, address_llm_predecompose_contract_aggregation_profile: addressRuntimeMeta?.predecomposeContract?.aggregation_profile ?? null, address_llm_predecompose_contract_period_scope: addressRuntimeMeta?.predecomposeContract?.period?.scope ?? null } }); } if (toolGate.runAddressLane) { const shouldPreferContextualLane = Boolean(carryover?.followupContext); if (shouldPreferContextualLane) { const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, { followupContext: carryover.followupContext }); if (contextualAddressLane?.handled) { return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta); } } const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage); if (primaryAddressLane?.handled) { return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressRuntimeMeta); } if (!shouldPreferContextualLane && carryover?.followupContext) { const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, { followupContext: carryover.followupContext }); if (contextualAddressLane?.handled) { return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta); } } } } const followupBinding = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 && session.investigation_state ? buildFollowupStateBinding({ userMessage, payloadContext: payload.context, investigationState: session.investigation_state }) : { normalizedQuestion: userMessage, mergedContext: payload.context, usage: null }; const normalizePayload = { llmProvider: payload.llmProvider, apiKey: payload.apiKey, model: payload.model, baseUrl: payload.baseUrl, temperature: payload.temperature, maxOutputTokens: payload.maxOutputTokens, promptVersion: payload.promptVersion ?? "address_query_runtime_v1", systemPrompt: payload.systemPrompt, developerPrompt: payload.developerPrompt, domainPrompt: payload.domainPrompt, fewShotExamples: payload.fewShotExamples, userQuestion: followupBinding.normalizedQuestion, context: followupBinding.mergedContext, useMock: Boolean(payload.useMock) }; const normalized = await this.normalizerService.normalize(normalizePayload); const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage); const initialBusinessScopeResolution = resolveBusinessScopeAlignment({ userMessage, companyAnchors, normalized: normalized.normalized, routeSummary: normalized.route_hint_summary }); const inferredDomainByMessage = inferP0DomainFromMessage(userMessage); const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" || inferredDomainByMessage === "vat_document_register_book" || inferredDomainByMessage === "month_close_costs_20_44" || inferredDomainByMessage === "fixed_asset_amortization" ? inferredDomainByMessage : null; const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({ userMessage, normalized: normalized.normalized, companyAnchors }); const domainPolarityGuardInitial = (0, assistantRuntimeGuards_1.resolveDomainPolarityGuard)({ userMessage, companyAnchors, focusDomainHint: focusDomainForGuards }); const claimAnchorAudit = (0, assistantClaimBoundEvidence_1.resolveClaimBoundAnchors)({ userMessage, companyAnchors, focusDomainHint: focusDomainForGuards, primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window }); const businessScopeResolution = resolveBusinessScopeFromLiveContext({ current: initialBusinessScopeResolution, temporalGuard, claimType: claimAnchorAudit.claim_type, focusDomainHint: focusDomainForGuards, userMessage, companyAnchors, followupApplied: Boolean(followupBinding.usage?.applied) }); const resolvedRouteSummary = businessScopeResolution.route_summary_resolved; const requirementExtraction = extractRequirements(resolvedRouteSummary, normalized.normalized, userMessage); let executionPlan = toExecutionPlan(resolvedRouteSummary, normalized.normalized, userMessage, requirementExtraction.byFragment); const rbpRoutePlanEnforcement = enforceRbpLiveRoutePlan({ executionPlan, claimType: claimAnchorAudit.claim_type, temporalGuard }); executionPlan = rbpRoutePlanEnforcement.executionPlan; const faRoutePlanEnforcement = enforceFaLiveRoutePlan({ executionPlan, claimType: claimAnchorAudit.claim_type, temporalGuard }); executionPlan = faRoutePlanEnforcement.executionPlan; executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard); executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial); const retrievalCalls = []; const retrievalResultsRaw = []; let retrievalResults = []; for (const planItem of executionPlan) { if (!planItem.should_execute) { retrievalCalls.push({ fragment_id: planItem.fragment_id, requirement_ids: planItem.requirement_ids, route: planItem.route, status: "skipped", query_text: planItem.fragment_text, reason: mapNoRouteReason(planItem.no_route_reason) }); retrievalResults.push(buildSkippedResult(planItem)); continue; } retrievalCalls.push({ fragment_id: planItem.fragment_id, requirement_ids: planItem.requirement_ids, route: planItem.route, status: "executed", query_text: planItem.fragment_text, reason: null }); try { const raw = await this.dataLayer.executeRouteRuntime(planItem.route, planItem.fragment_text); retrievalResultsRaw.push({ fragment_id: planItem.fragment_id, route: planItem.route, raw_result: raw }); retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw)); } catch (error) { const message = error instanceof Error ? error.message : String(error); retrievalCalls[retrievalCalls.length - 1].status = "failed"; retrievalCalls[retrievalCalls.length - 1].reason = message; const rawError = { status: "error", result_type: "summary", items: [], summary: { route: planItem.route }, evidence: [], why_included: [], selection_reason: [], risk_factors: [], business_interpretation: [], confidence: "low", limitations: ["Route executor failed."], errors: [message] }; retrievalResultsRaw.push({ fragment_id: planItem.fragment_id, route: planItem.route, raw_result: rawError }); retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError)); } } const polarityGuardResult = (0, assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults)({ retrievalResults, guard: domainPolarityGuardInitial }); retrievalResults = polarityGuardResult.retrievalResults; const targetedEvidenceResult = (0, assistantClaimBoundEvidence_1.applyTargetedEvidenceAcquisition)({ retrievalResults, claimAudit: claimAnchorAudit }); retrievalResults = targetedEvidenceResult.retrievalResults; const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({ retrievalResults, temporal: temporalGuard, focusDomainHint: focusDomainForGuards, polarity: polarityGuardResult.audit.polarity, companyAnchors, userMessage }); retrievalResults = evidenceGateResult.retrievalResults; const rbpLiveRouteAudit = collectRbpLiveRouteAudit({ claimType: claimAnchorAudit.claim_type, retrievalResults, planAudit: rbpRoutePlanEnforcement.audit }); const faLiveRouteAudit = collectFaLiveRouteAudit({ claimType: claimAnchorAudit.claim_type, retrievalResults, planAudit: faRoutePlanEnforcement.audit }); const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults); const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults); const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({ temporal: temporalGuard, polarity: polarityGuardResult.audit, evidence: evidenceGateResult.audit, claimAnchors: claimAnchorAudit, targetedEvidenceHitRate: targetedEvidenceResult.audit.targeted_evidence_hit_rate, businessScopeResolved: businessScopeResolution.business_scope_resolved }); const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard); const focusDomainHint = followupBinding.usage?.applied ? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null : null; const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage); const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) || (Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0)); const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors; const composition = (0, answerComposer_1.composeAssistantAnswer)({ userMessage, routeSummary: resolvedRouteSummary, retrievalResults, requirements: coverageEvaluation.requirements, coverageReport: coverageEvaluation.coverage, groundingCheck, focusDomainHint, questionTypeHint: questionTypeClass, companyAnchors, normalizationPeriodExplicit, enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11, enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 }); const safeAssistantReplyBase = (0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(composition.assistant_reply); const safeAssistantReply = String(safeAssistantReplyBase ?? "") .replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "") .replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "") .trim(); const answerStructureV11 = config_1.FEATURE_ASSISTANT_CONTRACTS_V11 ? config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11 && composition.answer_structure_v11 ? composition.answer_structure_v11 : buildAnswerStructureV11({ assistantReply: safeAssistantReply, coverageReport: coverageEvaluation.coverage, groundingCheck, retrievalResults }) : null; const investigationStateSnapshot = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && session.investigation_state ? (0, investigationState_1.updateInvestigationState)({ previous: session.investigation_state, timestamp: new Date().toISOString(), questionId: userItem.message_id, userMessage, routeSummary: resolvedRouteSummary, requirements: coverageEvaluation.requirements, coverageReport: coverageEvaluation.coverage, retrievalResults, replyType: composition.reply_type, followupApplied: Boolean(followupBinding.usage?.applied) }) : null; if (config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) { this.sessions.setInvestigationState(sessionId, investigationStateSnapshot); } const debug = { trace_id: normalized.trace_id, prompt_version: normalized.prompt_version, schema_version: normalized.schema_version, fallback_type: composition.fallback_type, route_summary: resolvedRouteSummary, fragments: extractFragments(normalized.normalized), requirements_extracted: coverageEvaluation.requirements, coverage_report: coverageEvaluation.coverage, routes: toDebugRoutes(resolvedRouteSummary), retrieval_status: retrievalResults.map((item) => ({ fragment_id: item.fragment_id, requirement_ids: item.requirement_ids, route: item.route, status: item.status, result_type: item.result_type })), retrieval_results: retrievalResults, answer_grounding_check: groundingCheck, dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized), question_type_class: questionTypeClass, company_anchors: companyAnchors, business_scope_raw: businessScopeResolution.business_scope_raw, business_scope_resolved: businessScopeResolution.business_scope_resolved, company_grounding_applied: businessScopeResolution.company_grounding_applied, scope_resolution_reason: businessScopeResolution.scope_resolution_reason, company_scope_resolution_reason: businessScopeResolution.scope_resolution_reason, raw_time_anchor: temporalGuard.raw_time_anchor, raw_time_scope: temporalGuard.raw_time_scope, resolved_time_anchor: temporalGuard.resolved_time_anchor, resolved_primary_period: temporalGuard.resolved_primary_period, effective_primary_period: temporalGuard.effective_primary_period, temporal_guard_input: temporalGuard.temporal_guard_input, temporal_alignment_status: temporalGuard.temporal_alignment_status, temporal_resolution_source: temporalGuard.temporal_resolution_source, temporal_guard_basis: temporalGuard.temporal_guard_basis, temporal_guard_applied: temporalGuard.temporal_guard_applied, temporal_guard_outcome: temporalGuard.temporal_guard_outcome, temporal_guard: temporalGuard, raw_numeric_tokens: polarityGuardResult.audit.raw_numeric_tokens, classified_numeric_tokens: polarityGuardResult.audit.classified_numeric_tokens, rejected_as_non_accounts: polarityGuardResult.audit.rejected_as_non_accounts, resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors, domain_polarity_guard: polarityGuardResult.audit, claim_anchor_audit: claimAnchorAudit, settlement_role: claimAnchorAudit.settlement_role ?? null, settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [], polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable", targeted_evidence_acquisition: targetedEvidenceResult.audit, evidence_admissibility_gate: evidenceGateResult.audit, ...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}), ...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}), eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis, grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard, ...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}), problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false, problem_units_used_count: composition.problem_units_used_count ?? 0, problem_answer_mode: composition.problem_answer_mode ?? "stage1_policy_v11", ...(Array.isArray(composition.problem_unit_ids_used) && composition.problem_unit_ids_used.length > 0 ? { problem_unit_ids_used: composition.problem_unit_ids_used } : {}), address_llm_predecompose_attempted: Boolean(addressRuntimeMetaForDeep?.attempted), address_llm_predecompose_applied: Boolean(addressRuntimeMetaForDeep?.applied), address_llm_predecompose_reason: addressRuntimeMetaForDeep?.reason ?? null, address_llm_predecompose_provider: addressRuntimeMetaForDeep?.provider ?? null, address_fallback_rule_hit: addressRuntimeMetaForDeep?.fallbackRuleHit ?? null, address_tool_gate_decision: addressRuntimeMetaForDeep?.toolGateDecision ?? null, address_tool_gate_reason: addressRuntimeMetaForDeep?.toolGateReason ?? null, address_llm_predecompose_contract: addressRuntimeMetaForDeep?.predecomposeContract ?? null, answer_structure_v11: answerStructureV11, investigation_state_snapshot: investigationStateSnapshot, normalized: normalized.normalized }; const assistantItem = { message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, session_id: sessionId, role: "assistant", text: safeAssistantReply, reply_type: composition.reply_type, created_at: new Date().toISOString(), trace_id: normalized.trace_id, debug }; this.sessions.appendItem(sessionId, assistantItem); const current = this.sessions.getSession(sessionId); if (current) { this.sessionLogger.persistSession(current); } const conversation = cloneItems(current?.items ?? []); (0, log_1.logJson)({ timestamp: new Date().toISOString(), level: "info", service: "assistant_loop", message: "assistant_message_processed", sessionId, eventType: "assistant_message", details: { session_id: sessionId, message_id: assistantItem.message_id, user_message: userMessage, normalizer_output: normalized.normalized, execution_plan: executionPlan, resolved_execution_state: extractExecutionState(normalized.normalized), routes: toDebugRoutes(resolvedRouteSummary), retrieval_calls: retrievalCalls, retrieval_results_raw: retrievalResultsRaw, retrieval_results_normalized: retrievalResults, requirements_extracted: coverageEvaluation.requirements, requirements_total: coverageEvaluation.coverage.requirements_total, requirements_covered: coverageEvaluation.coverage.requirements_covered, requirements_uncovered: coverageEvaluation.coverage.requirements_uncovered, coverage_status: coverageEvaluation.coverage.requirements_total === coverageEvaluation.coverage.requirements_covered && coverageEvaluation.coverage.requirements_uncovered.length === 0 && coverageEvaluation.coverage.requirements_partially_covered.length === 0 ? "full" : "partial_or_limited", answer_grounding_status: groundingCheck.status, reply_semantic_type: composition.reply_type, why_included_summary: groundingCheck.why_included_summary, selection_reason_summary: groundingCheck.selection_reason_summary, route_subject_match: groundingCheck.route_subject_match, clarification_target: coverageEvaluation.coverage.clarification_needed_for, dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized), question_type_class: questionTypeClass, company_anchors: companyAnchors, business_scope_raw: businessScopeResolution.business_scope_raw, business_scope_resolved: businessScopeResolution.business_scope_resolved, company_grounding_applied: businessScopeResolution.company_grounding_applied, scope_resolution_reason: businessScopeResolution.scope_resolution_reason, company_scope_resolution_reason: businessScopeResolution.scope_resolution_reason, raw_time_anchor: temporalGuard.raw_time_anchor, raw_time_scope: temporalGuard.raw_time_scope, resolved_time_anchor: temporalGuard.resolved_time_anchor, resolved_primary_period: temporalGuard.resolved_primary_period, effective_primary_period: temporalGuard.effective_primary_period, temporal_guard_input: temporalGuard.temporal_guard_input, temporal_alignment_status: temporalGuard.temporal_alignment_status, temporal_resolution_source: temporalGuard.temporal_resolution_source, temporal_guard_basis: temporalGuard.temporal_guard_basis, temporal_guard_applied: temporalGuard.temporal_guard_applied, temporal_guard_outcome: temporalGuard.temporal_guard_outcome, temporal_guard: temporalGuard, raw_numeric_tokens: polarityGuardResult.audit.raw_numeric_tokens, classified_numeric_tokens: polarityGuardResult.audit.classified_numeric_tokens, rejected_as_non_accounts: polarityGuardResult.audit.rejected_as_non_accounts, resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors, domain_polarity_guard: polarityGuardResult.audit, claim_anchor_audit: claimAnchorAudit, settlement_role: claimAnchorAudit.settlement_role ?? null, settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [], polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable", targeted_evidence_acquisition: targetedEvidenceResult.audit, evidence_admissibility_gate: evidenceGateResult.audit, ...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}), ...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}), eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis, grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard, ...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}), problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false, problem_units_used_count: composition.problem_units_used_count ?? 0, problem_answer_mode: composition.problem_answer_mode ?? "stage1_policy_v11", ...(Array.isArray(composition.problem_unit_ids_used) && composition.problem_unit_ids_used.length > 0 ? { problem_unit_ids_used: composition.problem_unit_ids_used } : {}), answer_structure_v11: answerStructureV11, investigation_state_snapshot: investigationStateSnapshot, fallback_type: composition.fallback_type, assistant_reply: safeAssistantReply, reply_type: composition.reply_type, trace_id: normalized.trace_id } }); return { ok: true, session_id: sessionId, assistant_reply: safeAssistantReply, reply_type: composition.reply_type, conversation_item: assistantItem, debug, conversation }; } } exports.AssistantService = AssistantService;