Stage 2 завершён: problem-first ответы и follow-up continuity - ассистент переведён от entity-heavy логики к problem-first ответам с problem-unit слоем, удержанием контекста в follow-up и очисткой пользовательского ответа от сырых технических ссылок.

This commit is contained in:
2026-03-26 14:53:52 +03:00
parent ece1abed76
commit 96353cfd48
2474 changed files with 21678 additions and 3292445 deletions
+448 -28
View File
@@ -10,21 +10,103 @@ function fallbackFromSummary(routeSummary) {
function uniqueStrings(values, limit = 6) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
}
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
const RAW_REF_TOKEN_PATTERN = /\b(?:source_ref|canonical_ref|entity_id|fragment_id|guid|uuid)\b/gi;
function looksLikeMojibake(value) {
const text = String(value ?? "");
if (!text.trim()) {
return false;
}
if (/(?:Р.|С.){5,}/u.test(text)) {
return true;
}
if (/[ЃѓЂђЌќЎў]/u.test(text)) {
return true;
}
return false;
}
function looksLikeTechnicalIdentifier(value) {
const text = String(value ?? "").trim();
if (!text) {
return false;
}
if (UUID_PATTERN.test(text)) {
UUID_PATTERN.lastIndex = 0;
return true;
}
UUID_PATTERN.lastIndex = 0;
if (LONG_HEX_PATTERN.test(text)) {
LONG_HEX_PATTERN.lastIndex = 0;
return true;
}
LONG_HEX_PATTERN.lastIndex = 0;
return /(?:evidence_source_ref_v1\||cmp%3a|batch_refresh_then_store:|^cmp:)/i.test(text);
}
function scrubRawTechnicalRefs(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return "";
}
return raw
.replace(RAW_REF_BLOB_PATTERN, "linked source")
.replace(UUID_PATTERN, "[id]")
.replace(LONG_HEX_PATTERN, "[id]")
.replace(RAW_REF_TOKEN_PATTERN, "reference")
.replace(/\(\s*\[id\]\s*\)/g, "")
.replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]")
.replace(/\s{2,}/g, " ")
.trim();
}
function sanitizeUserFacingReply(value) {
return scrubRawTechnicalRefs(value)
.replace(/[ \t]+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function sanitizeUserText(value) {
const normalized = scrubRawTechnicalRefs(String(value ?? "").replace(/\s+/g, " ").trim());
if (!normalized) {
return null;
}
if (looksLikeMojibake(normalized)) {
return null;
}
return normalized;
}
function sanitizeUserLines(values, limit = 6) {
const cleaned = values
.map((item) => sanitizeUserText(item))
.filter((item) => Boolean(item));
return uniqueStrings(cleaned, limit);
}
function formatList(items) {
if (items.length === 0) {
return "";
}
return items.map((item) => `- ${item}`).join("\n");
}
function formatSafeItemLine(entity, sourceId, riskScore) {
const entityLabel = sanitizeUserText(String(entity ?? "")) ?? "Record";
const idRaw = String(sourceId ?? "").trim();
const exposeId = idRaw.length > 0 && !looksLikeTechnicalIdentifier(idRaw);
const subject = exposeId ? `${entityLabel} (${idRaw})` : entityLabel;
if (riskScore !== undefined) {
return `${subject} - risk ${String(riskScore)}.`;
}
return `${subject}.`;
}
function extractTopFacts(results) {
const lines = [];
for (const result of results.filter((item) => item.status === "ok").slice(0, 3)) {
if (result.result_type === "chain") {
const top = result.items.slice(0, 3).map((item) => {
const counterparty = String(item.counterparty_id ?? "не указан");
const counterparty = String(item.counterparty_id ?? "").trim();
const operations = String(item.operations_count ?? "0");
const docs = String(item.document_refs_count ?? "0");
return `Контрагент ${counterparty}: операций ${operations}, документов в связке ${docs}.`;
const counterpartyLabel = counterparty.length > 0 && !looksLikeTechnicalIdentifier(counterparty) ? `Counterparty ${counterparty}` : "Counterparty";
return `${counterpartyLabel}: operations ${operations}, linked docs ${docs}.`;
});
lines.push(...top);
continue;
@@ -32,41 +114,41 @@ function extractTopFacts(results) {
if (result.result_type === "ranking") {
const top = result.items
.slice(0, 5)
.map((item) => `${item.rank ?? "•"}. ${String(item.entity ?? "Сущность")} — ${String(item.records_count ?? 0)}.`);
.map((item) => `${item.rank ?? "*"}. ${String(item.entity ?? "Entity")} - ${String(item.records_count ?? 0)}.`);
lines.push(...top);
continue;
}
if (result.result_type === "list") {
const top = result.items.slice(0, 5).map((item) => {
if (item.risk_score !== undefined) {
return `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}) — риск ${String(item.risk_score)}.`;
return formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? "", item.risk_score);
}
return `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}).`;
return formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? "");
});
lines.push(...top);
continue;
}
const top = result.items
.slice(0, 3)
.map((item) => `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}).`);
.map((item) => formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? ""));
lines.push(...top);
}
return lines;
return sanitizeUserLines(lines, 8);
}
function extractWhyIncluded(results) {
return uniqueStrings(results.flatMap((item) => item.why_included));
return sanitizeUserLines(results.flatMap((item) => item.why_included));
}
function extractSelectionReasons(results) {
return uniqueStrings(results.flatMap((item) => item.selection_reason));
return sanitizeUserLines(results.flatMap((item) => item.selection_reason));
}
function extractRiskFactors(results) {
return uniqueStrings(results.flatMap((item) => item.risk_factors));
return sanitizeUserLines(results.flatMap((item) => item.risk_factors));
}
function extractBusinessInterpretation(results) {
return uniqueStrings(results.flatMap((item) => item.business_interpretation));
return sanitizeUserLines(results.flatMap((item) => item.business_interpretation));
}
function extractLimitations(results) {
return uniqueStrings(results.flatMap((item) => item.limitations));
return sanitizeUserLines(results.flatMap((item) => item.limitations), 10);
}
function summaryValue(result, key) {
const summary = result.summary ?? {};
@@ -79,6 +161,55 @@ function summaryString(result, key) {
const value = summaryValue(result, key);
return typeof value === "string" ? value : null;
}
function summaryNumber(result, key) {
const value = summaryValue(result, key);
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function summaryStringArray(result, key) {
const value = summaryValue(result, key);
if (!Array.isArray(value)) {
return [];
}
return sanitizeUserLines(value.map((item) => String(item)), 6);
}
function buildFallbackWhyIncluded(results) {
const lines = [];
for (const result of results.slice(0, 2)) {
const routeFocus = summaryString(result, "route_focus");
const sourceRecords = summaryNumber(result, "source_records");
const filteredRecords = summaryNumber(result, "filtered_records_after_narrowing");
const checkedRecords = summaryNumber(result, "checked_records");
if (routeFocus) {
lines.push(`Проверка выполнена по профилю ${routeFocus}.`);
}
if (sourceRecords !== null && filteredRecords !== null && filteredRecords < sourceRecords) {
lines.push(`Применено сужение выборки: ${filteredRecords} из ${sourceRecords} записей.`);
}
if (checkedRecords !== null) {
lines.push(`Проверено записей в текущем проходе: ${checkedRecords}.`);
}
}
return sanitizeUserLines(lines, 4);
}
function buildFallbackSelectionReasons(results) {
const lines = [];
for (const result of results.slice(0, 2)) {
if (summaryBoolean(result, "semantic_narrowing_applied")) {
lines.push("Отбор выполнен по семантическому сужению предметной области.");
}
const rankingBasis = summaryStringArray(result, "ranking_basis");
if (rankingBasis.length > 0) {
lines.push(`Ранжирование основано на: ${rankingBasis.join(", ")}.`);
}
if (summaryBoolean(result, "broad_guard_applied")) {
lines.push("Применен broad-query guard для контроля ложной точности.");
}
}
if (lines.length === 0) {
lines.push("Отбор выполнен по совпадению предметных сигналов и доступной evidence-опоры.");
}
return sanitizeUserLines(lines, 4);
}
function suggestNextStep(requirements, coverage) {
const next = [];
if (coverage.clarification_needed_for.length > 0) {
@@ -95,9 +226,131 @@ function suggestNextStep(requirements, coverage) {
}
return next;
}
const PROBLEM_HEAVY_TYPES = new Set([
"document_conflict",
"broken_chain_segment",
"lifecycle_anomaly_node",
"unresolved_settlement_cluster",
"period_risk_cluster",
"cross_branch_inconsistency_cluster"
]);
function flattenEvidence(results) {
return results.flatMap((item) => item.evidence);
}
function flattenProblemUnits(results) {
const units = [];
for (const result of results) {
if (!Array.isArray(result.problem_units)) {
continue;
}
units.push(...result.problem_units);
}
const byId = new Map();
for (const unit of units) {
byId.set(unit.problem_unit_id, unit);
}
return Array.from(byId.values());
}
function selectProblemUnitSummary(results) {
let selected = null;
for (const result of results) {
if (!result.problem_unit_summary) {
continue;
}
if (!selected || result.problem_unit_summary.units_total > selected.units_total) {
selected = result.problem_unit_summary;
}
}
return selected;
}
function formatAffectedScope(unit) {
const scopeParts = [];
if (unit.affected_accounts.length > 0) {
scopeParts.push(`счета: ${unit.affected_accounts.slice(0, 2).join(", ")}`);
}
if (unit.affected_counterparties.length > 0) {
scopeParts.push(`контрагенты: ${unit.affected_counterparties.slice(0, 2).join(", ")}`);
}
if (unit.affected_documents.length > 0) {
scopeParts.push(`документы: ${unit.affected_documents.slice(0, 2).join(", ")}`);
}
if (scopeParts.length === 0 && unit.affected_entities.length > 0) {
scopeParts.push(`объекты: ${unit.affected_entities.slice(0, 2).join(", ")}`);
}
if (scopeParts.length === 0) {
return "затронутый контур требует уточнения";
}
return scopeParts.join("; ");
}
function buildProblemCentricActions(input) {
const actions = [];
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
if (unitTypes.has("broken_chain_segment")) {
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
}
if (unitTypes.has("unresolved_settlement_cluster")) {
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
}
if (unitTypes.has("period_risk_cluster")) {
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
}
if (unitTypes.has("cross_branch_inconsistency_cluster")) {
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
}
if (unitTypes.has("lifecycle_anomaly_node")) {
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
}
if (input.mode === "clarification_required") {
if (input.missingAnchors.period) {
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
}
if (input.missingAnchors.account) {
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
}
if (input.missingAnchors.documentOrObject) {
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
}
if (input.missingAnchors.counterparty) {
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
}
}
if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
}
return uniqueStrings(actions, 6);
}
function buildProblemCentricClarifications(input) {
if (input.mode !== "clarification_required") {
return [];
}
const questions = [];
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
if (input.missingAnchors.period) {
questions.push("Уточните период (например, 2020-06), в котором нужно проверить проблемный кластер.");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
}
if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
}
if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
}
if (unitTypes.has("broken_chain_segment")) {
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
}
if (unitTypes.has("period_risk_cluster")) {
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
}
if (unitTypes.has("unresolved_settlement_cluster")) {
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(questions, 6);
}
function buildClaimEvidenceLinks(results) {
const byClaim = new Map();
for (const evidence of flattenEvidence(results)) {
@@ -246,7 +499,7 @@ function buildClarificationQuestions(input) {
function buildRecommendedActions(input) {
const actions = [];
if (input.mode === "focused_grounded") {
actions.push("Проверьте 1-2 ключевые записи по source_ref и зафиксируйте итог в рабочем файле проверки.");
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
}
if (input.mode === "broad_partial") {
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
@@ -270,7 +523,7 @@ function buildRecommendedActions(input) {
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
}
if (input.sourceRefs.length > 0) {
actions.push(`Начните проверку с source_ref: ${input.sourceRefs.slice(0, 2).join(", ")}.`);
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
}
return uniqueStrings(actions, 6);
}
@@ -403,6 +656,122 @@ function buildDirectAnswer(input) {
}
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
}
function buildProblemCentricAnswerSummary(input) {
if (input.mode === "clarification_required") {
return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса.";
}
if (input.weakUnits) {
return "Сформирован problem-centric срез с ограниченной опорой; вывод предварительный и требует до-проверки.";
}
if (input.summary?.units_total && input.summary.units_total > 1) {
return `Сформирован problem-centric срез: выделено ${input.summary.units_total} проблемных кластера с приоритетами.`;
}
return "Сформирован problem-centric срез: выделен ключевой проблемный кластер и затронутый контур.";
}
function buildProblemCentricDirectAnswer(input) {
const lead = input.mode === "clarification_required"
? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен."
: input.weakUnits
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
const unitLines = input.units.map((unit) => {
const scope = formatAffectedScope(unit);
return `- ${unit.title}: ${unit.business_defect_class}; ${scope}; severity=${unit.severity.grade}, confidence=${unit.confidence.grade}.`;
});
if (unitLines.length === 0) {
return `${lead}\nПроблемные кластеры не удалось детализировать в текущем срезе.`;
}
return [lead, "Проблемные кластеры:", ...unitLines].join("\n");
}
function buildProblemCentricAnswerStructure(input) {
const weakUnits = input.selectedUnits.every((item) => item.confidence.grade === "low");
const unitMechanismNotes = uniqueStrings(input.selectedUnits
.map((item) => item.mechanism_summary)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const sourceRefs = uniqueStrings(input.evidenceItems
.map((item) => item.source_ref?.canonical_ref)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
const mechanismStatus = unitMechanismNotes.length === 0
? "unresolved"
: weakUnits || input.limitationReasonCodes.includes("missing_mechanism")
? "limited"
: "grounded";
const problemSpecificLimitations = [];
if (weakUnits) {
problemSpecificLimitations.push("Problem units remain weak-confidence; conclusions are intentionally limited.");
}
if (input.problemSummary?.duplicate_collapses && input.problemSummary.duplicate_collapses > 0) {
problemSpecificLimitations.push("Part of the problem signal was merged due to duplicate collapse.");
}
const limitations = uniqueStrings([
...problemSpecificLimitations,
...input.limitationReasonCodes.map((code) => limitationReasonToText(code)),
...extractLimitations(input.retrievalResults),
...input.groundingCheck.reasons
], 10);
const openUncertainties = uniqueStrings([
...input.groundingCheck.missing_requirements,
...(input.mode === "clarification_required" && input.missingAnchors.period ? ["missing_anchor:period"] : []),
...(input.mode === "clarification_required" && input.missingAnchors.account ? ["missing_anchor:account"] : []),
...(input.mode === "clarification_required" && input.missingAnchors.documentOrObject
? ["missing_anchor:document_or_object"]
: []),
...(input.mode === "clarification_required" && input.missingAnchors.counterparty ? ["missing_anchor:counterparty"] : [])
], 8);
return {
schema_version: "answer_structure_v1_1",
answer_summary: buildProblemCentricAnswerSummary({
mode: input.mode,
weakUnits,
summary: input.problemSummary
}),
direct_answer: buildProblemCentricDirectAnswer({
mode: input.mode,
units: input.selectedUnits,
weakUnits
}),
mechanism_block: {
status: mechanismStatus,
mechanism_notes: unitMechanismNotes,
limitation_reason_codes: input.limitationReasonCodes
},
evidence_block: {
evidence_ids: evidenceIds,
source_refs: sourceRefs,
mechanism_notes: unitMechanismNotes,
coverage_note: input.coverageReport.requirements_total > 0 &&
input.coverageReport.requirements_total === input.coverageReport.requirements_covered &&
input.coverageReport.requirements_uncovered.length === 0 &&
input.coverageReport.requirements_partially_covered.length === 0
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(input.claimEvidenceLinks.length > 0
? {
claim_evidence_links: input.claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: openUncertainties,
limitations
},
next_step_block: {
recommended_actions: buildProblemCentricActions({
units: input.selectedUnits,
mode: input.mode,
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport
}),
clarification_questions: buildProblemCentricClarifications({
units: input.selectedUnits,
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport,
mode: input.mode
})
}
};
}
function renderPolicyReply(structure) {
const mechanismLines = [`status=${structure.mechanism_block.status}`];
if (structure.mechanism_block.mechanism_notes.length > 0) {
@@ -414,18 +783,18 @@ function renderPolicyReply(structure) {
if (structure.mechanism_block.status === "unresolved" && structure.mechanism_block.mechanism_notes.length === 0) {
mechanismLines.push("mechanism_note is intentionally omitted due to weak or missing mechanism evidence");
}
const sourceRefCount = Array.isArray(structure.evidence_block.source_refs) ? structure.evidence_block.source_refs.length : 0;
const claimLinkCount = Array.isArray(structure.evidence_block.claim_evidence_links)
? structure.evidence_block.claim_evidence_links.length
: 0;
const evidenceLines = [
`coverage=${structure.evidence_block.coverage_note}`,
`evidence_ids=${structure.evidence_block.evidence_ids.length > 0 ? structure.evidence_block.evidence_ids.join(", ") : "none"}`
`supporting_evidence_count=${structure.evidence_block.evidence_ids.length}`,
`supporting_source_count=${sourceRefCount}`,
`claim_support_links=${claimLinkCount}`
];
if (Array.isArray(structure.evidence_block.source_refs) && structure.evidence_block.source_refs.length > 0) {
evidenceLines.push(`source_refs=${structure.evidence_block.source_refs.join(", ")}`);
}
if (Array.isArray(structure.evidence_block.claim_evidence_links) && structure.evidence_block.claim_evidence_links.length > 0) {
const compactLinks = structure.evidence_block.claim_evidence_links
.slice(0, 4)
.map((item) => `${item.claim_ref}:${item.evidence_ids.join("|")}`);
evidenceLines.push(`claim_evidence_links=${compactLinks.join("; ")}`);
if (sourceRefCount > 0) {
evidenceLines.push("Detailed source references are available in debug payload.");
}
const uncertaintyLines = [
...structure.uncertainty_block.open_uncertainties.map((item) => `open: ${item}`),
@@ -441,7 +810,7 @@ function renderPolicyReply(structure) {
if (nextStepLines.length === 0) {
nextStepLines.push("No additional action is required for this scoped answer.");
}
return [
return sanitizeUserFacingReply([
`Answer summary: ${structure.answer_summary}`,
`Direct answer:\n${structure.direct_answer}`,
`Mechanism block:\n${formatList(mechanismLines)}`,
@@ -450,7 +819,7 @@ function renderPolicyReply(structure) {
`Next step block:\n${formatList(nextStepLines)}`
]
.filter(Boolean)
.join("\n\n");
.join("\n\n"));
}
function composeAssistantAnswerV11(input) {
const fallbackType = fallbackFromSummary(input.routeSummary);
@@ -467,8 +836,15 @@ function composeAssistantAnswerV11(input) {
const mechanismNotes = uniqueStrings(evidenceItems
.map((item) => item.mechanism_note)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const problemUnits = flattenProblemUnits(input.retrievalResults);
const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults);
const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type));
const selectedProblemUnits = problemHeavyUnits.slice(0, 4);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
const lowConfidenceSignals = evidenceItems.filter((item) => item.confidence === "low").length;
const lowConfidenceShare = evidenceItems.length > 0 ? lowConfidenceSignals / evidenceItems.length : 0;
const lowConfidenceConcentration = lowConfidenceShare >= 0.6;
const hasSupport = okResults.length > 0 ||
partialResults.length > 0 ||
evidenceItems.length > 0 ||
@@ -501,6 +877,45 @@ function composeAssistantAnswerV11(input) {
policySignals
});
const missingAnchors = detectMissingAnchors(input.userMessage);
const hasProblemWeakSignal = policySignals.narrowing_strength !== "strong" ||
policySignals.minimum_evidence_failed ||
limitationReasonCodes.includes("missing_mechanism") ||
limitationReasonCodes.includes("weak_source_mapping") ||
aggregateEvidenceConfidence === "low" ||
lowConfidenceConcentration;
const hardBlockedMode = decision.mode === "out_of_scope" || decision.mode === "route_mismatch" || decision.mode === "backend_error";
const problemCentricModeEligible = decision.mode === "broad_partial" ||
decision.mode === "clarification_required" ||
(decision.mode === "focused_grounded" && hasProblemWeakSignal);
const shouldUseProblemCentricAnswer = Boolean(input.enableProblemCentricAnswerV1) &&
!hardBlockedMode &&
problemCentricModeEligible &&
(!focusedStrong || hasProblemWeakSignal) &&
selectedProblemUnits.length > 0;
if (shouldUseProblemCentricAnswer) {
const problemCentricStructure = buildProblemCentricAnswerStructure({
mode: decision.mode,
selectedUnits: selectedProblemUnits,
problemSummary: problemUnitSummary,
evidenceItems,
claimEvidenceLinks,
limitationReasonCodes,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults,
missingAnchors,
coverageReport: input.coverageReport
});
return {
assistant_reply: renderPolicyReply(problemCentricStructure),
fallback_type: decision.fallback_type,
reply_type: decision.reply_type,
answer_structure_v11: problemCentricStructure,
problem_centric_answer_applied: true,
problem_units_used_count: selectedProblemUnits.length,
problem_answer_mode: "stage2_problem_centric_v1",
problem_unit_ids_used: selectedProblemUnits.map((item) => item.problem_unit_id)
};
}
const clarificationQuestions = buildClarificationQuestions({
mode: decision.mode,
missingAnchors,
@@ -577,13 +992,18 @@ function composeAssistantAnswerV11(input) {
assistant_reply: renderPolicyReply(answerStructure),
fallback_type: decision.fallback_type,
reply_type: decision.reply_type,
answer_structure_v11: answerStructure
answer_structure_v11: answerStructure,
problem_centric_answer_applied: false,
problem_units_used_count: 0,
problem_answer_mode: "stage1_policy_v11"
};
}
function composeExplainableAnswer(input, scopeLabel) {
const facts = extractTopFacts(input.retrievalResults);
const whyIncluded = extractWhyIncluded(input.retrievalResults);
const selectionReasons = extractSelectionReasons(input.retrievalResults);
const whyIncludedRaw = extractWhyIncluded(input.retrievalResults);
const selectionReasonsRaw = extractSelectionReasons(input.retrievalResults);
const whyIncluded = whyIncludedRaw.length > 0 ? whyIncludedRaw : buildFallbackWhyIncluded(input.retrievalResults);
const selectionReasons = selectionReasonsRaw.length > 0 ? selectionReasonsRaw : buildFallbackSelectionReasons(input.retrievalResults);
const riskFactors = extractRiskFactors(input.retrievalResults);
const interpretation = extractBusinessInterpretation(input.retrievalResults);
const limitations = uniqueStrings([...extractLimitations(input.retrievalResults), ...input.groundingCheck.reasons]);