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

990 lines
48 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 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 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";
}
function toIsoDatePrefix(value) {
if (!value) {
return null;
}
const normalized = String(value).trim();
if (!normalized) {
return null;
}
const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/);
if (match) {
return match[1];
}
return null;
}
function deriveObservedPeriodWindow(rows) {
const dates = rows
.map((row) => toIsoDatePrefix(row.period))
.filter((item) => Boolean(item))
.sort();
if (dates.length === 0) {
return {
period_from: null,
period_to: null
};
}
return {
period_from: dates[0],
period_to: dates[dates.length - 1]
};
}
function composeAutoBroadenedPeriodPrefix(requested, observed) {
const requestedFrom = typeof requested.period_from === "string" ? requested.period_from : null;
const requestedTo = typeof requested.period_to === "string" ? requested.period_to : null;
if (requestedFrom && requestedTo && observed.period_from && observed.period_to) {
return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные ${observed.period_from}..${observed.period_to}.`;
}
if (requestedFrom && requestedTo) {
return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные по этому якорю.`;
}
return "По заданному периоду строк не найдено; показаны ближайшие доступные данные по этому якорю.";
}
function runtimeReadinessForLimitedCategory(category) {
if (category === "empty_match" || category === "missing_anchor") {
return "LIVE_QUERYABLE_WITH_LIMITS";
}
if (category === "recipe_visibility_gap") {
return "REQUIRES_SPECIALIZED_RECIPE";
}
if (category === "unsupported") {
return "DEEP_ONLY";
}
return "UNKNOWN";
}
function rowHasNonEmptyField(row, keys) {
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
}
function deriveRowStageDiagnostics(rawRows, rowsAfterAccountScope, rowsMaterialized) {
if (rawRows.length === 0 || rowsMaterialized > 0) {
return {
rawRowKeysSample: rawRows.length > 0 ? Object.keys(rawRows[0] ?? {}).slice(0, 20) : [],
materializationDropReason: "none"
};
}
if (rawRows.length > 0 && rowsAfterAccountScope === 0) {
return {
rawRowKeysSample: Object.keys(rawRows[0] ?? {}).slice(0, 20),
materializationDropReason: "dropped_by_account_scope_filter"
};
}
const rawRowKeysSample = Object.keys(rawRows[0] ?? {}).slice(0, 20);
const hasPeriodField = rawRows.some((row) => rowHasNonEmptyField(row, ["Период", "period", "Period"]));
const hasRegistratorField = rawRows.some((row) => rowHasNonEmptyField(row, ["Регистратор", "registrator", "Registrator", "document", "Recorder"]));
if (!hasPeriodField && !hasRegistratorField) {
return { rawRowKeysSample, materializationDropReason: "missing_period_and_registrator_fields" };
}
if (!hasPeriodField) {
return { rawRowKeysSample, materializationDropReason: "missing_period_field" };
}
if (!hasRegistratorField) {
return { rawRowKeysSample, materializationDropReason: "missing_registrator_field" };
}
return { rawRowKeysSample, materializationDropReason: "unknown_row_shape" };
}
function isAccountIntent(intent) {
return intent === "account_balance_snapshot" || intent === "documents_forming_balance";
}
function buildDefaultAccountScopeAudit(filters) {
const tokenRaw = typeof filters.account === "string" && filters.account.trim().length > 0 ? filters.account.trim() : null;
return {
accountTokenRaw: tokenRaw,
accountTokenNormalized: tokenRaw ? normalizeAccountToken(tokenRaw) : null,
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
accountScopeDropReason: "not_applicable"
};
}
function buildAccountScopeAudit(input) {
const tokenRaw = typeof input.filters.account === "string" && input.filters.account.trim().length > 0 ? input.filters.account.trim() : null;
const tokenNormalized = tokenRaw ? normalizeAccountToken(tokenRaw) : null;
if (!isAccountIntent(input.intent)) {
return {
accountTokenRaw: tokenRaw,
accountTokenNormalized: tokenNormalized,
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
accountScopeDropReason: "not_applicable"
};
}
if (input.accountScope.length === 0) {
return {
accountTokenRaw: tokenRaw,
accountTokenNormalized: tokenNormalized,
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
accountScopeDropReason: "no_account_scope_requested"
};
}
return {
accountTokenRaw: tokenRaw,
accountTokenNormalized: tokenNormalized,
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
accountScopeDropReason: input.rowsBeforeScope > 0 && input.rowsAfterScope === 0 ? "no_rows_after_scope_filter" : "rows_remaining_after_scope_filter"
};
}
function deriveMcpStageStatus(input) {
if (input.skipped) {
return "skipped";
}
if (input.errored) {
return "error";
}
if (input.rawRowsReceived === 0) {
return "no_raw_rows";
}
if (input.rowsMaterialized === 0) {
return "raw_rows_received_but_not_materialized";
}
if (input.rowsAnchorMatched === 0) {
return "materialized_but_not_anchor_matched";
}
if (input.rowsMatched === 0) {
return "materialized_but_filtered_out_by_recipe";
}
return "matched_non_empty";
}
function toLegacyMcpStatus(status) {
if (status === "materialized_but_not_anchor_matched" || status === "materialized_but_filtered_out_by_recipe") {
return "materialized_but_not_matched";
}
return status;
}
function composeLimitedReply(category, reason, nextStep) {
const heading = category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
: category === "missing_anchor"
? "Для точного адресного поиска не хватает обязательного якоря."
: category === "recipe_visibility_gap"
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
: category === "unsupported"
? "Этот запрос не подходит под address_query V1."
: "Не удалось выполнить адресный live-запрос в V1.";
const lines = [
heading,
`Причина: ${reason}.`
];
if (nextStep) {
lines.push(`Что нужно уточнить: ${nextStep}.`);
}
return lines.join("\n");
}
function buildLimitedExecutionResult(input) {
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
return {
handled: true,
reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep),
reply_type: "partial_coverage",
response_type: "LIMITED_WITH_REASON",
debug: {
detected_mode: input.mode.mode,
detected_mode_confidence: input.mode.confidence,
query_shape: input.shape.shape,
query_shape_confidence: input.shape.confidence,
detected_intent: input.intent.intent,
detected_intent_confidence: input.intent.confidence,
extracted_filters: input.filters,
missing_required_filters: input.missingRequiredFilters,
selected_recipe: input.selectedRecipe,
mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus),
account_scope_mode: input.accountScopeMode ?? "strict",
account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false,
anchor_type: input.anchor?.anchor_type ?? null,
anchor_value_raw: input.anchor?.anchor_value_raw ?? null,
anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null,
resolver_confidence: input.anchor?.resolver_confidence ?? null,
ambiguity_count: input.anchor?.ambiguity_count ?? 0,
match_failure_stage: input.matchFailureStage ?? "none",
match_failure_reason: input.matchFailureReason ?? null,
mcp_call_status: input.mcpCallStatus,
rows_fetched: input.rowsFetched,
raw_rows_received: input.rawRowsReceived ?? input.rowsFetched,
rows_after_account_scope: input.rowsAfterAccountScope ?? 0,
rows_after_recipe_filter: input.rowsAfterRecipeFilter ?? 0,
rows_materialized: input.rowsMaterialized ?? 0,
rows_matched: input.rowsMatched,
raw_row_keys_sample: input.rawRowKeysSample ?? [],
materialization_drop_reason: input.materializationDropReason ?? "none",
account_token_raw: accountScopeAudit.accountTokenRaw,
account_token_normalized: accountScopeAudit.accountTokenNormalized,
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
limited_reason_category: input.category,
response_type: "LIMITED_WITH_REASON",
limitations: input.limitations,
reasons: input.reasons
}
};
}
class AddressQueryService {
async tryHandle(userMessage, options = {}) {
if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
return null;
}
const followupContext = options.followupContext ?? null;
const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext);
if (!decompose) {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters);
if (intent.intent === "unknown") {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: filters.missing_required_filters,
selectedRecipe: null,
anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "intent пока не поддержан в address V1",
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
});
}
if (intent.intent === "open_items_by_counterparty_or_contract" &&
!filters.extracted_filters.counterparty &&
!filters.extracted_filters.contract) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: ["counterparty_or_contract"],
selectedRecipe: null,
anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "missing_anchor",
reasonText: "для open_items нужен якорь контрагента или договора",
nextStep: "укажите контрагента или номер/название договора",
limitations: ["open_items_requires_counterparty_or_contract_filter"],
reasons: baseReasons
});
}
if (recipeSelection.selected_recipe === null) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: recipeSelection.missing_required_filters,
selectedRecipe: null,
anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "recipe_visibility_gap",
reasonText: "для intent пока нет recipe в address V1",
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});
}
if (recipeSelection.missing_required_filters.length > 0) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: recipeSelection.missing_required_filters,
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "missing_anchor",
reasonText: "не хватает обязательных фильтров",
nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`,
limitations: ["missing_required_filters"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});
}
if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: [],
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "execution_error",
reasonText: "live address lane выключен feature-флагом",
nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1",
limitations: ["address_live_lane_disabled"],
reasons: baseReasons
});
}
const 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 (intent.intent === "list_open_contracts" && filteredRows.length > 0 && (0, composeStage_1.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 && 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);
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) {
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 = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows);
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;