ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.3: убран шаблонный LIMITED_WITH_REASON и переведен в контекстный мягкий отказ с полезными next-step.
This commit is contained in:
+174
-23
@@ -835,29 +835,173 @@ function toLegacyMcpStatus(status) {
|
||||
}
|
||||
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
|
||||
];
|
||||
function pickDeterministicVariant(seed, variants) {
|
||||
if (variants.length === 0) {
|
||||
return "";
|
||||
}
|
||||
let score = 0;
|
||||
for (const char of String(seed ?? "")) {
|
||||
score = (score + char.charCodeAt(0)) % 104_729;
|
||||
}
|
||||
return variants[score % variants.length];
|
||||
}
|
||||
function toNonEmptyFilterValue(value) {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
function isWeakOfferAnchorValue(value) {
|
||||
const normalized = String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.length < 3) {
|
||||
return true;
|
||||
}
|
||||
if (/^\d+$/u.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return /^(?:контрагент(?:ы|а|у|ом)?|договор(?:ы|а|у|ом)?|контракт(?:ы|а|у|ом)?|документ(?:ы|а|у|ом)?|оплат(?:а|ы|у|ой|ам)?|плат[её]ж(?:и|а|у|ом)?|операц(?:ия|ии|ий|ию|иями)?|период|данные|база|компания|организация)$/iu.test(normalized);
|
||||
}
|
||||
function normalizeMissingAnchorLabel(anchor) {
|
||||
if (anchor === "counterparty_or_contract") {
|
||||
return "контрагент или договор";
|
||||
}
|
||||
if (anchor === "counterparty") {
|
||||
return "контрагент";
|
||||
}
|
||||
if (anchor === "contract") {
|
||||
return "договор";
|
||||
}
|
||||
if (anchor === "account") {
|
||||
return "счет";
|
||||
}
|
||||
if (anchor === "document_ref") {
|
||||
return "документ";
|
||||
}
|
||||
if (anchor === "organization") {
|
||||
return "организация";
|
||||
}
|
||||
if (anchor === "period" || anchor === "period_from" || anchor === "period_to" || anchor === "as_of_date") {
|
||||
return "период/дата";
|
||||
}
|
||||
return anchor.replace(/_/gu, " ");
|
||||
}
|
||||
function buildLimitedScopeLine(filters) {
|
||||
const organization = toNonEmptyFilterValue(filters.organization);
|
||||
const asOfDate = toNonEmptyFilterValue(filters.as_of_date);
|
||||
const periodFrom = toNonEmptyFilterValue(filters.period_from);
|
||||
const periodTo = toNonEmptyFilterValue(filters.period_to);
|
||||
const scopeParts = [];
|
||||
if (organization) {
|
||||
scopeParts.push(`организация ${organization}`);
|
||||
}
|
||||
if (asOfDate) {
|
||||
scopeParts.push(`срез на ${asOfDate}`);
|
||||
}
|
||||
else if (periodFrom || periodTo) {
|
||||
scopeParts.push(`период ${periodFrom ?? "..."}..${periodTo ?? "..."}`);
|
||||
}
|
||||
if (scopeParts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return `Контекст запроса: ${scopeParts.join(", ")}.`;
|
||||
}
|
||||
function buildLimitedOffers(input) {
|
||||
const counterpartyRaw = toNonEmptyFilterValue(input.filters.counterparty);
|
||||
const contractRaw = toNonEmptyFilterValue(input.filters.contract);
|
||||
const counterparty = counterpartyRaw && !isWeakOfferAnchorValue(counterpartyRaw) ? counterpartyRaw : null;
|
||||
const contract = contractRaw && !isWeakOfferAnchorValue(contractRaw) ? contractRaw : null;
|
||||
const account = toNonEmptyFilterValue(input.filters.account);
|
||||
const offers = [];
|
||||
if (input.category === "missing_anchor") {
|
||||
const missingAnchors = Array.from(new Set((Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : [])
|
||||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||||
.filter((item) => item.length > 0)));
|
||||
if (missingAnchors.length > 0) {
|
||||
offers.push(`уточнить ориентир: ${missingAnchors.join(", ")}`);
|
||||
}
|
||||
if (missingAnchors.includes("контрагент или договор")) {
|
||||
offers.push("пример: «покажи документы по договору <номер> за 2020 год»");
|
||||
}
|
||||
}
|
||||
if (counterparty) {
|
||||
offers.push(`показать документы и платежи по контрагенту ${counterparty}`);
|
||||
}
|
||||
else if (contract) {
|
||||
offers.push(`показать документы и платежи по договору ${contract}`);
|
||||
}
|
||||
else {
|
||||
offers.push("показать документы/платежи по контрагенту или договору");
|
||||
}
|
||||
if (account) {
|
||||
offers.push(`проверить остаток и документы, формирующие остаток по счету ${account}`);
|
||||
}
|
||||
else {
|
||||
offers.push("показать незакрытые договоры или хвосты на дату");
|
||||
}
|
||||
const aggregateIntentSignal = input.shape.shape === "AGGREGATE_LOOKUP" ||
|
||||
/(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin)/iu.test(String(input.reason ?? ""));
|
||||
if (input.category === "unsupported" && aggregateIntentSignal) {
|
||||
offers.unshift("собрать фактическую базу по периоду, после чего посчитать метрику в расширенном анализе");
|
||||
}
|
||||
const nextStep = normalizeLimitedNextStep(input.nextStep ?? "");
|
||||
if (nextStep) {
|
||||
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
|
||||
offers.push(nextStep);
|
||||
}
|
||||
return Array.from(new Set(offers)).slice(0, 3);
|
||||
}
|
||||
function composeLimitedReply(input) {
|
||||
const reason = normalizeLimitedReason(input.reason);
|
||||
const headingSeed = `${input.category}|${input.shape.shape}|${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 reasonLine = input.category === "unsupported"
|
||||
? "Коротко: сценарий пока не покрыт текущими адресными маршрутами."
|
||||
: input.category === "missing_anchor"
|
||||
? "Коротко: не хватает конкретного ориентира (контрагент, договор, счет или период)."
|
||||
: input.category === "recipe_visibility_gap"
|
||||
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
|
||||
: `Коротко: ${reason}.`;
|
||||
const lines = [heading, reasonLine];
|
||||
const scopeLine = buildLimitedScopeLine(input.filters);
|
||||
if (scopeLine) {
|
||||
lines.push(scopeLine);
|
||||
}
|
||||
const offers = buildLimitedOffers({
|
||||
category: input.category,
|
||||
shape: input.shape,
|
||||
filters: input.filters,
|
||||
missingRequiredFilters: input.missingRequiredFilters,
|
||||
reason: input.reason,
|
||||
nextStep: input.nextStep
|
||||
});
|
||||
if (offers.length > 0) {
|
||||
lines.push(`Что могу сделать сейчас: ${offers.join("; ")}.`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -865,7 +1009,14 @@ function buildLimitedExecutionResult(input) {
|
||||
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep),
|
||||
reply_text: composeLimitedReply({
|
||||
category: input.category,
|
||||
reason: input.reasonText,
|
||||
nextStep: input.nextStep,
|
||||
shape: input.shape,
|
||||
filters: input.filters,
|
||||
missingRequiredFilters: input.missingRequiredFilters
|
||||
}),
|
||||
reply_type: "partial_coverage",
|
||||
response_type: "LIMITED_WITH_REASON",
|
||||
debug: {
|
||||
|
||||
Reference in New Issue
Block a user