ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Этап 4.3: унификация контракта ответов и добавление теста формы Stage 4
This commit is contained in:
parent
522575c7cc
commit
88a7da4f0a
|
|
@ -2667,7 +2667,19 @@ Implemented in current pass (Stage 4.2 boundary fallback contract alignment, 202
|
||||||
- focused Stage 4 pack passed: `4 files / 16 tests` (`assistantBoundaryFallbackReply`, `assistantSoftPolicyReply`, `assistantAnswerPolicyV11`, `assistantWave17RunRegression20260411`);
|
- focused Stage 4 pack passed: `4 files / 16 tests` (`assistantBoundaryFallbackReply`, `assistantSoftPolicyReply`, `assistantAnswerPolicyV11`, `assistantWave17RunRegression20260411`);
|
||||||
- `npm --prefix llm_normalizer/backend run build` passed.
|
- `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)
|
Implemented in current pass (Stage 4.3 contract consistency hardening, 2026-04-12):
|
||||||
|
1. Added unified Stage 4 reply renderer in answer policy layer to avoid section-shape drift across:
|
||||||
|
- focused grounded replies;
|
||||||
|
- weak/soft policy replies;
|
||||||
|
- boundary fallback replies.
|
||||||
|
2. Added explicit contract regression pack:
|
||||||
|
- `assistantStage4AnswerContractShape.test.ts`
|
||||||
|
- validates required Stage 4 blocks and guards against legacy section rollback (`Что сломано`, `Почему это похоже на проблему`, `На чем это основано`, `Ограничения`).
|
||||||
|
3. Validation snapshot:
|
||||||
|
- focused Stage 4 contract pack passed: `4 files / 14 tests` (`assistantStage4AnswerContractShape`, `assistantBoundaryFallbackReply`, `assistantSoftPolicyReply`, `assistantAnswerPolicyV11`);
|
||||||
|
- `npm --prefix llm_normalizer/backend run build` passed.
|
||||||
|
|
||||||
|
Status: In progress (Stage 4.1-4.3 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,30 @@ function formatList(items) {
|
||||||
}
|
}
|
||||||
return items.map((item) => `- ${item}`).join("\n");
|
return items.map((item) => `- ${item}`).join("\n");
|
||||||
}
|
}
|
||||||
|
function renderStage4ContractReply(input) {
|
||||||
|
const shortLine = ensureSentence(input.shortLine);
|
||||||
|
const checkedLines = dedupeNarrativeLines(input.checkedLines, 6);
|
||||||
|
const foundLines = dedupeNarrativeLines(input.foundLines, 6);
|
||||||
|
const unresolvedLines = dedupeNarrativeLines(input.unresolvedLines, 6);
|
||||||
|
const nextStepLines = dedupeNarrativeLines(input.nextStepLines, 5);
|
||||||
|
const contextLead = ensureSentence(String(input.contextLead ?? ""));
|
||||||
|
return sanitizeUserFacingReply([
|
||||||
|
`Коротко: ${shortLine}`,
|
||||||
|
contextLead || "",
|
||||||
|
`Что именно проверено:\n${formatList(checkedLines.length > 0
|
||||||
|
? checkedLines
|
||||||
|
: ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."])}`,
|
||||||
|
`Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."])}`,
|
||||||
|
`Что пока не доказано:\n${formatList(unresolvedLines.length > 0
|
||||||
|
? unresolvedLines
|
||||||
|
: ["Существенных ограничений в текущем срезе не выявлено."])}`,
|
||||||
|
`${input.nextStepTitle}:\n${formatList(nextStepLines.length > 0
|
||||||
|
? nextStepLines
|
||||||
|
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."])}`
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n"));
|
||||||
|
}
|
||||||
function formatSafeItemLine(entity, sourceId, riskScore) {
|
function formatSafeItemLine(entity, sourceId, riskScore) {
|
||||||
const entityLabel = sanitizeUserText(String(entity ?? "")) ?? "Record";
|
const entityLabel = sanitizeUserText(String(entity ?? "")) ?? "Record";
|
||||||
const idRaw = String(sourceId ?? "").trim();
|
const idRaw = String(sourceId ?? "").trim();
|
||||||
|
|
@ -1699,24 +1723,21 @@ function buildBoundaryFallbackReply(input) {
|
||||||
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
||||||
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
||||||
]);
|
]);
|
||||||
return sanitizeUserFacingReply([
|
return renderStage4ContractReply({
|
||||||
`Коротко: ${ensureSentence(heading)}`,
|
shortLine: heading,
|
||||||
`Что именно проверено:\n${formatList([
|
checkedLines: [
|
||||||
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
|
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
|
||||||
"Надежный доменный путь для текущей формулировки не подтвержден."
|
"Надежный доменный путь для текущей формулировки не подтвержден."
|
||||||
])}`,
|
],
|
||||||
`Что найдено:\n${formatList(nearbyCapabilities.length > 0
|
foundLines: nearbyCapabilities.length > 0
|
||||||
? nearbyCapabilities
|
? nearbyCapabilities
|
||||||
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."])}`,
|
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."],
|
||||||
`Что пока не доказано:\n${formatList([
|
unresolvedLines: ["Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."],
|
||||||
"Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."
|
nextStepLines: quickActionItems.length > 0
|
||||||
])}`,
|
|
||||||
`Что могу сделать сейчас:\n${formatList(quickActionItems.length > 0
|
|
||||||
? quickActionItems
|
? quickActionItems
|
||||||
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."])}`
|
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."],
|
||||||
]
|
nextStepTitle: "Что могу сделать сейчас"
|
||||||
.filter(Boolean)
|
});
|
||||||
.join("\n\n"));
|
|
||||||
}
|
}
|
||||||
const clarificationHints = buildNaturalClarificationHints({
|
const clarificationHints = buildNaturalClarificationHints({
|
||||||
missingAnchors: input.missingAnchors,
|
missingAnchors: input.missingAnchors,
|
||||||
|
|
@ -1726,24 +1747,23 @@ function buildBoundaryFallbackReply(input) {
|
||||||
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
|
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
|
||||||
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
||||||
]);
|
]);
|
||||||
return sanitizeUserFacingReply([
|
return renderStage4ContractReply({
|
||||||
`Коротко: ${ensureSentence(domainHeading)}`,
|
shortLine: domainHeading,
|
||||||
`Что именно проверено:\n${formatList([
|
checkedLines: [
|
||||||
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
|
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
|
||||||
"Оценена достаточность подтвержденной опоры для прямого вывода."
|
"Оценена достаточность подтвержденной опоры для прямого вывода."
|
||||||
])}`,
|
],
|
||||||
`Что найдено:\n${formatList(nearbyCapabilities.length > 0
|
foundLines: nearbyCapabilities.length > 0
|
||||||
? nearbyCapabilities.slice(0, 2)
|
? nearbyCapabilities.slice(0, 2)
|
||||||
: ["Подтвержденных близких сценариев в текущем проходе не найдено."])}`,
|
: ["Подтвержденных близких сценариев в текущем проходе не найдено."],
|
||||||
`Что пока не доказано:\n${formatList(clarificationHints.length > 0
|
unresolvedLines: clarificationHints.length > 0
|
||||||
? clarificationHints
|
? clarificationHints
|
||||||
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."])}`,
|
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."],
|
||||||
`Что могу сделать сейчас:\n${formatList(quickActionItems.length > 0
|
nextStepLines: quickActionItems.length > 0
|
||||||
? quickActionItems
|
? quickActionItems
|
||||||
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."])}`
|
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."],
|
||||||
]
|
nextStepTitle: "Что могу сделать сейчас"
|
||||||
.filter(Boolean)
|
});
|
||||||
.join("\n\n"));
|
|
||||||
}
|
}
|
||||||
function ensureSentence(value) {
|
function ensureSentence(value) {
|
||||||
const sanitized = sanitizeUserText(value) ?? String(value ?? "").trim();
|
const sanitized = sanitizeUserText(value) ?? String(value ?? "").trim();
|
||||||
|
|
@ -3677,25 +3697,14 @@ function renderPolicyReply(structure, context) {
|
||||||
limitationLines
|
limitationLines
|
||||||
};
|
};
|
||||||
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
|
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
|
||||||
const checkedLines = dedupeNarrativeLines(enriched.evidenceLines, 6);
|
return renderStage4ContractReply({
|
||||||
const foundLines = dedupeNarrativeLines([...enriched.brokenLines, ...enriched.whyLines], 6);
|
shortLine: enriched.shortLine,
|
||||||
const unresolvedLines = dedupeNarrativeLines(enriched.limitationLines, 6);
|
checkedLines: enriched.evidenceLines,
|
||||||
const nextStepLines = dedupeNarrativeLines(enriched.checkLines, 5);
|
foundLines: [...enriched.brokenLines, ...enriched.whyLines],
|
||||||
return sanitizeUserFacingReply([
|
unresolvedLines: enriched.limitationLines,
|
||||||
`Коротко: ${enriched.shortLine}`,
|
nextStepLines: enriched.checkLines,
|
||||||
`Что именно проверено:\n${formatList(checkedLines.length > 0
|
nextStepTitle: "Что проверить первым"
|
||||||
? checkedLines
|
});
|
||||||
: ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."])}`,
|
|
||||||
`Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."])}`,
|
|
||||||
`Что пока не доказано:\n${formatList(unresolvedLines.length > 0
|
|
||||||
? unresolvedLines
|
|
||||||
: ["Существенных ограничений в текущем срезе не выявлено."])}`,
|
|
||||||
`Что проверить первым:\n${formatList(nextStepLines.length > 0
|
|
||||||
? nextStepLines
|
|
||||||
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."])}`
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n\n"));
|
|
||||||
}
|
}
|
||||||
function shouldUseSoftPolicyReply(input) {
|
function shouldUseSoftPolicyReply(input) {
|
||||||
if (input.mode === "focused_grounded" || input.mode === "route_mismatch" || input.mode === "backend_error" || input.mode === "out_of_scope") {
|
if (input.mode === "focused_grounded" || input.mode === "route_mismatch" || input.mode === "backend_error" || input.mode === "out_of_scope") {
|
||||||
|
|
@ -3735,22 +3744,18 @@ function renderSoftPolicyReply(input) {
|
||||||
: input.mode === "no_grounded" || input.mode === "empty"
|
: input.mode === "no_grounded" || input.mode === "empty"
|
||||||
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
|
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
|
||||||
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
|
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
|
||||||
return sanitizeUserFacingReply([
|
return renderStage4ContractReply({
|
||||||
`Коротко: ${shortLine}`,
|
shortLine,
|
||||||
modeLine,
|
checkedLines: evidenceLines.length > 0
|
||||||
`Что именно проверено:\n${formatList(evidenceLines.length > 0
|
|
||||||
? evidenceLines
|
? evidenceLines
|
||||||
: ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."])}`,
|
: ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."],
|
||||||
`Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."])}`,
|
foundLines: foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."],
|
||||||
`Что пока не доказано:\n${formatList(limitationLines.length > 0
|
unresolvedLines: limitationLines.length > 0 ? [modeLine, ...limitationLines] : [modeLine, "Для полного вывода не хватает деталей по части требований."],
|
||||||
? limitationLines
|
nextStepLines: actionLines.length > 0
|
||||||
: ["Для полного вывода не хватает деталей по части требований."])}`,
|
|
||||||
`Что могу сделать сейчас:\n${formatList(actionLines.length > 0
|
|
||||||
? actionLines
|
? actionLines
|
||||||
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."])}`
|
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."],
|
||||||
]
|
nextStepTitle: "Что могу сделать сейчас"
|
||||||
.filter(Boolean)
|
});
|
||||||
.join("\n\n"));
|
|
||||||
}
|
}
|
||||||
function composeAssistantAnswerV11(input) {
|
function composeAssistantAnswerV11(input) {
|
||||||
const fallbackType = fallbackFromSummary(input.routeSummary);
|
const fallbackType = fallbackFromSummary(input.routeSummary);
|
||||||
|
|
|
||||||
|
|
@ -733,6 +733,50 @@ function formatList(items: string[]): string {
|
||||||
return items.map((item) => `- ${item}`).join("\n");
|
return items.map((item) => `- ${item}`).join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStage4ContractReply(input: {
|
||||||
|
shortLine: string;
|
||||||
|
checkedLines: string[];
|
||||||
|
foundLines: string[];
|
||||||
|
unresolvedLines: string[];
|
||||||
|
nextStepLines: string[];
|
||||||
|
nextStepTitle: "Что проверить первым" | "Что могу сделать сейчас";
|
||||||
|
contextLead?: string | null;
|
||||||
|
}): string {
|
||||||
|
const shortLine = ensureSentence(input.shortLine);
|
||||||
|
const checkedLines = dedupeNarrativeLines(input.checkedLines, 6);
|
||||||
|
const foundLines = dedupeNarrativeLines(input.foundLines, 6);
|
||||||
|
const unresolvedLines = dedupeNarrativeLines(input.unresolvedLines, 6);
|
||||||
|
const nextStepLines = dedupeNarrativeLines(input.nextStepLines, 5);
|
||||||
|
const contextLead = ensureSentence(String(input.contextLead ?? ""));
|
||||||
|
|
||||||
|
return sanitizeUserFacingReply(
|
||||||
|
[
|
||||||
|
`Коротко: ${shortLine}`,
|
||||||
|
contextLead || "",
|
||||||
|
`Что именно проверено:\n${formatList(
|
||||||
|
checkedLines.length > 0
|
||||||
|
? checkedLines
|
||||||
|
: ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."]
|
||||||
|
)}`,
|
||||||
|
`Что найдено:\n${formatList(
|
||||||
|
foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."]
|
||||||
|
)}`,
|
||||||
|
`Что пока не доказано:\n${formatList(
|
||||||
|
unresolvedLines.length > 0
|
||||||
|
? unresolvedLines
|
||||||
|
: ["Существенных ограничений в текущем срезе не выявлено."]
|
||||||
|
)}`,
|
||||||
|
`${input.nextStepTitle}:\n${formatList(
|
||||||
|
nextStepLines.length > 0
|
||||||
|
? nextStepLines
|
||||||
|
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."]
|
||||||
|
)}`
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatSafeItemLine(entity: unknown, sourceId: unknown, riskScore?: unknown): string {
|
function formatSafeItemLine(entity: unknown, sourceId: unknown, riskScore?: unknown): string {
|
||||||
const entityLabel = sanitizeUserText(String(entity ?? "")) ?? "Record";
|
const entityLabel = sanitizeUserText(String(entity ?? "")) ?? "Record";
|
||||||
const idRaw = String(sourceId ?? "").trim();
|
const idRaw = String(sourceId ?? "").trim();
|
||||||
|
|
@ -2023,30 +2067,23 @@ function buildBoundaryFallbackReply(input: {
|
||||||
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return sanitizeUserFacingReply(
|
return renderStage4ContractReply({
|
||||||
[
|
shortLine: heading,
|
||||||
`Коротко: ${ensureSentence(heading)}`,
|
checkedLines: [
|
||||||
`Что именно проверено:\n${formatList([
|
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
|
||||||
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
|
"Надежный доменный путь для текущей формулировки не подтвержден."
|
||||||
"Надежный доменный путь для текущей формулировки не подтвержден."
|
],
|
||||||
])}`,
|
foundLines:
|
||||||
`Что найдено:\n${formatList(
|
nearbyCapabilities.length > 0
|
||||||
nearbyCapabilities.length > 0
|
? nearbyCapabilities
|
||||||
? nearbyCapabilities
|
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."],
|
||||||
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."]
|
unresolvedLines: ["Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."],
|
||||||
)}`,
|
nextStepLines:
|
||||||
`Что пока не доказано:\n${formatList([
|
quickActionItems.length > 0
|
||||||
"Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."
|
? quickActionItems
|
||||||
])}`,
|
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."],
|
||||||
`Что могу сделать сейчас:\n${formatList(
|
nextStepTitle: "Что могу сделать сейчас"
|
||||||
quickActionItems.length > 0
|
});
|
||||||
? quickActionItems
|
|
||||||
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."]
|
|
||||||
)}`
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n\n")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clarificationHints = buildNaturalClarificationHints({
|
const clarificationHints = buildNaturalClarificationHints({
|
||||||
|
|
@ -2060,32 +2097,26 @@ function buildBoundaryFallbackReply(input: {
|
||||||
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return sanitizeUserFacingReply(
|
return renderStage4ContractReply({
|
||||||
[
|
shortLine: domainHeading,
|
||||||
`Коротко: ${ensureSentence(domainHeading)}`,
|
checkedLines: [
|
||||||
`Что именно проверено:\n${formatList([
|
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
|
||||||
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
|
"Оценена достаточность подтвержденной опоры для прямого вывода."
|
||||||
"Оценена достаточность подтвержденной опоры для прямого вывода."
|
],
|
||||||
])}`,
|
foundLines:
|
||||||
`Что найдено:\n${formatList(
|
nearbyCapabilities.length > 0
|
||||||
nearbyCapabilities.length > 0
|
? nearbyCapabilities.slice(0, 2)
|
||||||
? nearbyCapabilities.slice(0, 2)
|
: ["Подтвержденных близких сценариев в текущем проходе не найдено."],
|
||||||
: ["Подтвержденных близких сценариев в текущем проходе не найдено."]
|
unresolvedLines:
|
||||||
)}`,
|
clarificationHints.length > 0
|
||||||
`Что пока не доказано:\n${formatList(
|
? clarificationHints
|
||||||
clarificationHints.length > 0
|
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."],
|
||||||
? clarificationHints
|
nextStepLines:
|
||||||
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."]
|
quickActionItems.length > 0
|
||||||
)}`,
|
? quickActionItems
|
||||||
`Что могу сделать сейчас:\n${formatList(
|
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."],
|
||||||
quickActionItems.length > 0
|
nextStepTitle: "Что могу сделать сейчас"
|
||||||
? quickActionItems
|
});
|
||||||
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."]
|
|
||||||
)}`
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n\n")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureSentence(value: string): string {
|
function ensureSentence(value: string): string {
|
||||||
|
|
@ -4377,36 +4408,14 @@ function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRender
|
||||||
limitationLines
|
limitationLines
|
||||||
};
|
};
|
||||||
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
|
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
|
||||||
const checkedLines = dedupeNarrativeLines(enriched.evidenceLines, 6);
|
return renderStage4ContractReply({
|
||||||
const foundLines = dedupeNarrativeLines([...enriched.brokenLines, ...enriched.whyLines], 6);
|
shortLine: enriched.shortLine,
|
||||||
const unresolvedLines = dedupeNarrativeLines(enriched.limitationLines, 6);
|
checkedLines: enriched.evidenceLines,
|
||||||
const nextStepLines = dedupeNarrativeLines(enriched.checkLines, 5);
|
foundLines: [...enriched.brokenLines, ...enriched.whyLines],
|
||||||
|
unresolvedLines: enriched.limitationLines,
|
||||||
return sanitizeUserFacingReply(
|
nextStepLines: enriched.checkLines,
|
||||||
[
|
nextStepTitle: "Что проверить первым"
|
||||||
`Коротко: ${enriched.shortLine}`,
|
});
|
||||||
`Что именно проверено:\n${formatList(
|
|
||||||
checkedLines.length > 0
|
|
||||||
? checkedLines
|
|
||||||
: ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."]
|
|
||||||
)}`,
|
|
||||||
`Что найдено:\n${formatList(
|
|
||||||
foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."]
|
|
||||||
)}`,
|
|
||||||
`Что пока не доказано:\n${formatList(
|
|
||||||
unresolvedLines.length > 0
|
|
||||||
? unresolvedLines
|
|
||||||
: ["Существенных ограничений в текущем срезе не выявлено."]
|
|
||||||
)}`,
|
|
||||||
`Что проверить первым:\n${formatList(
|
|
||||||
nextStepLines.length > 0
|
|
||||||
? nextStepLines
|
|
||||||
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."]
|
|
||||||
)}`
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n\n")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldUseSoftPolicyReply(input: {
|
function shouldUseSoftPolicyReply(input: {
|
||||||
|
|
@ -4468,32 +4477,22 @@ function renderSoftPolicyReply(input: {
|
||||||
: input.mode === "no_grounded" || input.mode === "empty"
|
: input.mode === "no_grounded" || input.mode === "empty"
|
||||||
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
|
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
|
||||||
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
|
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
|
||||||
return sanitizeUserFacingReply(
|
return renderStage4ContractReply({
|
||||||
[
|
shortLine,
|
||||||
`Коротко: ${shortLine}`,
|
checkedLines:
|
||||||
modeLine,
|
evidenceLines.length > 0
|
||||||
`Что именно проверено:\n${formatList(
|
? evidenceLines
|
||||||
evidenceLines.length > 0
|
: ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."],
|
||||||
? evidenceLines
|
foundLines:
|
||||||
: ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."]
|
foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."],
|
||||||
)}`,
|
unresolvedLines:
|
||||||
`Что найдено:\n${formatList(
|
limitationLines.length > 0 ? [modeLine, ...limitationLines] : [modeLine, "Для полного вывода не хватает деталей по части требований."],
|
||||||
foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."]
|
nextStepLines:
|
||||||
)}`,
|
actionLines.length > 0
|
||||||
`Что пока не доказано:\n${formatList(
|
? actionLines
|
||||||
limitationLines.length > 0
|
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."],
|
||||||
? limitationLines
|
nextStepTitle: "Что могу сделать сейчас"
|
||||||
: ["Для полного вывода не хватает деталей по части требований."]
|
});
|
||||||
)}`,
|
|
||||||
`Что могу сделать сейчас:\n${formatList(
|
|
||||||
actionLines.length > 0
|
|
||||||
? actionLines
|
|
||||||
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."]
|
|
||||||
)}`
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n\n")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutput {
|
function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutput {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { composeAssistantAnswer } from "../src/services/answerComposer";
|
||||||
|
import type { UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
|
||||||
|
function routeSummary() {
|
||||||
|
return {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectStage4Blocks(reply: string) {
|
||||||
|
expect(reply).toContain("Коротко:");
|
||||||
|
expect(reply).toContain("Что именно проверено:");
|
||||||
|
expect(reply).toContain("Что найдено:");
|
||||||
|
expect(reply).toContain("Что пока не доказано:");
|
||||||
|
expect(reply).not.toContain("Что сломано:");
|
||||||
|
expect(reply).not.toContain("Почему это похоже на проблему:");
|
||||||
|
expect(reply).not.toContain("На чем это основано:");
|
||||||
|
expect(reply).not.toContain("Ограничения:");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant stage4 answer contract shape", () => {
|
||||||
|
it("uses Stage 4 blocks for focused grounded policy reply", () => {
|
||||||
|
const retrieval: UnifiedRetrievalResult = {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "store_feature_risk",
|
||||||
|
status: "ok",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [{ doc: "A-1" }],
|
||||||
|
summary: {
|
||||||
|
broad_query_detected: false,
|
||||||
|
broad_result_flag: false,
|
||||||
|
minimum_evidence_failed: false,
|
||||||
|
narrowing_strength: "strong"
|
||||||
|
},
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
evidence_id: "ev-1",
|
||||||
|
claim_ref: "requirement:R1",
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "A-1",
|
||||||
|
period: "2020-06",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|A-1|2020-06"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: "F1",
|
||||||
|
route: "store_feature_risk",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "A-1",
|
||||||
|
period: "2020-06"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: "Переход подтвержден документом и проводкой.",
|
||||||
|
confidence: "high",
|
||||||
|
payload: {
|
||||||
|
amount: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
why_included: ["релевантная запись"],
|
||||||
|
selection_reason: ["сильное совпадение"],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "high",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = composeAssistantAnswer({
|
||||||
|
userMessage: "Проверь документ A-1 за июнь 2020.",
|
||||||
|
routeSummary: routeSummary(),
|
||||||
|
retrievalResults: [retrieval],
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "проверить документ",
|
||||||
|
subject_tokens: [],
|
||||||
|
status: "covered",
|
||||||
|
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",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: ["релевантная запись"],
|
||||||
|
selection_reason_summary: ["сильное совпадение"]
|
||||||
|
},
|
||||||
|
enableAnswerPolicyV11: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.reply_type).toBe("factual_with_explanation");
|
||||||
|
expect(output.assistant_reply).toContain("Что проверить первым:");
|
||||||
|
expectStage4Blocks(output.assistant_reply);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Stage 4 blocks for weak broad-partial soft reply", () => {
|
||||||
|
const retrieval: UnifiedRetrievalResult = {
|
||||||
|
fragment_id: "F1",
|
||||||
|
requirement_ids: ["R1"],
|
||||||
|
route: "store_feature_risk",
|
||||||
|
status: "partial",
|
||||||
|
result_type: "summary",
|
||||||
|
items: [{ note: "weak candidate" }],
|
||||||
|
summary: {
|
||||||
|
broad_query_detected: true,
|
||||||
|
broad_result_flag: true,
|
||||||
|
minimum_evidence_failed: true,
|
||||||
|
narrowing_strength: "weak"
|
||||||
|
},
|
||||||
|
evidence: [],
|
||||||
|
why_included: [],
|
||||||
|
selection_reason: [],
|
||||||
|
risk_factors: [],
|
||||||
|
business_interpretation: [],
|
||||||
|
confidence: "low",
|
||||||
|
limitations: ["Weak evidence envelope"],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = composeAssistantAnswer({
|
||||||
|
userMessage: "Покажи общую картину рисков и аномалий по документам.",
|
||||||
|
routeSummary: routeSummary(),
|
||||||
|
retrievalResults: [retrieval],
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F1",
|
||||||
|
requirement_text: "общая картина рисков",
|
||||||
|
subject_tokens: [],
|
||||||
|
status: "partially_covered",
|
||||||
|
route: "store_feature_risk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: ["R1"],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "partial",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: ["R1"],
|
||||||
|
reasons: ["insufficient_detail"],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
enableAnswerPolicyV11: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.reply_type).toBe("partial_coverage");
|
||||||
|
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||||
|
expectStage4Blocks(output.assistant_reply);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Stage 4 blocks for boundary fallback reply", () => {
|
||||||
|
const output = composeAssistantAnswer({
|
||||||
|
userMessage: "Скажи курс доллара на завтра и дай прогноз инфляции.",
|
||||||
|
routeSummary: routeSummary(),
|
||||||
|
retrievalResults: [],
|
||||||
|
requirements: [],
|
||||||
|
coverageReport: {
|
||||||
|
requirements_total: 0,
|
||||||
|
requirements_covered: 0,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: []
|
||||||
|
},
|
||||||
|
groundingCheck: {
|
||||||
|
status: "no_grounded_answer",
|
||||||
|
route_subject_match: false,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: ["no grounded support"],
|
||||||
|
why_included_summary: [],
|
||||||
|
selection_reason_summary: []
|
||||||
|
},
|
||||||
|
enableAnswerPolicyV11: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.reply_type).toBe("clarification_required");
|
||||||
|
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||||
|
expectStage4Blocks(output.assistant_reply);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue