ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.6 Усилина оркестрация и детект data-scope для живого сленга. Убрана шаблонность soft-refusal в limited-ответах. Подрезана словарнаю жирность в фильтр-экстракторе

This commit is contained in:
2026-04-11 19:45:20 +03:00
parent 88d5561fad
commit 351993430f
20 changed files with 2060 additions and 830 deletions
@@ -1111,6 +1111,7 @@ function buildLimitedScopeLine(filters: AddressFilterSet): string | null {
function buildLimitedOffers(input: {
category: AddressLimitedReasonCategory;
shape: AddressQueryShapeDetection;
intent: AddressIntent;
filters: AddressFilterSet;
missingRequiredFilters: string[];
reason: string;
@@ -1139,6 +1140,14 @@ function buildLimitedOffers(input: {
}
}
if (input.intent === "list_receivables_counterparties") {
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
} else if (input.intent === "list_payables_counterparties") {
offers.push("показать контрагентов с максимальными хвостами кредиторки по 60/76");
} else if (input.intent === "open_items_by_counterparty_or_contract" || input.intent === "list_open_contracts") {
offers.push("показать незакрытые договоры и хвосты взаиморасчетов на дату");
}
if (counterparty) {
offers.push(`показать документы и платежи по контрагенту ${counterparty}`);
} else if (contract) {
@@ -1170,49 +1179,128 @@ function buildLimitedOffers(input: {
return Array.from(new Set(offers)).slice(0, 3);
}
function buildLimitedIntentSignalLine(input: {
intent: AddressIntent;
shape: AddressQueryShapeDetection;
}): string | null {
const byIntent: Partial<Record<AddressIntent, string>> = {
list_documents_by_counterparty: "Сигнал запроса: нужен срез документов/платежей по контрагенту.",
list_documents_by_contract: "Сигнал запроса: нужен срез документов/платежей по договору.",
bank_operations_by_counterparty: "Сигнал запроса: нужен срез банковских операций по контрагенту.",
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов."
};
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
DOCUMENT_LIST: "Сигнал запроса: список документов/операций.",
OBJECT_LOOKUP: "Сигнал запроса: поиск конкретных объектов.",
VERIFY_FACTUAL: "Сигнал запроса: проверка фактического состояния по данным.",
COMPOUND_FACTUAL_QUERY: "Сигнал запроса: комбинированная проверка взаимосвязанных фактов."
};
return byIntent[input.intent] ?? byShape[input.shape.shape] ?? null;
}
function hasAggregateLimitedSignal(input: {
shape: AddressQueryShapeDetection;
intent: AddressIntent;
reason: string;
}): boolean {
if (input.shape.shape === "AGGREGATE_LOOKUP") {
return true;
}
if (
input.intent === "counterparty_population_and_roles" ||
input.intent === "counterparty_activity_lifecycle" ||
input.intent === "contract_usage_overview" ||
input.intent === "supplier_payouts_profile" ||
input.intent === "customer_revenue_and_payments" ||
input.intent === "contract_usage_and_value"
) {
return true;
}
return /(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin|year)/iu.test(
String(input.reason ?? "")
);
}
function composeLimitedReply(input: {
category: AddressLimitedReasonCategory;
reason: string;
nextStep?: string;
shape: AddressQueryShapeDetection;
intent: AddressIntent;
filters: AddressFilterSet;
missingRequiredFilters: string[];
}): string {
const reason = normalizeLimitedReason(input.reason);
const headingSeed = `${input.category}|${input.shape.shape}|${reason}`;
const aggregateLimitedSignal = hasAggregateLimitedSignal({
shape: input.shape,
intent: input.intent,
reason: input.reason
});
const heading =
input.category === "empty_match"
? pickDeterministicVariant(headingSeed, [
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
"В текущем срезе данных по этому запросу совпадения не найдены."
"В текущем срезе данных по этому запросу совпадения не найдены.",
"По заданным фильтрам в текущем срезе совпадений пока нет."
])
: input.category === "missing_anchor"
? pickDeterministicVariant(headingSeed, [
"Чтобы ответ был точным, нужно чуть сильнее заякорить запрос.",
"Запрос понятен, но для надежного ответа не хватает опорного ориентира."
"Запрос понятен, но для надежного ответа не хватает опорного ориентира.",
"Вопрос по смыслу ясен, но пока не хватает конкретной опоры для выборки."
])
: input.category === "recipe_visibility_gap"
? pickDeterministicVariant(headingSeed, [
"Запрос понятен, но текущий сценарий выборки не дает нужной детализации.",
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки."
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки.",
"Сценарий запроса корректный, но текущая витрина не дает нужной детализации."
])
: input.category === "unsupported"
? pickDeterministicVariant(headingSeed, [
"По этому вопросу в текущем адресном контуре пока нет надежного маршрута ответа.",
"Сейчас в адресном режиме такой сценарий не закрыт без риска ошибочного вывода."
"Сейчас не дам прямой адресный ответ, чтобы не ошибиться в выводах.",
"В текущем адресном контуре этот запрос лучше не закрывать «в лоб» — риск неверной трактовки высок.",
"Для такого формата запроса нужен более широкий аналитический контур, иначе ответ будет ненадежным."
])
: "Не удалось завершить проверку в адресном режиме.";
const reasonSeed = `${headingSeed}|reason`;
const reasonLine =
input.category === "unsupported"
? "Коротко: сценарий пока не покрыт текущими адресными маршрутами."
? aggregateLimitedSignal
? pickDeterministicVariant(reasonSeed, [
"Это агрегатный/сравнительный вопрос: без расширенного анализа здесь легко дать ложную метрику.",
"Запрос про сводную аналитику или ранжирование, поэтому в address-контуре ответ сейчас будет ненадежным.",
"Нужна расширенная аналитическая обработка: адресный режим в этом кейсе не гарантирует корректный расчет."
])
: pickDeterministicVariant(reasonSeed, [
"Сценарий пока не закрыт текущими адресными маршрутами без потери точности.",
"Для этого запроса пока нет надежного ответа внутри текущего address-контура."
])
: input.category === "missing_anchor"
? "Коротко: не хватает конкретного ориентира (контрагент, договор, счет или период)."
? pickDeterministicVariant(reasonSeed, [
"Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (контрагент, договор, счет или период).",
"Для точного ответа нужен хотя бы один явный якорь: контрагент, договор, счет или период."
])
: input.category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${reason}.`;
? "Для уверенного ответа нужен более специализированный сценарий выборки."
: `${reason}.`;
const lines = [heading, reasonLine];
const signalLine = buildLimitedIntentSignalLine({
intent: input.intent,
shape: input.shape
});
if (signalLine && !(input.category === "unsupported" && aggregateLimitedSignal)) {
lines.push(signalLine);
}
const scopeLine = buildLimitedScopeLine(input.filters);
if (scopeLine) {
lines.push(scopeLine);
@@ -1221,6 +1309,7 @@ function composeLimitedReply(input: {
const offers = buildLimitedOffers({
category: input.category,
shape: input.shape,
intent: input.intent,
filters: input.filters,
missingRequiredFilters: input.missingRequiredFilters,
reason: input.reason,
@@ -1275,6 +1364,7 @@ function buildLimitedExecutionResult(input: {
reason: input.reasonText,
nextStep: input.nextStep,
shape: input.shape,
intent: input.intent.intent,
filters: input.filters,
missingRequiredFilters: input.missingRequiredFilters
}),