ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.2.1 - фикс деградаций по старым доменам + легкая доводка регрессий перед стартом 3его этапа

This commit is contained in:
2026-04-11 16:19:53 +03:00
parent b5bd4fd737
commit 66402439dc
22 changed files with 2640 additions and 61 deletions
+75 -11
View File
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.__autoRunsQuestionTestUtils = void 0;
exports.buildAutoRunsRouter = buildAutoRunsRouter;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
@@ -1148,6 +1149,61 @@ function sanitizeGeneratedQuestion(value) {
.replace(/\s+/g, " ")
.trim();
}
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
const AUTOGEN_QUESTION_TAIL_PATTERNS = [
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
];
function stripAutogenQuestionSuffix(value) {
return sanitizeGeneratedQuestion(value).replace(/[?!.:,;]+$/u, "").trim();
}
function isAutogenQuestionPlaceholder(value) {
const core = stripAutogenQuestionSuffix(value).toLowerCase();
return core.length > 0 && AUTOGEN_QUESTION_PLACEHOLDER_PATTERN.test(core);
}
function isLikelyAutogenQuestionTail(value) {
const core = stripAutogenQuestionSuffix(value).toLowerCase();
if (!core) {
return false;
}
if (isAutogenQuestionPlaceholder(core)) {
return true;
}
return AUTOGEN_QUESTION_TAIL_PATTERNS.some((pattern) => pattern.test(core));
}
function mergeAutogenQuestionTail(baseQuestion, tail) {
const base = stripAutogenQuestionSuffix(baseQuestion);
const suffix = stripAutogenQuestionSuffix(tail);
if (!base) {
return suffix ? `${suffix}?` : "";
}
if (!suffix) {
return `${base}?`;
}
return `${base} ${suffix}?`
.replace(/\s+/g, " ")
.trim();
}
function normalizeAutogenQuestionCandidates(candidates) {
const normalized = [];
for (const candidate of candidates) {
const question = sanitizeGeneratedQuestion(candidate);
if (!question) {
continue;
}
if (isAutogenQuestionPlaceholder(question)) {
continue;
}
if (isLikelyAutogenQuestionTail(question) && normalized.length > 0) {
const merged = mergeAutogenQuestionTail(normalized[normalized.length - 1], question);
if (merged) {
normalized[normalized.length - 1] = merged;
}
continue;
}
normalized.push(question);
}
return normalized.filter((item) => item.length > 0);
}
function splitQuestionCandidates(rawText) {
const normalized = repairAutogenMojibake(rawText).replace(/\r/g, "\n").trim();
if (!normalized)
@@ -1159,27 +1215,30 @@ function splitQuestionCandidates(rawText) {
.map((line) => sanitizeGeneratedQuestion(line))
.filter((line) => line.length > 0);
if (byLines.length > 1) {
return byLines;
return normalizeAutogenQuestionCandidates(byLines);
}
const questionMarkCount = (unescaped.match(/\?/g) ?? []).length;
if (questionMarkCount > 1) {
const byQuestion = unescaped
.split("?")
.map((chunk) => sanitizeGeneratedQuestion(chunk))
.filter((chunk) => chunk.length > 0)
.map((chunk) => (chunk.endsWith("?") ? chunk : `${chunk}?`));
if (byQuestion.length > 1) {
return byQuestion;
const questionChunks = Array.from(unescaped.matchAll(/[^?]+(?:\?|$)/g))
.map((match) => sanitizeGeneratedQuestion(match[0]))
.filter((chunk) => chunk.length > 0);
if (questionChunks.length > 1) {
const canSafelySplit = questionChunks.every((chunk) => !isAutogenQuestionPlaceholder(chunk) &&
!isLikelyAutogenQuestionTail(chunk) &&
sanitizeGeneratedQuestion(chunk).length >= 18);
if (canSafelySplit) {
return normalizeAutogenQuestionCandidates(questionChunks.map((chunk) => (chunk.endsWith("?") ? chunk : `${chunk}?`)));
}
}
}
const quoted = Array.from(unescaped.matchAll(/"([^"\n]{6,}?)"/g))
.map((match) => sanitizeGeneratedQuestion(match[1]))
.filter((line) => line.length > 0);
if (quoted.length > 1) {
return quoted;
return normalizeAutogenQuestionCandidates(quoted);
}
const cleaned = sanitizeGeneratedQuestion(unescaped);
return cleaned ? [cleaned] : [];
return cleaned ? normalizeAutogenQuestionCandidates([cleaned]) : [];
}
function parseAutogenOutputJson(rawText) {
const cleaned = repairAutogenMojibake(rawText)
@@ -1225,7 +1284,8 @@ function collectQuestionsFromCandidate(value, depth = 0) {
return [];
}
if (Array.isArray(value)) {
return value.flatMap((item) => collectQuestionsFromCandidate(item, depth + 1));
const expanded = value.flatMap((item) => collectQuestionsFromCandidate(item, depth + 1));
return normalizeAutogenQuestionCandidates(expanded);
}
if (typeof value === "string") {
const text = value.trim();
@@ -1271,6 +1331,10 @@ function extractQuestionsFromAutogenOutput(rawText) {
}
return collectQuestionsFromCandidate(rawText);
}
exports.__autoRunsQuestionTestUtils = {
splitQuestionCandidates,
extractQuestionsFromAutogenOutput
};
async function generateQwenSeedQuestionsLive(input) {
const seedExamples = collectCanonicalQuestions(40);
const fallbackExamples = fallbackDomainTemplates(input.domain);
+77 -5
View File
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.__evalRouteTestUtils = void 0;
exports.buildEvalRouter = buildEvalRouter;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
@@ -35,6 +36,61 @@ function normalizeQuestionChunk(value) {
.replace(/\s+/g, " ")
.trim();
}
const RUNTIME_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
const RUNTIME_QUESTION_TAIL_PATTERNS = [
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
];
function stripQuestionSuffix(value) {
return normalizeQuestionChunk(value).replace(/[?!.:,;]+$/u, "").trim();
}
function isRuntimeQuestionPlaceholder(value) {
const core = stripQuestionSuffix(value).toLowerCase();
return core.length > 0 && RUNTIME_QUESTION_PLACEHOLDER_PATTERN.test(core);
}
function isLikelyRuntimeQuestionTail(value) {
const core = stripQuestionSuffix(value).toLowerCase();
if (!core) {
return false;
}
if (isRuntimeQuestionPlaceholder(core)) {
return true;
}
return RUNTIME_QUESTION_TAIL_PATTERNS.some((pattern) => pattern.test(core));
}
function mergeRuntimeQuestionTail(baseQuestion, tail) {
const base = stripQuestionSuffix(baseQuestion);
const suffix = stripQuestionSuffix(tail);
if (!base) {
return suffix ? `${suffix}?` : "";
}
if (!suffix) {
return `${base}?`;
}
return `${base} ${suffix}?`
.replace(/\s+/g, " ")
.trim();
}
function normalizeRuntimeQuestionList(items) {
const normalized = [];
for (const item of items) {
const chunk = normalizeQuestionChunk(item);
if (!chunk) {
continue;
}
if (isRuntimeQuestionPlaceholder(chunk)) {
continue;
}
if (isLikelyRuntimeQuestionTail(chunk) && normalized.length > 0) {
const merged = mergeRuntimeQuestionTail(normalized[normalized.length - 1], chunk);
if (merged) {
normalized[normalized.length - 1] = merged;
}
continue;
}
normalized.push(chunk);
}
return normalized.filter((item) => item.length > 0);
}
function splitQuestionCandidate(raw) {
const normalized = String(raw ?? "").replace(/\r/g, "\n").trim();
if (!normalized) {
@@ -47,18 +103,30 @@ function splitQuestionCandidate(raw) {
const source = byLines.length > 1 ? byLines : [normalized];
const chunks = [];
for (const line of source) {
const normalizedLine = normalizeQuestionChunk(line);
if (!normalizedLine || isRuntimeQuestionPlaceholder(normalizedLine)) {
continue;
}
const questionLike = Array.from(line.matchAll(/[^?]+(?:\?|$)/g))
.map((match) => normalizeQuestionChunk(match[0]))
.filter((item) => item.length > 0);
if (questionLike.length > 1) {
for (const item of questionLike) {
chunks.push(item.endsWith("?") ? item : `${item}?`);
const canSafelySplit = questionLike.every((item) => !isRuntimeQuestionPlaceholder(item) &&
!isLikelyRuntimeQuestionTail(item) &&
normalizeQuestionChunk(item).length >= 18);
if (canSafelySplit) {
for (const item of questionLike) {
chunks.push(item.endsWith("?") ? item : `${item}?`);
}
}
else {
chunks.push(normalizedLine);
}
continue;
}
chunks.push(normalizeQuestionChunk(line));
chunks.push(normalizedLine);
}
return chunks.filter((item) => item.length > 0);
return normalizeRuntimeQuestionList(chunks);
}
function normalizeRuntimeQuestions(value) {
const raw = toArray(value)
@@ -67,7 +135,7 @@ function normalizeRuntimeQuestions(value) {
if (raw.length === 0) {
return [];
}
const expanded = raw.flatMap((item) => splitQuestionCandidate(item));
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
const deduped = [];
const seen = new Set();
for (const item of expanded) {
@@ -81,6 +149,10 @@ function normalizeRuntimeQuestions(value) {
}
return deduped;
}
exports.__evalRouteTestUtils = {
splitQuestionCandidate,
normalizeRuntimeQuestions
};
function normalizeCaseIds(value) {
if (!Array.isArray(value)) {
return undefined;
+125 -3
View File
@@ -1569,6 +1569,112 @@ function buildAnswerSummary(mode) {
return "Недостаточно опоры для обоснованного ответа.";
return "Не удалось собрать обоснованный ответ по текущему запросу.";
}
const BOUNDARY_CAPABILITY_SUGGESTIONS = [
{
key: "settlements_60_62",
label: "Взаиморасчеты 60/62",
helpText: "найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.",
signals: /(контраг|долг|сальдо|взаиморасчет|оплат|аванс|покупат|поставщ|банк|выписк|\b60\b|\b62\b|\b76\b)/iu
},
{
key: "vat_document_register_book",
label: "НДС 19/68",
helpText: "проверить цепочку документ -> счет-фактура -> регистр -> книга.",
signals: /(ндс|сч[её]т[-\s]?фактур|регистр|книга\s+покуп|книга\s+продаж|декларац|\b19\b|\b68\b)/iu
},
{
key: "month_close_costs_20_44",
label: "Закрытие месяца 20/44",
helpText: "проверить распределение затрат и остатки после регламентных операций.",
signals: /(закрыти[ея]|месяц|затрат|распределени|рбп|аморт|основн|ос\b|\b20\b|\b25\b|\b26\b|\b44\b)/iu
}
];
function formatNarrativeDomainLabel(domain) {
if (domain === "settlements_60_62") {
return "взаиморасчетов 60/62";
}
if (domain === "vat_document_register_book") {
return "НДС-контура 19/68";
}
if (domain === "month_close_costs_20_44") {
return "закрытия месяца (20/44)";
}
return "доступного учетного контура";
}
function pickBoundaryCapabilityLines(userMessage, limit = 3) {
const text = String(userMessage ?? "").toLowerCase();
const scored = BOUNDARY_CAPABILITY_SUGGESTIONS.map((item, index) => ({
item,
score: (text.match(item.signals) ?? []).length,
order: index
}));
const ranked = scored
.slice()
.sort((left, right) => right.score - left.score || left.order - right.order)
.map((entry) => entry.item);
const selected = ranked.slice(0, Math.max(2, limit));
return uniqueStrings(selected.map((item) => `${item.label}: ${item.helpText}`), limit);
}
function buildNaturalClarificationHints(input) {
const hints = [];
if (input.missingAnchors.period) {
hints.push("Укажи период проверки (например, июль 2020).");
}
if (input.missingAnchors.account) {
hints.push("Укажи счет или связку счетов (например, 60/62, 19/68 или 20/44).");
}
if (input.missingAnchors.counterparty) {
hints.push("Добавь контрагента или договор, чтобы зафиксировать контур проверки.");
}
if (input.missingAnchors.documentOrObject) {
hints.push("Укажи документ или объект, от которого строить проверку цепочки.");
}
if (input.missingAnchors.anomalyType) {
hints.push("Уточни тип отклонения: разрыв цепочки, неверное закрытие или аномальный хвост.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
hints.push(`Закрой уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(hints, 5);
}
function shouldUseBoundaryFallbackReply(input) {
if (input.mode === "out_of_scope") {
return true;
}
if (input.mode !== "clarification_required" && input.mode !== "no_grounded") {
return false;
}
const hasNoEvidenceRoutes = input.okResultsCount === 0 && input.partialResultsCount === 0;
const hasNoConfirmedCoverage = input.coverageReport.requirements_covered === 0 &&
input.coverageReport.requirements_partially_covered.length === 0;
const groundingBlocked = input.groundingCheck.status === "no_grounded_answer" ||
input.groundingCheck.status === "partial" ||
input.groundingCheck.status === "route_mismatch_blocked";
return hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked;
}
function buildBoundaryFallbackReply(input) {
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
if (input.focusDomain === null) {
return sanitizeUserFacingReply([
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
]
.filter(Boolean)
.join("\n\n"));
}
const clarificationHints = buildNaturalClarificationHints({
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport
});
return sanitizeUserFacingReply([
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : ""
]
.filter(Boolean)
.join("\n\n"));
}
function ensureSentence(value) {
const sanitized = sanitizeUserText(value) ?? String(value ?? "").trim();
const normalized = sanitized.replace(/\s+/g, " ").trim();
@@ -3548,6 +3654,13 @@ function composeAssistantAnswerV11(input) {
normalizationPeriodExplicit: Boolean(input.normalizationPeriodExplicit),
companyAnchors: input.companyAnchors ?? null
});
const useBoundaryFallbackReply = shouldUseBoundaryFallbackReply({
mode: guardedDecision.mode,
groundingCheck: input.groundingCheck,
coverageReport: input.coverageReport,
okResultsCount: okResults.length,
partialResultsCount: partialResults.length
});
const hasProblemWeakSignal = policySignals.narrowing_strength !== "strong" ||
policySignals.minimum_evidence_failed ||
limitationReasonCodes.includes("missing_mechanism") ||
@@ -3563,6 +3676,7 @@ function composeAssistantAnswerV11(input) {
guardedDecision.mode === "clarification_required" ||
(guardedDecision.mode === "focused_grounded" && hasProblemWeakSignal);
const shouldUseProblemCentricAnswer = Boolean(input.enableProblemCentricAnswerV1) &&
!useBoundaryFallbackReply &&
!hardBlockedMode &&
problemCentricModeEligible &&
(!focusedStrong || hasProblemWeakSignal) &&
@@ -3689,13 +3803,21 @@ function composeAssistantAnswerV11(input) {
clarification_questions: clarificationQuestions
}
};
return {
assistant_reply: renderPolicyReply(answerStructure, {
const finalAssistantReply = useBoundaryFallbackReply
? buildBoundaryFallbackReply({
userMessage: input.userMessage,
focusDomain: focusNarrativeDomain,
missingAnchors,
coverageReport: input.coverageReport
})
: renderPolicyReply(answerStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage,
userMessage: input.userMessage
}),
});
return {
assistant_reply: finalAssistantReply,
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
answer_structure_v11: answerStructure,
@@ -33,6 +33,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
effectiveAddressUserMessage: addressInputMessage,
followupContext,
llmPreDecomposeMeta: addressPreDecompose,
sessionItems: input.sessionItems,
useMock: input.useMock
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
@@ -1,4 +1,7 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveTemporalGuard = resolveTemporalGuard;
exports.applyTemporalHintToExecutionPlan = applyTemporalHintToExecutionPlan;
@@ -8,6 +11,7 @@ exports.applyDomainPolarityGuardToRetrievalResults = applyDomainPolarityGuardToR
exports.applyEvidenceAdmissibilityGate = applyEvidenceAdmissibilityGate;
exports.evaluateGroundedAnswerEligibility = evaluateGroundedAnswerEligibility;
exports.applyEligibilityToGroundingCheck = applyEligibilityToGroundingCheck;
const iconv_lite_1 = __importDefault(require("iconv-lite"));
const JULY_YEAR = "2020";
const JULY_MONTH = "07";
const JULY_WINDOW = {
@@ -747,8 +751,65 @@ function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
};
});
}
function mojibakeScoreForRuntimeGuards(value) {
const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const hardMarkers = (source.match(/[ѓ“‚„…†‡€‰‹‰ЉЊ‹Џ‘’“”•–—™љ›њћџ]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Гђ.|Г‘.)/g) ?? []).length;
const doubleEncodedMarkers = (source.match(/(?:Р“.|Р’.|Гѓ.|Г‚.)/gu) ?? []).length;
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
}
function looksLikeMojibakeForRuntimeGuards(value) {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[ѓ“‚„…†‡€‰‹‰ЉЊ‹Џ‘’“”•–—™љ›њћџ]/.test(source)) {
return true;
}
if ((source.match(/(?:Р.|С.|Гђ.|Г‘.)/g) ?? []).length >= 2) {
return true;
}
return (source.match(/(?:Р“.|Р’.|Гѓ.|Г‚.)/gu) ?? []).length >= 2;
}
function repairRuntimeGuardsMojibake(value) {
const source = String(value ?? "");
if (!looksLikeMojibakeForRuntimeGuards(source)) {
return source;
}
let candidate = source;
for (let pass = 0; pass < 3; pass += 1) {
let improved = false;
try {
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
if (mojibakeScoreForRuntimeGuards(fromWin1251) > mojibakeScoreForRuntimeGuards(candidate)) {
candidate = fromWin1251;
improved = true;
}
}
catch (_error) {
// noop
}
try {
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
if (mojibakeScoreForRuntimeGuards(fromLatin1) > mojibakeScoreForRuntimeGuards(candidate)) {
candidate = fromLatin1;
improved = true;
}
}
catch (_error) {
// noop
}
if (!improved) {
break;
}
}
return candidate;
}
function resolveDomainPolarityGuard(input) {
const lower = String(input.userMessage ?? "").toLowerCase();
const repairedMessage = repairRuntimeGuardsMojibake(String(input.userMessage ?? ""));
const lower = repairedMessage.toLowerCase();
const accountExtraction = extractAccountsFromTextDetailed(lower);
const accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...accountExtraction.resolved_account_anchors]);
const prefixes = new Set(accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item)));
@@ -1397,7 +1458,7 @@ function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
const reasonMap = {
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных. Temporal anchor outside snapshot window.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
+255 -13
View File
@@ -1086,7 +1086,12 @@ function hasCrossScopeConflictWithState(userMessage, state) {
const inferredDomain = inferP0DomainFromMessage(userMessage);
const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
if (inferredDomain && stateDomain && inferredDomain !== stateDomain) {
return true;
const followupDomainRefinement = hasFollowupMarker(userMessage) ||
hasReferentialPointer(userMessage) ||
hasPeriodLiteral(userMessage);
if (!followupDomainRefinement) {
return true;
}
}
const explicitAccounts = extractAccountTokens(userMessage);
const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
@@ -1116,9 +1121,11 @@ function inferP0DomainFromMessage(text) {
return null;
}
function hasStrongFollowupAnchors(userMessage, state) {
const normalizedMessage = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
const periodRefinementCue = /(?:^(?:\u0430\s+)?\u0435\u0441\u043b\u0438|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0437\u0430|\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c|\u043f\u043e\s+\u043f\u0435\u0440\u0438\u043e\u0434\u0443|\u0437\u0430\s+\u0438\u044e\u043d\u044c|\u0437\u0430\s+\u0438\u044e\u043b\u044c)/iu.test(normalizedMessage);
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
const periodLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) || hasReferentialPointer(userMessage);
const periodLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) || hasReferentialPointer(userMessage) || periodRefinementCue;
if (!periodLooksLikeFollowupRefinement) {
return true;
}
@@ -3013,26 +3020,33 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected"
};
}
const directDeepAnalysisSignal = hasDirectDeepAnalysisSignal(rawMessageForGate) ||
hasDirectDeepAnalysisSignal(repairedInputMessage);
const deepAnalysisPreferenceSignal = directDeepAnalysisSignal ||
hasDeepAnalysisPreferenceSignal(rawMessageForGate) ||
hasDeepAnalysisPreferenceSignal(repairedInputMessage);
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
const hasClassifierSignal = modeDetection.mode === "address_query";
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence);
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmContractIntentConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent_confidence);
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
const hasLlmCanonicalSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
llmContractMode === "address_query" &&
llmContractModeConfidence !== "low" &&
llmContractIntent !== null &&
llmContractIntent !== "unknown" &&
llmContractIntentConfidence !== "low";
((llmContractMode === "address_query" && llmContractModeConfidence !== "low") ||
(llmCanonicalAppliedSignal &&
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal)));
const hasLlmCanonicalDataSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
Boolean(llmPreDecomposeMeta?.applied) &&
llmContractMode === "address_query" &&
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
hasStrongDataIntentSignal(repairedInputMessage);
const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) ||
hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage);
const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
hasAccountingSignal(addressInputMessage) ||
hasAccountingSignal(repairedInputMessage);
hasAccountingSignal(repairedInputMessage) ||
sameDateAccountFollowupSignal;
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
llmContractIntent === "unknown";
@@ -3080,6 +3094,125 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
reason: "no_address_signal_after_l0"
};
}
function hasLooseAllTimeAddressLookupSignal(text) {
const repaired = repairAddressMojibake(String(text ?? ""));
const normalized = compactWhitespace(repaired.toLowerCase());
if (!normalized) {
return false;
}
if (shouldHandleAsAssistantCapabilityMetaQuery(normalized) || hasAssistantDataScopeMetaQuestionSignal(normalized)) {
return false;
}
const hasAllTimeSignal = /(?:\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|for\s+all\s+time|all\s+time|entire\s+period|full\s+period)/iu.test(normalized);
if (!hasAllTimeSignal) {
return false;
}
return /(?:\u0447\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u0447[\u0435\u0451]\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u043a\u0430\u0436\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|show|list|find)/iu.test(normalized);
}
function hasDeepSessionContinuationSignal(input) {
const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : [];
if (sessionItems.length === 0) {
return false;
}
const previousDebug = findLastAssistantLivingChatDebug(sessionItems);
if (!previousDebug || typeof previousDebug !== "object") {
return false;
}
const investigationState = previousDebug.investigation_state_snapshot;
if (!investigationState || typeof investigationState !== "object") {
return false;
}
const candidateTexts = [
input?.rawUserMessage,
input?.repairedRawUserMessage,
input?.effectiveAddressUserMessage,
input?.repairedEffectiveAddressUserMessage
]
.map((value) => compactWhitespace(repairAddressMojibake(String(value ?? "")).toLowerCase()))
.filter((value) => value.length > 0);
if (candidateTexts.length === 0) {
return false;
}
return candidateTexts.some((text) => {
const hasContinuationCue = /^(?:\u0438|\u0430|\u0442\u0430\u043a\u0436\u0435|\u0435\u0449[\u0435\u0451]|\u0434\u043e\u0431\u0430\u0432\u044c|\u0434\u043e\u043f\u043e\u043b\u043d\u0438|\u0443\u0442\u043e\u0447\u043d\u0438|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438|\u0442\u0435\u043f\u0435\u0440\u044c|then|also|and)\b/iu.test(text) ||
/(?:\u043f\u043e\s+\u0442\u043e\u043c\u0443\s+\u0436\u0435|\u043f\u043e\s+\u044d\u0442\u043e\u043c\u0443|\u0432\s+\u044d\u0442\u043e\u043c\s+\u0436\u0435|\u0438\s+\u043f\u043e\s+\u043f\u0435\u0440\u0438\u043e\u0434\u0443|\u0434\u043e\u0431\u0430\u0432\u044c\s+\u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u043c|\u0430\s+\u0435\u0441\u043b\u0438|\u0435\u0441\u043b\u0438\s+\u0442\u043e\u043b\u044c\u043a\u043e|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e)/iu.test(text);
const hasAccountOrPeriodCue = /(?:\u0441\u0447[\u0435\u0451]\u0442|account|\b\d{2}(?:[.,]\d{1,2})?\b|\b20\d{2}(?:[-/.]\d{1,2})?\b|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(text);
const hasDeepRebindCue = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|fixed\s*asset|\u043e\u0441\b|\u043d\u0434\u0441|vat|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447\u043a|\u0430\u043d\u043e\u043c\u0430\u043b|lifecycle|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447)/iu.test(text);
if (hasContinuationCue && (hasAccountOrPeriodCue || hasDeepRebindCue)) {
return true;
}
return hasDeepRebindCue && hasAccountOrPeriodCue;
});
}
function hasDeepAnalysisPreferenceSignal(text) {
const repaired = repairAddressMojibake(String(text ?? ""));
const lower = compactWhitespace(repaired.toLowerCase());
if (!lower) {
return false;
}
const riskOrAnomalySignal = /(?:\u0440\u0438\u0441\u043a|risk|\u0430\u043d\u043e\u043c\u0430\u043b|anomal|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|conflict|deviation|\u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d|\u043d\u0435\u0441\u044b\u043a\u043e\u0432\u043a|\u043d\u0435\u0441\u0445\u043e\u0434|\u043e\u0448\u0438\u0431|error|issue|\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower);
const chainSignal = /(?:\u0446\u0435\u043f\u043e\u0447\u043a|chain|trace\s*chain|lifecycle|\u0436\u0438\u0437\u043d\u0435\u043d\u043d[\u0430-\u044f]+\s+\u0446\u0438\u043a\u043b|state\s+transition|\u0440\u0430\u0437\u0440\u044b\u0432[\u0430-\u044f]*)/iu.test(lower);
const diagnosticsSignal = /(?:\u0440\u0430\u0437\u043b\u043e\u0436\u0438|\u0434\u0435\u043a\u043e\u043c\u043f\u043e\u0437|\u0440\u0430\u0437\u0431\u0435\u0440\u0438|\u043f\u043e\u0447\u0435\u043c\u0443|why|\u043a\u043e\u0440\u043d\u0435\u0432[\u0430-\u044f]+\s+\u043f\u0440\u0438\u0447\u0438\u043d|root\s*cause|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c[\u0430-\u044f]*|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower);
const closureSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|\u043d\u0435\s+\u0437\u0430\u043a\u0440\u044b\u043b[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)/iu.test(lower);
const closureIntentSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|period\s*close|close\s+period)/iu.test(lower);
const closureDiagnosticPhraseSignal = /(?:\u0447\u0442\u043e(?:\s+\S+){0,8}\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower);
const signalVsNoiseDiagnostic = /(?:\u043d\u0435\s+\u043f\u0440\u043e\u0441\u0442\u043e\s+(?:\u043d\u0430\s+)?\u0448\u0443\u043c|\u043f\u043e\u0445\u043e\u0436[\u0438\u0435]\s+(?:\u0438\u043c\u0435\u043d\u043d\u043e\s+)?\u043d\u0430\s+\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower);
const lifecycleMismatchSignal = /(?:\u043d\u0435\s+\u0442\u0435\u043c\s+\u0442\u0438\u043f(?:\u043e\u043c)?\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|wrong\s+closing\s+document|expected\s+transition)/iu.test(lower);
const lifecycleTransitionGapSignal = /(?:\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u0441\u0442\u0430\u0434\u0438[\u0438\u044f\u0435]\s+.*\u043f\u0440\u043e\u0439\u0434\u0435\u043d.*\u043f\u0435\u0440\u0435\u0445\u043e\u0434)/iu.test(lower);
const expectedActualMismatchSignal = /(?:\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a[\u0430-\u044f]+\s+\u0441\u043e\u0441\u0442\u043e\u044f\u043d[\u0438\u0435\u044f]+\s+.*\u0440\u0430\u0441\u0445\u043e\u0434[\u0430-\u044f]*\s+\u0441\s+\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d[\u0430-\u044f]*\s+\u0441\u043f\u0438\u0441\u0430\u043d)/iu.test(lower);
return riskOrAnomalySignal ||
lifecycleMismatchSignal ||
(chainSignal && lifecycleTransitionGapSignal) ||
expectedActualMismatchSignal ||
(chainSignal && diagnosticsSignal) ||
(riskOrAnomalySignal && (chainSignal || closureSignal || diagnosticsSignal || closureIntentSignal)) ||
(diagnosticsSignal && closureIntentSignal) ||
closureDiagnosticPhraseSignal ||
signalVsNoiseDiagnostic;
}
function hasDirectDeepAnalysisSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
return /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized);
}
function hasStrictDeepInvestigationCue(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
const hasInvestigativeVerb = /(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u0440\u0430\u0437\u043b\u043e\u0436(?:\u0438|\u0438\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|root\s*cause|trace\s*chain)/iu.test(normalized);
if (!hasInvestigativeVerb) {
return false;
}
return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|lifecycle|state\s+transition)/iu.test(normalized);
}
function hasAggregateBusinessAnalyticsSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
const hasMetricCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u043c\u0430\u0440\u0436|\u0440\u0435\u043d\u0442\u0430\u0431\u0435\u043b|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b|turnover|revenue|profit|margin)/iu.test(normalized);
if (!hasMetricCue) {
return false;
}
const hasRankingOrTrendCue = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0442\u043e\u043f|\u043b\u0443\u0447\u0448|\u0445\u0443\u0434\u0448|\u043c\u0430\u043a\u0441(?:\u0438\u043c\u0443\u043c)?|\u043c\u0438\u043d(?:\u0438\u043c\u0443\u043c)?|\u0434\u0438\u043d\u0430\u043c|\u0442\u0440\u0435\u043d\u0434|\u0441\u0440\u0430\u0432\u043d|ranking|top|best|worst)/iu.test(normalized);
const hasPeriodAggregateCue = /(?:\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u0433\u043e\u0434(?:\u0430|\u0443|\u044b)?|year|years|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446|\u043f\u0435\u0440\u0438\u043e\u0434)/iu.test(normalized);
return hasRankingOrTrendCue || hasPeriodAggregateCue;
}
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"list_open_contracts",
"open_items_by_counterparty_or_contract",
"list_documents_by_contract",
"bank_operations_by_contract",
"list_documents_by_counterparty",
"bank_operations_by_counterparty",
"list_contracts_by_counterparty",
"contract_usage_overview",
"contract_usage_and_value",
"vat_payable_forecast"
]);
function resolveAssistantOrchestrationDecision(input) {
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
@@ -3088,6 +3221,7 @@ function resolveAssistantOrchestrationDecision(input) {
const followupContext = input?.followupContext ?? null;
const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null;
const useMock = Boolean(input?.useMock);
const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null;
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) ||
hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) ||
@@ -3100,9 +3234,21 @@ function resolveAssistantOrchestrationDecision(input) {
hasDataRetrievalRequestSignal(repairedRawUserMessage) ||
hasDataRetrievalRequestSignal(effectiveAddressUserMessage) ||
hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage);
const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) ||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) ||
hasStrictDeepInvestigationCue(repairedRawUserMessage) ||
hasStrictDeepInvestigationCue(effectiveAddressUserMessage) ||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
const keepAddressLaneByIntent = Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent))) &&
!strictDeepInvestigationCueDetected;
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
hasStrongDataIntentSignal(effectiveAddressUserMessage) ||
@@ -3175,11 +3321,58 @@ function resolveAssistantOrchestrationDecision(input) {
};
}
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
llmContractMode === "address_query") ||
hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) ||
hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) ||
hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) ||
hasSameDateAccountFollowupSignalForPredecompose(repairedEffectiveAddressUserMessage) ||
hasLooseAllTimeAddressLookupSignal(rawUserMessage) ||
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
hasAddressFollowupContextSignal(rawUserMessage) ||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
const unsupportedIntentOrMode = modeDetection.mode !== "address_query" &&
(intentResolution.intent === "unknown" || llmContractMode === "unsupported");
const unsupportedAddressIntentFallbackToDeep = Boolean(!followupContext &&
baseToolGate?.runAddressLane &&
modeDetection.mode !== "address_query" &&
intentResolution.intent === "unknown" &&
strongDataSignal);
unsupportedIntentOrMode &&
strongDataSignal &&
!preserveAddressLaneSignal);
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
hasDeepAnalysisPreferenceSignal(effectiveAddressUserMessage) ||
hasDeepAnalysisPreferenceSignal(repairedEffectiveAddressUserMessage) ||
hasDirectDeepAnalysisSignal(rawUserMessage) ||
hasDirectDeepAnalysisSignal(repairedRawUserMessage) ||
hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) ||
hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage));
const vatExplainFollowupSignal = Boolean(followupContext &&
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
deepAnalysisPreferenceDetected &&
!keepAddressLaneByIntent &&
!vatExplainFollowupSignal &&
(!followupContext || !dataRetrievalSignal));
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
aggregateBusinessAnalyticsSignal &&
!keepAddressLaneByIntent &&
!followupContext);
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
baseToolGate?.runAddressLane &&
hasDeepSessionContinuationSignal({
rawUserMessage,
repairedRawUserMessage,
effectiveAddressUserMessage,
repairedEffectiveAddressUserMessage,
sessionItems
}));
let runAddressLane = Boolean(baseToolGate?.runAddressLane);
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
@@ -3188,6 +3381,23 @@ function resolveAssistantOrchestrationDecision(input) {
toolGateDecision = "skip_address_lane";
toolGateReason = "address_signal_unsupported_intent_fallback_to_deep";
}
if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "deep_analysis_signal_fallback_to_deep";
}
if (aggregateAnalyticsFallbackToDeep &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "aggregate_analytics_signal_fallback_to_deep";
}
if (deepSessionContinuationFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "deep_session_continuation_fallback_to_deep";
}
let livingDecision = resolveLivingAssistantModeDecision({
userMessage: rawUserMessage,
addressLaneTriggered: runAddressLane,
@@ -3201,6 +3411,26 @@ function resolveAssistantOrchestrationDecision(input) {
reason: "unsupported_address_intent_fallback_to_deep"
};
}
if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) {
livingDecision = {
mode: "deep_analysis",
reason: "deep_analysis_signal_fallback_to_deep"
};
}
if (aggregateAnalyticsFallbackToDeep &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep) {
livingDecision = {
mode: "deep_analysis",
reason: "aggregate_analytics_signal_fallback_to_deep"
};
}
if (deepSessionContinuationFallbackToDeep) {
livingDecision = {
mode: "deep_analysis",
reason: "deep_session_continuation_fallback_to_deep"
};
}
return {
runAddressLane,
toolGateDecision,
@@ -3218,6 +3448,9 @@ function resolveAssistantOrchestrationDecision(input) {
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext),
unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep,
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
final_decision: {
run_address_lane: runAddressLane,
tool_gate_decision: toolGateDecision,
@@ -3237,6 +3470,11 @@ function hasDataRetrievalRequestSignal(text) {
if (!lower) {
return false;
}
const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a)(?:$|[\s,.!?;:])/iu.test(lower);
const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(lower);
if (hasRussianRetrievalAction && hasRussianRetrievalObject) {
return true;
}
const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower);
const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bwhich\b|\bwho\b)/i.test(lower);
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
@@ -3403,6 +3641,10 @@ function hasAssistantDataScopeMetaQuestionSignal(text) {
if (!normalized) {
return false;
}
const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized);
if (hasSlangScopeQuestion) {
return true;
}
const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized);
const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized);
const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized);
+3
View File
@@ -22,6 +22,9 @@ function redactObject(value) {
return value;
}
function logJson(entry) {
if (process.env.NODE_ENV === "test" && process.env.FEATURE_JSON_STDOUT_LOGS_IN_TESTS !== "1") {
return;
}
const safe = {
...entry,
details: redactObject(entry.details)
+86 -11
View File
@@ -1415,6 +1415,67 @@ function sanitizeGeneratedQuestion(value: string): string {
.trim();
}
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
const AUTOGEN_QUESTION_TAIL_PATTERNS: RegExp[] = [
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
];
function stripAutogenQuestionSuffix(value: string): string {
return sanitizeGeneratedQuestion(value).replace(/[?!.:,;]+$/u, "").trim();
}
function isAutogenQuestionPlaceholder(value: string): boolean {
const core = stripAutogenQuestionSuffix(value).toLowerCase();
return core.length > 0 && AUTOGEN_QUESTION_PLACEHOLDER_PATTERN.test(core);
}
function isLikelyAutogenQuestionTail(value: string): boolean {
const core = stripAutogenQuestionSuffix(value).toLowerCase();
if (!core) {
return false;
}
if (isAutogenQuestionPlaceholder(core)) {
return true;
}
return AUTOGEN_QUESTION_TAIL_PATTERNS.some((pattern) => pattern.test(core));
}
function mergeAutogenQuestionTail(baseQuestion: string, tail: string): string {
const base = stripAutogenQuestionSuffix(baseQuestion);
const suffix = stripAutogenQuestionSuffix(tail);
if (!base) {
return suffix ? `${suffix}?` : "";
}
if (!suffix) {
return `${base}?`;
}
return `${base} ${suffix}?`
.replace(/\s+/g, " ")
.trim();
}
function normalizeAutogenQuestionCandidates(candidates: string[]): string[] {
const normalized: string[] = [];
for (const candidate of candidates) {
const question = sanitizeGeneratedQuestion(candidate);
if (!question) {
continue;
}
if (isAutogenQuestionPlaceholder(question)) {
continue;
}
if (isLikelyAutogenQuestionTail(question) && normalized.length > 0) {
const merged = mergeAutogenQuestionTail(normalized[normalized.length - 1], question);
if (merged) {
normalized[normalized.length - 1] = merged;
}
continue;
}
normalized.push(question);
}
return normalized.filter((item) => item.length > 0);
}
function splitQuestionCandidates(rawText: string): string[] {
const normalized = repairAutogenMojibake(rawText).replace(/\r/g, "\n").trim();
if (!normalized) return [];
@@ -1426,18 +1487,26 @@ function splitQuestionCandidates(rawText: string): string[] {
.map((line) => sanitizeGeneratedQuestion(line))
.filter((line) => line.length > 0);
if (byLines.length > 1) {
return byLines;
return normalizeAutogenQuestionCandidates(byLines);
}
const questionMarkCount = (unescaped.match(/\?/g) ?? []).length;
if (questionMarkCount > 1) {
const byQuestion = unescaped
.split("?")
.map((chunk) => sanitizeGeneratedQuestion(chunk))
.filter((chunk) => chunk.length > 0)
.map((chunk) => (chunk.endsWith("?") ? chunk : `${chunk}?`));
if (byQuestion.length > 1) {
return byQuestion;
const questionChunks = Array.from(unescaped.matchAll(/[^?]+(?:\?|$)/g))
.map((match) => sanitizeGeneratedQuestion(match[0]))
.filter((chunk) => chunk.length > 0);
if (questionChunks.length > 1) {
const canSafelySplit = questionChunks.every(
(chunk) =>
!isAutogenQuestionPlaceholder(chunk) &&
!isLikelyAutogenQuestionTail(chunk) &&
sanitizeGeneratedQuestion(chunk).length >= 18
);
if (canSafelySplit) {
return normalizeAutogenQuestionCandidates(
questionChunks.map((chunk) => (chunk.endsWith("?") ? chunk : `${chunk}?`))
);
}
}
}
@@ -1445,11 +1514,11 @@ function splitQuestionCandidates(rawText: string): string[] {
.map((match) => sanitizeGeneratedQuestion(match[1]))
.filter((line) => line.length > 0);
if (quoted.length > 1) {
return quoted;
return normalizeAutogenQuestionCandidates(quoted);
}
const cleaned = sanitizeGeneratedQuestion(unescaped);
return cleaned ? [cleaned] : [];
return cleaned ? normalizeAutogenQuestionCandidates([cleaned]) : [];
}
function parseAutogenOutputJson(rawText: string): unknown | null {
@@ -1496,7 +1565,8 @@ function collectQuestionsFromCandidate(value: unknown, depth = 0): string[] {
}
if (Array.isArray(value)) {
return value.flatMap((item) => collectQuestionsFromCandidate(item, depth + 1));
const expanded = value.flatMap((item) => collectQuestionsFromCandidate(item, depth + 1));
return normalizeAutogenQuestionCandidates(expanded);
}
if (typeof value === "string") {
@@ -1549,6 +1619,11 @@ function extractQuestionsFromAutogenOutput(rawText: string): string[] {
return collectQuestionsFromCandidate(rawText);
}
export const __autoRunsQuestionTestUtils = {
splitQuestionCandidates,
extractQuestionsFromAutogenOutput
};
async function generateQwenSeedQuestionsLive(input: {
count: number;
domain: string | null;
+85 -5
View File
@@ -73,6 +73,67 @@ function normalizeQuestionChunk(value: string): string {
.trim();
}
const RUNTIME_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
const RUNTIME_QUESTION_TAIL_PATTERNS: RegExp[] = [
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
];
function stripQuestionSuffix(value: string): string {
return normalizeQuestionChunk(value).replace(/[?!.:,;]+$/u, "").trim();
}
function isRuntimeQuestionPlaceholder(value: string): boolean {
const core = stripQuestionSuffix(value).toLowerCase();
return core.length > 0 && RUNTIME_QUESTION_PLACEHOLDER_PATTERN.test(core);
}
function isLikelyRuntimeQuestionTail(value: string): boolean {
const core = stripQuestionSuffix(value).toLowerCase();
if (!core) {
return false;
}
if (isRuntimeQuestionPlaceholder(core)) {
return true;
}
return RUNTIME_QUESTION_TAIL_PATTERNS.some((pattern) => pattern.test(core));
}
function mergeRuntimeQuestionTail(baseQuestion: string, tail: string): string {
const base = stripQuestionSuffix(baseQuestion);
const suffix = stripQuestionSuffix(tail);
if (!base) {
return suffix ? `${suffix}?` : "";
}
if (!suffix) {
return `${base}?`;
}
return `${base} ${suffix}?`
.replace(/\s+/g, " ")
.trim();
}
function normalizeRuntimeQuestionList(items: string[]): string[] {
const normalized: string[] = [];
for (const item of items) {
const chunk = normalizeQuestionChunk(item);
if (!chunk) {
continue;
}
if (isRuntimeQuestionPlaceholder(chunk)) {
continue;
}
if (isLikelyRuntimeQuestionTail(chunk) && normalized.length > 0) {
const merged = mergeRuntimeQuestionTail(normalized[normalized.length - 1], chunk);
if (merged) {
normalized[normalized.length - 1] = merged;
}
continue;
}
normalized.push(chunk);
}
return normalized.filter((item) => item.length > 0);
}
function splitQuestionCandidate(raw: string): string[] {
const normalized = String(raw ?? "").replace(/\r/g, "\n").trim();
if (!normalized) {
@@ -87,18 +148,32 @@ function splitQuestionCandidate(raw: string): string[] {
const chunks: string[] = [];
for (const line of source) {
const normalizedLine = normalizeQuestionChunk(line);
if (!normalizedLine || isRuntimeQuestionPlaceholder(normalizedLine)) {
continue;
}
const questionLike = Array.from(line.matchAll(/[^?]+(?:\?|$)/g))
.map((match) => normalizeQuestionChunk(match[0]))
.filter((item) => item.length > 0);
if (questionLike.length > 1) {
for (const item of questionLike) {
chunks.push(item.endsWith("?") ? item : `${item}?`);
const canSafelySplit = questionLike.every(
(item) =>
!isRuntimeQuestionPlaceholder(item) &&
!isLikelyRuntimeQuestionTail(item) &&
normalizeQuestionChunk(item).length >= 18
);
if (canSafelySplit) {
for (const item of questionLike) {
chunks.push(item.endsWith("?") ? item : `${item}?`);
}
} else {
chunks.push(normalizedLine);
}
continue;
}
chunks.push(normalizeQuestionChunk(line));
chunks.push(normalizedLine);
}
return chunks.filter((item) => item.length > 0);
return normalizeRuntimeQuestionList(chunks);
}
function normalizeRuntimeQuestions(value: unknown): string[] {
@@ -109,7 +184,7 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
return [];
}
const expanded = raw.flatMap((item) => splitQuestionCandidate(item));
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
const deduped: string[] = [];
const seen = new Set<string>();
for (const item of expanded) {
@@ -122,6 +197,11 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
return deduped;
}
export const __evalRouteTestUtils = {
splitQuestionCandidate,
normalizeRuntimeQuestions
};
function normalizeCaseIds(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
@@ -1854,6 +1854,146 @@ function buildAnswerSummary(mode: PolicyMode): string {
type P0NarrativeDomain = "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44" | null;
interface BoundaryCapabilitySuggestion {
key: "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44";
label: string;
helpText: string;
signals: RegExp;
}
const BOUNDARY_CAPABILITY_SUGGESTIONS: BoundaryCapabilitySuggestion[] = [
{
key: "settlements_60_62",
label: "Взаиморасчеты 60/62",
helpText: "найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.",
signals: /(контраг|долг|сальдо|взаиморасчет|оплат|аванс|покупат|поставщ|банк|выписк|\b60\b|\b62\b|\b76\b)/iu
},
{
key: "vat_document_register_book",
label: "НДС 19/68",
helpText: "проверить цепочку документ -> счет-фактура -> регистр -> книга.",
signals: /(ндс|сч[её]т[-\s]?фактур|регистр|книга\s+покуп|книга\s+продаж|декларац|\b19\b|\b68\b)/iu
},
{
key: "month_close_costs_20_44",
label: "Закрытие месяца 20/44",
helpText: "проверить распределение затрат и остатки после регламентных операций.",
signals: /(закрыти[ея]|месяц|затрат|распределени|рбп|аморт|основн|ос\b|\b20\b|\b25\b|\b26\b|\b44\b)/iu
}
];
function formatNarrativeDomainLabel(domain: P0NarrativeDomain): string {
if (domain === "settlements_60_62") {
return "взаиморасчетов 60/62";
}
if (domain === "vat_document_register_book") {
return "НДС-контура 19/68";
}
if (domain === "month_close_costs_20_44") {
return "закрытия месяца (20/44)";
}
return "доступного учетного контура";
}
function pickBoundaryCapabilityLines(userMessage: string, limit = 3): string[] {
const text = String(userMessage ?? "").toLowerCase();
const scored = BOUNDARY_CAPABILITY_SUGGESTIONS.map((item, index) => ({
item,
score: (text.match(item.signals) ?? []).length,
order: index
}));
const ranked = scored
.slice()
.sort((left, right) => right.score - left.score || left.order - right.order)
.map((entry) => entry.item);
const selected = ranked.slice(0, Math.max(2, limit));
return uniqueStrings(selected.map((item) => `${item.label}: ${item.helpText}`), limit);
}
function buildNaturalClarificationHints(input: {
missingAnchors: MissingAnchors;
coverageReport: RequirementCoverageReport;
}): string[] {
const hints: string[] = [];
if (input.missingAnchors.period) {
hints.push("Укажи период проверки (например, июль 2020).");
}
if (input.missingAnchors.account) {
hints.push("Укажи счет или связку счетов (например, 60/62, 19/68 или 20/44).");
}
if (input.missingAnchors.counterparty) {
hints.push("Добавь контрагента или договор, чтобы зафиксировать контур проверки.");
}
if (input.missingAnchors.documentOrObject) {
hints.push("Укажи документ или объект, от которого строить проверку цепочки.");
}
if (input.missingAnchors.anomalyType) {
hints.push("Уточни тип отклонения: разрыв цепочки, неверное закрытие или аномальный хвост.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
hints.push(`Закрой уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(hints, 5);
}
function shouldUseBoundaryFallbackReply(input: {
mode: PolicyMode;
groundingCheck: AnswerGroundingCheck;
coverageReport: RequirementCoverageReport;
okResultsCount: number;
partialResultsCount: number;
}): boolean {
if (input.mode === "out_of_scope") {
return true;
}
if (input.mode !== "clarification_required" && input.mode !== "no_grounded") {
return false;
}
const hasNoEvidenceRoutes = input.okResultsCount === 0 && input.partialResultsCount === 0;
const hasNoConfirmedCoverage =
input.coverageReport.requirements_covered === 0 &&
input.coverageReport.requirements_partially_covered.length === 0;
const groundingBlocked =
input.groundingCheck.status === "no_grounded_answer" ||
input.groundingCheck.status === "partial" ||
input.groundingCheck.status === "route_mismatch_blocked";
return hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked;
}
function buildBoundaryFallbackReply(input: {
userMessage: string;
focusDomain: P0NarrativeDomain;
missingAnchors: MissingAnchors;
coverageReport: RequirementCoverageReport;
}): string {
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
if (input.focusDomain === null) {
return sanitizeUserFacingReply(
[
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
]
.filter(Boolean)
.join("\n\n")
);
}
const clarificationHints = buildNaturalClarificationHints({
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport
});
return sanitizeUserFacingReply(
[
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : ""
]
.filter(Boolean)
.join("\n\n")
);
}
function ensureSentence(value: string): string {
const sanitized = sanitizeUserText(value) ?? String(value ?? "").trim();
const normalized = sanitized.replace(/\s+/g, " ").trim();
@@ -4219,6 +4359,13 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
normalizationPeriodExplicit: Boolean(input.normalizationPeriodExplicit),
companyAnchors: input.companyAnchors ?? null
});
const useBoundaryFallbackReply = shouldUseBoundaryFallbackReply({
mode: guardedDecision.mode,
groundingCheck: input.groundingCheck,
coverageReport: input.coverageReport,
okResultsCount: okResults.length,
partialResultsCount: partialResults.length
});
const hasProblemWeakSignal =
policySignals.narrowing_strength !== "strong" ||
policySignals.minimum_evidence_failed ||
@@ -4238,6 +4385,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
(guardedDecision.mode === "focused_grounded" && hasProblemWeakSignal);
const shouldUseProblemCentricAnswer =
Boolean(input.enableProblemCentricAnswerV1) &&
!useBoundaryFallbackReply &&
!hardBlockedMode &&
problemCentricModeEligible &&
(!focusedStrong || hasProblemWeakSignal) &&
@@ -4382,13 +4530,22 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
}
};
const finalAssistantReply = useBoundaryFallbackReply
? buildBoundaryFallbackReply({
userMessage: input.userMessage,
focusDomain: focusNarrativeDomain,
missingAnchors,
coverageReport: input.coverageReport
})
: renderPolicyReply(answerStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage,
userMessage: input.userMessage
});
return {
assistant_reply: renderPolicyReply(answerStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage,
userMessage: input.userMessage
}),
assistant_reply: finalAssistantReply,
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
answer_structure_v11: answerStructure,
@@ -2986,12 +2986,12 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence);
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client|\b[a-z]{2,}\b)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
const hasLlmCanonicalSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
((llmContractMode === "address_query" && llmContractModeConfidence !== "low") ||
(llmCanonicalAppliedSignal &&
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal || llmContractMode === "unsupported")));
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal)));
const hasLlmCanonicalDataSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
Boolean(llmPreDecomposeMeta?.applied) &&
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
@@ -3133,6 +3133,42 @@ function hasDirectDeepAnalysisSignal(text) {
}
return /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized);
}
function hasStrictDeepInvestigationCue(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
const hasInvestigativeVerb = /(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u0440\u0430\u0437\u043b\u043e\u0436(?:\u0438|\u0438\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|root\s*cause|trace\s*chain)/iu.test(normalized);
if (!hasInvestigativeVerb) {
return false;
}
return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|lifecycle|state\s+transition)/iu.test(normalized);
}
function hasAggregateBusinessAnalyticsSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
const hasMetricCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u043c\u0430\u0440\u0436|\u0440\u0435\u043d\u0442\u0430\u0431\u0435\u043b|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b|turnover|revenue|profit|margin)/iu.test(normalized);
if (!hasMetricCue) {
return false;
}
const hasRankingOrTrendCue = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0442\u043e\u043f|\u043b\u0443\u0447\u0448|\u0445\u0443\u0434\u0448|\u043c\u0430\u043a\u0441(?:\u0438\u043c\u0443\u043c)?|\u043c\u0438\u043d(?:\u0438\u043c\u0443\u043c)?|\u0434\u0438\u043d\u0430\u043c|\u0442\u0440\u0435\u043d\u0434|\u0441\u0440\u0430\u0432\u043d|ranking|top|best|worst)/iu.test(normalized);
const hasPeriodAggregateCue = /(?:\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u0433\u043e\u0434(?:\u0430|\u0443|\u044b)?|year|years|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446|\u043f\u0435\u0440\u0438\u043e\u0434)/iu.test(normalized);
return hasRankingOrTrendCue || hasPeriodAggregateCue;
}
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"list_open_contracts",
"open_items_by_counterparty_or_contract",
"list_documents_by_contract",
"bank_operations_by_contract",
"list_documents_by_counterparty",
"bank_operations_by_counterparty",
"list_contracts_by_counterparty",
"contract_usage_overview",
"contract_usage_and_value",
"vat_payable_forecast"
]);
export function resolveAssistantOrchestrationDecision(input) {
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
@@ -3154,9 +3190,21 @@ export function resolveAssistantOrchestrationDecision(input) {
hasDataRetrievalRequestSignal(repairedRawUserMessage) ||
hasDataRetrievalRequestSignal(effectiveAddressUserMessage) ||
hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage);
const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) ||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) ||
hasStrictDeepInvestigationCue(repairedRawUserMessage) ||
hasStrictDeepInvestigationCue(effectiveAddressUserMessage) ||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
const keepAddressLaneByIntent = Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent))) &&
!strictDeepInvestigationCueDetected;
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
hasStrongDataIntentSignal(effectiveAddressUserMessage) ||
@@ -3232,7 +3280,7 @@ export function resolveAssistantOrchestrationDecision(input) {
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
(llmContractMode === "address_query" || llmContractMode === "unsupported")) ||
llmContractMode === "address_query") ||
hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) ||
hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) ||
hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) ||
@@ -3245,10 +3293,11 @@ export function resolveAssistantOrchestrationDecision(input) {
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
const unsupportedIntentOrMode = modeDetection.mode !== "address_query" &&
(intentResolution.intent === "unknown" || llmContractMode === "unsupported");
const unsupportedAddressIntentFallbackToDeep = Boolean(!followupContext &&
baseToolGate?.runAddressLane &&
modeDetection.mode !== "address_query" &&
intentResolution.intent === "unknown" &&
unsupportedIntentOrMode &&
strongDataSignal &&
!preserveAddressLaneSignal);
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
@@ -3264,8 +3313,13 @@ export function resolveAssistantOrchestrationDecision(input) {
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
deepAnalysisPreferenceDetected &&
!keepAddressLaneByIntent &&
!vatExplainFollowupSignal &&
(!followupContext || !dataRetrievalSignal));
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
aggregateBusinessAnalyticsSignal &&
!keepAddressLaneByIntent &&
!followupContext);
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
baseToolGate?.runAddressLane &&
hasDeepSessionContinuationSignal({
@@ -3288,6 +3342,13 @@ export function resolveAssistantOrchestrationDecision(input) {
toolGateDecision = "skip_address_lane";
toolGateReason = "deep_analysis_signal_fallback_to_deep";
}
if (aggregateAnalyticsFallbackToDeep &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "aggregate_analytics_signal_fallback_to_deep";
}
if (deepSessionContinuationFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
@@ -3312,6 +3373,14 @@ export function resolveAssistantOrchestrationDecision(input) {
reason: "deep_analysis_signal_fallback_to_deep"
};
}
if (aggregateAnalyticsFallbackToDeep &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep) {
livingDecision = {
mode: "deep_analysis",
reason: "aggregate_analytics_signal_fallback_to_deep"
};
}
if (deepSessionContinuationFallbackToDeep) {
livingDecision = {
mode: "deep_analysis",
@@ -3336,6 +3405,7 @@ export function resolveAssistantOrchestrationDecision(input) {
followup_context_detected: Boolean(followupContext),
unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep,
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
final_decision: {
run_address_lane: runAddressLane,
@@ -3356,6 +3426,11 @@ function hasDataRetrievalRequestSignal(text) {
if (!lower) {
return false;
}
const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a)(?:$|[\s,.!?;:])/iu.test(lower);
const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(lower);
if (hasRussianRetrievalAction && hasRussianRetrievalObject) {
return true;
}
const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower);
const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bwhich\b|\bwho\b)/i.test(lower);
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
@@ -3522,6 +3597,10 @@ function hasAssistantDataScopeMetaQuestionSignal(text) {
if (!normalized) {
return false;
}
const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized);
if (hasSlangScopeQuestion) {
return true;
}
const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized);
const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized);
const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized);
@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import { composeAssistantAnswer } from "../src/services/answerComposer";
function buildRouteSummary(fallbackType: "none" | "clarification" | "out_of_scope" = "none") {
return {
mode: "deterministic_v2" as const,
message_in_scope: true,
scope_confidence: "high" as const,
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: fallbackType,
message: null
}
};
}
function buildCoverageReport() {
return {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
};
}
describe("assistant boundary fallback reply", () => {
it("uses soft refusal without template sections when domain is not covered", () => {
const output = composeAssistantAnswer({
userMessage: "Скажи курс доллара на завтра и дай прогноз инфляции.",
routeSummary: buildRouteSummary("none"),
retrievalResults: [],
requirements: [],
coverageReport: buildCoverageReport(),
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: ["no grounded support"],
why_included_summary: [],
selection_reason_summary: []
},
enableAnswerPolicyV11: true
});
expect(output.reply_type).toBe("clarification_required");
expect(output.assistant_reply).toMatch(/мягкий отказ/i);
expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:");
expect(output.assistant_reply).not.toContain("Что сломано:");
});
it("for covered domain without anchors uses soft clarification with nearby capability", () => {
const output = composeAssistantAnswer({
userMessage: "Покажи где по контрагентам хвосты по оплатам.",
routeSummary: buildRouteSummary("none"),
retrievalResults: [],
requirements: [],
coverageReport: buildCoverageReport(),
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: ["no grounded support"],
why_included_summary: [],
selection_reason_summary: []
},
enableAnswerPolicyV11: true
});
expect(output.reply_type).toBe("clarification_required");
expect(output.assistant_reply).toMatch(/не могу надежно ответить по сценарию/i);
expect(output.assistant_reply).toContain("Чтобы сразу перейти к проверке, уточни:");
expect(output.assistant_reply).toContain("Если удобнее, могу начать с близкого сценария:");
expect(output.assistant_reply).not.toContain("Что сломано:");
});
it("keeps out_of_scope reply type but responds with soft fallback text", () => {
const output = composeAssistantAnswer({
userMessage: "Скажи прогноз погоды на выходные.",
routeSummary: buildRouteSummary("out_of_scope"),
retrievalResults: [],
requirements: [],
coverageReport: buildCoverageReport(),
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: ["out of scope"],
why_included_summary: [],
selection_reason_summary: []
},
enableAnswerPolicyV11: true
});
expect(output.reply_type).toBe("out_of_scope");
expect(output.assistant_reply).toMatch(/мягкий отказ|не могу надежно/i);
expect(output.assistant_reply).not.toContain("Что сломано:");
});
});
@@ -126,6 +126,18 @@ describe("assistant living router mode decision", () => {
expect(decision.reason).toBe("assistant_data_scope_query_detected");
});
it("routes slang data-scope wording 'по каким конторам можем общаться' to chat", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "\u043f\u043e \u043a\u0430\u043a\u0438\u043c \u043a\u043e\u043d\u0442\u043e\u0440\u0430\u043c \u043c\u043e\u0436\u0435\u043c \u043e\u0431\u0449\u0430\u0442\u044c\u0441\u044f?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("chat");
expect(decision.reason).toBe("assistant_data_scope_query_detected");
});
it("routes data-scope wording without question mark when interrogative token is present", () => {
const decision = resolveLivingAssistantModeDecision({
userMessage: "каза какой компании подключена к 1с",
@@ -185,6 +197,54 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("routes unsupported turnover-by-organization query to deep analysis", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
effectiveAddressUserMessage: "\u041e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0441\u0447\u0435\u0442\u0443 '\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430' \u0437\u0430 2020 \u0433\u043e\u0434.",
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "account_balance_snapshot",
intent_confidence: "high"
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.livingMode).toBe("deep_analysis");
expect([
"address_signal_unsupported_intent_fallback_to_deep",
"aggregate_analytics_signal_fallback_to_deep"
]).toContain(String(decision.toolGateReason));
expect([
"unsupported_address_intent_fallback_to_deep",
"aggregate_analytics_signal_fallback_to_deep"
]).toContain(String(decision.livingReason));
});
it("routes profitability ranking query to deep analysis instead of address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
effectiveAddressUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
followupContext: null,
llmPreDecomposeMeta: null as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("aggregate_analytics_signal_fallback_to_deep");
expect(decision.livingMode).toBe("deep_analysis");
expect(decision.livingReason).toBe("aggregate_analytics_signal_fallback_to_deep");
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
});
it("keeps VAT explain follow-up in address lane when followup context is present", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "почему прогноз к уплате 0?",
@@ -236,6 +296,33 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
});
it("keeps list_open_contracts query in address lane despite 'unclosed' wording", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u043a\u043e\u043d\u0435\u0446 \u0434\u0435\u043a\u0430\u0431\u0440\u044f 2020 \u0433\u043e\u0434\u0430.",
followupContext: {
previous_intent: "month_close_costs_20_44"
},
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "address_query",
mode_confidence: "high",
intent: "list_open_contracts",
intent_confidence: "medium"
}
} as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
});
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { __autoRunsQuestionTestUtils } from "../src/routes/autoRuns";
describe("autoruns question extraction", () => {
it("merges conversational tails instead of producing micro-prompts", () => {
const parsed = __autoRunsQuestionTestUtils.splitQuestionCandidates(
"Покажи рисковые хвосты по расчетам с поставщиками? и коротко? без воды?"
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/коротко/i);
expect(parsed[0]).toMatch(/без воды/i);
});
it("keeps independent questions separated", () => {
const parsed = __autoRunsQuestionTestUtils.splitQuestionCandidates(
"Где зависли оплаты по счету 60? Какие акты сверки с риском расхождения по 62?"
);
expect(parsed).toHaveLength(2);
});
it("extracts questions from JSON payload and skips placeholders", () => {
const parsed = __autoRunsQuestionTestUtils.extractQuestionsFromAutogenOutput(
JSON.stringify({
questions: ["Вопросы", "Покажи хвосты по поставщикам", "и коротко?"]
})
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/поставщик/i);
expect(parsed[0]).toMatch(/коротко/i);
});
});
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { __evalRouteTestUtils } from "../src/routes/eval";
describe("eval runtime question splitting", () => {
it("merges short conversational tails into previous question", () => {
const parsed = __evalRouteTestUtils.splitQuestionCandidate(
"Покажи контрагентов с риском несверки по акту? и коротко? без воды?"
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/коротко/i);
expect(parsed[0]).toMatch(/без воды/i);
});
it("keeps independent full questions as separate items", () => {
const parsed = __evalRouteTestUtils.splitQuestionCandidate(
"Где зависли оплаты по счету 60? Какие документы не закрылись по 62 за июль 2020?"
);
expect(parsed).toHaveLength(2);
expect(parsed[0]).toMatch(/\?/);
expect(parsed[1]).toMatch(/\?/);
});
it("normalizes list input and removes placeholders and duplicates", () => {
const parsed = __evalRouteTestUtils.normalizeRuntimeQuestions([
"Вопросы",
"Покажи хвосты по поставщикам",
"и коротко?"
]);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatch(/поставщик/i);
expect(parsed[0]).toMatch(/коротко/i);
});
});