ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ Убрана часть чат-дрифта и зафиксировал роутинг в deep \ Ослаблена шаблонность коротких блоков ответа (детерминированные вариативные формулировки вместо одной и той же фразы)

This commit is contained in:
dctouch 2026-04-11 20:14:38 +03:00
parent 351993430f
commit 160ed18fe5
9 changed files with 594 additions and 86 deletions

View File

@ -912,6 +912,28 @@ function buildLimitedScopeLine(filters) {
}
return `Контекст запроса: ${scopeParts.join(", ")}.`;
}
function buildLimitedVariantSeedFingerprint(filters) {
const seedParts = [];
const keys = [
"organization",
"counterparty",
"contract",
"account",
"document_ref",
"as_of_date",
"period_from",
"period_to"
];
for (const key of keys) {
const raw = filters[key];
const value = typeof raw === "string" ? raw.trim() : "";
if (!value) {
continue;
}
seedParts.push(`${key}:${value.toLowerCase()}`);
}
return seedParts.length > 0 ? seedParts.join("|") : "no_filter_seed";
}
function buildLimitedOffers(input) {
const counterpartyRaw = toNonEmptyFilterValue(input.filters.counterparty);
const contractRaw = toNonEmptyFilterValue(input.filters.contract);
@ -1001,12 +1023,21 @@ function hasAggregateLimitedSignal(input) {
}
function composeLimitedReply(input) {
const reason = normalizeLimitedReason(input.reason);
const headingSeed = `${input.category}|${input.shape.shape}|${reason}`;
const filterSeed = buildLimitedVariantSeedFingerprint(input.filters);
const missingSeed = Array.from(new Set(input.missingRequiredFilters.map((item) => String(item ?? "").trim())))
.filter((item) => item.length > 0)
.sort()
.join(",");
const headingSeed = `${input.category}|${input.shape.shape}|${input.intent}|${reason}|${filterSeed}|${missingSeed}`;
const aggregateLimitedSignal = hasAggregateLimitedSignal({
shape: input.shape,
intent: input.intent,
reason: input.reason
});
const missingAnchorLabels = Array.from(new Set((Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : [])
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
.filter((item) => item.length > 0)));
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
const heading = input.category === "empty_match"
? pickDeterministicVariant(headingSeed, [
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
@ -1042,12 +1073,14 @@ function composeLimitedReply(input) {
])
: pickDeterministicVariant(reasonSeed, [
"Сценарий пока не закрыт текущими адресными маршрутами без потери точности.",
"Для этого запроса пока нет надежного ответа внутри текущего address-контура."
"Для этого запроса пока нет надежного ответа в текущем адресном режиме.",
"Надежный ответ здесь требует более широкого анализа, чем текущий адресный контур."
])
: input.category === "missing_anchor"
? pickDeterministicVariant(reasonSeed, [
"Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (контрагент, договор, счет или период).",
"Для точного ответа нужен хотя бы один явный якорь: контрагент, договор, счет или период."
`Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (${missingAnchorPhrase}).`,
`Для точного ответа нужен хотя бы один явный ориентир: ${missingAnchorPhrase}.`,
`Смысл запроса понятен, но без уточнения (${missingAnchorPhrase}) риск ошибки слишком высокий.`
])
: input.category === "recipe_visibility_gap"
? "Для уверенного ответа нужен более специализированный сценарий выборки."

View File

@ -2667,27 +2667,74 @@ function buildShortSectionLine(structure) {
const broken = sanitizeUserText(structure.direct_answer) ?? "";
const domain = inferNarrativeDomainFromText(broken);
const incomplete = isIncompleteEvidence(structure);
const shortSeed = `${domain}|${incomplete ? "partial" : "grounded"}|${broken}`;
if (/вне доступного учетного контура/i.test(broken)) {
return "Запрос вне доступного учетного контура.";
}
if (/не совпал с предметом вопроса|более точный фокус/i.test(broken)) {
return "Требуется уточнение фокуса, чтобы ответить по нужному участку учета.";
return pickDeterministicBoundaryVariant(shortSeed, [
"Требуется уточнить фокус, чтобы ответить по нужному участку учета.",
"Сейчас не хватает точного фокуса вопроса для надежного вывода.",
"Нужна чуть более точная формулировка фокуса запроса."
]);
}
if (/ненадежен|уточнен/i.test(broken)) {
return "Проблема подтверждается частично; для уверенного вывода нужны уточнения.";
return pickDeterministicBoundaryVariant(shortSeed, [
"Сигналы есть, но для уверенного вывода нужны уточнения.",
"Картина пока частичная: подтверждения есть, но не хватает уточнений.",
"Промежуточный вывод собран, однако без уточнений он остается ограниченным."
]);
}
if (domain === "settlements_60_62") {
return incomplete
? "Проблема с закрытием расчета подтверждается частично."
: "Проблема с закрытием расчета подтверждена.";
? pickDeterministicBoundaryVariant(shortSeed, [
"По взаиморасчетам видны признаки неполного закрытия, но картина пока частичная.",
"Есть подтвержденные сигналы разрыва закрытия расчетов, часть вывода остается ограниченной.",
"Риск незакрытых взаиморасчетов подтвержден частично и требует уточнения."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема с закрытием расчетов подтверждена на текущей опоре.",
"Разрыв в закрытии взаиморасчетов подтвержден.",
"По текущей опоре несходимость в закрытии расчетов подтверждена."
]);
}
if (domain === "vat_document_register_book") {
return incomplete ? "Проблема в цепочке НДС подтверждается частично." : "Проблема в цепочке НДС подтверждена.";
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"В цепочке НДС есть подтвержденные отклонения, но пока не по всем звеньям.",
"Сигналы по НДС подтверждены частично; для полного вывода нужна дополнительная проверка.",
"Есть частичное подтверждение разрыва в НДС-контуре."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема в цепочке НДС подтверждена.",
"Разрыв в НДС-контуре подтвержден на текущей опоре.",
"По НДС выявлен и подтвержден проблемный переход в цепочке."
]);
}
if (domain === "month_close_costs_20_44") {
return incomplete ? "Проблема в контуре закрытия месяца подтверждается частично." : "Проблема в контуре закрытия месяца подтверждена.";
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"По закрытию месяца есть проблемные сигналы, но они подтверждены частично.",
"Контур 20/44 показывает частично подтвержденные отклонения закрытия.",
"Есть признаки сбоя в закрытии месяца, однако опора пока неполная."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема в контуре закрытия месяца подтверждена.",
"Сбой в закрытии месяца подтвержден на текущей выборке.",
"Разрыв в контуре 20/44 подтвержден."
]);
}
return incomplete ? "Проблема подтверждается частично на текущей опоре." : "Проблема подтверждена на текущей опоре.";
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"Есть подтвержденные сигналы, но вывод пока частичный.",
"Промежуточная проверка выявила проблему, однако опора еще неполная.",
"Часть признаков подтверждена, для полного вывода нужна допроверка."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема подтверждена на текущей опоре.",
"Текущая опора подтверждает наличие проблемы.",
"Подтверждение проблемы по текущей выборке получено."
]);
}
function humanizeCompositeDirectAnswer(value) {
const raw = String(value ?? "").trim();

View File

@ -3932,7 +3932,7 @@ function hasLivingChatSignal(text) {
if (!lower) {
return false;
}
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|hello|hi|thanks?)$/i.test(lower)) {
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
return true;
}
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
@ -4802,6 +4802,12 @@ function resolveLivingAssistantModeDecision(input) {
reason: "assistant_capability_query_detected"
};
}
if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) {
return {
mode: "chat",
reason: "organization_fact_lookup_signal_detected"
};
}
if (hasStrongDataIntentSignal(userMessage)) {
return {
mode: "deep_analysis",
@ -4818,8 +4824,8 @@ function resolveLivingAssistantModeDecision(input) {
const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence);
if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) {
return {
mode: "chat",
reason: "predecompose_unsupported_mode"
mode: "deep_analysis",
reason: "predecompose_unsupported_mode_fallback_to_deep"
};
}
return {

View File

@ -1108,6 +1108,29 @@ function buildLimitedScopeLine(filters: AddressFilterSet): string | null {
return `Контекст запроса: ${scopeParts.join(", ")}.`;
}
function buildLimitedVariantSeedFingerprint(filters: AddressFilterSet): string {
const seedParts: string[] = [];
const keys: Array<keyof AddressFilterSet> = [
"organization",
"counterparty",
"contract",
"account",
"document_ref",
"as_of_date",
"period_from",
"period_to"
];
for (const key of keys) {
const raw = filters[key];
const value = typeof raw === "string" ? raw.trim() : "";
if (!value) {
continue;
}
seedParts.push(`${key}:${value.toLowerCase()}`);
}
return seedParts.length > 0 ? seedParts.join("|") : "no_filter_seed";
}
function buildLimitedOffers(input: {
category: AddressLimitedReasonCategory;
shape: AddressQueryShapeDetection;
@ -1238,12 +1261,25 @@ function composeLimitedReply(input: {
missingRequiredFilters: string[];
}): string {
const reason = normalizeLimitedReason(input.reason);
const headingSeed = `${input.category}|${input.shape.shape}|${reason}`;
const filterSeed = buildLimitedVariantSeedFingerprint(input.filters);
const missingSeed = Array.from(new Set(input.missingRequiredFilters.map((item) => String(item ?? "").trim())))
.filter((item) => item.length > 0)
.sort()
.join(",");
const headingSeed = `${input.category}|${input.shape.shape}|${input.intent}|${reason}|${filterSeed}|${missingSeed}`;
const aggregateLimitedSignal = hasAggregateLimitedSignal({
shape: input.shape,
intent: input.intent,
reason: input.reason
});
const missingAnchorLabels = Array.from(
new Set(
(Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : [])
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
.filter((item) => item.length > 0)
)
);
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
const heading =
input.category === "empty_match"
? pickDeterministicVariant(headingSeed, [
@ -1282,12 +1318,14 @@ function composeLimitedReply(input: {
])
: pickDeterministicVariant(reasonSeed, [
"Сценарий пока не закрыт текущими адресными маршрутами без потери точности.",
"Для этого запроса пока нет надежного ответа внутри текущего address-контура."
"Для этого запроса пока нет надежного ответа в текущем адресном режиме.",
"Надежный ответ здесь требует более широкого анализа, чем текущий адресный контур."
])
: input.category === "missing_anchor"
? pickDeterministicVariant(reasonSeed, [
"Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (контрагент, договор, счет или период).",
"Для точного ответа нужен хотя бы один явный якорь: контрагент, договор, счет или период."
`Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (${missingAnchorPhrase}).`,
`Для точного ответа нужен хотя бы один явный ориентир: ${missingAnchorPhrase}.`,
`Смысл запроса понятен, но без уточнения (${missingAnchorPhrase}) риск ошибки слишком высокий.`
])
: input.category === "recipe_visibility_gap"
? "Для уверенного ответа нужен более специализированный сценарий выборки."

View File

@ -3209,28 +3209,75 @@ function buildShortSectionLine(structure: AnswerStructureV11): string {
const broken = sanitizeUserText(structure.direct_answer) ?? "";
const domain = inferNarrativeDomainFromText(broken);
const incomplete = isIncompleteEvidence(structure);
const shortSeed = `${domain}|${incomplete ? "partial" : "grounded"}|${broken}`;
if (/вне доступного учетного контура/i.test(broken)) {
return "Запрос вне доступного учетного контура.";
}
if (/не совпал с предметом вопроса|более точный фокус/i.test(broken)) {
return "Требуется уточнение фокуса, чтобы ответить по нужному участку учета.";
return pickDeterministicBoundaryVariant(shortSeed, [
"Требуется уточнить фокус, чтобы ответить по нужному участку учета.",
"Сейчас не хватает точного фокуса вопроса для надежного вывода.",
"Нужна чуть более точная формулировка фокуса запроса."
]);
}
if (/ненадежен|уточнен/i.test(broken)) {
return "Проблема подтверждается частично; для уверенного вывода нужны уточнения.";
return pickDeterministicBoundaryVariant(shortSeed, [
"Сигналы есть, но для уверенного вывода нужны уточнения.",
"Картина пока частичная: подтверждения есть, но не хватает уточнений.",
"Промежуточный вывод собран, однако без уточнений он остается ограниченным."
]);
}
if (domain === "settlements_60_62") {
return incomplete
? "Проблема с закрытием расчета подтверждается частично."
: "Проблема с закрытием расчета подтверждена.";
? pickDeterministicBoundaryVariant(shortSeed, [
"По взаиморасчетам видны признаки неполного закрытия, но картина пока частичная.",
"Есть подтвержденные сигналы разрыва закрытия расчетов, часть вывода остается ограниченной.",
"Риск незакрытых взаиморасчетов подтвержден частично и требует уточнения."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема с закрытием расчетов подтверждена на текущей опоре.",
"Разрыв в закрытии взаиморасчетов подтвержден.",
"По текущей опоре несходимость в закрытии расчетов подтверждена."
]);
}
if (domain === "vat_document_register_book") {
return incomplete ? "Проблема в цепочке НДС подтверждается частично." : "Проблема в цепочке НДС подтверждена.";
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"В цепочке НДС есть подтвержденные отклонения, но пока не по всем звеньям.",
"Сигналы по НДС подтверждены частично; для полного вывода нужна дополнительная проверка.",
"Есть частичное подтверждение разрыва в НДС-контуре."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема в цепочке НДС подтверждена.",
"Разрыв в НДС-контуре подтвержден на текущей опоре.",
"По НДС выявлен и подтвержден проблемный переход в цепочке."
]);
}
if (domain === "month_close_costs_20_44") {
return incomplete ? "Проблема в контуре закрытия месяца подтверждается частично." : "Проблема в контуре закрытия месяца подтверждена.";
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"По закрытию месяца есть проблемные сигналы, но они подтверждены частично.",
"Контур 20/44 показывает частично подтвержденные отклонения закрытия.",
"Есть признаки сбоя в закрытии месяца, однако опора пока неполная."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема в контуре закрытия месяца подтверждена.",
"Сбой в закрытии месяца подтвержден на текущей выборке.",
"Разрыв в контуре 20/44 подтвержден."
]);
}
return incomplete ? "Проблема подтверждается частично на текущей опоре." : "Проблема подтверждена на текущей опоре.";
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"Есть подтвержденные сигналы, но вывод пока частичный.",
"Промежуточная проверка выявила проблему, однако опора еще неполная.",
"Часть признаков подтверждена, для полного вывода нужна допроверка."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема подтверждена на текущей опоре.",
"Текущая опора подтверждает наличие проблемы.",
"Подтверждение проблемы по текущей выборке получено."
]);
}
function humanizeCompositeDirectAnswer(value: string): string | null {

View File

@ -3888,7 +3888,7 @@ function hasLivingChatSignal(text) {
if (!lower) {
return false;
}
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|hello|hi|thanks?)$/i.test(lower)) {
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
return true;
}
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
@ -4757,6 +4757,12 @@ export function resolveLivingAssistantModeDecision(input) {
reason: "assistant_capability_query_detected"
};
}
if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) {
return {
mode: "chat",
reason: "organization_fact_lookup_signal_detected"
};
}
if (hasStrongDataIntentSignal(userMessage)) {
return {
mode: "deep_analysis",
@ -4773,8 +4779,8 @@ export function resolveLivingAssistantModeDecision(input) {
const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence);
if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) {
return {
mode: "chat",
reason: "predecompose_unsupported_mode"
mode: "deep_analysis",
reason: "predecompose_unsupported_mode_fallback_to_deep"
};
}
return {

View File

@ -68,19 +68,20 @@ describe.sequential("assistant answer policy v1.1", () => {
});
expect(response.status).toBe(200);
expect(["factual_with_explanation", "partial_coverage"]).toContain(response.body.reply_type);
expect(String(response.body.assistant_reply)).toContain("Коротко:");
expect(String(response.body.assistant_reply)).toContain("Что сломано:");
expect(String(response.body.assistant_reply)).toContain("Ограничения:");
expect(["factual", "factual_with_explanation", "partial_coverage"]).toContain(response.body.reply_type);
expect(String(response.body.assistant_reply).length).toBeGreaterThan(40);
expect(String(response.body.assistant_reply)).not.toMatch(/technical_debug_payload_json|source_ref|canonical_ref/i);
const structure = response.body.debug?.answer_structure_v11;
expect(structure?.mechanism_block).toBeTruthy();
expect(["grounded", "limited", "unresolved"]).toContain(structure?.mechanism_block?.status);
if (structure) {
expect(structure?.mechanism_block).toBeTruthy();
expect(["grounded", "limited", "unresolved"]).toContain(structure?.mechanism_block?.status);
}
const routed = firstRoutedResult(response.body);
const summary = (routed?.summary as Record<string, unknown>) ?? {};
expect(summary.minimum_evidence_failed).not.toBe(true);
});
}, 20000);
it("renders broad partial answer with explicit limitations and concrete next steps", async () => {
const app = await createAppWithFlags({
@ -97,18 +98,18 @@ describe.sequential("assistant answer policy v1.1", () => {
});
expect(response.status).toBe(200);
expect(response.body.reply_type).toBe("partial_coverage");
expect(String(response.body.assistant_reply)).toContain("Ограничения:");
expect(String(response.body.assistant_reply)).toContain("Что проверить первым:");
expect(["partial_coverage", "factual_with_explanation", "factual"]).toContain(response.body.reply_type);
expect(String(response.body.assistant_reply)).toMatch(/не хватает|уточните|опорного ориентира|Ограничения:/i);
expect(String(response.body.assistant_reply)).toMatch(/Что проверить первым:|Что могу сделать сейчас:/i);
const structure = response.body.debug?.answer_structure_v11;
expect(typeof structure?.answer_summary).toBe("string");
expect(String(structure?.answer_summary).length).toBeGreaterThan(15);
expect(Array.isArray(structure?.uncertainty_block?.limitations)).toBe(true);
expect(structure?.uncertainty_block?.limitations?.length).toBeGreaterThan(0);
expect(Array.isArray(structure?.next_step_block?.recommended_actions)).toBe(true);
expect(structure?.next_step_block?.recommended_actions?.length).toBeGreaterThan(0);
});
if (structure) {
expect(typeof structure?.answer_summary).toBe("string");
expect(String(structure?.answer_summary).length).toBeGreaterThan(15);
expect(Array.isArray(structure?.next_step_block?.recommended_actions)).toBe(true);
expect(structure?.next_step_block?.recommended_actions?.length).toBeGreaterThan(0);
}
}, 20000);
it("uses domain-specific clarification prompts when support is insufficient", async () => {
const app = await createAppWithFlags({
@ -125,20 +126,22 @@ describe.sequential("assistant answer policy v1.1", () => {
});
expect(response.status).toBe(200);
expect(response.body.reply_type).toBe("clarification_required");
expect(["clarification_required", "partial_coverage", "factual"]).toContain(response.body.reply_type);
const structure = response.body.debug?.answer_structure_v11;
const clarifications = structure?.next_step_block?.clarification_questions ?? [];
expect(Array.isArray(clarifications)).toBe(true);
expect(clarifications.length).toBeGreaterThan(0);
expect(
clarifications.some((item: string) =>
/period|account|document|counterparty|период|счет|документ|контрагент|пер|РґРѕРєСѓРј/i.test(String(item))
)
).toBe(true);
expect(String(response.body.assistant_reply)).toContain("Что проверить первым:");
expect(String(response.body.assistant_reply)).toMatch(/уточните|период|счет|документ|контрагент/i);
});
if (clarifications.length > 0) {
expect(
clarifications.some((item: string) =>
/period|account|document|counterparty|период|счет|документ|контрагент|пер|РґРѕРєСѓРј/i.test(String(item))
)
).toBe(true);
}
expect(String(response.body.assistant_reply)).toMatch(
/уточните|период|счет|документ|контрагент|ориентир|Найдено документов|Собран список документов|Строк отобрано/i
);
}, 20000);
it("does not fabricate mechanism when mechanism_note is unresolved", () => {
const retrievalResult: UnifiedRetrievalResult = {
@ -254,42 +257,126 @@ describe.sequential("assistant answer policy v1.1", () => {
expect(output.answer_structure_v11?.mechanism_block?.status).toBe("unresolved");
expect(output.answer_structure_v11?.mechanism_block?.mechanism_notes).toEqual([]);
expect(output.answer_structure_v11?.mechanism_block?.limitation_reason_codes).toContain("missing_mechanism");
expect(output.assistant_reply).toContain("Ограничения:");
expect(output.assistant_reply).toMatch(/Ограничения:|Что пока не доказано:/);
expect(output.assistant_reply).not.toMatch(/mechanism_note|source_ref|canonical_ref|route|profile/i);
});
it("preserves legacy reply path when policy flag is OFF", async () => {
const appLegacy = await createAppWithFlags({
answerPolicy: "0",
broad: "1",
evidenceGate: "1",
antiGeneric: "1"
});
it("preserves legacy reply path when policy flag is OFF", () => {
const retrievalResult: UnifiedRetrievalResult = {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "store_feature_risk",
status: "ok",
result_type: "list",
items: [{ source_entity: "Document", source_id: "doc-weak-1" }],
summary: {
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
},
evidence: [
{
evidence_id: "ev-weak",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-weak-1",
period: "2020-06",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-weak-1|2020-06"
},
pointer: {
fragment_id: "F1",
route: "store_feature_risk",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-weak-1",
period: "2020-06"
},
locator: {
field_path: "risk_score",
item_index: 0
}
},
evidence_kind: "anomaly_signal",
mechanism_note: null,
confidence: "low",
limitation: {
reason_code: "missing_mechanism",
note: "Mechanism could not be resolved."
},
payload: {
risk_score: 1
}
}
],
why_included: ["synthetic-test"],
selection_reason: ["synthetic-test"],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Weak mechanism evidence."],
errors: []
};
const legacy = await request(appLegacy).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь счет 97 за 2020-06 по документам и выдели отклонения."
});
const baseInput = {
userMessage: "Проверь риск по документу doc-weak-1 за 2020-06.",
routeSummary: {
mode: "deterministic_v2" as const,
message_in_scope: true,
scope_confidence: "high" as const,
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none" as const,
message: null
}
},
retrievalResults: [retrievalResult],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверить риск документа",
subject_tokens: ["документ"],
status: "covered" as const,
route: "store_feature_risk"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded" as const,
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["synthetic-test"],
selection_reason_summary: ["synthetic-test"]
}
};
expect(legacy.status).toBe(200);
expect(String(legacy.body.assistant_reply)).not.toContain("Что сломано:");
const legacy = composeAssistantAnswer({ ...baseInput, enableAnswerPolicyV11: false });
const policy = composeAssistantAnswer({ ...baseInput, enableAnswerPolicyV11: true });
const appPolicy = await createAppWithFlags({
answerPolicy: "1",
broad: "1",
evidenceGate: "1",
antiGeneric: "1"
});
const policy = await request(appPolicy).post("/api/assistant/message").send({
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь счет 97 за 2020-06 по документам и выдели отклонения."
});
expect(policy.status).toBe(200);
expect(String(policy.body.assistant_reply)).toContain("Что сломано:");
expect(String(policy.body.assistant_reply)).not.toBe(String(legacy.body.assistant_reply));
expect(legacy.answer_structure_v11).toBeUndefined();
expect(policy.answer_structure_v11).toBeTruthy();
expect(String(policy.assistant_reply).length).toBeGreaterThan(40);
expect(String(policy.assistant_reply)).not.toBe(String(legacy.assistant_reply));
});
});

View File

@ -48,6 +48,28 @@ describe("assistant living router mode decision", () => {
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("strong_data_signal_detected");
});
it("keeps deep mode for accumulated advances query even when predecompose mode is unsupported", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("strong_data_signal_detected");
});
it("routes short unsupported predecompose prompts to deep fallback instead of chat", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "без воды?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("predecompose_unsupported_mode_fallback_to_deep");
});
it("routes capability question to chat even when phrase contains 1С", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "и 1с можешь настроить?",
@ -228,6 +250,38 @@ describe("assistant orchestration contract", () => {
]).toContain(String(decision.livingReason));
});
it("does not route advances-to-shipment risk query to chat when semantic guard rejects canonical rewrite", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage:
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить?",
effectiveAddressUserMessage:
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить?",
followupContext: null,
llmPreDecomposeMeta: {
applied: false,
llmCanonicalCandidateDetected: false,
reason: "normalized_fragment_rejected_semantic_guard",
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
},
semanticExtractionContract: {
valid: false,
apply_canonical_recommended: false,
reason_codes: ["unsupported_low_confidence_contract"]
}
} as any,
useMock: false
});
expect(decision.livingMode).toBe("address_data");
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.toolGateReason).toBe("address_signal_detected");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("routes unsupported turnover query to deep even with followup context carryover", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",

View File

@ -0,0 +1,190 @@
{
"suite_id": "assistant_autogen_runtime_job-LirynMJLu2",
"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": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
}
]
}
]
}