ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.3: убран шаблонный LIMITED_WITH_REASON и переведен в контекстный мягкий отказ с полезными next-step.
This commit is contained in:
parent
c2fa360eb9
commit
a0d0f95dde
|
|
@ -2382,6 +2382,25 @@ Implemented in current pass (Stage 3.2 semantic route arbitration):
|
|||
- Stage 3 focused suite: `10` files / `76` tests passed.
|
||||
- Type build: `npm --prefix llm_normalizer/backend run build` passed.
|
||||
|
||||
Implemented in current pass (Stage 3.3 soft-refusal + anti-template limited replies):
|
||||
1. Reworked address-lane limited reply composer from fixed template to contextual soft-refusal:
|
||||
- Added deterministic phrasing variants (stable, non-random) to reduce repeated boilerplate.
|
||||
- Kept concise "Коротко" structure while replacing hard repeated wording.
|
||||
2. Added context-aware explanation and recovery offers for limited responses:
|
||||
- Response now injects request scope hints when available (organization, as-of date, period window).
|
||||
- Added "Что могу сделать сейчас" with nearest supported scenarios (documents/payments by anchor, open contracts/tails, account-balance drilldown).
|
||||
- Added weak-anchor suppression for user-facing hints to avoid low-value auto-substitutions in suggestions.
|
||||
3. Improved unsupported aggregate handling UX:
|
||||
- For aggregate/ranking style queries, reply now proposes evidence-first path (collect factual base -> continue in extended analysis) instead of hard static fallback phrase.
|
||||
4. Improved missing-anchor UX:
|
||||
- Missing anchor tokens are normalized to user-facing terms (e.g., `counterparty_or_contract` -> "контрагент или договор").
|
||||
- Added concrete reformulation hint for underspecified anchor cases.
|
||||
5. Regression updates:
|
||||
- Updated `addressQueryRuntimeM23.test.ts` soft out-of-scope assertion to validate contextual soft-refusal structure.
|
||||
6. Validation snapshot:
|
||||
- Extended regression pack: `11` files / `344` tests passed.
|
||||
- Type build: `npm --prefix llm_normalizer/backend run build` passed.
|
||||
|
||||
Acceptance (Stage 3):
|
||||
1. LLM outputs strictly validated schema for extraction/decomposition (no free-form).
|
||||
2. Deterministic guards can block or downgrade answers when evidence insufficient.
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -1025,32 +1025,211 @@ function toLegacyMcpStatus(
|
|||
return status;
|
||||
}
|
||||
|
||||
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
|
||||
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)}.`);
|
||||
function pickDeterministicVariant(seed: string, variants: string[]): string {
|
||||
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: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function isWeakOfferAnchorValue(value: string): boolean {
|
||||
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: string): string {
|
||||
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: AddressFilterSet): string | null {
|
||||
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: string[] = [];
|
||||
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: {
|
||||
category: AddressLimitedReasonCategory;
|
||||
shape: AddressQueryShapeDetection;
|
||||
filters: AddressFilterSet;
|
||||
missingRequiredFilters: string[];
|
||||
reason: string;
|
||||
nextStep?: string;
|
||||
}): string[] {
|
||||
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: string[] = [];
|
||||
|
||||
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) {
|
||||
offers.push(nextStep);
|
||||
}
|
||||
|
||||
return Array.from(new Set(offers)).slice(0, 3);
|
||||
}
|
||||
|
||||
function composeLimitedReply(input: {
|
||||
category: AddressLimitedReasonCategory;
|
||||
reason: string;
|
||||
nextStep?: string;
|
||||
shape: AddressQueryShapeDetection;
|
||||
filters: AddressFilterSet;
|
||||
missingRequiredFilters: string[];
|
||||
}): string {
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -1091,7 +1270,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: {
|
||||
|
|
|
|||
|
|
@ -2352,7 +2352,8 @@ describe("address query limited taxonomy and stage diagnostics", () => {
|
|||
expect(result?.debug.detected_intent).toBe("unknown");
|
||||
expect(result?.debug.limited_reason_category).toBe("unsupported");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply.toLowerCase()).toContain("вне поддерживаемого контура");
|
||||
expect(reply.toLowerCase()).toContain("адресн");
|
||||
expect(reply).toContain("Что могу сделать сейчас:");
|
||||
expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
{
|
||||
"suite_id": "assistant_autogen_runtime_job-0tckNFdleX",
|
||||
"suite_version": "0.1.0",
|
||||
"schema_version": "assistant_autogen_runtime_v0_1",
|
||||
"scenario_count": 15,
|
||||
"case_ids": [
|
||||
"AUTO-001",
|
||||
"AUTO-002",
|
||||
"AUTO-003",
|
||||
"AUTO-004",
|
||||
"AUTO-005",
|
||||
"AUTO-006",
|
||||
"AUTO-007",
|
||||
"AUTO-008",
|
||||
"AUTO-009",
|
||||
"AUTO-010",
|
||||
"AUTO-011",
|
||||
"AUTO-012",
|
||||
"AUTO-013",
|
||||
"AUTO-014",
|
||||
"AUTO-015"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "AUTO-001",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Кому из контрагентов мы уже месяц отдаем товары, но на счетах все еще красуется минусовое сальдо - это реально зеленый свет для ручного вмешательства?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-002",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-003",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Покажи контрагентов, по которым сальдо у нас выглядит так, будто оно врет - ну точно не совпадает с тем, что они нам прислали. Это уже критично для сверки."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-004",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-005",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "В каких случаях мы видим ситуацию, когда документы есть, а денег - нет и пока не предвидится?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-006",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Какие контрагенты висят с закрытыми отгрузками, но с открытыми документами оплаты, что явно выглядит как кейс для ручной проверки?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-007",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Покажи контрагентов, у которых есть неоплаченные задолженности по договорам на конец месяца - это уже красный свет для бухгалтера."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-008",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "По каким заказчикам мы можем выделить непростую картину: сальдо нулевое, а история платежей явно говорит о том, что все не так просто?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-009",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-010",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-011",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Покажи контрагентов, по которым на конец месяца сальдо выглядит так, будто документы собраны криво и их нужно перепроверить."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-012",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Какие у нас зависшие авансы или предоплаты уже давно пора либо закрыть, либо хотя бы проверить - это уже не просто вопрос времени?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-013",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "По каким контрагентам мы можем заметить такую картину: оплачено меньше, чем отгружено, и это явно требует вмешательства бухгалтера."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-014",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Какие незакрытые документы по договорам у нас уже давно пора проверить - это грозит серьезными проблемами?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "AUTO-015",
|
||||
"scenario_tag": "autogen_runtime",
|
||||
"question_type": "direct",
|
||||
"broadness_level": "medium",
|
||||
"turns": [
|
||||
{
|
||||
"user_message": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue