NODEDC_1C/llm_normalizer/backend/dist/services/answerComposer.js

705 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.composeAssistantAnswer = composeAssistantAnswer;
function fallbackFromSummary(routeSummary) {
if (!routeSummary || routeSummary.mode !== "deterministic_v2") {
return "none";
}
return routeSummary.fallback.type;
}
function uniqueStrings(values, limit = 6) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
}
function formatList(items) {
if (items.length === 0) {
return "";
}
return items.map((item) => `- ${item}`).join("\n");
}
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 operations = String(item.operations_count ?? "0");
const docs = String(item.document_refs_count ?? "0");
return `Контрагент ${counterparty}: операций ${operations}, документов в связке ${docs}.`;
});
lines.push(...top);
continue;
}
if (result.result_type === "ranking") {
const top = result.items
.slice(0, 5)
.map((item) => `${item.rank ?? "•"}. ${String(item.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 `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}).`;
});
lines.push(...top);
continue;
}
const top = result.items
.slice(0, 3)
.map((item) => `${String(item.source_entity ?? "Запись")} (${String(item.source_id ?? "")}).`);
lines.push(...top);
}
return lines;
}
function extractWhyIncluded(results) {
return uniqueStrings(results.flatMap((item) => item.why_included));
}
function extractSelectionReasons(results) {
return uniqueStrings(results.flatMap((item) => item.selection_reason));
}
function extractRiskFactors(results) {
return uniqueStrings(results.flatMap((item) => item.risk_factors));
}
function extractBusinessInterpretation(results) {
return uniqueStrings(results.flatMap((item) => item.business_interpretation));
}
function extractLimitations(results) {
return uniqueStrings(results.flatMap((item) => item.limitations));
}
function summaryValue(result, key) {
const summary = result.summary ?? {};
return Object.prototype.hasOwnProperty.call(summary, key) ? summary[key] : undefined;
}
function summaryBoolean(result, key) {
return summaryValue(result, key) === true;
}
function summaryString(result, key) {
const value = summaryValue(result, key);
return typeof value === "string" ? value : null;
}
function suggestNextStep(requirements, coverage) {
const next = [];
if (coverage.clarification_needed_for.length > 0) {
next.push("Уточните период, счет, документ или контрагента для требований: " + coverage.clarification_needed_for.join(", ") + ".");
}
if (coverage.requirements_uncovered.length > 0) {
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
}
if (coverage.out_of_scope_requirements.length > 0) {
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
}
if (next.length === 0 && requirements.length > 0) {
next.push("Следующим шагом можно открыть технический разбор и углубить проверку по выбранным объектам.");
}
return next;
}
function flattenEvidence(results) {
return results.flatMap((item) => item.evidence);
}
function buildClaimEvidenceLinks(results) {
const byClaim = new Map();
for (const evidence of flattenEvidence(results)) {
const claimRef = String(evidence.claim_ref ?? "").trim();
const evidenceId = String(evidence.evidence_id ?? "").trim();
if (!claimRef || !evidenceId) {
continue;
}
const current = byClaim.get(claimRef) ?? [];
current.push(evidenceId);
byClaim.set(claimRef, current);
}
return Array.from(byClaim.entries())
.slice(0, 10)
.map(([claim_ref, evidenceIds]) => ({
claim_ref,
evidence_ids: uniqueStrings(evidenceIds, 10)
}));
}
function aggregatePolicySignals(results) {
const broad_query_detected = results.some((item) => summaryBoolean(item, "broad_query_detected"));
const broad_result_flag = results.some((item) => summaryBoolean(item, "broad_result_flag"));
const minimum_evidence_failed = results.some((item) => summaryBoolean(item, "minimum_evidence_failed"));
let degraded_to = null;
for (const result of results) {
const degraded = summaryString(result, "degraded_to");
if (degraded === "clarification") {
degraded_to = "clarification";
break;
}
if (degraded === "partial") {
degraded_to = "partial";
}
}
const narrowingOrder = {
weak: 0,
medium: 1,
strong: 2
};
let narrowing_strength = null;
for (const result of results) {
const value = summaryString(result, "narrowing_strength");
if (value !== "weak" && value !== "medium" && value !== "strong") {
continue;
}
if (!narrowing_strength || narrowingOrder[value] < narrowingOrder[narrowing_strength]) {
narrowing_strength = value;
}
}
return {
broad_query_detected,
broad_result_flag,
minimum_evidence_failed,
degraded_to,
narrowing_strength
};
}
function confidenceToScore(value) {
if (value === "high")
return 3;
if (value === "medium")
return 2;
return 1;
}
function aggregateConfidence(results, evidenceItems) {
const scores = [];
for (const evidence of evidenceItems) {
scores.push(confidenceToScore(evidence.confidence));
}
for (const result of results) {
if (result.status === "error") {
continue;
}
scores.push(confidenceToScore(result.confidence));
}
if (scores.length === 0) {
return "low";
}
const average = scores.reduce((acc, item) => acc + item, 0) / scores.length;
if (average >= 2.6)
return "high";
if (average >= 1.8)
return "medium";
return "low";
}
function collectLimitationReasonCodes(evidenceItems) {
const codes = evidenceItems
.map((item) => item.limitation?.reason_code ?? null)
.filter((item) => Boolean(item));
return uniqueStrings(codes, 8);
}
function limitationReasonToText(code) {
if (code === "snapshot_only")
return "Evidence is snapshot-only and may lag source-of-record.";
if (code === "heuristic_inference")
return "Part of the conclusion relies on heuristic inference.";
if (code === "missing_mechanism")
return "Mechanism is unresolved for part of the evidence.";
if (code === "weak_source_mapping")
return "Source mapping is weak for part of the evidence.";
if (code === "insufficient_detail")
return "Evidence lacks detail for a strong factual claim.";
return "Some evidence limitations remain unresolved.";
}
function detectMissingAnchors(userMessage) {
const lower = String(userMessage ?? "").toLowerCase();
const hasPeriod = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/.test(lower);
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b\d{2}(?:\.\d{2})?\b)/i.test(lower);
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
return {
period: !hasPeriod,
account: !hasAccount,
documentOrObject: !hasDocumentOrObject,
counterparty: !hasCounterparty,
anomalyType: !hasAnomalyType
};
}
function buildClarificationQuestions(input) {
const questions = [];
const shouldAsk = input.mode === "clarification_required" || input.coverageReport.clarification_needed_for.length > 0;
if (!shouldAsk) {
return questions;
}
if (input.missingAnchors.period) {
questions.push("Уточните период проверки (например, 2020-06).");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
}
if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
}
if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или группу контрагентов.");
}
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(questions, 6);
}
function buildRecommendedActions(input) {
const actions = [];
if (input.mode === "focused_grounded") {
actions.push("Проверьте 1-2 ключевые записи по source_ref и зафиксируйте итог в рабочем файле проверки.");
}
if (input.mode === "broad_partial") {
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
}
if (input.mode === "clarification_required") {
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
}
if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
}
if (input.coverageReport.requirements_partially_covered.length > 0) {
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
}
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
}
if (input.limitationReasonCodes.includes("snapshot_only")) {
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
}
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
}
if (input.sourceRefs.length > 0) {
actions.push(`Начните проверку с source_ref: ${input.sourceRefs.slice(0, 2).join(", ")}.`);
}
return uniqueStrings(actions, 6);
}
function firstMeaningfulFact(results) {
const facts = extractTopFacts(results);
return facts.length > 0 ? facts[0] : null;
}
function buildPolicyDecision(input) {
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
if (input.fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
return {
mode: "out_of_scope",
fallback_type: "out_of_scope",
reply_type: "out_of_scope"
};
}
if (input.groundingCheck.status === "route_mismatch_blocked") {
return {
mode: "route_mismatch",
fallback_type: "partial",
reply_type: "route_mismatch_blocked"
};
}
if ((input.policySignals.degraded_to === "clarification" && input.policySignals.minimum_evidence_failed) ||
(input.fallbackType === "clarification" && !input.hasSupport) ||
(input.groundingCheck.status === "no_grounded_answer" && !input.hasSupport)) {
return {
mode: "clarification_required",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (input.errorResults.length > 0 && input.okResults.length === 0 && input.partialResults.length === 0) {
return {
mode: "backend_error",
fallback_type: input.fallbackType,
reply_type: "backend_error"
};
}
if (input.okResults.length === 0 && input.partialResults.length === 0 && input.emptyResults.length > 0) {
return {
mode: "empty",
fallback_type: input.fallbackType,
reply_type: "empty_but_valid"
};
}
if (input.groundingCheck.status === "no_grounded_answer" && input.okResults.length === 0 && input.partialResults.length === 0) {
return {
mode: "no_grounded",
fallback_type: input.fallbackType,
reply_type: "no_grounded_answer"
};
}
if (input.focusedStrong &&
!input.policySignals.broad_query_detected &&
!input.policySignals.minimum_evidence_failed &&
!hasCoverageGaps) {
return {
mode: "focused_grounded",
fallback_type: "none",
reply_type: "factual_with_explanation"
};
}
if (input.okResults.length > 0 ||
input.partialResults.length > 0 ||
hasCoverageGaps ||
input.policySignals.minimum_evidence_failed ||
input.policySignals.broad_result_flag ||
input.groundingCheck.status === "partial") {
return {
mode: "broad_partial",
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
return {
mode: "backend_error",
fallback_type: "unknown",
reply_type: "backend_error"
};
}
function buildAnswerSummary(mode) {
if (mode === "focused_grounded")
return "Сформирован прямой ответ на основе подтвержденной опоры.";
if (mode === "broad_partial")
return "Вывод ограничен: есть частичная опора, но не полный coverage.";
if (mode === "clarification_required")
return "Нужны уточнения: без сужения strong factual вывод ненадежен.";
if (mode === "out_of_scope")
return "Запрос вне доступного учетного контура.";
if (mode === "route_mismatch")
return "Результат маршрута не совпал с предметом вопроса.";
if (mode === "empty")
return "В текущем срезе данных релевантные записи не обнаружены.";
if (mode === "no_grounded")
return "Недостаточно опоры для обоснованного ответа.";
return "Не удалось собрать обоснованный ответ по текущему запросу.";
}
function buildDirectAnswer(input) {
const topFact = firstMeaningfulFact(input.retrievalResults);
if (input.mode === "focused_grounded") {
return topFact ?? "Подтвержденный результат получен; можно продолжать предметную проверку без деградации.";
}
if (input.mode === "broad_partial") {
if (topFact) {
return `Доступен ограниченный подтвержденный фрагмент: ${topFact}`;
}
return "Есть только ограниченная опора; вывод дан в частичном режиме без ложной точности.";
}
if (input.mode === "clarification_required") {
return "Текущий запрос слишком широкий или недоопределен; надежный factual вывод пока невозможен.";
}
if (input.mode === "out_of_scope") {
return "Могу отвечать только в пределах данных доступного учетного контура.";
}
if (input.mode === "route_mismatch") {
return "Предмет результата не совпал с предметом вопроса; требуется уточнение фокуса.";
}
if (input.mode === "empty") {
return "В текущем срезе данных проблемные записи по заданному условию не найдены.";
}
if (input.mode === "no_grounded") {
return "Недостаточно подтвержденной опоры для ответа в требуемой точности.";
}
if (input.policySignals.minimum_evidence_failed) {
return "Маршрут отработал, но минимальная evidence-опора не пройдена.";
}
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
}
function renderPolicyReply(structure) {
const mechanismLines = [`status=${structure.mechanism_block.status}`];
if (structure.mechanism_block.mechanism_notes.length > 0) {
mechanismLines.push(...structure.mechanism_block.mechanism_notes.map((item) => `note: ${item}`));
}
if (structure.mechanism_block.limitation_reason_codes.length > 0) {
mechanismLines.push(`limitation_codes: ${structure.mechanism_block.limitation_reason_codes.join(", ")}`);
}
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 evidenceLines = [
`coverage=${structure.evidence_block.coverage_note}`,
`evidence_ids=${structure.evidence_block.evidence_ids.length > 0 ? structure.evidence_block.evidence_ids.join(", ") : "none"}`
];
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("; ")}`);
}
const uncertaintyLines = [
...structure.uncertainty_block.open_uncertainties.map((item) => `open: ${item}`),
...structure.uncertainty_block.limitations.map((item) => `limit: ${item}`)
];
if (uncertaintyLines.length === 0) {
uncertaintyLines.push("No material uncertainty detected in current scoped answer.");
}
const nextStepLines = [
...structure.next_step_block.recommended_actions.map((item) => `action: ${item}`),
...structure.next_step_block.clarification_questions.map((item) => `clarify: ${item}`)
];
if (nextStepLines.length === 0) {
nextStepLines.push("No additional action is required for this scoped answer.");
}
return [
`Answer summary: ${structure.answer_summary}`,
`Direct answer:\n${structure.direct_answer}`,
`Mechanism block:\n${formatList(mechanismLines)}`,
`Evidence block:\n${formatList(evidenceLines)}`,
`Uncertainty block:\n${formatList(uncertaintyLines)}`,
`Next step block:\n${formatList(nextStepLines)}`
]
.filter(Boolean)
.join("\n\n");
}
function composeAssistantAnswerV11(input) {
const fallbackType = fallbackFromSummary(input.routeSummary);
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
const evidenceItems = flattenEvidence(input.retrievalResults);
const policySignals = aggregatePolicySignals(input.retrievalResults);
const limitationReasonCodes = collectLimitationReasonCodes(evidenceItems);
const sourceRefs = uniqueStrings(evidenceItems
.map((item) => item.source_ref?.canonical_ref)
.filter((item) => typeof item === "string" && item.trim().length > 0), 8);
const mechanismNotes = uniqueStrings(evidenceItems
.map((item) => item.mechanism_note)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
const hasSupport = okResults.length > 0 ||
partialResults.length > 0 ||
evidenceItems.length > 0 ||
input.retrievalResults.some((item) => item.items.length > 0);
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
const hasCriticalEvidenceLimitation = limitationReasonCodes.includes("weak_source_mapping") ||
limitationReasonCodes.includes("insufficient_detail");
const hasNonLowRouteConfidence = input.retrievalResults.some((item) => item.status === "ok" && item.confidence !== "low");
const focusedStrong = okResults.length > 0 &&
input.groundingCheck.status === "grounded" &&
!hasCoverageGaps &&
!policySignals.broad_query_detected &&
!policySignals.broad_result_flag &&
!policySignals.minimum_evidence_failed &&
!hasCriticalEvidenceLimitation &&
(aggregateEvidenceConfidence !== "low" || hasNonLowRouteConfidence);
const decision = buildPolicyDecision({
fallbackType,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
okResults,
partialResults,
emptyResults,
errorResults,
hasSupport,
focusedStrong,
policySignals
});
const missingAnchors = detectMissingAnchors(input.userMessage);
const clarificationQuestions = buildClarificationQuestions({
mode: decision.mode,
missingAnchors,
coverageReport: input.coverageReport,
policySignals
});
const recommendedActions = buildRecommendedActions({
mode: decision.mode,
coverageReport: input.coverageReport,
policySignals,
limitationReasonCodes,
sourceRefs
});
const limitations = uniqueStrings([
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
...extractLimitations(input.retrievalResults),
...input.groundingCheck.reasons,
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
...(policySignals.broad_query_detected && policySignals.narrowing_strength === "weak"
? ["Broad query remains weakly narrowed; precision is intentionally limited."]
: [])
], 10);
const openUncertainties = uniqueStrings([
...input.groundingCheck.missing_requirements,
...(decision.mode === "clarification_required" && missingAnchors.period ? ["missing_anchor:period"] : []),
...(decision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
...(decision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
...(decision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : [])
], 8);
const mechanismStatus = mechanismNotes.length === 0
? "unresolved"
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
? "limited"
: "grounded";
const answerStructure = {
schema_version: "answer_structure_v1_1",
answer_summary: buildAnswerSummary(decision.mode),
direct_answer: buildDirectAnswer({
mode: decision.mode,
retrievalResults: input.retrievalResults,
policySignals
}),
mechanism_block: {
status: mechanismStatus,
mechanism_notes: mechanismNotes,
limitation_reason_codes: limitationReasonCodes
},
evidence_block: {
evidence_ids: uniqueStrings(evidenceItems.map((item) => item.evidence_id), 10),
source_refs: sourceRefs,
mechanism_notes: mechanismNotes,
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",
...(claimEvidenceLinks.length > 0
? {
claim_evidence_links: claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: openUncertainties,
limitations
},
next_step_block: {
recommended_actions: recommendedActions,
clarification_questions: clarificationQuestions
}
};
return {
assistant_reply: renderPolicyReply(answerStructure),
fallback_type: decision.fallback_type,
reply_type: decision.reply_type,
answer_structure_v11: answerStructure
};
}
function composeExplainableAnswer(input, scopeLabel) {
const facts = extractTopFacts(input.retrievalResults);
const whyIncluded = extractWhyIncluded(input.retrievalResults);
const selectionReasons = extractSelectionReasons(input.retrievalResults);
const riskFactors = extractRiskFactors(input.retrievalResults);
const interpretation = extractBusinessInterpretation(input.retrievalResults);
const limitations = uniqueStrings([...extractLimitations(input.retrievalResults), ...input.groundingCheck.reasons]);
const nextSteps = suggestNextStep(input.requirements, input.coverageReport);
const lead = scopeLabel === "full"
? "Итог: запрос обработан по предмету, найденные объекты подтверждены данными контура."
: "Итог: запрос обработан частично, ниже подтвержденная часть и ограничения.";
return [
lead,
facts.length > 0 ? "Подтвержденные результаты:\n" + formatList(facts) : "",
whyIncluded.length > 0 ? "Почему это попало в ответ:\n" + formatList(whyIncluded) : "",
selectionReasons.length > 0 ? "Основание отбора:\n" + formatList(selectionReasons) : "",
riskFactors.length > 0 ? "Подтверждающие признаки:\n" + formatList(riskFactors) : "",
interpretation.length > 0 ? "Практический смысл:\n" + formatList(interpretation) : "",
limitations.length > 0 ? "Ограничения:\n" + formatList(limitations) : "",
nextSteps.length > 0 ? "Что проверить дальше:\n" + formatList(nextSteps) : ""
]
.filter(Boolean)
.join("\n\n");
}
function composeAssistantAnswer(input) {
if (input.enableAnswerPolicyV11) {
return composeAssistantAnswerV11(input);
}
const fallbackType = fallbackFromSummary(input.routeSummary);
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
const hasBroadMinimumEvidenceSignal = input.retrievalResults.some((item) => summaryBoolean(item, "broad_guard_applied") && summaryBoolean(item, "minimum_evidence_failed"));
const hasBroadClarificationSignal = input.retrievalResults.some((item) => summaryBoolean(item, "broad_guard_applied") &&
summaryBoolean(item, "minimum_evidence_failed") &&
summaryString(item, "degraded_to") === "clarification");
if (fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
return {
assistant_reply: "Я могу отвечать только по данным вашей учетной базы. Этот запрос выходит за рамки доступного контура.",
fallback_type: "out_of_scope",
reply_type: "out_of_scope"
};
}
if (input.groundingCheck.status === "route_mismatch_blocked") {
return {
assistant_reply: [
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
"Уточните формулировку (например, нужный счет/участок учета), и я выполню повторный проход."
].join("\n\n"),
fallback_type: "partial",
reply_type: "route_mismatch_blocked"
};
}
if (input.groundingCheck.status === "no_grounded_answer" && okResults.length === 0 && !hasBroadMinimumEvidenceSignal) {
return {
assistant_reply: "Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
fallback_type: fallbackType,
reply_type: "no_grounded_answer"
};
}
if (hasBroadClarificationSignal && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (fallbackType === "clarification" && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы закрыть все части вопроса корректно.",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (errorResults.length > 0 && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
fallback_type: fallbackType,
reply_type: "backend_error"
};
}
if (partialResults.length > 0 && okResults.length === 0) {
return {
assistant_reply: composeExplainableAnswer(input, "partial"),
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
if (okResults.length === 0 && partialResults.length === 0 && emptyResults.length > 0) {
return {
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
fallback_type: fallbackType,
reply_type: "empty_but_valid"
};
}
const hasPartialCoverage = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0 ||
input.groundingCheck.status === "partial" ||
errorResults.length > 0;
if (okResults.length > 0 && hasPartialCoverage) {
return {
assistant_reply: composeExplainableAnswer(input, "partial"),
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
if (okResults.length > 0) {
return {
assistant_reply: composeExplainableAnswer(input, "full"),
fallback_type: "none",
reply_type: "factual_with_explanation"
};
}
return {
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
fallback_type: "unknown",
reply_type: "backend_error"
};
}