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

This commit is contained in:
dctouch 2026-04-11 16:19:53 +03:00
parent b5bd4fd737
commit 66402439dc
22 changed files with 2640 additions and 61 deletions

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);

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;

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,

View File

@ -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);

View File

@ -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: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",

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);

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)

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;

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;

View File

@ -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,

View File

@ -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);

View File

@ -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("Что сломано:");
});
});

View File

@ -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 года",

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -673,5 +673,221 @@
"question_text": "Проверь зависшие авансы и уточни, можно ли их перепривязать на текущие отгрузки или пора списывать как нереальные?",
"answer_text": "Я ассистент по анализу данных 1С в режиме чтения.\nЧто умею по группам:\n1. НДС: Расчеты и аналитика по НДС на основании данных 1С. (например: vat_period_snapshot, vat_payable_forecast, vat_turnover_breakdown).\n2. Контрагенты: Срезы активности, платежей и документов по контрагентам. (например: list_documents_by_counterparty, bank_operations_by_counterparty, list_contracts_by_counterparty).\n3. Задолженности и расчеты: Аналитика закрытия расчетов, сальдо и признаков незакрытых цепочек. (например: settlement_closure_state, advance_offset_state, open_items_snapshot).\n4. Деньги и остатки: Остатки и динамика по денежным счетам и кассе. (например: balance_snapshot, turnover_by_period).\n5. Ограничения: Операции, которые ассистент не выполняет в этом рантайме. (например: explain_boundary, suggest_safe_next_step).\nЕсли хотите, раскрою любую группу точечно и дам готовую формулировку запроса.\nЧто не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
}
},
{
"annotation_id": "ann-mnubukbt-h5fjkfe",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-002",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-002",
"message_index": 1,
"rating": 3,
"comment": "нужен анализ маршрута для ответа на этот вопрос - расшщирение доменов",
"manual_case_decision": "candidate_for_implementation",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:44:50.920Z",
"updated_at": "2026-04-11T12:44:50.920Z",
"context": {
"message_id": "msg-tL1QVBeDxY",
"trace_id": "Ylge9xWuRuJLvV",
"reply_type": "clarification_required",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?",
"answer_text": "Сейчас не могу надежно ответить по сценарию взаиморасчетов 60/62: не хватает опоры. Чтобы сразу перейти к проверке, уточни:\n- Укажи период проверки (например, июль 2020).\n- Укажи счет или связку счетов (например, 60/62, 19/68 или 20/44).\n- Добавь контрагента или договор, чтобы зафиксировать контур проверки.\n- Укажи документ или объект, от которого строить проверку цепочки.\n- Уточни тип отклонения: разрыв цепочки, неверное закрытие или аномальный хвост. Если удобнее, могу начать с близкого сценария:\n- Взаиморасчеты 60/62: найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.\n- НДС 19/68: проверить цепочку документ -> счет-фактура -> регистр -> книга."
}
},
{
"annotation_id": "ann-mnubvu0x-3pv3ehr",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-004",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-004",
"message_index": 1,
"rating": 3,
"comment": "нужен анализ маршрута для ответа на этот вопрос - расшщирение доменов - однозначно к дорабюотке - анализируем заказчиков которые чаще встречаются по годам и выводим топ 10",
"manual_case_decision": "needs_dialog_policy_fix",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:45:50.144Z",
"updated_at": "2026-04-11T12:45:50.144Z",
"context": {
"message_id": "msg-rj83nhGOV7",
"trace_id": "address-H9VJ13GWWC",
"reply_type": "partial_coverage",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?",
"answer_text": "Чтобы ответить надежно, нужен более точный ориентир в запросе.\nКоротко: в запросе не хватает конкретного ориентира (контрагент, договор или период).\nЧто можно сделать дальше: укажите контрагента или номер/название договора."
}
},
{
"annotation_id": "ann-mnubwxmg-4qqv21q",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-005",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-005",
"message_index": 1,
"rating": 3,
"comment": "нужен анализ маршрута для ответа на этот вопрос - расшщирение доменов - простой вопрос - показываем просто открытые договора без приходов денег",
"manual_case_decision": "candidate_for_implementation",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:46:41.463Z",
"updated_at": "2026-04-11T12:46:41.463Z",
"context": {
"message_id": "msg-2kDN4UKCbY",
"trace_id": "address-L0WwEsakCe",
"reply_type": "partial_coverage",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "В каких случаях мы видим ситуацию, когда документы есть, а денег - нет и пока не предвидится?",
"answer_text": "Чтобы ответить надежно, нужен более точный ориентир в запросе.\nКоротко: в запросе не хватает конкретного ориентира (контрагент, договор или период).\nЧто можно сделать дальше: уточните точное имя контрагента или добавьте ИНН."
}
},
{
"annotation_id": "ann-mnubxf3s-bkoxdg5",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-006",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-006",
"message_index": 1,
"rating": 3,
"comment": "нужен анализ маршрута для ответа на этот вопрос - расшщирение доменов - надо внедрять",
"manual_case_decision": "candidate_for_implementation",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:47:04.120Z",
"updated_at": "2026-04-11T12:47:04.120Z",
"context": {
"message_id": "msg--ZmllegVvV",
"trace_id": "cG51b5sIOWKwTi",
"reply_type": "clarification_required",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "Какие контрагенты висят с закрытыми отгрузками, но с открытыми документами оплаты, что явно выглядит как кейс для ручной проверки?",
"answer_text": "Сейчас не могу надежно ответить по сценарию взаиморасчетов 60/62: не хватает опоры. Чтобы сразу перейти к проверке, уточни:\n- Укажи период проверки (например, июль 2020).\n- Укажи счет или связку счетов (например, 60/62, 19/68 или 20/44).\n- Уточни тип отклонения: разрыв цепочки, неверное закрытие или аномальный хвост.\n- Закрой уточнения для требований: R1. Если удобнее, могу начать с близкого сценария:\n- Взаиморасчеты 60/62: найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.\n- НДС 19/68: проверить цепочку документ -> счет-фактура -> регистр -> книга."
}
},
{
"annotation_id": "ann-mnubyvd4-kv1ycys",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-009",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-009",
"message_index": 1,
"rating": 3,
"comment": "покеаываем клиентов с открытими договорами которые висят более месяца бенз денег - выводимм том 10",
"manual_case_decision": "candidate_for_implementation",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:48:11.847Z",
"updated_at": "2026-04-11T12:48:11.847Z",
"context": {
"message_id": "msg-6GpvEMKGcQ",
"trace_id": "address-lGGvDiH21w",
"reply_type": "partial_coverage",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?",
"answer_text": "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима.\nКоротко: этот сценарий пока не поддержан в текущем адресном контуре.\nЧто можно сделать дальше: могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету."
}
},
{
"annotation_id": "ann-mnubzypb-okrr0gn",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-013",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-013",
"message_index": 1,
"rating": 3,
"comment": "однозначно на расширение доменов",
"manual_case_decision": "candidate_for_implementation",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:49:02.831Z",
"updated_at": "2026-04-11T12:49:02.831Z",
"context": {
"message_id": "msg-bZZQxlpKcO",
"trace_id": "address-JiGLTVe3_0",
"reply_type": "partial_coverage",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "По каким контрагентам мы можем заметить такую картину: оплачено меньше, чем отгружено, и это явно требует вмешательства бухгалтера.",
"answer_text": "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима.\nКоротко: этот сценарий пока не поддержан в текущем адресном контуре.\nЧто можно сделать дальше: могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету."
}
},
{
"annotation_id": "ann-mnuc1qax-x2caipm",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-014",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-014",
"message_index": 1,
"rating": 3,
"comment": "проблема тыт шум - важно показать договора с незакрытыми доками - топ по времени висения - от самой длинной дистании до кооротной",
"manual_case_decision": "needs_dialog_policy_fix",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:50:25.257Z",
"updated_at": "2026-04-11T12:50:25.257Z",
"context": {
"message_id": "msg-3Leq0R75YH",
"trace_id": "E1O-azugbPvNG9",
"reply_type": "clarification_required",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "Какие незакрытые документы по договорам у нас уже давно пора проверить - это грозит серьезными проблемами?",
"answer_text": "По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона. Что могу сделать рядом по смыслу:\n- Взаиморасчеты 60/62: найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.\n- НДС 19/68: проверить цепочку документ -> счет-фактура -> регистр -> книга.\n- Закрытие месяца 20/44: проверить распределение затрат и остатки после регламентных операций. Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
}
},
{
"annotation_id": "ann-mnuc2887-4k0f7x0",
"run_id": "assistant-stage1-ywEyJgFkC4",
"case_id": "AUTO-015",
"session_id": "assistant-stage1-ywEyJgFkC4-AUTO-015",
"message_index": 1,
"rating": 3,
"comment": "нужен анализ и отработка домена",
"manual_case_decision": "candidate_for_implementation",
"annotation_author": "manual_reviewer",
"resolved": false,
"resolved_at": null,
"resolved_by": null,
"created_at": "2026-04-11T12:50:48.487Z",
"updated_at": "2026-04-11T12:50:48.487Z",
"context": {
"message_id": "msg-6Fqd6XrBv2",
"trace_id": "address-TnBvKVAVfu",
"reply_type": "partial_coverage",
"eval_target": "assistant_stage1",
"prompt_version": "address_query_runtime_v1",
"domain": null,
"query_class": null,
"question_text": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться.",
"answer_text": "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима.\nКоротко: этот сценарий пока не поддержан в текущем адресном контуре.\nЧто можно сделать дальше: могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету."
}
}
]

View File

@ -1,4 +1,74 @@
[
{
"generation_id": "gen-mnubheq4-7h5v00u",
"created_at": "2026-04-11T12:34:37.133Z",
"mode": "qwen_seed",
"count": 15,
"domain": null,
"questions": [
"Кому из контрагентов мы уже месяц отдаем товары, но на счетах все еще красуется минусовое сальдо - это реально зеленый свет для ручного вмешательства?",
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?",
"Покажи контрагентов, по которым сальдо у нас выглядит так, будто оно врет - ну точно не совпадает с тем, что они нам прислали. Это уже критично для сверки.",
"Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?",
"В каких случаях мы видим ситуацию, когда документы есть, а денег - нет и пока не предвидится?",
"Какие контрагенты висят с закрытыми отгрузками, но с открытыми документами оплаты, что явно выглядит как кейс для ручной проверки?",
"Покажи контрагентов, у которых есть неоплаченные задолженности по договорам на конец месяца - это уже красный свет для бухгалтера.",
"По каким заказчикам мы можем выделить непростую картину: сальдо нулевое, а история платежей явно говорит о том, что все не так просто?",
"Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?",
"В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности.",
"Покажи контрагентов, по которым на конец месяца сальдо выглядит так, будто документы собраны криво и их нужно перепроверить.",
"Какие у нас зависшие авансы или предоплаты уже давно пора либо закрыть, либо хотя бы проверить - это уже не просто вопрос времени?",
"По каким контрагентам мы можем заметить такую картину: оплачено меньше, чем отгружено, и это явно требует вмешательства бухгалтера.",
"Какие незакрытые документы по договорам у нас уже давно пора проверить - это грозит серьезными проблемами?",
"Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_qwen_seed_20260411123437_gen-mnubheq4-7h5v00u.json",
"context": {
"llm_provider": "local",
"model": "Qwen2.5 14B Instruct 1M",
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": "normalizer_v2_0_2",
"prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.\nYou are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and ",
"autogen_personality_id": "general",
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты"
}
},
{
"generation_id": "gen-mnua8bfg-00u7c2z",
"created_at": "2026-04-11T11:59:33.340Z",
"mode": "qwen_seed",
"count": 15,
"domain": null,
"questions": [
"questions",
"По каким покупателям у нас долгий хвост на конец месяца, и это уже больше похоже на системную проблему с цепочкой взаиморасчетов?",
"йо По каким поставщикам у нас на конец месяца остались хвосты, которые уже не похожи на обычную задержку документов, а выглядят как реальная проблема в цепочке?",
"слушай Где по покупателям у нас висит история \"отгрузили - денег нет - закрытия нет\", и по каким контрагентам это уже требует ручной проверки? без воды",
"подскажи плиз Покажи контрагентов, по которым сальдо у нас, скорее всего, не совпадет с их актом сверки, если его запросить прямо сейчас. по факту",
"короче Где у нас есть оплаты, но не хватает документов, которые должны были закрыть взаиморасчеты? и коротко",
"мож По каким контрагентам, наоборот, документы есть, а нормального закрытия оплатами не видно? прям сейчас",
"а ну-ка Есть ли такие зависшие авансы, которые уже давно надо было либо закрыть, либо хотя бы перепроверить руками? за весь период",
"йо Какие реализации на конец периода выглядят так, будто они зависли и будут портить картину по выручке, если их не проверить заранее?",
"слушай По каким отгрузкам видно, что проблема не просто в том, что клиент не оплатил, а в том, что сама связка документов собрана криво? без воды",
"подскажи плиз Покажи реализации, где хвост выглядит особенно неприятно: сумма не маленькая, возраст хвоста уже заметный, и при этом не видно нормального завершения цепочки. по факту",
"короче Где по 90/62 история похожа на \"вроде все проведено, но если копнуть, закрытие держится на кривой связке\"? и коротко",
"мож Есть ли случаи, где реализация попала в период, а подтверждающие документы или оплата до сих пор живут в какой-то полуразобранной логике? прям сейчас",
"а ну-ка По каким продажам на конец месяца видно, что бухгалтер потом будет долго распутывать, почему все это не сошлось нормально? за весь период",
"йо Какие банковские движения выглядят так, будто выписка есть, а нормального отражения в учете под ней не хватает?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_qwen_seed_20260411115933_gen-mnua8bfg-00u7c2z.json",
"context": {
"llm_provider": "local",
"model": "Qwen2.5 14B Instruct 1M",
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": "normalizer_v2_0_2",
"prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.\nYou are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and ",
"autogen_personality_id": "general",
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты"
}
},
{
"generation_id": "gen-mnte8abx-ax3v3tr",
"created_at": "2026-04-10T21:03:44.205Z",

View File

@ -0,0 +1,254 @@
{
"suite_id": "assistant_autogen_gen-mnua8bfg-00u7c2z",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_suite_v0_1",
"generated_at": "2026-04-11T11:59:33.340Z",
"generation_id": "gen-mnua8bfg-00u7c2z",
"mode": "qwen_seed",
"domain": null,
"scenario_count": 15,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010",
"AUTO-011",
"AUTO-012",
"AUTO-013",
"AUTO-014",
"AUTO-015"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "questions"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-002",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "По каким покупателям у нас долгий хвост на конец месяца, и это уже больше похоже на системную проблему с цепочкой взаиморасчетов?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-003",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо По каким поставщикам у нас на конец месяца остались хвосты, которые уже не похожи на обычную задержку документов, а выглядят как реальная проблема в цепочке?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-004",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "слушай Где по покупателям у нас висит история \"отгрузили - денег нет - закрытия нет\", и по каким контрагентам это уже требует ручной проверки? без воды"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-005",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "подскажи плиз Покажи контрагентов, по которым сальдо у нас, скорее всего, не совпадет с их актом сверки, если его запросить прямо сейчас. по факту"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-006",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "короче Где у нас есть оплаты, но не хватает документов, которые должны были закрыть взаиморасчеты? и коротко"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-007",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "мож По каким контрагентам, наоборот, документы есть, а нормального закрытия оплатами не видно? прям сейчас"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-008",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "а ну-ка Есть ли такие зависшие авансы, которые уже давно надо было либо закрыть, либо хотя бы перепроверить руками? за весь период"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-009",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо Какие реализации на конец периода выглядят так, будто они зависли и будут портить картину по выручке, если их не проверить заранее?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-010",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "слушай По каким отгрузкам видно, что проблема не просто в том, что клиент не оплатил, а в том, что сама связка документов собрана криво? без воды"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-011",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "подскажи плиз Покажи реализации, где хвост выглядит особенно неприятно: сумма не маленькая, возраст хвоста уже заметный, и при этом не видно нормального завершения цепочки. по факту"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-012",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "короче Где по 90/62 история похожа на \"вроде все проведено, но если копнуть, закрытие держится на кривой связке\"? и коротко"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-013",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "мож Есть ли случаи, где реализация попала в период, а подтверждающие документы или оплата до сих пор живут в какой-то полуразобранной логике? прям сейчас"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-014",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "а ну-ка По каким продажам на конец месяца видно, что бухгалтер потом будет долго распутывать, почему все это не сошлось нормально? за весь период"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-015",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо Какие банковские движения выглядят так, будто выписка есть, а нормального отражения в учете под ней не хватает?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
}
]
}

View File

@ -0,0 +1,254 @@
{
"suite_id": "assistant_autogen_gen-mnubheq4-7h5v00u",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_suite_v0_1",
"generated_at": "2026-04-11T12:34:37.132Z",
"generation_id": "gen-mnubheq4-7h5v00u",
"mode": "qwen_seed",
"domain": null,
"scenario_count": 15,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010",
"AUTO-011",
"AUTO-012",
"AUTO-013",
"AUTO-014",
"AUTO-015"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Кому из контрагентов мы уже месяц отдаем товары, но на счетах все еще красуется минусовое сальдо - это реально зеленый свет для ручного вмешательства?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-002",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-003",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, по которым сальдо у нас выглядит так, будто оно врет - ну точно не совпадает с тем, что они нам прислали. Это уже критично для сверки."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-004",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-005",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "В каких случаях мы видим ситуацию, когда документы есть, а денег - нет и пока не предвидится?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-006",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие контрагенты висят с закрытыми отгрузками, но с открытыми документами оплаты, что явно выглядит как кейс для ручной проверки?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-007",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, у которых есть неоплаченные задолженности по договорам на конец месяца - это уже красный свет для бухгалтера."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-008",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "По каким заказчикам мы можем выделить непростую картину: сальдо нулевое, а история платежей явно говорит о том, что все не так просто?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-009",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-010",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-011",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, по которым на конец месяца сальдо выглядит так, будто документы собраны криво и их нужно перепроверить."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-012",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие у нас зависшие авансы или предоплаты уже давно пора либо закрыть, либо хотя бы проверить - это уже не просто вопрос времени?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-013",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "По каким контрагентам мы можем заметить такую картину: оплачено меньше, чем отгружено, и это явно требует вмешательства бухгалтера."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-014",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие незакрытые документы по договорам у нас уже давно пора проверить - это грозит серьезными проблемами?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-015",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
}
]
}

View File

@ -0,0 +1,238 @@
{
"suite_id": "assistant_autogen_runtime_job-pIsvZJjub9",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_runtime_v0_1",
"scenario_count": 19,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010",
"AUTO-011",
"AUTO-012",
"AUTO-013",
"AUTO-014",
"AUTO-015",
"AUTO-016",
"AUTO-017",
"AUTO-018",
"AUTO-019"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "questions"
}
]
},
{
"case_id": "AUTO-002",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "По каким покупателям у нас долгий хвост на конец месяца, и это уже больше похоже на системную проблему с цепочкой взаиморасчетов?"
}
]
},
{
"case_id": "AUTO-003",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо По каким поставщикам у нас на конец месяца остались хвосты, которые уже не похожи на обычную задержку документов, а выглядят как реальная проблема в цепочке?"
}
]
},
{
"case_id": "AUTO-004",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "слушай Где по покупателям у нас висит история \"отгрузили - денег нет - закрытия нет\", и по каким контрагентам это уже требует ручной проверки?"
}
]
},
{
"case_id": "AUTO-005",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "без воды?"
}
]
},
{
"case_id": "AUTO-006",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "подскажи плиз Покажи контрагентов, по которым сальдо у нас, скорее всего, не совпадет с их актом сверки, если его запросить прямо сейчас. по факту"
}
]
},
{
"case_id": "AUTO-007",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "короче Где у нас есть оплаты, но не хватает документов, которые должны были закрыть взаиморасчеты?"
}
]
},
{
"case_id": "AUTO-008",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "и коротко?"
}
]
},
{
"case_id": "AUTO-009",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "мож По каким контрагентам, наоборот, документы есть, а нормального закрытия оплатами не видно?"
}
]
},
{
"case_id": "AUTO-010",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "прям сейчас?"
}
]
},
{
"case_id": "AUTO-011",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "а ну-ка Есть ли такие зависшие авансы, которые уже давно надо было либо закрыть, либо хотя бы перепроверить руками?"
}
]
},
{
"case_id": "AUTO-012",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "за весь период?"
}
]
},
{
"case_id": "AUTO-013",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо Какие реализации на конец периода выглядят так, будто они зависли и будут портить картину по выручке, если их не проверить заранее?"
}
]
},
{
"case_id": "AUTO-014",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "слушай По каким отгрузкам видно, что проблема не просто в том, что клиент не оплатил, а в том, что сама связка документов собрана криво?"
}
]
},
{
"case_id": "AUTO-015",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "подскажи плиз Покажи реализации, где хвост выглядит особенно неприятно: сумма не маленькая, возраст хвоста уже заметный, и при этом не видно нормального завершения цепочки. по факту"
}
]
},
{
"case_id": "AUTO-016",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "короче Где по 90/62 история похожа на \"вроде все проведено, но если копнуть, закрытие держится на кривой связке\"?"
}
]
},
{
"case_id": "AUTO-017",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "мож Есть ли случаи, где реализация попала в период, а подтверждающие документы или оплата до сих пор живут в какой-то полуразобранной логике?"
}
]
},
{
"case_id": "AUTO-018",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "а ну-ка По каким продажам на конец месяца видно, что бухгалтер потом будет долго распутывать, почему все это не сошлось нормально?"
}
]
},
{
"case_id": "AUTO-019",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо Какие банковские движения выглядят так, будто выписка есть, а нормального отражения в учете под ней не хватает?"
}
]
}
]
}

View File

@ -0,0 +1,190 @@
{
"suite_id": "assistant_autogen_runtime_job-tvH0FxMgzD",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_runtime_v0_1",
"scenario_count": 15,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010",
"AUTO-011",
"AUTO-012",
"AUTO-013",
"AUTO-014",
"AUTO-015"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Кому из контрагентов мы уже месяц отдаем товары, но на счетах все еще красуется минусовое сальдо - это реально зеленый свет для ручного вмешательства?"
}
]
},
{
"case_id": "AUTO-002",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?"
}
]
},
{
"case_id": "AUTO-003",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, по которым сальдо у нас выглядит так, будто оно врет - ну точно не совпадает с тем, что они нам прислали. Это уже критично для сверки."
}
]
},
{
"case_id": "AUTO-004",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?"
}
]
},
{
"case_id": "AUTO-005",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "В каких случаях мы видим ситуацию, когда документы есть, а денег - нет и пока не предвидится?"
}
]
},
{
"case_id": "AUTO-006",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие контрагенты висят с закрытыми отгрузками, но с открытыми документами оплаты, что явно выглядит как кейс для ручной проверки?"
}
]
},
{
"case_id": "AUTO-007",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, у которых есть неоплаченные задолженности по договорам на конец месяца - это уже красный свет для бухгалтера."
}
]
},
{
"case_id": "AUTO-008",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "По каким заказчикам мы можем выделить непростую картину: сальдо нулевое, а история платежей явно говорит о том, что все не так просто?"
}
]
},
{
"case_id": "AUTO-009",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?"
}
]
},
{
"case_id": "AUTO-010",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности."
}
]
},
{
"case_id": "AUTO-011",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, по которым на конец месяца сальдо выглядит так, будто документы собраны криво и их нужно перепроверить."
}
]
},
{
"case_id": "AUTO-012",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие у нас зависшие авансы или предоплаты уже давно пора либо закрыть, либо хотя бы проверить - это уже не просто вопрос времени?"
}
]
},
{
"case_id": "AUTO-013",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "По каким контрагентам мы можем заметить такую картину: оплачено меньше, чем отгружено, и это явно требует вмешательства бухгалтера."
}
]
},
{
"case_id": "AUTO-014",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие незакрытые документы по договорам у нас уже давно пора проверить - это грозит серьезными проблемами?"
}
]
},
{
"case_id": "AUTO-015",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
}
]
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-GmILXRUMgZ",
"timestamp": "2026-04-11T12:28:27.408Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "-Xq2kvvsT5YnbS",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "LOJZh5gvT38Lh8",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "33PFaWdNYgR-i0",
"request_count_for_case": 0
}
]
}