"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AddressQueryService = void 0; const config_1 = require("../config"); const addressRecipeCatalog_1 = require("./addressRecipeCatalog"); const addressMcpClient_1 = require("./addressMcpClient"); const decomposeStage_1 = require("./address_runtime/decomposeStage"); const resolveStage_1 = require("./address_runtime/resolveStage"); const composeStage_1 = require("./address_runtime/composeStage"); const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"]; const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1"; const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000; const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000; const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000; const PARTY_ANCHOR_STOPWORDS = new Set([ "ооо", "ао", "зао", "ип", "llc", "ltd", "company", "компания", "контрагент", "counterparty", "по", "by" ]); const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([ "что", "чо", "были", "был", "была", "было", "ли", "какие", "какой", "покажи", "показать", "выведи", "списания", "списание", "поступления", "поступление", "доки", "документ", "документы", "документов", "банковские", "операции", "платежи", "платеж", "платежи", "плс", "please" ]); const ACCOUNT_ALIAS_MAP = { "51": ["расчетный счет", "расчетные счета", "bank account"], "52": ["валютный счет", "валютные счета", "currency account"], "60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"], "62": ["покупатель", "покупателями", "расчеты с покупателями"], "76": ["прочие расчеты", "прочими дебиторами и кредиторами"] }; const COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE = ` ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период, ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Регистратор, "" КАК СчетДт, "" КАК СчетКт, 0 КАК Сумма, ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Контрагент ИЗ Справочник.Контрагенты КАК Контрагенты `; let counterpartyCatalogCache = null; function parseFiniteNumber(value) { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string") { const parsed = Number(value.replace(",", ".").trim()); if (Number.isFinite(parsed)) { return parsed; } } return null; } function valueAsString(value) { if (value === null || value === undefined) { return ""; } return String(value); } function transliterateCyrillicToLatin(value) { const map = { а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "e", ж: "zh", з: "z", и: "i", й: "y", к: "k", л: "l", м: "m", н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f", х: "h", ц: "ts", ч: "ch", ш: "sh", щ: "sch", ъ: "", ы: "y", ь: "", э: "e", ю: "yu", я: "ya" }; let out = ""; for (const char of String(value ?? "").toLowerCase()) { out += map[char] ?? char; } return out; } function normalizeSearchText(value) { return String(value ?? "") .toLowerCase() .replace(/ё/g, "е") .replace(/[^a-zа-я0-9]+/gi, " ") .replace(/\s+/g, " ") .trim(); } function tokenizeAnchor(value) { return normalizeSearchText(value) .split(" ") .map((token) => token.trim()) .filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token)); } function anchorTokenVariants(token) { const source = String(token ?? "").trim().toLowerCase(); if (!source) { return []; } const variants = new Set([source]); if (/^[а-яё]+$/iu.test(source) && source.length >= 4) { const withoutEnding = source.replace(/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой|е|у|ы|а|я|и|ю)$/iu, ""); if (withoutEnding.length >= 3) { variants.add(withoutEnding); } const withoutTrailingVowel = source.replace(/[аеёиоуыэюя]$/iu, ""); if (withoutTrailingVowel.length >= 3) { variants.add(withoutTrailingVowel); } } return Array.from(variants); } function matchesAnchorText(searchable, anchor) { const searchableNormalized = normalizeSearchText(searchable); const searchableLatin = transliterateCyrillicToLatin(searchableNormalized); const tokens = tokenizeAnchor(anchor); if (tokens.length === 0) { const direct = normalizeSearchText(anchor); if (!direct) { return false; } return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct)); } return tokens.every((token) => { const variants = anchorTokenVariants(token); return variants.some((variant) => { const tokenLatin = transliterateCyrillicToLatin(variant); return searchableNormalized.includes(variant) || searchableLatin.includes(tokenLatin); }); }); } function isLikelyLowQualityPartyAnchor(value) { const normalized = normalizeSearchText(String(value ?? "")); if (!normalized) { return true; } const tokens = normalized.split(" ").filter(Boolean); if (tokens.length === 0) { return true; } const meaningfulTokens = tokens.filter((token) => { if (token.length < 2) { return false; } if (PARTY_ANCHOR_STOPWORDS.has(token) || LOW_QUALITY_PARTY_ANCHOR_TOKENS.has(token)) { return false; } if (/^(?:19|20)\d{2}$/.test(token)) { return false; } return true; }); return meaningfulTokens.length === 0; } function normalizeAccountToken(value) { const source = String(value ?? "").trim().replace(",", "."); const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/); if (!match) { return source.toLowerCase(); } const base = match[1]; if (!match[2]) { return base; } const sub = String(Number(match[2])); return `${base}.${sub}`; } function extractAccountTokens(searchable) { const result = []; const matcher = /\b(\d{2})(?:[.,](\d{1,2}))?\b/g; let hit = null; while ((hit = matcher.exec(searchable)) !== null) { const base = hit[1]; const sub = hit[2] ? String(Number(hit[2])) : null; result.push(sub ? `${base}.${sub}` : base); } return uniqueStrings(result); } function accountTokenMatches(requestedToken, candidateToken) { const requested = normalizeAccountToken(requestedToken); const candidate = normalizeAccountToken(candidateToken); if (requested === candidate) { return true; } if (!requested.includes(".")) { return candidate.startsWith(`${requested}.`) || candidate === requested; } return false; } function baseAccountCode(value) { const normalized = normalizeAccountToken(value); const match = normalized.match(/^(\d{2})/); return match ? match[1] : null; } function uniqueStrings(values) { return Array.from(new Set(values .map((item) => item.trim()) .filter((item) => item.length > 0))); } function normalizeCounterpartyName(value) { return normalizeSearchText(String(value ?? "")) .replace(/\s+/g, " ") .trim(); } function extractCounterpartyCatalogNames(rows) { return uniqueStrings(rows .map((row) => { const direct = valueAsString(row.Контрагент ?? row.counterparty ?? row.Counterparty).trim() || valueAsString(row.Регистратор ?? row.registrator ?? row.Registrator).trim(); return direct; }) .map((value) => value.trim()) .filter((value) => value.length >= 2)); } function scoreCounterpartyCandidate(name, anchor) { if (!matchesAnchorText(name, anchor)) { return null; } const normalizedName = normalizeCounterpartyName(name); const normalizedAnchor = normalizeCounterpartyName(anchor); if (!normalizedName || !normalizedAnchor) { return null; } let score = 0; if (normalizedName === normalizedAnchor) { score += 10_000; } else if (normalizedName.includes(normalizedAnchor)) { score += 5_000; } else if (normalizedAnchor.includes(normalizedName) && normalizedName.length >= 4) { score += 2_000; } const anchorTokens = tokenizeAnchor(anchor); for (const token of anchorTokens) { const variants = anchorTokenVariants(token); let tokenScore = 0; for (const variant of variants) { if (normalizedName.includes(variant)) { tokenScore = Math.max(tokenScore, Math.max(2, variant.length) * 20); } } if (tokenScore === 0) { return null; } score += tokenScore; } const lengthPenalty = Math.abs(normalizedName.length - normalizedAnchor.length); score -= lengthPenalty; return score; } function shouldAttemptCounterpartyCatalogResolution(intent, filters) { const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; if (!counterparty || isLikelyLowQualityPartyAnchor(counterparty)) { return false; } return (intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile" || intent === "contract_usage_and_value" || intent === "list_contracts_by_counterparty" || intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty" || intent === "open_items_by_counterparty_or_contract" || intent === "list_payables_counterparties" || intent === "list_receivables_counterparties"); } async function resolveCounterpartyViaCatalog(anchorRaw) { const requested = String(anchorRaw ?? "").trim(); if (!requested || isLikelyLowQualityPartyAnchor(requested)) { return { tried: false, resolvedValue: null, confidence: null, ambiguityCount: 0 }; } const now = Date.now(); const cacheFresh = counterpartyCatalogCache !== null && now - counterpartyCatalogCache.loadedAt <= COUNTERPARTY_CATALOG_CACHE_TTL_MS; let names = cacheFresh ? [...counterpartyCatalogCache.names] : []; if (!cacheFresh) { const mcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ query: COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(COUNTERPARTY_CATALOG_LOOKUP_LIMIT)), limit: COUNTERPARTY_CATALOG_LOOKUP_LIMIT }); if (!mcp.error) { names = extractCounterpartyCatalogNames(mcp.raw_rows); if (names.length > 0) { counterpartyCatalogCache = { names: [...names], loadedAt: now }; } } else if (counterpartyCatalogCache && counterpartyCatalogCache.names.length > 0) { names = [...counterpartyCatalogCache.names]; } else { return { tried: true, resolvedValue: null, confidence: null, ambiguityCount: 0 }; } } if (names.length === 0) { return { tried: true, resolvedValue: null, confidence: null, ambiguityCount: 0 }; } const scored = names .map((name) => { const score = scoreCounterpartyCandidate(name, requested); return score === null ? null : { name, score }; }) .filter((item) => Boolean(item)) .sort((a, b) => b.score - a.score || a.name.length - b.name.length || a.name.localeCompare(b.name, "ru")); if (scored.length === 0) { return { tried: true, resolvedValue: null, confidence: null, ambiguityCount: 0 }; } const topScore = scored[0].score; const topCandidates = scored.filter((item) => item.score === topScore); const bestCandidate = topCandidates[0]; const normalizedRequested = normalizeCounterpartyName(requested); const normalizedBest = normalizeCounterpartyName(bestCandidate.name); const isExact = normalizedBest === normalizedRequested; const isStrongContains = normalizedBest.includes(normalizedRequested); if (topCandidates.length > 1 && !isExact && !isStrongContains) { return { tried: true, resolvedValue: null, confidence: "low", ambiguityCount: topCandidates.length - 1 }; } return { tried: true, resolvedValue: bestCandidate.name, confidence: isExact ? "high" : isStrongContains ? "medium" : topCandidates.length === 1 ? "medium" : "low", ambiguityCount: topCandidates.length - 1 }; } function collectAnalyticsStrings(row) { const fixedKeys = [ "СубконтоДт1", "СубконтоДт2", "СубконтоДт3", "СубконтоКт1", "СубконтоКт2", "СубконтоКт3", "SubcontoDt1", "SubcontoDt2", "SubcontoDt3", "SubcontoKt1", "SubcontoKt2", "SubcontoKt3", "subconto_dt1", "subconto_dt2", "subconto_dt3", "subconto_kt1", "subconto_kt2", "subconto_kt3", "Counterparty", "Контрагент", "Contract", "Договор" ]; const collected = []; for (const key of fixedKeys) { const value = valueAsString(row[key]).trim(); if (value) { collected.push(value); } } for (const [key, rawValue] of Object.entries(row)) { const lowerKey = key.toLowerCase(); if (lowerKey.includes("subconto") || lowerKey.includes("субконто") || lowerKey.includes("контраг") || lowerKey.includes("договор")) { const value = valueAsString(rawValue).trim(); if (value) { collected.push(value); } } } return uniqueStrings(collected); } function toNormalizedRows(rows) { return rows .map((row) => { const period = valueAsString(row.Период ?? row.period ?? row.Period).trim() || null; const registrator = valueAsString(row.Регистратор ?? row.registrator ?? row.Registrator).trim() || valueAsString(row.document ?? row.Recorder).trim() || "(без названия)"; const accountDt = valueAsString(row.СчетДт ?? row.account_dt ?? row.AccountDt).trim() || null; const accountKt = valueAsString(row.СчетКт ?? row.account_kt ?? row.AccountKt).trim() || null; const amount = parseFiniteNumber(row.Сумма ?? row.amount ?? row.Amount); const analytics = collectAnalyticsStrings(row); return { period, registrator, account_dt: accountDt, account_kt: accountKt, amount, analytics }; }) .filter((item) => Boolean(item.period || item.registrator)); } function rowSearchableText(row) { return [row.registrator, row.account_dt ?? "", row.account_kt ?? "", ...row.analytics].join(" ").toLowerCase(); } function rowMatchesAnyAccount(row, accountScope) { if (accountScope.length === 0) { return true; } const searchable = [row.account_dt ?? "", row.account_kt ?? "", row.registrator, ...row.analytics].join(" "); const extractedTokens = extractAccountTokens(searchable); const normalizedSearch = normalizeSearchText(searchable); const translitSearch = transliterateCyrillicToLatin(normalizedSearch); return accountScope.some((account) => { const normalizedRequested = normalizeAccountToken(String(account ?? "").trim()); if (!normalizedRequested) { return false; } if (extractedTokens.some((candidate) => accountTokenMatches(normalizedRequested, candidate))) { return true; } const base = baseAccountCode(normalizedRequested); if (!base) { return false; } const aliases = ACCOUNT_ALIAS_MAP[base] ?? []; return aliases.some((alias) => { const normalizedAlias = normalizeSearchText(alias); const aliasLatin = transliterateCyrillicToLatin(normalizedAlias); return normalizedSearch.includes(normalizedAlias) || translitSearch.includes(aliasLatin); }); }); } function applyAccountScopeFilter(rows, accountScope) { if (accountScope.length === 0) { return rows; } return rows.filter((row) => rowMatchesAnyAccount(row, accountScope)); } function applyAddressFilters(rows, filters) { let filtered = [...rows]; let mismatchReason = null; if (filters.account && String(filters.account).trim()) { const scopedAccount = String(filters.account).trim(); const before = filtered.length; filtered = filtered.filter((row) => rowMatchesAnyAccount(row, [scopedAccount])); if (before > 0 && filtered.length === 0 && mismatchReason === null) { mismatchReason = "account_anchor_not_matched_in_materialized_rows"; } } if (filters.counterparty && String(filters.counterparty).trim()) { const needle = String(filters.counterparty); const before = filtered.length; filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); if (before > 0 && filtered.length === 0 && mismatchReason === null) { mismatchReason = "counterparty_anchor_not_matched_in_materialized_rows"; } } if (filters.contract && String(filters.contract).trim()) { const needle = String(filters.contract); const before = filtered.length; filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); if (before > 0 && filtered.length === 0 && mismatchReason === null) { mismatchReason = "contract_anchor_not_matched_in_materialized_rows"; } } if (filters.document_ref && String(filters.document_ref).trim()) { const needle = String(filters.document_ref); const before = filtered.length; filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle)); if (before > 0 && filtered.length === 0 && mismatchReason === null) { mismatchReason = "document_ref_anchor_not_matched_in_materialized_rows"; } } return { rows: filtered, mismatchReason }; } function applyIntentSpecificFilter(intent, rows) { if (intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_contract") { const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i; return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase())); } if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") { const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i; const matched = rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0); return matched.length > 0 ? matched : rows; } if (intent === "documents_forming_balance") { const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|списаниесрасчетногосчета|поступлениенарасчетныйсчет|invoice|document|sale|purchase)/i; const matched = rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0); return matched.length > 0 ? matched : rows; } return rows; } function hasExplicitPeriodWindow(filters) { return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) || (typeof filters.period_to === "string" && filters.period_to.trim().length > 0)); } function canAutoBroadenPeriodWindow(intent, filters) { if (!hasExplicitPeriodWindow(filters)) { return false; } return (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty" || intent === "list_documents_by_contract" || intent === "bank_operations_by_contract"); } function invertSort(sort) { return sort === "period_asc" ? "period_desc" : "period_asc"; } function isAnchorRecoveryIntent(intent) { return (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty" || intent === "list_contracts_by_counterparty" || intent === "list_documents_by_contract" || intent === "bank_operations_by_contract" || intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts"); } function isDocumentOrBankAnchorIntent(intent) { return (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty" || intent === "list_documents_by_contract" || intent === "bank_operations_by_contract"); } function toIsoDatePrefix(value) { if (!value) { return null; } const normalized = String(value).trim(); if (!normalized) { return null; } const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/); if (match) { return match[1]; } return null; } function deriveObservedPeriodWindow(rows) { const dates = rows .map((row) => toIsoDatePrefix(row.period)) .filter((item) => Boolean(item)) .sort(); if (dates.length === 0) { return { period_from: null, period_to: null }; } return { period_from: dates[0], period_to: dates[dates.length - 1] }; } function composeAutoBroadenedPeriodPrefix(requested, observed) { const requestedFrom = typeof requested.period_from === "string" ? requested.period_from : null; const requestedTo = typeof requested.period_to === "string" ? requested.period_to : null; if (requestedFrom && requestedTo && observed.period_from && observed.period_to) { return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные ${observed.period_from}..${observed.period_to}.`; } if (requestedFrom && requestedTo) { return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные по этому якорю.`; } return "По заданному периоду строк не найдено; показаны ближайшие доступные данные по этому якорю."; } function runtimeReadinessForLimitedCategory(category) { if (category === "empty_match" || category === "missing_anchor") { return "LIVE_QUERYABLE_WITH_LIMITS"; } if (category === "recipe_visibility_gap") { return "REQUIRES_SPECIALIZED_RECIPE"; } if (category === "unsupported") { return "DEEP_ONLY"; } return "UNKNOWN"; } function rowHasNonEmptyField(row, keys) { return keys.some((key) => String(row[key] ?? "").trim().length > 0); } function deriveRowStageDiagnostics(rawRows, rowsAfterAccountScope, rowsMaterialized) { if (rawRows.length === 0 || rowsMaterialized > 0) { return { rawRowKeysSample: rawRows.length > 0 ? Object.keys(rawRows[0] ?? {}).slice(0, 20) : [], materializationDropReason: "none" }; } if (rawRows.length > 0 && rowsAfterAccountScope === 0) { return { rawRowKeysSample: Object.keys(rawRows[0] ?? {}).slice(0, 20), materializationDropReason: "dropped_by_account_scope_filter" }; } const rawRowKeysSample = Object.keys(rawRows[0] ?? {}).slice(0, 20); const hasPeriodField = rawRows.some((row) => rowHasNonEmptyField(row, ["Период", "period", "Period"])); const hasRegistratorField = rawRows.some((row) => rowHasNonEmptyField(row, ["Регистратор", "registrator", "Registrator", "document", "Recorder"])); if (!hasPeriodField && !hasRegistratorField) { return { rawRowKeysSample, materializationDropReason: "missing_period_and_registrator_fields" }; } if (!hasPeriodField) { return { rawRowKeysSample, materializationDropReason: "missing_period_field" }; } if (!hasRegistratorField) { return { rawRowKeysSample, materializationDropReason: "missing_registrator_field" }; } return { rawRowKeysSample, materializationDropReason: "unknown_row_shape" }; } function isAccountIntent(intent) { return intent === "account_balance_snapshot" || intent === "documents_forming_balance"; } function buildDefaultAccountScopeAudit(filters) { const tokenRaw = typeof filters.account === "string" && filters.account.trim().length > 0 ? filters.account.trim() : null; return { accountTokenRaw: tokenRaw, accountTokenNormalized: tokenRaw ? normalizeAccountToken(tokenRaw) : null, accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, accountScopeDropReason: "not_applicable" }; } function buildAccountScopeAudit(input) { const tokenRaw = typeof input.filters.account === "string" && input.filters.account.trim().length > 0 ? input.filters.account.trim() : null; const tokenNormalized = tokenRaw ? normalizeAccountToken(tokenRaw) : null; if (!isAccountIntent(input.intent)) { return { accountTokenRaw: tokenRaw, accountTokenNormalized: tokenNormalized, accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, accountScopeDropReason: "not_applicable" }; } if (input.accountScope.length === 0) { return { accountTokenRaw: tokenRaw, accountTokenNormalized: tokenNormalized, accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, accountScopeDropReason: "no_account_scope_requested" }; } return { accountTokenRaw: tokenRaw, accountTokenNormalized: tokenNormalized, accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED], accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY, accountScopeDropReason: input.rowsBeforeScope > 0 && input.rowsAfterScope === 0 ? "no_rows_after_scope_filter" : "rows_remaining_after_scope_filter" }; } function deriveMcpStageStatus(input) { if (input.skipped) { return "skipped"; } if (input.errored) { return "error"; } if (input.rawRowsReceived === 0) { return "no_raw_rows"; } if (input.rowsMaterialized === 0) { return "raw_rows_received_but_not_materialized"; } if (input.rowsAnchorMatched === 0) { return "materialized_but_not_anchor_matched"; } if (input.rowsMatched === 0) { return "materialized_but_filtered_out_by_recipe"; } return "matched_non_empty"; } function toLegacyMcpStatus(status) { if (status === "materialized_but_not_anchor_matched" || status === "materialized_but_filtered_out_by_recipe") { return "materialized_but_not_matched"; } return status; } function composeLimitedReply(category, reason, nextStep) { const heading = category === "empty_match" ? "В live-данных по текущему фильтру записи не найдены." : category === "missing_anchor" ? "Для точного адресного поиска не хватает обязательного якоря." : category === "recipe_visibility_gap" ? "Текущий live recipe не дает нужную видимость данных для этого сценария." : category === "unsupported" ? "Этот запрос не подходит под address_query V1." : "Не удалось выполнить адресный live-запрос в V1."; const lines = [ heading, `Причина: ${reason}.` ]; if (nextStep) { lines.push(`Что нужно уточнить: ${nextStep}.`); } return lines.join("\n"); } function buildLimitedExecutionResult(input) { const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters); return { handled: true, reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep), reply_type: "partial_coverage", response_type: "LIMITED_WITH_REASON", debug: { detected_mode: input.mode.mode, detected_mode_confidence: input.mode.confidence, query_shape: input.shape.shape, query_shape_confidence: input.shape.confidence, detected_intent: input.intent.intent, detected_intent_confidence: input.intent.confidence, extracted_filters: input.filters, missing_required_filters: input.missingRequiredFilters, selected_recipe: input.selectedRecipe, mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus), account_scope_mode: input.accountScopeMode ?? "strict", account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false, anchor_type: input.anchor?.anchor_type ?? null, anchor_value_raw: input.anchor?.anchor_value_raw ?? null, anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null, resolver_confidence: input.anchor?.resolver_confidence ?? null, ambiguity_count: input.anchor?.ambiguity_count ?? 0, match_failure_stage: input.matchFailureStage ?? "none", match_failure_reason: input.matchFailureReason ?? null, mcp_call_status: input.mcpCallStatus, rows_fetched: input.rowsFetched, raw_rows_received: input.rawRowsReceived ?? input.rowsFetched, rows_after_account_scope: input.rowsAfterAccountScope ?? 0, rows_after_recipe_filter: input.rowsAfterRecipeFilter ?? 0, rows_materialized: input.rowsMaterialized ?? 0, rows_matched: input.rowsMatched, raw_row_keys_sample: input.rawRowKeysSample ?? [], materialization_drop_reason: input.materializationDropReason ?? "none", account_token_raw: accountScopeAudit.accountTokenRaw, account_token_normalized: accountScopeAudit.accountTokenNormalized, account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: runtimeReadinessForLimitedCategory(input.category), limited_reason_category: input.category, response_type: "LIMITED_WITH_REASON", limitations: input.limitations, reasons: input.reasons } }; } class AddressQueryService { async tryHandle(userMessage, options = {}) { if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { return null; } const followupContext = options.followupContext ?? null; const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext); if (!decompose) { return null; } const { mode, shape, intent, filters, baseReasons } = decompose; let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters); const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters); if (intent.intent === "unknown") { return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: filters.missing_required_filters, selectedRecipe: null, anchor, mcpCallStatus: "skipped", rowsFetched: 0, rowsMatched: 0, category: "unsupported", reasonText: "intent пока не поддержан в address V1", nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору", limitations: ["intent_not_supported_in_v1"], reasons: baseReasons }); } if (intent.intent === "open_items_by_counterparty_or_contract" && !filters.extracted_filters.counterparty && !filters.extracted_filters.contract) { return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: ["counterparty_or_contract"], selectedRecipe: null, anchor, mcpCallStatus: "skipped", rowsFetched: 0, rowsMatched: 0, category: "missing_anchor", reasonText: "для open_items нужен якорь контрагента или договора", nextStep: "укажите контрагента или номер/название договора", limitations: ["open_items_requires_counterparty_or_contract_filter"], reasons: baseReasons }); } if (recipeSelection.selected_recipe === null) { return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: recipeSelection.missing_required_filters, selectedRecipe: null, anchor, mcpCallStatus: "skipped", rowsFetched: 0, rowsMatched: 0, category: "recipe_visibility_gap", reasonText: "для intent пока нет recipe в address V1", nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis", limitations: ["recipe_not_available"], reasons: [...baseReasons, ...recipeSelection.selection_reason] }); } if (recipeSelection.missing_required_filters.length > 0) { return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: recipeSelection.missing_required_filters, selectedRecipe: recipeSelection.selected_recipe.recipe_id, anchor, mcpCallStatus: "skipped", rowsFetched: 0, rowsMatched: 0, category: "missing_anchor", reasonText: "не хватает обязательных фильтров", nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`, limitations: ["missing_required_filters"], reasons: [...baseReasons, ...recipeSelection.selection_reason] }); } if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1) { return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: [], selectedRecipe: recipeSelection.selected_recipe.recipe_id, anchor, mcpCallStatus: "skipped", rowsFetched: 0, rowsMatched: 0, category: "execution_error", reasonText: "live address lane выключен feature-флагом", nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1", limitations: ["address_live_lane_disabled"], reasons: baseReasons }); } const rawCounterpartyAnchor = typeof filters.extracted_filters.counterparty === "string" ? filters.extracted_filters.counterparty.trim() : ""; if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) { const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor); if (catalogResolution.resolvedValue) { if (normalizeCounterpartyName(rawCounterpartyAnchor) !== normalizeCounterpartyName(catalogResolution.resolvedValue)) { filters.warnings.push("counterparty_anchor_resolved_via_catalog_lookup"); } } else if (catalogResolution.tried) { filters.warnings.push(catalogResolution.ambiguityCount > 0 ? "counterparty_anchor_catalog_lookup_ambiguous" : "counterparty_anchor_catalog_lookup_no_match"); } anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters); if (anchor.anchor_type === "counterparty") { anchor = { ...anchor, anchor_value_raw: rawCounterpartyAnchor || anchor.anchor_value_raw }; if (catalogResolution.resolvedValue) { anchor = { ...anchor, anchor_value_resolved: catalogResolution.resolvedValue, resolver_confidence: catalogResolution.confidence ?? anchor.resolver_confidence, ambiguity_count: Math.max(anchor.ambiguity_count, catalogResolution.ambiguityCount) }; } else if (catalogResolution.ambiguityCount > 0) { anchor = { ...anchor, resolver_confidence: "low", ambiguity_count: Math.max(anchor.ambiguity_count, catalogResolution.ambiguityCount) }; } } } const plan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(recipeSelection.selected_recipe, filters.extracted_filters); const mcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ query: plan.query, limit: plan.limit }); if (mcp.error) { const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters); return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: [], selectedRecipe: recipeSelection.selected_recipe.recipe_id, accountScopeMode: plan.account_scope_mode, anchor, mcpCallStatus: deriveMcpStageStatus({ errored: true, rawRowsReceived: mcp.raw_rows.length, rowsMaterialized: 0, rowsAnchorMatched: 0, rowsMatched: 0 }), accountScopeAudit: errorScopeAudit, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, rowsAfterAccountScope: mcp.rows.length, rowsAfterRecipeFilter: 0, rowsMaterialized: 0, rowsMatched: mcp.matched_rows, rawRowKeysSample: [], materializationDropReason: "none", category: "execution_error", reasonText: "live MCP вызов завершился ошибкой", nextStep: mcp.error, limitations: ["mcp_call_failed"], reasons: [...baseReasons, mcp.error] }); } const normalizedRawRows = toNormalizedRows(mcp.raw_rows); const scopedRows = applyAccountScopeFilter(normalizedRawRows, plan.account_scope); const accountScopeFallbackApplied = plan.account_scope_mode === "preferred" && plan.account_scope.length > 0 && normalizedRawRows.length > 0 && scopedRows.length === 0; const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows; anchor = (0, resolveStage_1.refineAnchorFromRows)(anchor, normalizedRows); const filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved ? { ...filters.extracted_filters, counterparty: anchor.anchor_value_resolved } : anchor.anchor_type === "contract" && anchor.anchor_value_resolved ? { ...filters.extracted_filters, contract: anchor.anchor_value_resolved } : filters.extracted_filters; const accountScopeAudit = buildAccountScopeAudit({ intent: intent.intent, filters: filtersForMatching, accountScope: plan.account_scope, rowsBeforeScope: normalizedRawRows.length, rowsAfterScope: normalizedRows.length }); const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching); const filterByAnchors = anchorFilter.rows; const filteredRows = applyIntentSpecificFilter(intent.intent, filterByAnchors); const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length); const stageStatus = deriveMcpStageStatus({ rawRowsReceived: mcp.raw_rows.length, rowsMaterialized: normalizedRows.length, rowsAnchorMatched: filterByAnchors.length, rowsMatched: filteredRows.length }); const matchFailureStage = stageStatus === "materialized_but_not_anchor_matched" ? "materialized_but_not_anchor_matched" : stageStatus === "materialized_but_filtered_out_by_recipe" ? "materialized_but_filtered_out_by_recipe" : "none"; const matchFailureReason = matchFailureStage === "materialized_but_not_anchor_matched" ? anchorFilter.mismatchReason ?? "anchor_not_matched_after_materialization" : matchFailureStage === "materialized_but_filtered_out_by_recipe" ? "rows_filtered_out_by_intent_recipe_after_anchor_match" : null; if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) { const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors); const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors; if (recoveredRows.length > 0) { const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows, { userMessage }); const recoveryReason = recoveredBankRows.length > 0 ? "contract_docs_recovered_via_bank_fallback" : "contract_docs_recovered_via_anchor_rows"; const replyPrefix = recoveredBankRows.length > 0 ? "Документный фильтр в live дал пустой набор; показываю связанные банковские операции по договору." : "Документный фильтр в live дал пустой набор; показываю найденные строки по договорному якорю."; return { handled: true, reply_text: `${replyPrefix}\n${factual.text}`, reply_type: (0, composeStage_1.inferReplyType)(factual.responseType), response_type: factual.responseType, debug: { detected_mode: mode.mode, detected_mode_confidence: mode.confidence, query_shape: shape.shape, query_shape_confidence: shape.confidence, detected_intent: intent.intent, detected_intent_confidence: intent.confidence, extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: recipeSelection.selected_recipe.recipe_id, mcp_call_status_legacy: toLegacyMcpStatus("matched_non_empty"), account_scope_mode: plan.account_scope_mode, account_scope_fallback_applied: accountScopeFallbackApplied, anchor_type: anchor.anchor_type, anchor_value_raw: anchor.anchor_value_raw, anchor_value_resolved: anchor.anchor_value_resolved, resolver_confidence: anchor.resolver_confidence, ambiguity_count: anchor.ambiguity_count, match_failure_stage: "none", match_failure_reason: null, mcp_call_status: "matched_non_empty", rows_fetched: mcp.fetched_rows, raw_rows_received: mcp.raw_rows.length, rows_after_account_scope: normalizedRows.length, rows_after_recipe_filter: filterByAnchors.length, rows_materialized: normalizedRows.length, rows_matched: recoveredRows.length, raw_row_keys_sample: rowDiagnostics.rawRowKeysSample, materialization_drop_reason: rowDiagnostics.materializationDropReason, account_token_raw: accountScopeAudit.accountTokenRaw, account_token_normalized: accountScopeAudit.accountTokenNormalized, account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: factual.responseType, limitations: [...filters.warnings, recoveryReason], reasons: [...baseReasons, recoveryReason] } }; } } if (filteredRows.length === 0 && isAnchorRecoveryIntent(intent.intent) && (stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) { const currentLimit = typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit) ? Math.max(1, Math.trunc(filters.extracted_filters.limit)) : plan.limit; if (currentLimit < ADDRESS_ANCHOR_RECOVERY_LIMIT) { const expandedLimitFilters = { ...filters.extracted_filters, limit: ADDRESS_ANCHOR_RECOVERY_LIMIT }; const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, expandedLimitFilters); if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) { const expandedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters); if (expandedPlan.limit > currentLimit) { const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ query: expandedPlan.query, limit: expandedPlan.limit }); if (!expandedMcp.error) { const expandedRawRows = toNormalizedRows(expandedMcp.raw_rows); const expandedScopedRows = applyAccountScopeFilter(expandedRawRows, expandedPlan.account_scope); const expandedAccountScopeFallbackApplied = expandedPlan.account_scope_mode === "preferred" && expandedPlan.account_scope.length > 0 && expandedRawRows.length > 0 && expandedScopedRows.length === 0; const expandedNormalizedRows = expandedAccountScopeFallbackApplied ? expandedRawRows : expandedScopedRows; let expandedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, expandedLimitFilters); expandedAnchor = (0, resolveStage_1.refineAnchorFromRows)(expandedAnchor, expandedNormalizedRows); const expandedFiltersForMatching = expandedAnchor.anchor_type === "counterparty" && expandedAnchor.anchor_value_resolved ? { ...expandedLimitFilters, counterparty: expandedAnchor.anchor_value_resolved } : expandedAnchor.anchor_type === "contract" && expandedAnchor.anchor_value_resolved ? { ...expandedLimitFilters, contract: expandedAnchor.anchor_value_resolved } : expandedLimitFilters; const expandedAccountScopeAudit = buildAccountScopeAudit({ intent: intent.intent, filters: expandedFiltersForMatching, accountScope: expandedPlan.account_scope, rowsBeforeScope: expandedRawRows.length, rowsAfterScope: expandedNormalizedRows.length }); const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching); const expandedRowsByAnchor = expandedAnchorFilter.rows; const expandedFilteredRows = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor); if (expandedFilteredRows.length > 0) { const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length); const expandedStageStatus = deriveMcpStageStatus({ rawRowsReceived: expandedMcp.raw_rows.length, rowsMaterialized: expandedNormalizedRows.length, rowsAnchorMatched: expandedRowsByAnchor.length, rowsMatched: expandedFilteredRows.length }); const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows, { userMessage }); const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`; const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"]; const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"]; return { handled: true, reply_text: `${expandedPrefix}\n${expandedFactual.text}`, reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType), response_type: expandedFactual.responseType, debug: { detected_mode: mode.mode, detected_mode_confidence: mode.confidence, query_shape: shape.shape, query_shape_confidence: shape.confidence, detected_intent: intent.intent, detected_intent_confidence: intent.confidence, extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: expandedSelection.selected_recipe.recipe_id, mcp_call_status_legacy: toLegacyMcpStatus(expandedStageStatus), account_scope_mode: expandedPlan.account_scope_mode, account_scope_fallback_applied: expandedAccountScopeFallbackApplied, anchor_type: expandedAnchor.anchor_type, anchor_value_raw: expandedAnchor.anchor_value_raw, anchor_value_resolved: expandedAnchor.anchor_value_resolved, resolver_confidence: expandedAnchor.resolver_confidence, ambiguity_count: expandedAnchor.ambiguity_count, match_failure_stage: "none", match_failure_reason: null, mcp_call_status: expandedStageStatus, rows_fetched: expandedMcp.fetched_rows, raw_rows_received: expandedMcp.raw_rows.length, rows_after_account_scope: expandedNormalizedRows.length, rows_after_recipe_filter: expandedRowsByAnchor.length, rows_materialized: expandedNormalizedRows.length, rows_matched: expandedFilteredRows.length, raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample, materialization_drop_reason: expandedRowDiagnostics.materializationDropReason, account_token_raw: expandedAccountScopeAudit.accountTokenRaw, account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized, account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: expandedFactual.responseType, limitations: expandedLimitations, reasons: expandedReasons } }; } } } } } } if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) { const autoBroadenedFilters = { ...filters.extracted_filters }; delete autoBroadenedFilters.period_from; delete autoBroadenedFilters.period_to; const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, autoBroadenedFilters); if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) { const broadenedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters); const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ query: broadenedPlan.query, limit: broadenedPlan.limit }); if (!broadenedMcp.error) { const broadenedRawRows = toNormalizedRows(broadenedMcp.raw_rows); const broadenedScopedRows = applyAccountScopeFilter(broadenedRawRows, broadenedPlan.account_scope); const broadenedAccountScopeFallbackApplied = broadenedPlan.account_scope_mode === "preferred" && broadenedPlan.account_scope.length > 0 && broadenedRawRows.length > 0 && broadenedScopedRows.length === 0; const broadenedNormalizedRows = broadenedAccountScopeFallbackApplied ? broadenedRawRows : broadenedScopedRows; let broadenedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, autoBroadenedFilters); broadenedAnchor = (0, resolveStage_1.refineAnchorFromRows)(broadenedAnchor, broadenedNormalizedRows); const broadenedFiltersForMatching = broadenedAnchor.anchor_type === "counterparty" && broadenedAnchor.anchor_value_resolved ? { ...autoBroadenedFilters, counterparty: broadenedAnchor.anchor_value_resolved } : broadenedAnchor.anchor_type === "contract" && broadenedAnchor.anchor_value_resolved ? { ...autoBroadenedFilters, contract: broadenedAnchor.anchor_value_resolved } : autoBroadenedFilters; const broadenedAccountScopeAudit = buildAccountScopeAudit({ intent: intent.intent, filters: broadenedFiltersForMatching, accountScope: broadenedPlan.account_scope, rowsBeforeScope: broadenedRawRows.length, rowsAfterScope: broadenedNormalizedRows.length }); const broadenedAnchorFilter = applyAddressFilters(broadenedNormalizedRows, broadenedFiltersForMatching); const broadenedRowsByAnchor = broadenedAnchorFilter.rows; const broadenedFilteredRows = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor); if (broadenedFilteredRows.length > 0) { const broadenedRowDiagnostics = deriveRowStageDiagnostics(broadenedMcp.raw_rows, broadenedNormalizedRows.length, broadenedNormalizedRows.length); const broadenedStageStatus = deriveMcpStageStatus({ rawRowsReceived: broadenedMcp.raw_rows.length, rowsMaterialized: broadenedNormalizedRows.length, rowsAnchorMatched: broadenedRowsByAnchor.length, rowsMatched: broadenedFilteredRows.length }); const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows); const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow); const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows, { userMessage }); const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"]; const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"]; return { handled: true, reply_text: `${broadenedPrefix}\n${broadenedFactual.text}`, reply_type: (0, composeStage_1.inferReplyType)(broadenedFactual.responseType), response_type: broadenedFactual.responseType, debug: { detected_mode: mode.mode, detected_mode_confidence: mode.confidence, query_shape: shape.shape, query_shape_confidence: shape.confidence, detected_intent: intent.intent, detected_intent_confidence: intent.confidence, extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: broadenedSelection.selected_recipe.recipe_id, mcp_call_status_legacy: toLegacyMcpStatus(broadenedStageStatus), account_scope_mode: broadenedPlan.account_scope_mode, account_scope_fallback_applied: broadenedAccountScopeFallbackApplied, anchor_type: broadenedAnchor.anchor_type, anchor_value_raw: broadenedAnchor.anchor_value_raw, anchor_value_resolved: broadenedAnchor.anchor_value_resolved, resolver_confidence: broadenedAnchor.resolver_confidence, ambiguity_count: broadenedAnchor.ambiguity_count, match_failure_stage: "none", match_failure_reason: null, mcp_call_status: broadenedStageStatus, rows_fetched: broadenedMcp.fetched_rows, raw_rows_received: broadenedMcp.raw_rows.length, rows_after_account_scope: broadenedNormalizedRows.length, rows_after_recipe_filter: broadenedRowsByAnchor.length, rows_materialized: broadenedNormalizedRows.length, rows_matched: broadenedFilteredRows.length, raw_row_keys_sample: broadenedRowDiagnostics.rawRowKeysSample, materialization_drop_reason: broadenedRowDiagnostics.materializationDropReason, account_token_raw: broadenedAccountScopeAudit.accountTokenRaw, account_token_normalized: broadenedAccountScopeAudit.accountTokenNormalized, account_scope_fields_checked: broadenedAccountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: broadenedAccountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: broadenedAccountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: broadenedFactual.responseType, limitations: broadenedLimitations, reasons: broadenedReasons } }; } } } } if (filteredRows.length === 0 && isDocumentOrBankAnchorIntent(intent.intent) && !hasExplicitPeriodWindow(filters.extracted_filters) && (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract")) { const currentLimit = typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit) ? Math.max(1, Math.trunc(filters.extracted_filters.limit)) : plan.limit; const historicalFilters = { ...filters.extracted_filters, sort: invertSort(filters.extracted_filters.sort), limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT) }; const historicalSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, historicalFilters); if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) { const historicalPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(historicalSelection.selected_recipe, historicalFilters); const historicalMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ query: historicalPlan.query, limit: historicalPlan.limit }); if (!historicalMcp.error) { const historicalRawRows = toNormalizedRows(historicalMcp.raw_rows); const historicalScopedRows = applyAccountScopeFilter(historicalRawRows, historicalPlan.account_scope); const historicalAccountScopeFallbackApplied = historicalPlan.account_scope_mode === "preferred" && historicalPlan.account_scope.length > 0 && historicalRawRows.length > 0 && historicalScopedRows.length === 0; const historicalNormalizedRows = historicalAccountScopeFallbackApplied ? historicalRawRows : historicalScopedRows; let historicalAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, historicalFilters); historicalAnchor = (0, resolveStage_1.refineAnchorFromRows)(historicalAnchor, historicalNormalizedRows); const historicalFiltersForMatching = historicalAnchor.anchor_type === "counterparty" && historicalAnchor.anchor_value_resolved ? { ...historicalFilters, counterparty: historicalAnchor.anchor_value_resolved } : historicalAnchor.anchor_type === "contract" && historicalAnchor.anchor_value_resolved ? { ...historicalFilters, contract: historicalAnchor.anchor_value_resolved } : historicalFilters; const historicalAccountScopeAudit = buildAccountScopeAudit({ intent: intent.intent, filters: historicalFiltersForMatching, accountScope: historicalPlan.account_scope, rowsBeforeScope: historicalRawRows.length, rowsAfterScope: historicalNormalizedRows.length }); const historicalAnchorFilter = applyAddressFilters(historicalNormalizedRows, historicalFiltersForMatching); const historicalRowsByAnchor = historicalAnchorFilter.rows; const historicalFilteredRows = applyIntentSpecificFilter(intent.intent, historicalRowsByAnchor); if (historicalFilteredRows.length > 0) { const historicalRowDiagnostics = deriveRowStageDiagnostics(historicalMcp.raw_rows, historicalNormalizedRows.length, historicalNormalizedRows.length); const historicalStageStatus = deriveMcpStageStatus({ rawRowsReceived: historicalMcp.raw_rows.length, rowsMaterialized: historicalNormalizedRows.length, rowsAnchorMatched: historicalRowsByAnchor.length, rowsMatched: historicalFilteredRows.length }); const historicalFactual = (0, composeStage_1.composeFactualReply)(intent.intent, historicalFilteredRows, { userMessage }); const historicalPrefix = "Найдены данные в историческом срезе базы по вашему запросу."; const historicalSuggestion = intent.intent === "list_documents_by_counterparty" ? "\nЕсли нужно, могу дополнительно показать платежи и договоры по этому контрагенту." : ""; const historicalLimitations = [...filters.warnings, "historical_window_sort_recovery_applied"]; const historicalReasons = [...baseReasons, "historical_window_sort_recovery_applied"]; return { handled: true, reply_text: `${historicalPrefix}\n${historicalFactual.text}${historicalSuggestion}`, reply_type: (0, composeStage_1.inferReplyType)(historicalFactual.responseType), response_type: historicalFactual.responseType, debug: { detected_mode: mode.mode, detected_mode_confidence: mode.confidence, query_shape: shape.shape, query_shape_confidence: shape.confidence, detected_intent: intent.intent, detected_intent_confidence: intent.confidence, extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: historicalSelection.selected_recipe.recipe_id, mcp_call_status_legacy: toLegacyMcpStatus(historicalStageStatus), account_scope_mode: historicalPlan.account_scope_mode, account_scope_fallback_applied: historicalAccountScopeFallbackApplied, anchor_type: historicalAnchor.anchor_type, anchor_value_raw: historicalAnchor.anchor_value_raw, anchor_value_resolved: historicalAnchor.anchor_value_resolved, resolver_confidence: historicalAnchor.resolver_confidence, ambiguity_count: historicalAnchor.ambiguity_count, match_failure_stage: "none", match_failure_reason: null, mcp_call_status: historicalStageStatus, rows_fetched: historicalMcp.fetched_rows, raw_rows_received: historicalMcp.raw_rows.length, rows_after_account_scope: historicalNormalizedRows.length, rows_after_recipe_filter: historicalRowsByAnchor.length, rows_materialized: historicalNormalizedRows.length, rows_matched: historicalFilteredRows.length, raw_row_keys_sample: historicalRowDiagnostics.rawRowKeysSample, materialization_drop_reason: historicalRowDiagnostics.materializationDropReason, account_token_raw: historicalAccountScopeAudit.accountTokenRaw, account_token_normalized: historicalAccountScopeAudit.accountTokenNormalized, account_scope_fields_checked: historicalAccountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: historicalAccountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: historicalAccountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: historicalFactual.responseType, limitations: historicalLimitations, reasons: historicalReasons } }; } } } } if (filteredRows.length === 0 && isDocumentOrBankAnchorIntent(intent.intent) && normalizedRows.length > 0 && filterByAnchors.length > 0 && (stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) { const documentBankFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows); if (documentBankFallbackRows.length > 0) { const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, documentBankFallbackRows, { userMessage }); const fallbackPrefix = "По вашему запросу показываю найденные документы и операции в доступном срезе базы."; const fallbackSuggestion = intent.intent === "list_documents_by_counterparty" ? "\nЕсли нужно, могу дополнительно сузить период или показать только платежи." : ""; const fallbackLimitations = [...filters.warnings, "anchor_not_matched_fallback_rows"]; const fallbackReasons = [...baseReasons, "anchor_not_matched_fallback_rows"]; return { handled: true, reply_text: `${fallbackPrefix}\n${fallbackFactual.text}${fallbackSuggestion}`, reply_type: (0, composeStage_1.inferReplyType)(fallbackFactual.responseType), response_type: fallbackFactual.responseType, debug: { detected_mode: mode.mode, detected_mode_confidence: mode.confidence, query_shape: shape.shape, query_shape_confidence: shape.confidence, detected_intent: intent.intent, detected_intent_confidence: intent.confidence, extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: recipeSelection.selected_recipe.recipe_id, mcp_call_status_legacy: "matched_non_empty", account_scope_mode: plan.account_scope_mode, account_scope_fallback_applied: accountScopeFallbackApplied, anchor_type: anchor.anchor_type, anchor_value_raw: anchor.anchor_value_raw, anchor_value_resolved: anchor.anchor_value_resolved, resolver_confidence: anchor.resolver_confidence, ambiguity_count: anchor.ambiguity_count, match_failure_stage: matchFailureStage, match_failure_reason: matchFailureReason, mcp_call_status: "matched_non_empty", rows_fetched: mcp.fetched_rows, raw_rows_received: mcp.raw_rows.length, rows_after_account_scope: normalizedRows.length, rows_after_recipe_filter: filterByAnchors.length, rows_materialized: normalizedRows.length, rows_matched: documentBankFallbackRows.length, raw_row_keys_sample: rowDiagnostics.rawRowKeysSample, materialization_drop_reason: rowDiagnostics.materializationDropReason, account_token_raw: accountScopeAudit.accountTokenRaw, account_token_normalized: accountScopeAudit.accountTokenNormalized, account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: fallbackFactual.responseType, limitations: fallbackLimitations, reasons: fallbackReasons } }; } } if (filteredRows.length === 0) { const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0; const hadAnchorMatchedRows = filterByAnchors.length > 0; const isVisibilityGapCandidate = hadBaseRows && hadAnchorMatchedRows && (intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty" || intent.intent === "list_documents_by_contract" || intent.intent === "bank_operations_by_contract"); const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched"; const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe"; const isFollowupAnchorCarryover = Array.isArray(filters.warnings) && (filters.warnings.includes("counterparty_from_followup_context") || filters.warnings.includes("contract_from_followup_context")); const anchorMismatchByCounterparty = isAnchorMismatch && String(matchFailureReason ?? "").includes("counterparty_anchor_not_matched"); const anchorMismatchByContract = isAnchorMismatch && String(matchFailureReason ?? "").includes("contract_anchor_not_matched"); const isLowQualityPartyAnchor = (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") && isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw); const requestedPeriodFrom = typeof filters.extracted_filters.period_from === "string" ? filters.extracted_filters.period_from : null; const requestedPeriodTo = typeof filters.extracted_filters.period_to === "string" ? filters.extracted_filters.period_to : null; const requestedPeriodHint = requestedPeriodFrom && requestedPeriodTo ? ` (период ${requestedPeriodFrom}..${requestedPeriodTo} сохранен)` : ""; const anchorMismatchCategory = isFollowupAnchorCarryover ? "empty_match" : anchorMismatchByCounterparty || anchorMismatchByContract ? "missing_anchor" : !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor"; const category = isAnchorMismatch ? anchorMismatchCategory : isRecipeFilteredOut ? "recipe_visibility_gap" : isVisibilityGapCandidate ? "recipe_visibility_gap" : "empty_match"; const reasonText = isAnchorMismatch ? anchorMismatchByCounterparty ? "контрагент по указанному имени/алиасу не найден в materialized live-строках" : anchorMismatchByContract ? "договор по указанному номеру/названию не найден в materialized live-строках" : anchorMismatchCategory === "missing_anchor" ? "якорь контрагента/договора не найден в materialized live-строках" : "по указанному якорю и фильтрам в live-выборке нет строк" : isRecipeFilteredOut ? "строки по якорю найдены, но отфильтрованы intent-specific recipe" : isVisibilityGapCandidate ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" : "по выбранным фильтрам в live-выборке нет строк"; const nextStep = isAnchorMismatch ? anchorMismatchByCounterparty ? `уточните точное имя контрагента или добавьте ИНН${requestedPeriodHint}` : anchorMismatchByContract ? `уточните номер/наименование договора${requestedPeriodHint}` : anchorMismatchCategory === "missing_anchor" ? "уточните контрагента точным именем или добавьте ИНН/договор" : "уточните период или снимите часть фильтров" : isRecipeFilteredOut ? "сузьте период, уточните контрагента или документный тип" : isVisibilityGapCandidate ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" : "уточните период, контрагента, договор или снимите часть фильтров"; const limitations = isAnchorMismatch ? [ anchorMismatchByCounterparty ? "counterparty_anchor_not_matched_after_materialization" : anchorMismatchByContract ? "contract_anchor_not_matched_after_materialization" : anchorMismatchCategory === "missing_anchor" ? "anchor_not_matched_after_materialization" : "no_rows_for_anchor_after_materialization" ] : isRecipeFilteredOut ? ["rows_filtered_out_by_recipe_after_anchor_match"] : [ isVisibilityGapCandidate ? "document_or_bank_visibility_gap_after_base_filter" : "no_rows_after_recipe_and_scope_filter" ]; return buildLimitedExecutionResult({ mode, shape, intent, filters: filters.extracted_filters, missingRequiredFilters: [], selectedRecipe: recipeSelection.selected_recipe.recipe_id, accountScopeMode: plan.account_scope_mode, accountScopeFallbackApplied, accountScopeAudit, anchor, matchFailureStage, matchFailureReason, mcpCallStatus: stageStatus, rowsFetched: mcp.fetched_rows, rawRowsReceived: mcp.raw_rows.length, rowsAfterAccountScope: normalizedRows.length, rowsAfterRecipeFilter: filterByAnchors.length, rowsMaterialized: normalizedRows.length, rowsMatched: 0, rawRowKeysSample: rowDiagnostics.rawRowKeysSample, materializationDropReason: rowDiagnostics.materializationDropReason, category, reasonText, nextStep, limitations, reasons: baseReasons }); } const factual = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows, { userMessage }); return { handled: true, reply_text: factual.text, reply_type: (0, composeStage_1.inferReplyType)(factual.responseType), response_type: factual.responseType, debug: { detected_mode: mode.mode, detected_mode_confidence: mode.confidence, query_shape: shape.shape, query_shape_confidence: shape.confidence, detected_intent: intent.intent, detected_intent_confidence: intent.confidence, extracted_filters: filters.extracted_filters, missing_required_filters: [], selected_recipe: recipeSelection.selected_recipe.recipe_id, mcp_call_status_legacy: toLegacyMcpStatus(stageStatus), account_scope_mode: plan.account_scope_mode, account_scope_fallback_applied: accountScopeFallbackApplied, anchor_type: anchor.anchor_type, anchor_value_raw: anchor.anchor_value_raw, anchor_value_resolved: anchor.anchor_value_resolved, resolver_confidence: anchor.resolver_confidence, ambiguity_count: anchor.ambiguity_count, match_failure_stage: "none", match_failure_reason: null, mcp_call_status: stageStatus, rows_fetched: mcp.fetched_rows, raw_rows_received: mcp.raw_rows.length, rows_after_account_scope: normalizedRows.length, rows_after_recipe_filter: filterByAnchors.length, rows_materialized: normalizedRows.length, rows_matched: filteredRows.length, raw_row_keys_sample: rowDiagnostics.rawRowKeysSample, materialization_drop_reason: rowDiagnostics.materializationDropReason, account_token_raw: accountScopeAudit.accountTokenRaw, account_token_normalized: accountScopeAudit.accountTokenNormalized, account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked, account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy, account_scope_drop_reason: accountScopeAudit.accountScopeDropReason, runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS", limited_reason_category: null, response_type: factual.responseType, limitations: filters.warnings, reasons: baseReasons } }; } } exports.AddressQueryService = AddressQueryService;