ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.4: расширен soft-refusal в deep/living, чтобы при слабом/непокрытом домене не падать в шаблонный Коротко/Что сломано.
This commit is contained in:
+52
-6
@@ -1601,6 +1601,16 @@ function formatNarrativeDomainLabel(domain) {
|
||||
}
|
||||
return "доступного учетного контура";
|
||||
}
|
||||
function pickDeterministicBoundaryVariant(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 pickBoundaryCapabilityLines(userMessage, limit = 3) {
|
||||
const text = String(userMessage ?? "").toLowerCase();
|
||||
const scored = BOUNDARY_CAPABILITY_SUGGESTIONS.map((item, index) => ({
|
||||
@@ -1615,6 +1625,16 @@ function pickBoundaryCapabilityLines(userMessage, limit = 3) {
|
||||
const selected = ranked.slice(0, Math.max(2, limit));
|
||||
return uniqueStrings(selected.map((item) => `${item.label}: ${item.helpText}`), limit);
|
||||
}
|
||||
function buildBoundaryQuickActionLine(capabilities) {
|
||||
const actions = capabilities
|
||||
.slice(0, 2)
|
||||
.map((item) => item.replace(/:\s*/u, " — ").trim())
|
||||
.filter((item) => item.length > 0);
|
||||
if (actions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
|
||||
}
|
||||
function buildNaturalClarificationHints(input) {
|
||||
const hints = [];
|
||||
if (input.missingAnchors.period) {
|
||||
@@ -1641,7 +1661,7 @@ function shouldUseBoundaryFallbackReply(input) {
|
||||
if (input.mode === "out_of_scope") {
|
||||
return true;
|
||||
}
|
||||
if (input.mode !== "clarification_required" && input.mode !== "no_grounded") {
|
||||
if (input.mode !== "clarification_required" && input.mode !== "no_grounded" && input.mode !== "broad_partial") {
|
||||
return false;
|
||||
}
|
||||
const hasNoEvidenceRoutes = input.okResultsCount === 0 && input.partialResultsCount === 0;
|
||||
@@ -1650,14 +1670,31 @@ function shouldUseBoundaryFallbackReply(input) {
|
||||
const groundingBlocked = input.groundingCheck.status === "no_grounded_answer" ||
|
||||
input.groundingCheck.status === "partial" ||
|
||||
input.groundingCheck.status === "route_mismatch_blocked";
|
||||
return hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked;
|
||||
if (hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked) {
|
||||
return true;
|
||||
}
|
||||
const domainNotCovered = input.focusDomain === null || input.focusDomainGroundingBlocked;
|
||||
const weakEvidenceEnvelope = input.okResultsCount === 0 &&
|
||||
(input.partialResultsCount === 0 ||
|
||||
input.aggregateEvidenceConfidence === "low" ||
|
||||
input.hasCriticalEvidenceLimitation);
|
||||
if (domainNotCovered && hasNoConfirmedCoverage && groundingBlocked && weakEvidenceEnvelope) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function buildBoundaryFallbackReply(input) {
|
||||
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
||||
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
|
||||
if (input.focusDomain === null) {
|
||||
return sanitizeUserFacingReply([
|
||||
const heading = pickDeterministicBoundaryVariant(input.userMessage, [
|
||||
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
||||
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
||||
]);
|
||||
return sanitizeUserFacingReply([
|
||||
heading,
|
||||
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
|
||||
quickActionLine ?? "",
|
||||
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -1667,10 +1704,15 @@ function buildBoundaryFallbackReply(input) {
|
||||
missingAnchors: input.missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
});
|
||||
return sanitizeUserFacingReply([
|
||||
const domainHeading = pickDeterministicBoundaryVariant(`${input.userMessage}|${input.focusDomain}`, [
|
||||
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
|
||||
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
||||
]);
|
||||
return sanitizeUserFacingReply([
|
||||
domainHeading,
|
||||
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
|
||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : ""
|
||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "",
|
||||
quickActionLine ?? ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"));
|
||||
@@ -3659,7 +3701,11 @@ function composeAssistantAnswerV11(input) {
|
||||
groundingCheck: input.groundingCheck,
|
||||
coverageReport: input.coverageReport,
|
||||
okResultsCount: okResults.length,
|
||||
partialResultsCount: partialResults.length
|
||||
partialResultsCount: partialResults.length,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
focusDomainGroundingBlocked,
|
||||
aggregateEvidenceConfidence,
|
||||
hasCriticalEvidenceLimitation
|
||||
});
|
||||
const hasProblemWeakSignal = policySignals.narrowing_strength !== "strong" ||
|
||||
policySignals.minimum_evidence_failed ||
|
||||
|
||||
@@ -1895,6 +1895,17 @@ function formatNarrativeDomainLabel(domain: P0NarrativeDomain): string {
|
||||
return "доступного учетного контура";
|
||||
}
|
||||
|
||||
function pickDeterministicBoundaryVariant(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 pickBoundaryCapabilityLines(userMessage: string, limit = 3): string[] {
|
||||
const text = String(userMessage ?? "").toLowerCase();
|
||||
const scored = BOUNDARY_CAPABILITY_SUGGESTIONS.map((item, index) => ({
|
||||
@@ -1910,6 +1921,17 @@ function pickBoundaryCapabilityLines(userMessage: string, limit = 3): string[] {
|
||||
return uniqueStrings(selected.map((item) => `${item.label}: ${item.helpText}`), limit);
|
||||
}
|
||||
|
||||
function buildBoundaryQuickActionLine(capabilities: string[]): string | null {
|
||||
const actions = capabilities
|
||||
.slice(0, 2)
|
||||
.map((item) => item.replace(/:\s*/u, " — ").trim())
|
||||
.filter((item) => item.length > 0);
|
||||
if (actions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
|
||||
}
|
||||
|
||||
function buildNaturalClarificationHints(input: {
|
||||
missingAnchors: MissingAnchors;
|
||||
coverageReport: RequirementCoverageReport;
|
||||
@@ -1942,11 +1964,15 @@ function shouldUseBoundaryFallbackReply(input: {
|
||||
coverageReport: RequirementCoverageReport;
|
||||
okResultsCount: number;
|
||||
partialResultsCount: number;
|
||||
focusDomain: P0NarrativeDomain;
|
||||
focusDomainGroundingBlocked: boolean;
|
||||
aggregateEvidenceConfidence: EvidenceConfidence;
|
||||
hasCriticalEvidenceLimitation: boolean;
|
||||
}): boolean {
|
||||
if (input.mode === "out_of_scope") {
|
||||
return true;
|
||||
}
|
||||
if (input.mode !== "clarification_required" && input.mode !== "no_grounded") {
|
||||
if (input.mode !== "clarification_required" && input.mode !== "no_grounded" && input.mode !== "broad_partial") {
|
||||
return false;
|
||||
}
|
||||
const hasNoEvidenceRoutes = input.okResultsCount === 0 && input.partialResultsCount === 0;
|
||||
@@ -1957,7 +1983,19 @@ function shouldUseBoundaryFallbackReply(input: {
|
||||
input.groundingCheck.status === "no_grounded_answer" ||
|
||||
input.groundingCheck.status === "partial" ||
|
||||
input.groundingCheck.status === "route_mismatch_blocked";
|
||||
return hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked;
|
||||
if (hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked) {
|
||||
return true;
|
||||
}
|
||||
const domainNotCovered = input.focusDomain === null || input.focusDomainGroundingBlocked;
|
||||
const weakEvidenceEnvelope =
|
||||
input.okResultsCount === 0 &&
|
||||
(input.partialResultsCount === 0 ||
|
||||
input.aggregateEvidenceConfidence === "low" ||
|
||||
input.hasCriticalEvidenceLimitation);
|
||||
if (domainNotCovered && hasNoConfirmedCoverage && groundingBlocked && weakEvidenceEnvelope) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function buildBoundaryFallbackReply(input: {
|
||||
@@ -1967,11 +2005,20 @@ function buildBoundaryFallbackReply(input: {
|
||||
coverageReport: RequirementCoverageReport;
|
||||
}): string {
|
||||
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
||||
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
|
||||
if (input.focusDomain === null) {
|
||||
return sanitizeUserFacingReply(
|
||||
const heading = pickDeterministicBoundaryVariant(
|
||||
input.userMessage,
|
||||
[
|
||||
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
||||
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
||||
]
|
||||
);
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
heading,
|
||||
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
|
||||
quickActionLine ?? "",
|
||||
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -1983,11 +2030,19 @@ function buildBoundaryFallbackReply(input: {
|
||||
missingAnchors: input.missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
});
|
||||
return sanitizeUserFacingReply(
|
||||
const domainHeading = pickDeterministicBoundaryVariant(
|
||||
`${input.userMessage}|${input.focusDomain}`,
|
||||
[
|
||||
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
|
||||
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
||||
]
|
||||
);
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
domainHeading,
|
||||
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
|
||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : ""
|
||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "",
|
||||
quickActionLine ?? ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
@@ -4364,7 +4419,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
groundingCheck: input.groundingCheck,
|
||||
coverageReport: input.coverageReport,
|
||||
okResultsCount: okResults.length,
|
||||
partialResultsCount: partialResults.length
|
||||
partialResultsCount: partialResults.length,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
focusDomainGroundingBlocked,
|
||||
aggregateEvidenceConfidence,
|
||||
hasCriticalEvidenceLimitation
|
||||
});
|
||||
const hasProblemWeakSignal =
|
||||
policySignals.narrowing_strength !== "strong" ||
|
||||
|
||||
@@ -54,6 +54,7 @@ describe("assistant boundary fallback reply", () => {
|
||||
expect(output.reply_type).toBe("clarification_required");
|
||||
expect(output.assistant_reply).toMatch(/мягкий отказ/i);
|
||||
expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:");
|
||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
|
||||
@@ -76,9 +77,10 @@ describe("assistant boundary fallback reply", () => {
|
||||
});
|
||||
|
||||
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).not.toContain("Что сломано:");
|
||||
});
|
||||
|
||||
@@ -101,7 +103,66 @@ describe("assistant boundary fallback reply", () => {
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("out_of_scope");
|
||||
expect(output.assistant_reply).toMatch(/мягкий отказ|не могу надежно/i);
|
||||
expect(output.assistant_reply).toMatch(/мягкий отказ|не могу надежно|не хватает подтвержденной опоры/i);
|
||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
});
|
||||
|
||||
it("uses soft refusal in broad_partial mode when domain is not covered and evidence is weak", () => {
|
||||
const output = composeAssistantAnswer({
|
||||
userMessage: "What is the expected inflation next quarter and FX trend?",
|
||||
routeSummary: buildRouteSummary("none"),
|
||||
retrievalResults: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "store_canonical",
|
||||
status: "partial",
|
||||
result_type: "summary",
|
||||
items: [{ note: "weak candidate" }],
|
||||
summary: { note: "weak" },
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "low",
|
||||
limitations: ["weak_source_mapping"],
|
||||
errors: []
|
||||
} as any
|
||||
],
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "forecast and trend",
|
||||
subject_tokens: [],
|
||||
status: "clarification_needed",
|
||||
route: null
|
||||
}
|
||||
],
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: ["R1"],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "partial",
|
||||
route_subject_match: false,
|
||||
missing_requirements: ["R1"],
|
||||
reasons: ["weak_source_mapping"],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
enableAnswerPolicyV11: true
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("partial_coverage");
|
||||
expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:");
|
||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user