"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AddressQueryService = void 0; const config_1 = require("../config"); const addressQueryClassifier_1 = require("./addressQueryClassifier"); const addressQueryShapeClassifier_1 = require("./addressQueryShapeClassifier"); const addressIntentResolver_1 = require("./addressIntentResolver"); const addressFilterExtractor_1 = require("./addressFilterExtractor"); const addressRecipeCatalog_1 = require("./addressRecipeCatalog"); const addressMcpClient_1 = require("./addressMcpClient"); const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"]; const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1"; const PARTY_ANCHOR_STOPWORDS = new Set([ "ооо", "ао", "зао", "ип", "llc", "ltd", "company", "компания", "контрагент", "counterparty", "по", "by" ]); const ACCOUNT_ALIAS_MAP = { "51": ["расчетный счет", "расчетные счета", "bank account"], "52": ["валютный счет", "валютные счета", "currency account"], "60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"], "62": ["покупатель", "покупателями", "расчеты с покупателями"], "76": ["прочие расчеты", "прочими дебиторами и кредиторами"] }; 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 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 tokenLatin = transliterateCyrillicToLatin(token); return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin); }); } 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 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") { const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i; return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase())); } if (intent === "list_documents_by_counterparty") { const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i; return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0); } if (intent === "documents_forming_balance") { const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|списаниесрасчетногосчета|поступлениенарасчетныйсчет|invoice|document|sale|purchase)/i; return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0); } return rows; } function formatTopRows(rows, limit = 6) { return rows.slice(0, limit).map((row, index) => { const period = row.period ?? "дата не указана"; const amount = row.amount !== null ? `${row.amount}` : "сумма не указана"; const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / "); const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : ""; return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`; }); } function inferReplyType(responseType) { if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { return "factual"; } return "partial_coverage"; } 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 resolvePrimaryAnchor(intent, filters) { const account = typeof filters.account === "string" ? filters.account.trim() : ""; const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : ""; const contract = typeof filters.contract === "string" ? filters.contract.trim() : ""; const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : ""; if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") { if (account) { return { anchor_type: "account", anchor_value_raw: account, anchor_value_resolved: account, resolver_confidence: "high", ambiguity_count: 0 }; } } if (counterparty) { return { anchor_type: "counterparty", anchor_value_raw: counterparty, anchor_value_resolved: counterparty, resolver_confidence: "medium", ambiguity_count: 0 }; } if (contract) { return { anchor_type: "contract", anchor_value_raw: contract, anchor_value_resolved: contract, resolver_confidence: "medium", ambiguity_count: 0 }; } if (documentRef) { return { anchor_type: "document_ref", anchor_value_raw: documentRef, anchor_value_resolved: documentRef, resolver_confidence: "medium", ambiguity_count: 0 }; } return { anchor_type: "unknown", anchor_value_raw: null, anchor_value_resolved: null, resolver_confidence: "low", ambiguity_count: 0 }; } function refineAnchorFromRows(anchor, rows) { if (rows.length === 0) { return anchor; } if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") { return anchor; } const needleRaw = String(anchor.anchor_value_raw ?? "").trim(); if (!needleRaw) { return anchor; } const candidates = uniqueStrings(rows .flatMap((row) => row.analytics) .map((value) => value.trim()) .filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw))); if (candidates.length === 0) { return anchor; } if (candidates.length === 1) { return { ...anchor, anchor_value_resolved: candidates[0], resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium", ambiguity_count: 0 }; } return { ...anchor, anchor_value_resolved: candidates[0], resolver_confidence: "low", ambiguity_count: candidates.length - 1 }; } 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 } }; } function contractCandidatesFromRows(rows) { const candidates = []; for (const row of rows) { for (const token of [row.registrator, ...row.analytics]) { const normalized = token.trim(); if (!normalized) { continue; } if (/договор|contract|дог\./i.test(normalized)) { candidates.push(normalized); } } } return uniqueStrings(candidates); } function composeFactualReply(intent, rows) { if (intent === "account_balance_snapshot") { const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); const lines = [ "Адресный срез по счету собран (по движениям live MCP).", `Строк отобрано: ${rows.length}.`, `Сумма по отобранным движениям: ${movementSum}.`, ...formatTopRows(rows, 4) ]; return { responseType: "FACTUAL_SUMMARY", text: lines.join("\n") }; } if (intent === "documents_forming_balance") { const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0); const lines = [ "Собран drilldown документов, формирующих остаток по счету на указанную дату.", `Документных строк отобрано: ${rows.length}.`, `Сумма по отобранным движениям: ${movementSum}.`, ...formatTopRows(rows, 8), "Можно уточнить выборку по контрагенту, договору или периоду." ]; return { responseType: "FACTUAL_LIST", text: lines.join("\n") }; } if (intent === "list_open_contracts") { const contracts = contractCandidatesFromRows(rows); const lines = [ "Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).", `Строк движения: ${rows.length}.`, `Договорных кандидатов: ${contracts.length}.` ]; lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)); return { responseType: "FACTUAL_LIST", text: lines.join("\n") }; } if (intent === "open_items_by_counterparty_or_contract") { const lines = [ "Собраны открытые позиции по указанному фильтру (контрагент/договор).", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6) ]; return { responseType: "FACTUAL_LIST", text: lines.join("\n") }; } if (intent === "list_documents_by_counterparty") { const lines = [ "Собран список документов по контрагенту (live address lane).", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 8) ]; return { responseType: "FACTUAL_LIST", text: lines.join("\n") }; } if (intent === "bank_operations_by_counterparty") { const lines = [ "Собран список банковских операций по контрагенту (live address lane).", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 8) ]; return { responseType: "FACTUAL_LIST", text: lines.join("\n") }; } const title = intent === "list_payables_counterparties" ? "Срез обязательств (payables) собран по движениям с account scope 60/76." : intent === "list_receivables_counterparties" ? "Срез требований (receivables) собран по движениям с account scope 62/76." : "Срез адресного запроса собран."; const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)]; return { responseType: "FACTUAL_LIST", text: lines.join("\n") }; } class AddressQueryService { async tryHandle(userMessage) { if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) { return null; } const mode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage); if (mode.mode !== "address_query") { return null; } const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage); if (shape.shape === "EXPLAIN_OR_REASON") { return null; } const intent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage); const filters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent); let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters); const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters); const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons]; 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 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 = 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 (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) { 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: filteredRows.length, rawRowKeysSample: rowDiagnostics.rawRowKeysSample, materializationDropReason: rowDiagnostics.materializationDropReason, category: "recipe_visibility_gap", reasonText: "в live строках нет договорных якорей для уверенного списка незакрытых договоров", nextStep: "сузьте запрос по контрагенту или добавьте номер договора", limitations: ["no_contract_anchors_in_live_rows"], reasons: baseReasons }); } 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"); const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched"; const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe"; const category = isAnchorMismatch ? "missing_anchor" : isRecipeFilteredOut ? "recipe_visibility_gap" : isVisibilityGapCandidate ? "recipe_visibility_gap" : "empty_match"; const reasonText = isAnchorMismatch ? "якорь контрагента/договора не найден в материализованных live-строках" : isRecipeFilteredOut ? "строки по якорю найдены, но отфильтрованы intent-specific recipe" : isVisibilityGapCandidate ? "в текущем live recipe нет достаточной document/bank видимости после фильтрации" : "по выбранным фильтрам в live-выборке нет строк"; const nextStep = isAnchorMismatch ? "уточните контрагента точным именем или добавьте ИНН/договор" : isRecipeFilteredOut ? "сузьте период, уточните контрагента или документный тип" : isVisibilityGapCandidate ? "нужен специализированный recipe для document/bank контуров или более точный документный anchor" : "уточните период, контрагента, договор или снимите часть фильтров"; const limitations = isAnchorMismatch ? ["anchor_not_matched_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 = composeFactualReply(intent.intent, filteredRows); return { handled: true, reply_text: factual.text, reply_type: 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;