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

1311 lines
76 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
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;
const SYNTHETIC_PLACEHOLDER_PATTERN = /\bunknown_entity(?::[^\s,;]+)?\b/gi;
const SYNTHETIC_FALLBACK_MARKER_PATTERN = /\b(?:unknown_source|unknown_record)\b/gi;
const SYNTHETIC_ROUTE_TOKEN_PATTERN = /\bbatch_refresh_then_store:[^\s,;]+/gi;
const CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN = /(?:[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]){2,}/u;
const LATIN_MOJIBAKE_FRAGMENT_PATTERN = /(?:[\u00D0\u00D1][\u0080-\u00FF]){2,}/u;
const SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN = /^[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]{1,2}$/u;
const PREFIXED_SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN = /^[\p{L}\p{N}_-]+[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]{1,2}$/u;
const MOJIBAKE_SINGLE_MARKER_PATTERN = /^[\u0420\u0421\u00D0\u00D1]$/u;
const MOJIBAKE_MARKER_CHAR_PATTERN = /[\u0402\u0403\u040A\u040C\u040E\u040F\u0452\u0453\u0459\u045A\u045C\u045E\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/u;
const CYRILLIC_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN = /(?:[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]){2,}/gu;
const LATIN_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN = /(?:[\u00D0\u00D1][\u0080-\u00FF]){2,}/g;
const MOJIBAKE_MARKER_CHAR_GLOBAL_PATTERN = /[\u0402\u0403\u040A\u040C\u040E\u040F\u0452\u0453\u0459\u045A\u045C\u045E\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/gu;
function normalizeToken(value) {
return value.replace(/^[^\p{L}\p{N}_-]+|[^\p{L}\p{N}_-]+$/gu, "");
}
function isLikelyMojibakeToken(value) {
const token = normalizeToken(String(value ?? ""));
if (!token) {
return false;
}
if (MOJIBAKE_SINGLE_MARKER_PATTERN.test(token)) {
return true;
}
if (SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN.test(token)) {
return true;
}
if (token.length <= 8 && PREFIXED_SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN.test(token)) {
return true;
}
return CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN.test(token) || LATIN_MOJIBAKE_FRAGMENT_PATTERN.test(token);
}
function countMojibakeTokens(value) {
return String(value ?? "")
.split(/[\s,.;:!?()[\]{}"']+/g)
.filter((token) => token.length > 0)
.filter((token) => isLikelyMojibakeToken(token)).length;
}
function countMojibakeSingleMarkers(value) {
return String(value ?? "")
.split(/[\s,.;:!?()[\]{}"']+/g)
.filter((token) => token.length > 0)
.map((token) => normalizeToken(token))
.filter((token) => MOJIBAKE_SINGLE_MARKER_PATTERN.test(token)).length;
}
function stripMojibakeFragments(value) {
const removedByToken = String(value ?? "")
.split(/(\s+)/g)
.map((part) => {
if (/^\s+$/u.test(part)) {
return part;
}
return isLikelyMojibakeToken(part) ? "" : part;
})
.join("");
return removedByToken
.replace(CYRILLIC_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN, "")
.replace(LATIN_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN, "")
.replace(MOJIBAKE_MARKER_CHAR_GLOBAL_PATTERN, "")
.replace(/\s+([,.;:!?])/g, "$1")
.replace(/\s{2,}/g, " ")
.trim();
}
function looksLikeMojibake(value) {
const text = String(value ?? "");
if (!text.trim()) {
return false;
}
const tokenHits = countMojibakeTokens(text);
const singleMarkers = countMojibakeSingleMarkers(text);
if (tokenHits >= 2 || (tokenHits >= 1 && singleMarkers >= 1) || singleMarkers >= 3) {
return true;
}
if (MOJIBAKE_MARKER_CHAR_PATTERN.test(text)) {
return true;
}
if (CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN.test(text) || LATIN_MOJIBAKE_FRAGMENT_PATTERN.test(text)) {
return true;
}
if (/\uFFFD/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 stripSyntheticPlaceholders(value) {
return String(value ?? "")
.replace(SYNTHETIC_PLACEHOLDER_PATTERN, "")
.replace(SYNTHETIC_FALLBACK_MARKER_PATTERN, "")
.replace(SYNTHETIC_ROUTE_TOKEN_PATTERN, "")
.replace(/[;,:]\s*[;,:]+/g, "; ")
.replace(/\s{2,}/g, " ")
.trim();
}
function sanitizeUserFacingReply(value) {
const normalized = scrubRawTechnicalRefs(value).replace(/[ \t]+\n/g, "\n");
const cleanedLines = normalized
.split(/\r?\n/g)
.map((line) => stripSyntheticPlaceholders(line))
.map((line) => stripMojibakeFragments(line))
.map((line) => line.trim())
.filter((line) => line.length > 0)
.filter((line) => !looksLikeMojibake(line));
const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
return cleaned || "Available data requires clarification for a reliable user-facing answer.";
}
function sanitizeUserText(value) {
const normalized = stripMojibakeFragments(stripSyntheticPlaceholders(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 ?? "").trim();
const operations = String(item.operations_count ?? "0");
const docs = String(item.document_refs_count ?? "0");
const counterpartyLabel = counterparty.length > 0 && !looksLikeTechnicalIdentifier(counterparty) ? `Counterparty ${counterparty}` : "Counterparty";
return `${counterpartyLabel}: operations ${operations}, linked docs ${docs}.`;
});
lines.push(...top);
continue;
}
if (result.result_type === "ranking") {
const top = result.items
.slice(0, 5)
.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 formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? "", item.risk_score);
}
return formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? "");
});
lines.push(...top);
continue;
}
const top = result.items
.slice(0, 3)
.map((item) => formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? ""));
lines.push(...top);
}
return sanitizeUserLines(lines, 8);
}
function extractWhyIncluded(results) {
return sanitizeUserLines(results.flatMap((item) => item.why_included));
}
function extractSelectionReasons(results) {
return sanitizeUserLines(results.flatMap((item) => item.selection_reason));
}
function extractRiskFactors(results) {
return sanitizeUserLines(results.flatMap((item) => item.risk_factors));
}
function extractBusinessInterpretation(results) {
return sanitizeUserLines(results.flatMap((item) => item.business_interpretation));
}
function extractLimitations(results) {
return sanitizeUserLines(results.flatMap((item) => item.limitations), 10);
}
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 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) {
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;
}
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 accountScope = sanitizeUserLines(unit.affected_accounts, 2);
const counterpartyScope = sanitizeUserLines(unit.affected_counterparties, 2);
const documentScope = sanitizeUserLines(unit.affected_documents, 2);
const entityScope = sanitizeUserLines(unit.affected_entities, 2);
const scopeParts = [];
if (accountScope.length > 0) {
scopeParts.push(`accounts: ${accountScope.join(", ")}`);
}
if (counterpartyScope.length > 0) {
scopeParts.push(`counterparties: ${counterpartyScope.join(", ")}`);
}
if (documentScope.length > 0) {
scopeParts.push(`documents: ${documentScope.join(", ")}`);
}
if (scopeParts.length === 0 && entityScope.length > 0) {
scopeParts.push(`entities: ${entityScope.join(", ")}`);
}
if (scopeParts.length === 0) {
return "affected scope requires clarification";
}
return scopeParts.join("; ");
}
function formatLifecycleScope(unit) {
if (!unit.lifecycle_domain) {
return null;
}
const parts = [`domain=${unit.lifecycle_domain}`];
if (unit.current_lifecycle_state) {
parts.push(`current=${unit.current_lifecycle_state}`);
}
if (unit.expected_lifecycle_state) {
parts.push(`expected=${unit.expected_lifecycle_state}`);
}
if (unit.lifecycle_defect_type) {
parts.push(`defect=${unit.lifecycle_defect_type}`);
}
if (unit.missing_transition) {
parts.push(`missing_transition=${unit.missing_transition}`);
}
if (unit.invalid_transition) {
parts.push(`invalid_transition=${unit.invalid_transition}`);
}
if (unit.stale_duration) {
parts.push(`stale_duration=${unit.stale_duration}`);
}
return parts.join(", ");
}
function rankProblemUnitsForAnswer(units, lifecycleAnswerEnabled) {
if (!lifecycleAnswerEnabled) {
return units.slice().sort((left, right) => {
const severityDiff = right.severity.score - left.severity.score;
if (severityDiff !== 0)
return severityDiff;
return right.confidence.score - left.confidence.score;
});
}
return units.slice().sort((left, right) => {
const lifecycleRankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0);
if (lifecycleRankDiff !== 0)
return lifecycleRankDiff;
const lifecycleConfidenceDiff = (right.lifecycle_confidence?.score ?? 0) - (left.lifecycle_confidence?.score ?? 0);
if (lifecycleConfidenceDiff !== 0)
return lifecycleConfidenceDiff;
const severityDiff = right.severity.score - left.severity.score;
if (severityDiff !== 0)
return severityDiff;
return right.confidence.score - left.confidence.score;
});
}
function hasLifecycleResolution(units) {
return units.some((unit) => Boolean(unit.lifecycle_domain) &&
Boolean(unit.current_lifecycle_state) &&
Boolean(unit.expected_lifecycle_state) &&
Boolean(unit.lifecycle_defect_type));
}
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 состоянии.");
}
for (const unit of input.units) {
if (unit.lifecycle_defect_type === "stale_active_state") {
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
}
if (unit.lifecycle_defect_type === "misclosed_state") {
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
}
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
}
}
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)) {
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 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
}
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(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
}
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 buildProblemCentricAnswerSummary(input) {
if (input.lifecycleEnriched && input.summary?.lifecycle_enriched_units && input.summary.lifecycle_enriched_units > 0) {
if (input.mode === "clarification_required") {
return "Выявлены lifecycle-дефекты, но для надежного вывода требуется уточнение предметных якорей.";
}
return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`;
}
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
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
: input.lifecycleAnswerEnabled && hasLifecycleResolution(input.units)
? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода."
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
const unitLines = input.units.map((unit) => {
const scope = formatAffectedScope(unit);
const lifecycleScope = input.lifecycleAnswerEnabled ? formatLifecycleScope(unit) : null;
const lifecycleInterpretation = input.lifecycleAnswerEnabled && unit.business_lifecycle_interpretation
? sanitizeUserText(unit.business_lifecycle_interpretation)
: null;
const title = sanitizeUserText(unit.title) ?? "Problem cluster detected";
const defect = sanitizeUserText(unit.business_defect_class) ?? "detected_issue";
const segments = [
`${title}: ${defect}`,
scope,
lifecycleScope,
lifecycleInterpretation,
`severity=${unit.severity.grade}`,
`confidence=${unit.confidence.grade}`,
unit.lifecycle_confidence ? `lifecycle_confidence=${unit.lifecycle_confidence.grade}` : null
].filter((item) => Boolean(item));
return `- ${segments.join("; ")}.`;
});
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 lifecycleEnriched = input.lifecycleAnswerEnabled && hasLifecycleResolution(input.selectedUnits);
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,
lifecycleEnriched
}),
direct_answer: buildProblemCentricDirectAnswer({
mode: input.mode,
units: input.selectedUnits,
weakUnits,
lifecycleAnswerEnabled: input.lifecycleAnswerEnabled
}),
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) {
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 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}`,
`supporting_evidence_count=${structure.evidence_block.evidence_ids.length}`,
`supporting_source_count=${sourceRefCount}`,
`claim_support_links=${claimLinkCount}`
];
if (sourceRefCount > 0) {
evidenceLines.push("Detailed source references are available in debug payload.");
}
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 sanitizeUserFacingReply([
`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 lifecycleAnswerEnabled = Boolean(input.enableLifecycleAnswerV1);
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 = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled).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 ||
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 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,
lifecycleAnswerEnabled
});
const lifecycleModeActive = lifecycleAnswerEnabled && hasLifecycleResolution(selectedProblemUnits);
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: lifecycleModeActive ? "stage3_lifecycle_aware_v1" : "stage2_problem_centric_v1",
problem_unit_ids_used: selectedProblemUnits.map((item) => item.problem_unit_id)
};
}
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,
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 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]);
const nextSteps = suggestNextStep(input.requirements, input.coverageReport);
const lead = scopeLabel === "full"
? "ИСРѕРі: запрос обработан РїРѕ предмету, найденные объекты подтверждены данными контура."
: "ИСРѕРі: запрос обработан частично, РЅРёР¶Рµ подтвержденная часть Рё ограничения.";
return sanitizeUserFacingReply([
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 legacyEvidenceItems = flattenEvidence(input.retrievalResults);
const legacyLimitationReasonCodes = collectLimitationReasonCodes(legacyEvidenceItems);
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 ||
legacyLimitationReasonCodes.includes("weak_source_mapping") ||
legacyLimitationReasonCodes.includes("missing_mechanism");
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"
};
}