ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Этап 4.2: унифицирован fallback-ответ в 5-блочный контракт и обновлены регрессии
This commit is contained in:
parent
5a8edfb4f3
commit
522575c7cc
|
|
@ -2651,7 +2651,23 @@ Implemented in current pass (Stage 4.1 answer-layer contract rollout, 2026-04-12
|
||||||
- focused Stage 4 answer pack: `5 files / 30 tests passed`;
|
- focused Stage 4 answer pack: `5 files / 30 tests passed`;
|
||||||
- `npm --prefix llm_normalizer/backend run build` passed.
|
- `npm --prefix llm_normalizer/backend run build` passed.
|
||||||
|
|
||||||
Status: In progress (Stage 4.1 completed; continue with quality loop on real runs/manual comments)
|
Implemented in current pass (Stage 4.2 boundary fallback contract alignment, 2026-04-12):
|
||||||
|
1. Reworked boundary fallback replies to the same Stage 4 human-centric contract shape:
|
||||||
|
- `Коротко`
|
||||||
|
- `Что именно проверено`
|
||||||
|
- `Что найдено`
|
||||||
|
- `Что пока не доказано`
|
||||||
|
- `Что могу сделать сейчас`
|
||||||
|
2. Preserved fallback semantics and safety behavior:
|
||||||
|
- soft refusal and clarification flow remain deterministic;
|
||||||
|
- no route/runtime contract changes.
|
||||||
|
3. Updated boundary regression assertions to the unified Stage 4 answer shape:
|
||||||
|
- `assistantBoundaryFallbackReply`.
|
||||||
|
4. Validation snapshot:
|
||||||
|
- focused Stage 4 pack passed: `4 files / 16 tests` (`assistantBoundaryFallbackReply`, `assistantSoftPolicyReply`, `assistantAnswerPolicyV11`, `assistantWave17RunRegression20260411`);
|
||||||
|
- `npm --prefix llm_normalizer/backend run build` passed.
|
||||||
|
|
||||||
|
Status: In progress (Stage 4.1-4.2 completed; continue with quality loop on real runs/manual comments)
|
||||||
|
|
||||||
## Stage 5 (P3): Quality Loop Driven By GUI Markup
|
## Stage 5 (P3): Quality Loop Driven By GUI Markup
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1635,6 +1635,13 @@ function buildBoundaryQuickActionLine(capabilities) {
|
||||||
}
|
}
|
||||||
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
|
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
|
||||||
}
|
}
|
||||||
|
function buildBoundaryQuickActionItems(capabilities) {
|
||||||
|
const actions = capabilities
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((item) => item.replace(/:\s*/u, " — ").trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
return uniqueStrings(actions, 3);
|
||||||
|
}
|
||||||
function buildNaturalClarificationHints(input) {
|
function buildNaturalClarificationHints(input) {
|
||||||
const hints = [];
|
const hints = [];
|
||||||
if (input.missingAnchors.period) {
|
if (input.missingAnchors.period) {
|
||||||
|
|
@ -1686,16 +1693,27 @@ function shouldUseBoundaryFallbackReply(input) {
|
||||||
function buildBoundaryFallbackReply(input) {
|
function buildBoundaryFallbackReply(input) {
|
||||||
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
||||||
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
|
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
|
||||||
|
const quickActionItems = buildBoundaryQuickActionItems(nearbyCapabilities);
|
||||||
if (input.focusDomain === null) {
|
if (input.focusDomain === null) {
|
||||||
const heading = pickDeterministicBoundaryVariant(input.userMessage, [
|
const heading = pickDeterministicBoundaryVariant(input.userMessage, [
|
||||||
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
||||||
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
||||||
]);
|
]);
|
||||||
return sanitizeUserFacingReply([
|
return sanitizeUserFacingReply([
|
||||||
heading,
|
`Коротко: ${ensureSentence(heading)}`,
|
||||||
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
|
`Что именно проверено:\n${formatList([
|
||||||
quickActionLine ?? "",
|
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
|
||||||
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
|
"Надежный доменный путь для текущей формулировки не подтвержден."
|
||||||
|
])}`,
|
||||||
|
`Что найдено:\n${formatList(nearbyCapabilities.length > 0
|
||||||
|
? nearbyCapabilities
|
||||||
|
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."])}`,
|
||||||
|
`Что пока не доказано:\n${formatList([
|
||||||
|
"Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."
|
||||||
|
])}`,
|
||||||
|
`Что могу сделать сейчас:\n${formatList(quickActionItems.length > 0
|
||||||
|
? quickActionItems
|
||||||
|
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."])}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n"));
|
.join("\n\n"));
|
||||||
|
|
@ -1709,10 +1727,20 @@ function buildBoundaryFallbackReply(input) {
|
||||||
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
||||||
]);
|
]);
|
||||||
return sanitizeUserFacingReply([
|
return sanitizeUserFacingReply([
|
||||||
domainHeading,
|
`Коротко: ${ensureSentence(domainHeading)}`,
|
||||||
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
|
`Что именно проверено:\n${formatList([
|
||||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "",
|
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
|
||||||
quickActionLine ?? ""
|
"Оценена достаточность подтвержденной опоры для прямого вывода."
|
||||||
|
])}`,
|
||||||
|
`Что найдено:\n${formatList(nearbyCapabilities.length > 0
|
||||||
|
? nearbyCapabilities.slice(0, 2)
|
||||||
|
: ["Подтвержденных близких сценариев в текущем проходе не найдено."])}`,
|
||||||
|
`Что пока не доказано:\n${formatList(clarificationHints.length > 0
|
||||||
|
? clarificationHints
|
||||||
|
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."])}`,
|
||||||
|
`Что могу сделать сейчас:\n${formatList(quickActionItems.length > 0
|
||||||
|
? quickActionItems
|
||||||
|
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."])}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n"));
|
.join("\n\n"));
|
||||||
|
|
|
||||||
|
|
@ -1932,6 +1932,14 @@ function buildBoundaryQuickActionLine(capabilities: string[]): string | null {
|
||||||
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
|
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildBoundaryQuickActionItems(capabilities: string[]): string[] {
|
||||||
|
const actions = capabilities
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((item) => item.replace(/:\s*/u, " — ").trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
return uniqueStrings(actions, 3);
|
||||||
|
}
|
||||||
|
|
||||||
function buildNaturalClarificationHints(input: {
|
function buildNaturalClarificationHints(input: {
|
||||||
missingAnchors: MissingAnchors;
|
missingAnchors: MissingAnchors;
|
||||||
coverageReport: RequirementCoverageReport;
|
coverageReport: RequirementCoverageReport;
|
||||||
|
|
@ -2006,6 +2014,7 @@ function buildBoundaryFallbackReply(input: {
|
||||||
}): string {
|
}): string {
|
||||||
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
||||||
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
|
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
|
||||||
|
const quickActionItems = buildBoundaryQuickActionItems(nearbyCapabilities);
|
||||||
if (input.focusDomain === null) {
|
if (input.focusDomain === null) {
|
||||||
const heading = pickDeterministicBoundaryVariant(
|
const heading = pickDeterministicBoundaryVariant(
|
||||||
input.userMessage,
|
input.userMessage,
|
||||||
|
|
@ -2016,10 +2025,24 @@ function buildBoundaryFallbackReply(input: {
|
||||||
);
|
);
|
||||||
return sanitizeUserFacingReply(
|
return sanitizeUserFacingReply(
|
||||||
[
|
[
|
||||||
heading,
|
`Коротко: ${ensureSentence(heading)}`,
|
||||||
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
|
`Что именно проверено:\n${formatList([
|
||||||
quickActionLine ?? "",
|
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
|
||||||
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
|
"Надежный доменный путь для текущей формулировки не подтвержден."
|
||||||
|
])}`,
|
||||||
|
`Что найдено:\n${formatList(
|
||||||
|
nearbyCapabilities.length > 0
|
||||||
|
? nearbyCapabilities
|
||||||
|
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."]
|
||||||
|
)}`,
|
||||||
|
`Что пока не доказано:\n${formatList([
|
||||||
|
"Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."
|
||||||
|
])}`,
|
||||||
|
`Что могу сделать сейчас:\n${formatList(
|
||||||
|
quickActionItems.length > 0
|
||||||
|
? quickActionItems
|
||||||
|
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."]
|
||||||
|
)}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
|
|
@ -2039,10 +2062,26 @@ function buildBoundaryFallbackReply(input: {
|
||||||
);
|
);
|
||||||
return sanitizeUserFacingReply(
|
return sanitizeUserFacingReply(
|
||||||
[
|
[
|
||||||
domainHeading,
|
`Коротко: ${ensureSentence(domainHeading)}`,
|
||||||
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
|
`Что именно проверено:\n${formatList([
|
||||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "",
|
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
|
||||||
quickActionLine ?? ""
|
"Оценена достаточность подтвержденной опоры для прямого вывода."
|
||||||
|
])}`,
|
||||||
|
`Что найдено:\n${formatList(
|
||||||
|
nearbyCapabilities.length > 0
|
||||||
|
? nearbyCapabilities.slice(0, 2)
|
||||||
|
: ["Подтвержденных близких сценариев в текущем проходе не найдено."]
|
||||||
|
)}`,
|
||||||
|
`Что пока не доказано:\n${formatList(
|
||||||
|
clarificationHints.length > 0
|
||||||
|
? clarificationHints
|
||||||
|
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."]
|
||||||
|
)}`,
|
||||||
|
`Что могу сделать сейчас:\n${formatList(
|
||||||
|
quickActionItems.length > 0
|
||||||
|
? quickActionItems
|
||||||
|
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."]
|
||||||
|
)}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ describe("assistant boundary fallback reply", () => {
|
||||||
|
|
||||||
expect(output.reply_type).toBe("clarification_required");
|
expect(output.reply_type).toBe("clarification_required");
|
||||||
expect(output.assistant_reply).toMatch(/мягкий отказ/i);
|
expect(output.assistant_reply).toMatch(/мягкий отказ/i);
|
||||||
expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:");
|
expect(output.assistant_reply).toContain("Что именно проверено:");
|
||||||
|
expect(output.assistant_reply).toContain("Что найдено:");
|
||||||
|
expect(output.assistant_reply).toContain("Что пока не доказано:");
|
||||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||||
});
|
});
|
||||||
|
|
@ -78,8 +80,9 @@ describe("assistant boundary fallback reply", () => {
|
||||||
|
|
||||||
expect(output.reply_type).toBe("clarification_required");
|
expect(output.reply_type).toBe("clarification_required");
|
||||||
expect(output.assistant_reply).toMatch(/не могу надежно ответить по сценарию|не хватает подтвержденной опоры/i);
|
expect(output.assistant_reply).toMatch(/не могу надежно ответить по сценарию|не хватает подтвержденной опоры/i);
|
||||||
expect(output.assistant_reply).toContain("Чтобы сразу перейти к проверке, уточни:");
|
expect(output.assistant_reply).toContain("Что именно проверено:");
|
||||||
expect(output.assistant_reply).toContain("Если удобнее, могу начать с близкого сценария:");
|
expect(output.assistant_reply).toContain("Что найдено:");
|
||||||
|
expect(output.assistant_reply).toContain("Что пока не доказано:");
|
||||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||||
});
|
});
|
||||||
|
|
@ -161,8 +164,9 @@ describe("assistant boundary fallback reply", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(output.reply_type).toBe("partial_coverage");
|
expect(output.reply_type).toBe("partial_coverage");
|
||||||
expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:");
|
expect(output.assistant_reply).toContain("Что найдено:");
|
||||||
|
expect(output.assistant_reply).toContain("Что пока не доказано:");
|
||||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue