Stage 3: улучшена логика жизненного цикла и очищены ответы ассистента
This commit is contained in:
+217
-120
@@ -14,15 +14,85 @@ const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]
|
||||
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;
|
||||
}
|
||||
if (/(?:Р.|С.){5,}/u.test(text)) {
|
||||
const tokenHits = countMojibakeTokens(text);
|
||||
const singleMarkers = countMojibakeSingleMarkers(text);
|
||||
if (tokenHits >= 2 || (tokenHits >= 1 && singleMarkers >= 1) || singleMarkers >= 3) {
|
||||
return true;
|
||||
}
|
||||
if (/[ЃѓЂђЌќЎў]/u.test(text)) {
|
||||
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;
|
||||
@@ -59,14 +129,29 @@ function scrubRawTechnicalRefs(value) {
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function sanitizeUserFacingReply(value) {
|
||||
return scrubRawTechnicalRefs(value)
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
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 = scrubRawTechnicalRefs(String(value ?? "").replace(/\s+/g, " ").trim());
|
||||
const normalized = stripMojibakeFragments(stripSyntheticPlaceholders(scrubRawTechnicalRefs(String(value ?? "").replace(/\s+/g, " ").trim())));
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
@@ -180,13 +265,13 @@ function buildFallbackWhyIncluded(results) {
|
||||
const filteredRecords = summaryNumber(result, "filtered_records_after_narrowing");
|
||||
const checkedRecords = summaryNumber(result, "checked_records");
|
||||
if (routeFocus) {
|
||||
lines.push(`Проверка выполнена по профилю ${routeFocus}.`);
|
||||
lines.push(`Проверка выполнена по профилю ${routeFocus}.`);
|
||||
}
|
||||
if (sourceRecords !== null && filteredRecords !== null && filteredRecords < sourceRecords) {
|
||||
lines.push(`Применено сужение выборки: ${filteredRecords} из ${sourceRecords} записей.`);
|
||||
lines.push(`Применено сужение выборки: ${filteredRecords} из ${sourceRecords} записей.`);
|
||||
}
|
||||
if (checkedRecords !== null) {
|
||||
lines.push(`Проверено записей в текущем проходе: ${checkedRecords}.`);
|
||||
lines.push(`Проверено записей в текущем проходе: ${checkedRecords}.`);
|
||||
}
|
||||
}
|
||||
return sanitizeUserLines(lines, 4);
|
||||
@@ -195,34 +280,34 @@ function buildFallbackSelectionReasons(results) {
|
||||
const lines = [];
|
||||
for (const result of results.slice(0, 2)) {
|
||||
if (summaryBoolean(result, "semantic_narrowing_applied")) {
|
||||
lines.push("Отбор выполнен по семантическому сужению предметной области.");
|
||||
lines.push("Отбор выполнен по семантическому сужению предметной области.");
|
||||
}
|
||||
const rankingBasis = summaryStringArray(result, "ranking_basis");
|
||||
if (rankingBasis.length > 0) {
|
||||
lines.push(`Ранжирование основано на: ${rankingBasis.join(", ")}.`);
|
||||
lines.push(`Ранжирование основано на: ${rankingBasis.join(", ")}.`);
|
||||
}
|
||||
if (summaryBoolean(result, "broad_guard_applied")) {
|
||||
lines.push("Применен broad-query guard для контроля ложной точности.");
|
||||
lines.push("Применен broad-query guard для контроля ложной точности.");
|
||||
}
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
lines.push("Отбор выполнен по совпадению предметных сигналов и доступной evidence-опоры.");
|
||||
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(", ") + ".");
|
||||
next.push("Уточните период, счет, документ или контрагента для требований: " + coverage.clarification_needed_for.join(", ") + ".");
|
||||
}
|
||||
if (coverage.requirements_uncovered.length > 0) {
|
||||
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
|
||||
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
|
||||
}
|
||||
if (coverage.out_of_scope_requirements.length > 0) {
|
||||
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
|
||||
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
|
||||
}
|
||||
if (next.length === 0 && requirements.length > 0) {
|
||||
next.push("Следующим шагом можно открыть технический разбор и углубить проверку по выбранным объектам.");
|
||||
next.push("Следующим шагом можно открыть технический разбор и углубить проверку по выбранным объектам.");
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -264,21 +349,25 @@ function selectProblemUnitSummary(results) {
|
||||
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 (unit.affected_accounts.length > 0) {
|
||||
scopeParts.push(`счета: ${unit.affected_accounts.slice(0, 2).join(", ")}`);
|
||||
if (accountScope.length > 0) {
|
||||
scopeParts.push(`accounts: ${accountScope.join(", ")}`);
|
||||
}
|
||||
if (unit.affected_counterparties.length > 0) {
|
||||
scopeParts.push(`контрагенты: ${unit.affected_counterparties.slice(0, 2).join(", ")}`);
|
||||
if (counterpartyScope.length > 0) {
|
||||
scopeParts.push(`counterparties: ${counterpartyScope.join(", ")}`);
|
||||
}
|
||||
if (unit.affected_documents.length > 0) {
|
||||
scopeParts.push(`документы: ${unit.affected_documents.slice(0, 2).join(", ")}`);
|
||||
if (documentScope.length > 0) {
|
||||
scopeParts.push(`documents: ${documentScope.join(", ")}`);
|
||||
}
|
||||
if (scopeParts.length === 0 && unit.affected_entities.length > 0) {
|
||||
scopeParts.push(`объекты: ${unit.affected_entities.slice(0, 2).join(", ")}`);
|
||||
if (scopeParts.length === 0 && entityScope.length > 0) {
|
||||
scopeParts.push(`entities: ${entityScope.join(", ")}`);
|
||||
}
|
||||
if (scopeParts.length === 0) {
|
||||
return "затронутый контур требует уточнения";
|
||||
return "affected scope requires clarification";
|
||||
}
|
||||
return scopeParts.join("; ");
|
||||
}
|
||||
@@ -339,47 +428,47 @@ 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("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
}
|
||||
if (unitTypes.has("cross_branch_inconsistency_cluster")) {
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
}
|
||||
if (unitTypes.has("lifecycle_anomaly_node")) {
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
}
|
||||
for (const unit of input.units) {
|
||||
if (unit.lifecycle_defect_type === "stale_active_state") {
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "misclosed_state") {
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
}
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
if (input.missingAnchors.period) {
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
}
|
||||
}
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
return uniqueStrings(actions, 6);
|
||||
}
|
||||
@@ -390,28 +479,28 @@ function buildProblemCentricClarifications(input) {
|
||||
const questions = [];
|
||||
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
|
||||
if (input.missingAnchors.period) {
|
||||
questions.push("Уточните период (например, 2020-06), в котором нужно проверить проблемный кластер.");
|
||||
questions.push("Уточните период (например, 2020-06), в котором нужно проверить проблемный кластер.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
}
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
return uniqueStrings(questions, 6);
|
||||
}
|
||||
@@ -522,10 +611,10 @@ function limitationReasonToText(code) {
|
||||
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);
|
||||
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,
|
||||
@@ -541,53 +630,53 @@ function buildClarificationQuestions(input) {
|
||||
return questions;
|
||||
}
|
||||
if (input.missingAnchors.period) {
|
||||
questions.push("Уточните период проверки (например, 2020-06).");
|
||||
questions.push("Уточните период проверки (например, 2020-06).");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
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 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
}
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
if (input.coverageReport.requirements_partially_covered.length > 0) {
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("snapshot_only")) {
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
}
|
||||
if (input.sourceRefs.length > 0) {
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
}
|
||||
return uniqueStrings(actions, 6);
|
||||
}
|
||||
@@ -674,84 +763,88 @@ function buildPolicyDecision(input) {
|
||||
}
|
||||
function buildAnswerSummary(mode) {
|
||||
if (mode === "focused_grounded")
|
||||
return "Сформирован прямой ответ на основе подтвержденной опоры.";
|
||||
return "Сформирован прямой ответ на основе подтвержденной опоры.";
|
||||
if (mode === "broad_partial")
|
||||
return "Вывод ограничен: есть частичная опора, но не полный coverage.";
|
||||
return "Вывод ограничен: есть частичная опора, но не полный coverage.";
|
||||
if (mode === "clarification_required")
|
||||
return "Нужны уточнения: без сужения strong factual вывод ненадежен.";
|
||||
return "Нужны уточнения: без сужения strong factual вывод ненадежен.";
|
||||
if (mode === "out_of_scope")
|
||||
return "Запрос вне доступного учетного контура.";
|
||||
return "Запрос вне доступного учетного контура.";
|
||||
if (mode === "route_mismatch")
|
||||
return "Результат маршрута не совпал с предметом вопроса.";
|
||||
return "Результат маршрута не совпал с предметом вопроса.";
|
||||
if (mode === "empty")
|
||||
return "В текущем срезе данных релевантные записи не обнаружены.";
|
||||
return "В текущем срезе данных релевантные записи не обнаружены.";
|
||||
if (mode === "no_grounded")
|
||||
return "Недостаточно опоры для обоснованного ответа.";
|
||||
return "Не удалось собрать обоснованный ответ по текущему запросу.";
|
||||
return "Недостаточно опоры для обоснованного ответа.";
|
||||
return "Не удалось собрать обоснованный ответ по текущему запросу.";
|
||||
}
|
||||
function buildDirectAnswer(input) {
|
||||
const topFact = firstMeaningfulFact(input.retrievalResults);
|
||||
if (input.mode === "focused_grounded") {
|
||||
return topFact ?? "Подтвержденный результат получен; можно продолжать предметную проверку без деградации.";
|
||||
return topFact ?? "Подтвержденный результат получен; можно продолжать предметную проверку без деградации.";
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
if (topFact) {
|
||||
return `Доступен ограниченный подтвержденный фрагмент: ${topFact}`;
|
||||
return `Доступен ограниченный подтвержденный фрагмент: ${topFact}`;
|
||||
}
|
||||
return "Есть только ограниченная опора; вывод дан в частичном режиме без ложной точности.";
|
||||
return "Есть только ограниченная опора; вывод дан в частичном режиме без ложной точности.";
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Текущий запрос слишком широкий или недоопределен; надежный factual вывод пока невозможен.";
|
||||
return "Текущий запрос слишком широкий или недоопределен; надежный factual вывод пока невозможен.";
|
||||
}
|
||||
if (input.mode === "out_of_scope") {
|
||||
return "Могу отвечать только в пределах данных доступного учетного контура.";
|
||||
return "Могу отвечать только в пределах данных доступного учетного контура.";
|
||||
}
|
||||
if (input.mode === "route_mismatch") {
|
||||
return "Предмет результата не совпал с предметом вопроса; требуется уточнение фокуса.";
|
||||
return "Предмет результата не совпал с предметом вопроса; требуется уточнение фокуса.";
|
||||
}
|
||||
if (input.mode === "empty") {
|
||||
return "В текущем срезе данных проблемные записи по заданному условию не найдены.";
|
||||
return "В текущем срезе данных проблемные записи по заданному условию не найдены.";
|
||||
}
|
||||
if (input.mode === "no_grounded") {
|
||||
return "Недостаточно подтвержденной опоры для ответа в требуемой точности.";
|
||||
return "Недостаточно подтвержденной опоры для ответа в требуемой точности.";
|
||||
}
|
||||
if (input.policySignals.minimum_evidence_failed) {
|
||||
return "Маршрут отработал, но минимальная evidence-опора не пройдена.";
|
||||
return "Маршрут отработал, но минимальная evidence-опора не пройдена.";
|
||||
}
|
||||
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
|
||||
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-дефекты, но для надежного вывода требуется уточнение предметных якорей.";
|
||||
}
|
||||
return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`;
|
||||
return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`;
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса.";
|
||||
return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса.";
|
||||
}
|
||||
if (input.weakUnits) {
|
||||
return "Сформирован problem-centric срез с ограниченной опорой; вывод предварительный и требует до-проверки.";
|
||||
return "Сформирован problem-centric срез с ограниченной опорой; вывод предварительный и требует до-проверки.";
|
||||
}
|
||||
if (input.summary?.units_total && input.summary.units_total > 1) {
|
||||
return `Сформирован problem-centric срез: выделено ${input.summary.units_total} проблемных кластера с приоритетами.`;
|
||||
return `Сформирован problem-centric срез: выделено ${input.summary.units_total} проблемных кластера с приоритетами.`;
|
||||
}
|
||||
return "Сформирован problem-centric срез: выделен ключевой проблемный кластер и затронутый контур.";
|
||||
return "Сформирован problem-centric срез: выделен ключевой проблемный кластер и затронутый контур.";
|
||||
}
|
||||
function buildProblemCentricDirectAnswer(input) {
|
||||
const lead = input.mode === "clarification_required"
|
||||
? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен."
|
||||
? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен."
|
||||
: input.weakUnits
|
||||
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
|
||||
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
|
||||
: input.lifecycleAnswerEnabled && hasLifecycleResolution(input.units)
|
||||
? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода."
|
||||
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
|
||||
? "Выделены 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 : 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 = [
|
||||
`${unit.title}: ${unit.business_defect_class}`,
|
||||
`${title}: ${defect}`,
|
||||
scope,
|
||||
lifecycleScope,
|
||||
lifecycleInterpretation,
|
||||
@@ -762,9 +855,9 @@ function buildProblemCentricDirectAnswer(input) {
|
||||
return `- ${segments.join("; ")}.`;
|
||||
});
|
||||
if (unitLines.length === 0) {
|
||||
return `${lead}\nПроблемные кластеры не удалось детализировать в текущем срезе.`;
|
||||
return `${lead}\nПроблемные кластеры не удалось детализировать в текущем срезе.`;
|
||||
}
|
||||
return [lead, "Проблемные кластеры:", ...unitLines].join("\n");
|
||||
return [lead, "Проблемные кластеры:", ...unitLines].join("\n");
|
||||
}
|
||||
function buildProblemCentricAnswerStructure(input) {
|
||||
const weakUnits = input.selectedUnits.every((item) => item.confidence.grade === "low");
|
||||
@@ -1098,20 +1191,20 @@ function composeExplainableAnswer(input, scopeLabel) {
|
||||
const limitations = uniqueStrings([...extractLimitations(input.retrievalResults), ...input.groundingCheck.reasons]);
|
||||
const nextSteps = suggestNextStep(input.requirements, input.coverageReport);
|
||||
const lead = scopeLabel === "full"
|
||||
? "Итог: запрос обработан по предмету, найденные объекты подтверждены данными контура."
|
||||
: "Итог: запрос обработан частично, ниже подтвержденная часть и ограничения.";
|
||||
return [
|
||||
? "Ртог: запрос обработан РїРѕ предмету, найденные объекты подтверждены данными контура."
|
||||
: "Ртог: запрос обработан частично, РЅРёР¶Рµ подтвержденная часть Рё ограничения.";
|
||||
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) : ""
|
||||
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");
|
||||
.join("\n\n"));
|
||||
}
|
||||
function composeAssistantAnswer(input) {
|
||||
if (input.enableAnswerPolicyV11) {
|
||||
@@ -1122,13 +1215,15 @@ function composeAssistantAnswer(input) {
|
||||
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: "Я могу отвечать только по данным вашей учетной базы. Этот запрос выходит за рамки доступного контура.",
|
||||
assistant_reply: "РЇ РјРѕРіСѓ отвечать только РїРѕ данным вашей учетной базы. Ртот запрос выходит Р·Р° рамки доступного контура.",
|
||||
fallback_type: "out_of_scope",
|
||||
reply_type: "out_of_scope"
|
||||
};
|
||||
@@ -1136,8 +1231,8 @@ function composeAssistantAnswer(input) {
|
||||
if (input.groundingCheck.status === "route_mismatch_blocked") {
|
||||
return {
|
||||
assistant_reply: [
|
||||
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
|
||||
"Уточните формулировку (например, нужный счет/участок учета), и я выполню повторный проход."
|
||||
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
|
||||
"Уточните формулировку (например, нужный счет/участок учета), и я выполню повторный проход."
|
||||
].join("\n\n"),
|
||||
fallback_type: "partial",
|
||||
reply_type: "route_mismatch_blocked"
|
||||
@@ -1145,28 +1240,28 @@ function composeAssistantAnswer(input) {
|
||||
}
|
||||
if (input.groundingCheck.status === "no_grounded_answer" && okResults.length === 0 && !hasBroadMinimumEvidenceSignal) {
|
||||
return {
|
||||
assistant_reply: "Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
|
||||
assistant_reply: "Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
|
||||
fallback_type: fallbackType,
|
||||
reply_type: "no_grounded_answer"
|
||||
};
|
||||
}
|
||||
if (hasBroadClarificationSignal && okResults.length === 0 && partialResults.length === 0) {
|
||||
return {
|
||||
assistant_reply: "Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
|
||||
assistant_reply: "Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
|
||||
fallback_type: "clarification",
|
||||
reply_type: "clarification_required"
|
||||
};
|
||||
}
|
||||
if (fallbackType === "clarification" && okResults.length === 0 && partialResults.length === 0) {
|
||||
return {
|
||||
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы закрыть все части вопроса корректно.",
|
||||
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы закрыть все части вопроса корректно.",
|
||||
fallback_type: "clarification",
|
||||
reply_type: "clarification_required"
|
||||
};
|
||||
}
|
||||
if (errorResults.length > 0 && okResults.length === 0 && partialResults.length === 0) {
|
||||
return {
|
||||
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
|
||||
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
|
||||
fallback_type: fallbackType,
|
||||
reply_type: "backend_error"
|
||||
};
|
||||
@@ -1180,7 +1275,7 @@ function composeAssistantAnswer(input) {
|
||||
}
|
||||
if (okResults.length === 0 && partialResults.length === 0 && emptyResults.length > 0) {
|
||||
return {
|
||||
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
|
||||
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
|
||||
fallback_type: fallbackType,
|
||||
reply_type: "empty_but_valid"
|
||||
};
|
||||
@@ -1190,7 +1285,9 @@ function composeAssistantAnswer(input) {
|
||||
input.coverageReport.clarification_needed_for.length > 0 ||
|
||||
input.coverageReport.out_of_scope_requirements.length > 0 ||
|
||||
input.groundingCheck.status === "partial" ||
|
||||
errorResults.length > 0;
|
||||
errorResults.length > 0 ||
|
||||
legacyLimitationReasonCodes.includes("weak_source_mapping") ||
|
||||
legacyLimitationReasonCodes.includes("missing_mechanism");
|
||||
if (okResults.length > 0 && hasPartialCoverage) {
|
||||
return {
|
||||
assistant_reply: composeExplainableAnswer(input, "partial"),
|
||||
@@ -1206,7 +1303,7 @@ function composeAssistantAnswer(input) {
|
||||
};
|
||||
}
|
||||
return {
|
||||
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
|
||||
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
|
||||
fallback_type: "unknown",
|
||||
reply_type: "backend_error"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user