NODEDC_1C/llm_normalizer/backend/dist/services/addressQueryService.js

1752 lines
93 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 normalizeAnalysisDateHint(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
if (!isoPrefix) {
return null;
}
const year = Number(isoPrefix[1]);
const month = Number(isoPrefix[2]);
const day = Number(isoPrefix[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
}
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",
"Договор",
"Organization",
"Организация",
"ОрганизацияПредставление",
"organization",
"organization_name"
];
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("договор") ||
lowerKey.includes("organization") ||
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.organization && String(filters.organization).trim()) {
const needle = String(filters.organization);
const before = filtered.length;
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
mismatchReason = "organization_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 normalizeLimitedReason(reason) {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/address\s*v1/giu, "текущий адресный режим"],
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
[/live\s+recipe/giu, "текущий сценарий выборки"],
[/materialized\s+live-строках/giu, "доступном срезе данных"],
[/live-выборке/giu, "выборке данных"],
[/live-данных/giu, "данных"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\blookup\b/giu, "поиск"],
[/\bintent\b/giu, "сценария"],
[/\brecipe\b/giu, "шаблон выборки"],
[/\byakor\b/giu, "ориентир"],
[/\banchor\b/giu, "ориентир"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function normalizeLimitedNextStep(nextStep) {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
[/\bintent\b/giu, "сценарий"],
[/\blookup\b/giu, "поиск"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
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"
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
: category === "missing_anchor"
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
: category === "recipe_visibility_gap"
? "Запрос понятен, но текущий режим не дает нужной детализации."
: category === "unsupported"
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
: "Не удалось завершить проверку в адресном режиме.";
const reasonLine = category === "unsupported"
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
: category === "missing_anchor"
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
: category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${normalizeLimitedReason(reason)}.`;
const lines = [
heading,
reasonLine
];
if (nextStep) {
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(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 } = decompose;
const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) {
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
if (!hasTemporalFilter) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: analysisDate
};
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
baseReasons.push("as_of_date_from_analysis_context");
}
}
const composeOptionsFromFilters = (filterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined
});
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: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
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: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
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, composeOptionsFromFilters(filters.extracted_filters));
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, composeOptionsFromFilters(expandedLimitFilters));
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, composeOptionsFromFilters(autoBroadenedFilters));
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, composeOptionsFromFilters(historicalFilters));
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, composeOptionsFromFilters(filters.extracted_filters));
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, composeOptionsFromFilters(filters.extracted_filters));
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;