ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры

This commit is contained in:
2026-04-10 19:22:40 +03:00
parent 19dbaff741
commit 80d108e506
116 changed files with 13949 additions and 2039 deletions
@@ -30,6 +30,7 @@ interface NormalizedAddressRow {
interface AddressTryHandleOptions {
followupContext?: AddressFollowupContext | null;
analysisDateHint?: string | null;
}
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
@@ -121,6 +122,36 @@ function parseFiniteNumber(value: unknown): number | null {
return null;
}
function normalizeAnalysisDateHint(value: unknown): string | null {
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: unknown): string {
if (value === null || value === undefined) {
return "";
@@ -788,6 +819,58 @@ function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCatego
return "UNKNOWN";
}
function normalizeLimitedReason(reason: string): string {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements: Array<[RegExp, string]> = [
[/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: string): string {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements: Array<[RegExp, string]> = [
[/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();
}
interface RowStageDiagnostics {
rawRowKeysSample: string[];
materializationDropReason:
@@ -945,20 +1028,28 @@ function toLegacyMcpStatus(
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
const heading =
category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
: category === "missing_anchor"
? "Для точного адресного поиска не хватает обязательного якоря."
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
: category === "recipe_visibility_gap"
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
? "Запрос понятен, но текущий режим не дает нужной детализации."
: category === "unsupported"
? "Этот запрос не подходит под address_query V1."
: "Не удалось выполнить адресный live-запрос в V1.";
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
: "Не удалось завершить проверку в адресном режиме.";
const reasonLine =
category === "unsupported"
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
: category === "missing_anchor"
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
: category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${normalizeLimitedReason(reason)}.`;
const lines = [
heading,
`Причина: ${reason}.`
reasonLine
];
if (nextStep) {
lines.push(`Что нужно уточнить: ${nextStep}.`);
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
}
return lines.join("\n");
}
@@ -1057,7 +1148,24 @@ export class AddressQueryService {
if (!decompose) {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
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: AddressFilterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
@@ -1079,8 +1187,8 @@ export class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "intent пока не поддержан в address V1",
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
});
@@ -1123,8 +1231,8 @@ export class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "recipe_visibility_gap",
reasonText: "для intent пока нет recipe в address V1",
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});