ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.2.1 - фикс деградаций по старым доменам + легкая доводка регрессий перед стартом 3его этапа
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -73,6 +73,67 @@ function normalizeQuestionChunk(value: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
const RUNTIME_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
|
||||
const RUNTIME_QUESTION_TAIL_PATTERNS: RegExp[] = [
|
||||
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
|
||||
];
|
||||
|
||||
function stripQuestionSuffix(value: string): string {
|
||||
return normalizeQuestionChunk(value).replace(/[?!.:,;]+$/u, "").trim();
|
||||
}
|
||||
|
||||
function isRuntimeQuestionPlaceholder(value: string): boolean {
|
||||
const core = stripQuestionSuffix(value).toLowerCase();
|
||||
return core.length > 0 && RUNTIME_QUESTION_PLACEHOLDER_PATTERN.test(core);
|
||||
}
|
||||
|
||||
function isLikelyRuntimeQuestionTail(value: string): boolean {
|
||||
const core = stripQuestionSuffix(value).toLowerCase();
|
||||
if (!core) {
|
||||
return false;
|
||||
}
|
||||
if (isRuntimeQuestionPlaceholder(core)) {
|
||||
return true;
|
||||
}
|
||||
return RUNTIME_QUESTION_TAIL_PATTERNS.some((pattern) => pattern.test(core));
|
||||
}
|
||||
|
||||
function mergeRuntimeQuestionTail(baseQuestion: string, tail: string): string {
|
||||
const base = stripQuestionSuffix(baseQuestion);
|
||||
const suffix = stripQuestionSuffix(tail);
|
||||
if (!base) {
|
||||
return suffix ? `${suffix}?` : "";
|
||||
}
|
||||
if (!suffix) {
|
||||
return `${base}?`;
|
||||
}
|
||||
return `${base} ${suffix}?`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizeRuntimeQuestionList(items: string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
for (const item of items) {
|
||||
const chunk = normalizeQuestionChunk(item);
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
if (isRuntimeQuestionPlaceholder(chunk)) {
|
||||
continue;
|
||||
}
|
||||
if (isLikelyRuntimeQuestionTail(chunk) && normalized.length > 0) {
|
||||
const merged = mergeRuntimeQuestionTail(normalized[normalized.length - 1], chunk);
|
||||
if (merged) {
|
||||
normalized[normalized.length - 1] = merged;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
normalized.push(chunk);
|
||||
}
|
||||
return normalized.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
function splitQuestionCandidate(raw: string): string[] {
|
||||
const normalized = String(raw ?? "").replace(/\r/g, "\n").trim();
|
||||
if (!normalized) {
|
||||
@@ -87,18 +148,32 @@ function splitQuestionCandidate(raw: string): string[] {
|
||||
|
||||
const chunks: string[] = [];
|
||||
for (const line of source) {
|
||||
const normalizedLine = normalizeQuestionChunk(line);
|
||||
if (!normalizedLine || isRuntimeQuestionPlaceholder(normalizedLine)) {
|
||||
continue;
|
||||
}
|
||||
const questionLike = Array.from(line.matchAll(/[^?]+(?:\?|$)/g))
|
||||
.map((match) => normalizeQuestionChunk(match[0]))
|
||||
.filter((item) => item.length > 0);
|
||||
if (questionLike.length > 1) {
|
||||
for (const item of questionLike) {
|
||||
chunks.push(item.endsWith("?") ? item : `${item}?`);
|
||||
const canSafelySplit = questionLike.every(
|
||||
(item) =>
|
||||
!isRuntimeQuestionPlaceholder(item) &&
|
||||
!isLikelyRuntimeQuestionTail(item) &&
|
||||
normalizeQuestionChunk(item).length >= 18
|
||||
);
|
||||
if (canSafelySplit) {
|
||||
for (const item of questionLike) {
|
||||
chunks.push(item.endsWith("?") ? item : `${item}?`);
|
||||
}
|
||||
} else {
|
||||
chunks.push(normalizedLine);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
chunks.push(normalizeQuestionChunk(line));
|
||||
chunks.push(normalizedLine);
|
||||
}
|
||||
return chunks.filter((item) => item.length > 0);
|
||||
return normalizeRuntimeQuestionList(chunks);
|
||||
}
|
||||
|
||||
function normalizeRuntimeQuestions(value: unknown): string[] {
|
||||
@@ -109,7 +184,7 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
const expanded = raw.flatMap((item) => splitQuestionCandidate(item));
|
||||
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
|
||||
const deduped: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of expanded) {
|
||||
@@ -122,6 +197,11 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
|
||||
return deduped;
|
||||
}
|
||||
|
||||
export const __evalRouteTestUtils = {
|
||||
splitQuestionCandidate,
|
||||
normalizeRuntimeQuestions
|
||||
};
|
||||
|
||||
function normalizeCaseIds(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
|
||||
@@ -1854,6 +1854,146 @@ function buildAnswerSummary(mode: PolicyMode): string {
|
||||
|
||||
type P0NarrativeDomain = "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44" | null;
|
||||
|
||||
interface BoundaryCapabilitySuggestion {
|
||||
key: "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44";
|
||||
label: string;
|
||||
helpText: string;
|
||||
signals: RegExp;
|
||||
}
|
||||
|
||||
const BOUNDARY_CAPABILITY_SUGGESTIONS: BoundaryCapabilitySuggestion[] = [
|
||||
{
|
||||
key: "settlements_60_62",
|
||||
label: "Взаиморасчеты 60/62",
|
||||
helpText: "найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.",
|
||||
signals: /(контраг|долг|сальдо|взаиморасчет|оплат|аванс|покупат|поставщ|банк|выписк|\b60\b|\b62\b|\b76\b)/iu
|
||||
},
|
||||
{
|
||||
key: "vat_document_register_book",
|
||||
label: "НДС 19/68",
|
||||
helpText: "проверить цепочку документ -> счет-фактура -> регистр -> книга.",
|
||||
signals: /(ндс|сч[её]т[-\s]?фактур|регистр|книга\s+покуп|книга\s+продаж|декларац|\b19\b|\b68\b)/iu
|
||||
},
|
||||
{
|
||||
key: "month_close_costs_20_44",
|
||||
label: "Закрытие месяца 20/44",
|
||||
helpText: "проверить распределение затрат и остатки после регламентных операций.",
|
||||
signals: /(закрыти[ея]|месяц|затрат|распределени|рбп|аморт|основн|ос\b|\b20\b|\b25\b|\b26\b|\b44\b)/iu
|
||||
}
|
||||
];
|
||||
|
||||
function formatNarrativeDomainLabel(domain: P0NarrativeDomain): string {
|
||||
if (domain === "settlements_60_62") {
|
||||
return "взаиморасчетов 60/62";
|
||||
}
|
||||
if (domain === "vat_document_register_book") {
|
||||
return "НДС-контура 19/68";
|
||||
}
|
||||
if (domain === "month_close_costs_20_44") {
|
||||
return "закрытия месяца (20/44)";
|
||||
}
|
||||
return "доступного учетного контура";
|
||||
}
|
||||
|
||||
function pickBoundaryCapabilityLines(userMessage: string, limit = 3): string[] {
|
||||
const text = String(userMessage ?? "").toLowerCase();
|
||||
const scored = BOUNDARY_CAPABILITY_SUGGESTIONS.map((item, index) => ({
|
||||
item,
|
||||
score: (text.match(item.signals) ?? []).length,
|
||||
order: index
|
||||
}));
|
||||
const ranked = scored
|
||||
.slice()
|
||||
.sort((left, right) => right.score - left.score || left.order - right.order)
|
||||
.map((entry) => entry.item);
|
||||
const selected = ranked.slice(0, Math.max(2, limit));
|
||||
return uniqueStrings(selected.map((item) => `${item.label}: ${item.helpText}`), limit);
|
||||
}
|
||||
|
||||
function buildNaturalClarificationHints(input: {
|
||||
missingAnchors: MissingAnchors;
|
||||
coverageReport: RequirementCoverageReport;
|
||||
}): string[] {
|
||||
const hints: string[] = [];
|
||||
if (input.missingAnchors.period) {
|
||||
hints.push("Укажи период проверки (например, июль 2020).");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
hints.push("Укажи счет или связку счетов (например, 60/62, 19/68 или 20/44).");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
hints.push("Добавь контрагента или договор, чтобы зафиксировать контур проверки.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
hints.push("Укажи документ или объект, от которого строить проверку цепочки.");
|
||||
}
|
||||
if (input.missingAnchors.anomalyType) {
|
||||
hints.push("Уточни тип отклонения: разрыв цепочки, неверное закрытие или аномальный хвост.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
hints.push(`Закрой уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
return uniqueStrings(hints, 5);
|
||||
}
|
||||
|
||||
function shouldUseBoundaryFallbackReply(input: {
|
||||
mode: PolicyMode;
|
||||
groundingCheck: AnswerGroundingCheck;
|
||||
coverageReport: RequirementCoverageReport;
|
||||
okResultsCount: number;
|
||||
partialResultsCount: number;
|
||||
}): boolean {
|
||||
if (input.mode === "out_of_scope") {
|
||||
return true;
|
||||
}
|
||||
if (input.mode !== "clarification_required" && input.mode !== "no_grounded") {
|
||||
return false;
|
||||
}
|
||||
const hasNoEvidenceRoutes = input.okResultsCount === 0 && input.partialResultsCount === 0;
|
||||
const hasNoConfirmedCoverage =
|
||||
input.coverageReport.requirements_covered === 0 &&
|
||||
input.coverageReport.requirements_partially_covered.length === 0;
|
||||
const groundingBlocked =
|
||||
input.groundingCheck.status === "no_grounded_answer" ||
|
||||
input.groundingCheck.status === "partial" ||
|
||||
input.groundingCheck.status === "route_mismatch_blocked";
|
||||
return hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked;
|
||||
}
|
||||
|
||||
function buildBoundaryFallbackReply(input: {
|
||||
userMessage: string;
|
||||
focusDomain: P0NarrativeDomain;
|
||||
missingAnchors: MissingAnchors;
|
||||
coverageReport: RequirementCoverageReport;
|
||||
}): string {
|
||||
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
|
||||
if (input.focusDomain === null) {
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
||||
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
|
||||
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
const clarificationHints = buildNaturalClarificationHints({
|
||||
missingAnchors: input.missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
});
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
|
||||
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
|
||||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
function ensureSentence(value: string): string {
|
||||
const sanitized = sanitizeUserText(value) ?? String(value ?? "").trim();
|
||||
const normalized = sanitized.replace(/\s+/g, " ").trim();
|
||||
@@ -4219,6 +4359,13 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
normalizationPeriodExplicit: Boolean(input.normalizationPeriodExplicit),
|
||||
companyAnchors: input.companyAnchors ?? null
|
||||
});
|
||||
const useBoundaryFallbackReply = shouldUseBoundaryFallbackReply({
|
||||
mode: guardedDecision.mode,
|
||||
groundingCheck: input.groundingCheck,
|
||||
coverageReport: input.coverageReport,
|
||||
okResultsCount: okResults.length,
|
||||
partialResultsCount: partialResults.length
|
||||
});
|
||||
const hasProblemWeakSignal =
|
||||
policySignals.narrowing_strength !== "strong" ||
|
||||
policySignals.minimum_evidence_failed ||
|
||||
@@ -4238,6 +4385,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
(guardedDecision.mode === "focused_grounded" && hasProblemWeakSignal);
|
||||
const shouldUseProblemCentricAnswer =
|
||||
Boolean(input.enableProblemCentricAnswerV1) &&
|
||||
!useBoundaryFallbackReply &&
|
||||
!hardBlockedMode &&
|
||||
problemCentricModeEligible &&
|
||||
(!focusedStrong || hasProblemWeakSignal) &&
|
||||
@@ -4382,13 +4530,22 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
}
|
||||
};
|
||||
|
||||
const finalAssistantReply = useBoundaryFallbackReply
|
||||
? buildBoundaryFallbackReply({
|
||||
userMessage: input.userMessage,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
})
|
||||
: renderPolicyReply(answerStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
|
||||
return {
|
||||
assistant_reply: renderPolicyReply(answerStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage,
|
||||
userMessage: input.userMessage
|
||||
}),
|
||||
assistant_reply: finalAssistantReply,
|
||||
fallback_type: guardedDecision.fallback_type,
|
||||
reply_type: guardedDecision.reply_type,
|
||||
answer_structure_v11: answerStructure,
|
||||
|
||||
@@ -2986,12 +2986,12 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||
const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence);
|
||||
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client|\b[a-z]{2,}\b)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||
const hasLlmCanonicalSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
((llmContractMode === "address_query" && llmContractModeConfidence !== "low") ||
|
||||
(llmCanonicalAppliedSignal &&
|
||||
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal || llmContractMode === "unsupported")));
|
||||
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal)));
|
||||
const hasLlmCanonicalDataSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
Boolean(llmPreDecomposeMeta?.applied) &&
|
||||
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
|
||||
@@ -3133,6 +3133,42 @@ function hasDirectDeepAnalysisSignal(text) {
|
||||
}
|
||||
return /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized);
|
||||
}
|
||||
function hasStrictDeepInvestigationCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasInvestigativeVerb = /(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u0440\u0430\u0437\u043b\u043e\u0436(?:\u0438|\u0438\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|root\s*cause|trace\s*chain)/iu.test(normalized);
|
||||
if (!hasInvestigativeVerb) {
|
||||
return false;
|
||||
}
|
||||
return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|lifecycle|state\s+transition)/iu.test(normalized);
|
||||
}
|
||||
function hasAggregateBusinessAnalyticsSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasMetricCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u043c\u0430\u0440\u0436|\u0440\u0435\u043d\u0442\u0430\u0431\u0435\u043b|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b|turnover|revenue|profit|margin)/iu.test(normalized);
|
||||
if (!hasMetricCue) {
|
||||
return false;
|
||||
}
|
||||
const hasRankingOrTrendCue = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0442\u043e\u043f|\u043b\u0443\u0447\u0448|\u0445\u0443\u0434\u0448|\u043c\u0430\u043a\u0441(?:\u0438\u043c\u0443\u043c)?|\u043c\u0438\u043d(?:\u0438\u043c\u0443\u043c)?|\u0434\u0438\u043d\u0430\u043c|\u0442\u0440\u0435\u043d\u0434|\u0441\u0440\u0430\u0432\u043d|ranking|top|best|worst)/iu.test(normalized);
|
||||
const hasPeriodAggregateCue = /(?:\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u0433\u043e\u0434(?:\u0430|\u0443|\u044b)?|year|years|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446|\u043f\u0435\u0440\u0438\u043e\u0434)/iu.test(normalized);
|
||||
return hasRankingOrTrendCue || hasPeriodAggregateCue;
|
||||
}
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"list_documents_by_contract",
|
||||
"bank_operations_by_contract",
|
||||
"list_documents_by_counterparty",
|
||||
"bank_operations_by_counterparty",
|
||||
"list_contracts_by_counterparty",
|
||||
"contract_usage_overview",
|
||||
"contract_usage_and_value",
|
||||
"vat_payable_forecast"
|
||||
]);
|
||||
export function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
@@ -3154,9 +3190,21 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
hasDataRetrievalRequestSignal(repairedRawUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(effectiveAddressUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage);
|
||||
const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
|
||||
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
|
||||
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
|
||||
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
|
||||
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) ||
|
||||
hasStrictDeepInvestigationCue(repairedRawUserMessage) ||
|
||||
hasStrictDeepInvestigationCue(effectiveAddressUserMessage) ||
|
||||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
|
||||
const keepAddressLaneByIntent = Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent))) &&
|
||||
!strictDeepInvestigationCueDetected;
|
||||
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
|
||||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
|
||||
hasStrongDataIntentSignal(effectiveAddressUserMessage) ||
|
||||
@@ -3232,7 +3280,7 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||
llmPreDecomposeMeta?.applied &&
|
||||
(llmContractMode === "address_query" || llmContractMode === "unsupported")) ||
|
||||
llmContractMode === "address_query") ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) ||
|
||||
@@ -3245,10 +3293,11 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
|
||||
const unsupportedIntentOrMode = modeDetection.mode !== "address_query" &&
|
||||
(intentResolution.intent === "unknown" || llmContractMode === "unsupported");
|
||||
const unsupportedAddressIntentFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
modeDetection.mode !== "address_query" &&
|
||||
intentResolution.intent === "unknown" &&
|
||||
unsupportedIntentOrMode &&
|
||||
strongDataSignal &&
|
||||
!preserveAddressLaneSignal);
|
||||
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
||||
@@ -3264,8 +3313,13 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||||
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
deepAnalysisPreferenceDetected &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!vatExplainFollowupSignal &&
|
||||
(!followupContext || !dataRetrievalSignal));
|
||||
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
aggregateBusinessAnalyticsSignal &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!followupContext);
|
||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
hasDeepSessionContinuationSignal({
|
||||
@@ -3288,6 +3342,13 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
toolGateDecision = "skip_address_lane";
|
||||
toolGateReason = "deep_analysis_signal_fallback_to_deep";
|
||||
}
|
||||
if (aggregateAnalyticsFallbackToDeep &&
|
||||
!unsupportedAddressIntentFallbackToDeep &&
|
||||
!deepAnalysisSignalFallbackToDeep) {
|
||||
runAddressLane = false;
|
||||
toolGateDecision = "skip_address_lane";
|
||||
toolGateReason = "aggregate_analytics_signal_fallback_to_deep";
|
||||
}
|
||||
if (deepSessionContinuationFallbackToDeep) {
|
||||
runAddressLane = false;
|
||||
toolGateDecision = "skip_address_lane";
|
||||
@@ -3312,6 +3373,14 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
reason: "deep_analysis_signal_fallback_to_deep"
|
||||
};
|
||||
}
|
||||
if (aggregateAnalyticsFallbackToDeep &&
|
||||
!unsupportedAddressIntentFallbackToDeep &&
|
||||
!deepAnalysisSignalFallbackToDeep) {
|
||||
livingDecision = {
|
||||
mode: "deep_analysis",
|
||||
reason: "aggregate_analytics_signal_fallback_to_deep"
|
||||
};
|
||||
}
|
||||
if (deepSessionContinuationFallbackToDeep) {
|
||||
livingDecision = {
|
||||
mode: "deep_analysis",
|
||||
@@ -3336,6 +3405,7 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
followup_context_detected: Boolean(followupContext),
|
||||
unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep,
|
||||
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
|
||||
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
|
||||
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
|
||||
final_decision: {
|
||||
run_address_lane: runAddressLane,
|
||||
@@ -3356,6 +3426,11 @@ function hasDataRetrievalRequestSignal(text) {
|
||||
if (!lower) {
|
||||
return false;
|
||||
}
|
||||
const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a)(?:$|[\s,.!?;:])/iu.test(lower);
|
||||
const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(lower);
|
||||
if (hasRussianRetrievalAction && hasRussianRetrievalObject) {
|
||||
return true;
|
||||
}
|
||||
const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower);
|
||||
const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bwhich\b|\bwho\b)/i.test(lower);
|
||||
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
|
||||
@@ -3522,6 +3597,10 @@ function hasAssistantDataScopeMetaQuestionSignal(text) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized);
|
||||
if (hasSlangScopeQuestion) {
|
||||
return true;
|
||||
}
|
||||
const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized);
|
||||
const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized);
|
||||
const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized);
|
||||
|
||||
Reference in New Issue
Block a user