ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.6 Усилина оркестрация и детект data-scope для живого сленга. Убрана шаблонность soft-refusal в limited-ответах. Подрезана словарнаю жирность в фильтр-экстракторе
This commit is contained in:
parent
88d5561fad
commit
351993430f
|
|
@ -2416,6 +2416,43 @@ Implemented in current pass (Stage 3.4 deep/living soft-refusal boundary widenin
|
|||
- Stage 3 focused suite (+boundary): `12` files / `348` tests passed.
|
||||
- Type build: `npm --prefix llm_normalizer/backend run build` passed.
|
||||
|
||||
Implemented in current pass (Stage 3.5 evidence-first soft policy reply):
|
||||
1. Added soft policy renderer for weak deep-policy envelopes:
|
||||
- For weak `broad_partial` and clarification/no-grounded envelopes, final user reply now uses concise non-sectional soft format (`Коротко`, `Что уже проверено`, `Что пока не доказано`, `Что могу сделать сейчас`).
|
||||
- Prevents repeated rigid section template in weak-evidence responses while preserving actionable next steps.
|
||||
2. Added explicit policy gate for soft rendering:
|
||||
- `shouldUseSoftPolicyReply(...)` evaluates mode + coverage gaps + weak evidence signals (`broad_query_detected`, `minimum_evidence_failed`, low confidence, critical limitation reason codes).
|
||||
- Keeps structured sectioned renderer for strong grounded envelopes.
|
||||
3. Preserved strong answer path:
|
||||
- `focused_grounded` / high-confidence cases continue to use structured policy layout with explicit sections.
|
||||
4. Added regression coverage:
|
||||
- New `assistantSoftPolicyReply.test.ts`:
|
||||
- weak broad-partial -> soft non-template reply;
|
||||
- strong grounded -> structured sectioned reply.
|
||||
5. Validation snapshot:
|
||||
- Stage 3 focused suite (+soft policy): `13` files / `350` tests passed.
|
||||
- Type build: `npm --prefix llm_normalizer/backend run build` passed.
|
||||
|
||||
Implemented in current pass (Stage 3.6 route arbitration hardening + followup isolation):
|
||||
1. Isolated standalone address topics from stale followup carryover:
|
||||
- Added standalone-topic detector (`hasStandaloneAddressTopicSignal`) and applied it in followup signal/carryover gates.
|
||||
- Explicit standalone requests with their own anchor (`date/account/object`) no longer inherit previous address followup context by default.
|
||||
2. Hardened orchestration for real regression cases:
|
||||
- Added explicit open-contract lookup signal guard (`hasOpenContractsAddressSignal`) to keep `list_open_contracts` style requests in address lane even with stale deep context.
|
||||
- Extended aggregate fallback arbitration so stale followup context cannot pin standalone aggregate queries in address lane (`aggregate_analytics_signal_fallback_to_deep` remains allowed).
|
||||
3. Improved data-scope slang detection resilience:
|
||||
- Added direct slang lead detection for queries like `по каким конторам можем общаться` / `какая база подрублена`.
|
||||
- Preserved deterministic chat/data-scope routing despite aggressive predecompose rewrites.
|
||||
4. Reduced limited-reply template repetition:
|
||||
- Reworked unsupported/aggregate limited phrasing in `composeLimitedReply(...)` to softer non-rigid variants.
|
||||
- Suppressed redundant `Сигнал запроса` line for unsupported aggregate envelopes.
|
||||
5. Reduced dictionary overfitting pressure in filter extraction:
|
||||
- Shrunk `COUNTERPARTY_TOKEN_NOISE` to compact core stopwords.
|
||||
- Added pattern-based filler/slang/profanity detector (`isCounterpartyFillerToken`) instead of expanding static token lists.
|
||||
6. Validation snapshot:
|
||||
- Targeted regression pack: `4` files / `318` tests passed (`assistantLivingRouter`, `assistantLivingChatMode`, `addressQueryRuntimeM23`, `assistantSoftPolicyReply`).
|
||||
- Type build: `npm --prefix llm_normalizer/backend run build` passed.
|
||||
|
||||
Acceptance (Stage 3):
|
||||
1. LLM outputs strictly validated schema for extraction/decomposition (no free-form).
|
||||
2. Deterministic guards can block or downgrade answers when evidence insufficient.
|
||||
|
|
|
|||
|
|
@ -3,7 +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.__evalRouteAsyncTestUtils = exports.__evalRouteTestUtils = void 0;
|
||||
exports.buildEvalRouter = buildEvalRouter;
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
|
|
@ -153,6 +153,10 @@ exports.__evalRouteTestUtils = {
|
|||
splitQuestionCandidate,
|
||||
normalizeRuntimeQuestions
|
||||
};
|
||||
exports.__evalRouteAsyncTestUtils = {
|
||||
readReportedCaseIds,
|
||||
syncJobWithSessions
|
||||
};
|
||||
function normalizeCaseIds(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
|
|
@ -296,10 +300,28 @@ function readSessionConversation(runId, caseId) {
|
|||
return [];
|
||||
}
|
||||
}
|
||||
function readReportedCaseIds(job) {
|
||||
const output = new Set();
|
||||
const reportRecord = toRecord(job.report);
|
||||
const reportResults = toArray(reportRecord?.results)
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item) => item !== null);
|
||||
for (const result of reportResults) {
|
||||
const caseId = toStringSafe(result.case_id);
|
||||
if (!caseId)
|
||||
continue;
|
||||
output.add(caseId);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function isTerminalCaseStatus(status) {
|
||||
return status === "completed" || status === "failed";
|
||||
}
|
||||
function syncJobWithSessions(job) {
|
||||
if (!job.run_id || !job.eval_target.startsWith("assistant_")) {
|
||||
return;
|
||||
}
|
||||
const reportCaseIds = readReportedCaseIds(job);
|
||||
let completed = 0;
|
||||
let hasRunning = false;
|
||||
for (const item of job.cases) {
|
||||
|
|
@ -307,11 +329,16 @@ function syncJobWithSessions(job) {
|
|||
item.messages = messages;
|
||||
const assistantMessages = messages.filter((entry) => entry.role === "assistant").length;
|
||||
const userMessages = messages.filter((entry) => entry.role === "user").length;
|
||||
if (assistantMessages >= item.turns_total && item.turns_total > 0) {
|
||||
const reportMarkedDone = reportCaseIds.has(item.case_id);
|
||||
if ((assistantMessages >= item.turns_total && item.turns_total > 0) || reportMarkedDone) {
|
||||
item.status = "completed";
|
||||
completed += 1;
|
||||
continue;
|
||||
}
|
||||
if (isTerminalCaseStatus(item.status) && isTerminalCaseStatus(job.status)) {
|
||||
completed += 1;
|
||||
continue;
|
||||
}
|
||||
if (userMessages > 0 || messages.length > 0) {
|
||||
item.status = "running";
|
||||
hasRunning = true;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,82 @@ const MONTH_PERIOD_NUMERIC_YEAR_MONTH_PATTERN = /(?:^|[\s,.;:!?()\-])(?:за|for
|
|||
const MONTH_PERIOD_NAME_PATTERN = /(?:^|[\s,.;:!?()\-])(?:за|for|на|in)?\s*([a-zа-яё]+)\s+(20\d{2})(?:\s*г(?:од|ода|\\.)?)?(?=$|[\s,.;:!?()\-])/iu;
|
||||
const MONTH_PERIOD_NAME_YEAR_FIRST_PATTERN = /(?:^|[\s,.;:!?()\-])(?:за|for|на|in)?\s*(20\d{2})(?:\s*г(?:од|ода|\\.)?)?\s+([a-zа-яё]+)(?=$|[\s,.;:!?()\-])/iu;
|
||||
const DOC_SIGNAL_PATTERN = "(?:док(?:и|умент|ументы|ументов|умам|ума)|docs?|documents?|docy|doci|doki|dokument(?:y|ov|am|a)?)";
|
||||
const COUNTERPARTY_TOKEN_NOISE = new Set([
|
||||
"за",
|
||||
"с",
|
||||
"по",
|
||||
"у",
|
||||
"на",
|
||||
"и",
|
||||
"или",
|
||||
"в",
|
||||
"к",
|
||||
"год",
|
||||
"года",
|
||||
"г",
|
||||
"year",
|
||||
"кто",
|
||||
"что",
|
||||
"где",
|
||||
"когда",
|
||||
"сколько",
|
||||
"почему",
|
||||
"зачем",
|
||||
"какой",
|
||||
"какая",
|
||||
"какие",
|
||||
"каких",
|
||||
"каким",
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"есть",
|
||||
"можно",
|
||||
"могу",
|
||||
"можем",
|
||||
"нет",
|
||||
"покажи",
|
||||
"показать",
|
||||
"скажи",
|
||||
"выведи",
|
||||
"show",
|
||||
"list",
|
||||
"контра",
|
||||
"контре",
|
||||
"контрагент",
|
||||
"компания",
|
||||
"организация",
|
||||
"client",
|
||||
"customer",
|
||||
"supplier",
|
||||
"vendor",
|
||||
"partner",
|
||||
"company",
|
||||
"counterparty"
|
||||
]);
|
||||
function isCounterpartyFillerToken(token) {
|
||||
const normalized = String(token ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:пл[сз]|пж|пжлст|pls|please|пожалуйста)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:бл[яе]|блять|нах|нахуй|епт|ёпт|епта)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:док(?:и|ам|ами|умент(?:ы|ов)?)?|docs?|docy|doci|doki|dokument(?:y|ov|am|a)?)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:pokazh?|pokazhi|pokaji|pokezh|kakie|kakoi|kakaya|est|za|po|na|s|vse|all|poka)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isCounterpartyNoiseToken(rawToken) {
|
||||
const normalized = String(rawToken ?? "").trim().toLowerCase();
|
||||
return COUNTERPARTY_TOKEN_NOISE.has(normalized) || isCounterpartyFillerToken(normalized);
|
||||
}
|
||||
function textMojibakeScore(value) {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||
|
|
@ -425,101 +501,17 @@ function extractLooseByAnchorValue(text) {
|
|||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = token.toLowerCase();
|
||||
const stopWords = new Set([
|
||||
"какой",
|
||||
"какая",
|
||||
"какие",
|
||||
"каких",
|
||||
"каким",
|
||||
"какими",
|
||||
"каком",
|
||||
"кто",
|
||||
"что",
|
||||
"мы",
|
||||
"видим",
|
||||
"контрагенту",
|
||||
"контрагента",
|
||||
"контрагентам",
|
||||
"контре",
|
||||
"компании",
|
||||
"компанию",
|
||||
"организации",
|
||||
"организацию",
|
||||
"поставщику",
|
||||
"поставщика",
|
||||
"поставщикам",
|
||||
"клиенту",
|
||||
"клиента",
|
||||
"клиентам",
|
||||
"покупателю",
|
||||
"покупателя",
|
||||
"покупателям",
|
||||
"заказчикам",
|
||||
"партнеру",
|
||||
"партнера",
|
||||
"договору",
|
||||
"договора",
|
||||
"контракту",
|
||||
"контракта",
|
||||
"счету",
|
||||
"счёту",
|
||||
"дате",
|
||||
"периоду",
|
||||
"период",
|
||||
"есть",
|
||||
"же",
|
||||
"сводные",
|
||||
"сводный",
|
||||
"сводная",
|
||||
"сводную",
|
||||
"сводном",
|
||||
"сводного",
|
||||
"сводному",
|
||||
"неуказанному",
|
||||
"неуказанный",
|
||||
"неуказанная",
|
||||
"неуказанное",
|
||||
"неуказанному",
|
||||
"указанному",
|
||||
"указанный",
|
||||
"указанная",
|
||||
"указанное",
|
||||
"объекту",
|
||||
"объект",
|
||||
"документам",
|
||||
"документами",
|
||||
"докам",
|
||||
"взаиморасчетам",
|
||||
"взаиморасчётам",
|
||||
"теперь",
|
||||
"сейчас",
|
||||
"вернись",
|
||||
"вернуться",
|
||||
"вернуть",
|
||||
"раскрой",
|
||||
"раскрыть",
|
||||
"раскройте",
|
||||
"связанный",
|
||||
"связанные",
|
||||
"связанных",
|
||||
"связанным",
|
||||
"связанному",
|
||||
"related",
|
||||
"linked",
|
||||
"нему",
|
||||
"ней",
|
||||
"нее",
|
||||
"ним",
|
||||
"этому",
|
||||
"тому",
|
||||
"этомуже",
|
||||
"томуже"
|
||||
]);
|
||||
if (stopWords.has(lowered)) {
|
||||
const normalizedToken = cleanupAnchorValue(token);
|
||||
if (!normalizedToken) {
|
||||
return undefined;
|
||||
}
|
||||
return token;
|
||||
if (!hasStrongCounterpartyTokenShape(normalizedToken)) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isLikelyCounterpartyToken(normalizedToken)) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizedToken;
|
||||
}
|
||||
function extractContractTokenHeuristic(text) {
|
||||
const source = String(text ?? "");
|
||||
|
|
@ -545,206 +537,20 @@ function extractContractTokenHeuristic(text) {
|
|||
}
|
||||
function isLikelyCounterpartyToken(rawToken) {
|
||||
const token = String(rawToken ?? "").trim();
|
||||
const lowered = token.toLowerCase();
|
||||
if (!token || token.length < 2) {
|
||||
if (!token || token.length < 3) {
|
||||
return false;
|
||||
}
|
||||
const lowered = token.toLowerCase();
|
||||
if (/^\d+$/.test(lowered)) {
|
||||
return false;
|
||||
}
|
||||
if (/^(?:19|20)\d{2}$/.test(lowered)) {
|
||||
return false;
|
||||
}
|
||||
const stopWords = new Set([
|
||||
"за",
|
||||
"с",
|
||||
"по",
|
||||
"у",
|
||||
"на",
|
||||
"и",
|
||||
"или",
|
||||
"какие",
|
||||
"какой",
|
||||
"какая",
|
||||
"какое",
|
||||
"каких",
|
||||
"каким",
|
||||
"какими",
|
||||
"каком",
|
||||
"какому",
|
||||
"какую",
|
||||
"кто",
|
||||
"что",
|
||||
"чего",
|
||||
"где",
|
||||
"когда",
|
||||
"почему",
|
||||
"зачем",
|
||||
"сколько",
|
||||
"чьи",
|
||||
"чья",
|
||||
"чей",
|
||||
"чью",
|
||||
"мы",
|
||||
"видим",
|
||||
"самый",
|
||||
"самая",
|
||||
"самое",
|
||||
"самые",
|
||||
"крупный",
|
||||
"крупная",
|
||||
"крупное",
|
||||
"крупные",
|
||||
"жирный",
|
||||
"жирная",
|
||||
"жирное",
|
||||
"жирные",
|
||||
"больше",
|
||||
"меньше",
|
||||
"платит",
|
||||
"платят",
|
||||
"прогноз",
|
||||
"forecast",
|
||||
"план",
|
||||
"плана",
|
||||
"ндс",
|
||||
"vat",
|
||||
"налог",
|
||||
"оплата",
|
||||
"оплаты",
|
||||
"платеж",
|
||||
"платёж",
|
||||
"платежа",
|
||||
"платежи",
|
||||
"денег",
|
||||
"деньги",
|
||||
"объем",
|
||||
"объём",
|
||||
"док",
|
||||
"доки",
|
||||
"документ",
|
||||
"документы",
|
||||
"документов",
|
||||
"документами",
|
||||
"документу",
|
||||
"документе",
|
||||
"документа",
|
||||
"документах",
|
||||
"докам",
|
||||
"доками",
|
||||
"банк",
|
||||
"банковские",
|
||||
"операции",
|
||||
"платежи",
|
||||
"платеж",
|
||||
"платёж",
|
||||
"контрагент",
|
||||
"контрагенту",
|
||||
"контрагента",
|
||||
"контрагентам",
|
||||
"компания",
|
||||
"компании",
|
||||
"организация",
|
||||
"организации",
|
||||
"поставщикам",
|
||||
"клиентам",
|
||||
"покупателям",
|
||||
"заказчикам",
|
||||
"аванс",
|
||||
"авансы",
|
||||
"проблемный",
|
||||
"проблемные",
|
||||
"проблемным",
|
||||
"закрытия",
|
||||
"закрыть",
|
||||
"закрыты",
|
||||
"год",
|
||||
"года",
|
||||
"г",
|
||||
"плс",
|
||||
"pls",
|
||||
"пж",
|
||||
"пжлст",
|
||||
"пожалуйста",
|
||||
"бля",
|
||||
"блять",
|
||||
"епт",
|
||||
"ёпт",
|
||||
"епта",
|
||||
"нах",
|
||||
"нахуй",
|
||||
"есть",
|
||||
"же",
|
||||
"сводные",
|
||||
"сводный",
|
||||
"сводная",
|
||||
"сводную",
|
||||
"сводном",
|
||||
"сводного",
|
||||
"сводному",
|
||||
"неуказанному",
|
||||
"неуказанный",
|
||||
"неуказанная",
|
||||
"неуказанное",
|
||||
"указанному",
|
||||
"указанный",
|
||||
"указанная",
|
||||
"указанное",
|
||||
"объекту",
|
||||
"объект",
|
||||
"покеж",
|
||||
"покажи",
|
||||
"скажи",
|
||||
"показать",
|
||||
"выведи",
|
||||
"show",
|
||||
"list",
|
||||
"please",
|
||||
"теперь",
|
||||
"сейчас",
|
||||
"вернись",
|
||||
"вернуться",
|
||||
"вернуть",
|
||||
"раскрой",
|
||||
"раскрыть",
|
||||
"раскройте",
|
||||
"нему",
|
||||
"ней",
|
||||
"ним",
|
||||
"этому",
|
||||
"тому",
|
||||
"этомуже",
|
||||
"томуже",
|
||||
"vse",
|
||||
"all",
|
||||
"kakie",
|
||||
"kakoi",
|
||||
"est",
|
||||
"za",
|
||||
"po",
|
||||
"na",
|
||||
"s",
|
||||
"poka",
|
||||
"pokaji",
|
||||
"skazhi",
|
||||
"pokazhi",
|
||||
"pokazh",
|
||||
"pokezh",
|
||||
"doki",
|
||||
"doky",
|
||||
"dokument",
|
||||
"dokumenty",
|
||||
"documents",
|
||||
"docs",
|
||||
"связанный",
|
||||
"связанные",
|
||||
"связанных",
|
||||
"связанным",
|
||||
"связанному",
|
||||
"related",
|
||||
"linked"
|
||||
]);
|
||||
return !stopWords.has(lowered);
|
||||
if (/^(?:(?:19|20)?\d{2})(?:-?й)?(?:г|год|года)?$/iu.test(lowered)) {
|
||||
return false;
|
||||
}
|
||||
return !isCounterpartyNoiseToken(lowered);
|
||||
}
|
||||
function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||
const value = String(rawValue ?? "")
|
||||
|
|
@ -816,61 +622,25 @@ function hasDocsOrBankSignal(text) {
|
|||
const lowered = String(text ?? "").toLowerCase();
|
||||
return new RegExp(`(?:${DOC_SIGNAL_PATTERN}|банк|выписк|списан|поступлен|платеж|платёж|оплат|transactions?|bank\\s+ops|bank\\s+operations?|payment|payments?|platezh|oplata)`, "iu").test(lowered);
|
||||
}
|
||||
function extractCounterpartyFromFreeTextHeuristic(text) {
|
||||
if (!hasDocsOrBankSignal(text)) {
|
||||
return undefined;
|
||||
function hasStrongCounterpartyTokenShape(token) {
|
||||
const source = String(token ?? "").trim();
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
const tokens = String(text ?? "")
|
||||
.split(/[^\p{L}\p{N}._-]+/u)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
if (tokens.length === 0) {
|
||||
return undefined;
|
||||
if (/[0-9]/u.test(source) || /[._/-]/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
const monthTokens = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"сент",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек",
|
||||
"january",
|
||||
"february",
|
||||
"march",
|
||||
"april",
|
||||
"may",
|
||||
"june",
|
||||
"july",
|
||||
"august",
|
||||
"september",
|
||||
"october",
|
||||
"november",
|
||||
"december"
|
||||
];
|
||||
for (const token of tokens) {
|
||||
const lowered = token.toLowerCase();
|
||||
if (!isLikelyCounterpartyToken(lowered)) {
|
||||
continue;
|
||||
if (/[A-ZА-ЯЁ]/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
|
||||
continue;
|
||||
// Keep only compact lowercase slang aliases (e.g. "svk"), not arbitrary words.
|
||||
if (/^[a-z]{2,6}$/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (monthTokens.some((prefix) => lowered.startsWith(prefix))) {
|
||||
continue;
|
||||
if (/^[а-яё]+$/iu.test(source) && source.length <= 4) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) {
|
||||
continue;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
return false;
|
||||
}
|
||||
function extractImplicitCounterpartyValue(text) {
|
||||
const input = String(text ?? "");
|
||||
|
|
@ -878,7 +648,7 @@ function extractImplicitCounterpartyValue(text) {
|
|||
const beforeDocsMatch = input.match(beforeDocsPattern);
|
||||
if (beforeDocsMatch) {
|
||||
const candidate = String(beforeDocsMatch[1] ?? "").trim();
|
||||
if (isLikelyCounterpartyToken(candidate)) {
|
||||
if (hasStrongCounterpartyTokenShape(candidate) && isLikelyCounterpartyToken(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
|
@ -886,7 +656,7 @@ function extractImplicitCounterpartyValue(text) {
|
|||
const afterDocsMatch = input.match(afterDocsPattern);
|
||||
if (afterDocsMatch) {
|
||||
const candidate = String(afterDocsMatch[1] ?? "").trim();
|
||||
if (isLikelyCounterpartyToken(candidate)) {
|
||||
if (hasStrongCounterpartyTokenShape(candidate) && isLikelyCounterpartyToken(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
|
@ -932,6 +702,9 @@ function extractLeadingCounterpartyTokenHeuristic(text) {
|
|||
];
|
||||
for (const token of tokens.slice(0, 3)) {
|
||||
const lowered = token.toLowerCase();
|
||||
if (!hasStrongCounterpartyTokenShape(token)) {
|
||||
continue;
|
||||
}
|
||||
if (!isLikelyCounterpartyToken(lowered)) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1048,16 +821,6 @@ function extractAddressFilters(userMessage, intent) {
|
|||
warnings.push("counterparty_anchor_derived_from_implicit_phrase");
|
||||
}
|
||||
}
|
||||
if (!filters.counterparty &&
|
||||
(intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty")) {
|
||||
const heuristicCounterparty = extractCounterpartyFromFreeTextHeuristic(text);
|
||||
if (heuristicCounterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(heuristicCounterparty);
|
||||
warnings.push("counterparty_anchor_derived_from_free_text_heuristic");
|
||||
}
|
||||
}
|
||||
if (!filters.counterparty &&
|
||||
(intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
|
|
|
|||
|
|
@ -930,6 +930,15 @@ function buildLimitedOffers(input) {
|
|||
offers.push("пример: «покажи документы по договору <номер> за 2020 год»");
|
||||
}
|
||||
}
|
||||
if (input.intent === "list_receivables_counterparties") {
|
||||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||||
}
|
||||
else if (input.intent === "list_payables_counterparties") {
|
||||
offers.push("показать контрагентов с максимальными хвостами кредиторки по 60/76");
|
||||
}
|
||||
else if (input.intent === "open_items_by_counterparty_or_contract" || input.intent === "list_open_contracts") {
|
||||
offers.push("показать незакрытые договоры и хвосты взаиморасчетов на дату");
|
||||
}
|
||||
if (counterparty) {
|
||||
offers.push(`показать документы и платежи по контрагенту ${counterparty}`);
|
||||
}
|
||||
|
|
@ -956,38 +965,101 @@ function buildLimitedOffers(input) {
|
|||
}
|
||||
return Array.from(new Set(offers)).slice(0, 3);
|
||||
}
|
||||
function buildLimitedIntentSignalLine(input) {
|
||||
const byIntent = {
|
||||
list_documents_by_counterparty: "Сигнал запроса: нужен срез документов/платежей по контрагенту.",
|
||||
list_documents_by_contract: "Сигнал запроса: нужен срез документов/платежей по договору.",
|
||||
bank_operations_by_counterparty: "Сигнал запроса: нужен срез банковских операций по контрагенту.",
|
||||
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
||||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов."
|
||||
};
|
||||
const byShape = {
|
||||
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
||||
DOCUMENT_LIST: "Сигнал запроса: список документов/операций.",
|
||||
OBJECT_LOOKUP: "Сигнал запроса: поиск конкретных объектов.",
|
||||
VERIFY_FACTUAL: "Сигнал запроса: проверка фактического состояния по данным.",
|
||||
COMPOUND_FACTUAL_QUERY: "Сигнал запроса: комбинированная проверка взаимосвязанных фактов."
|
||||
};
|
||||
return byIntent[input.intent] ?? byShape[input.shape.shape] ?? null;
|
||||
}
|
||||
function hasAggregateLimitedSignal(input) {
|
||||
if (input.shape.shape === "AGGREGATE_LOOKUP") {
|
||||
return true;
|
||||
}
|
||||
if (input.intent === "counterparty_population_and_roles" ||
|
||||
input.intent === "counterparty_activity_lifecycle" ||
|
||||
input.intent === "contract_usage_overview" ||
|
||||
input.intent === "supplier_payouts_profile" ||
|
||||
input.intent === "customer_revenue_and_payments" ||
|
||||
input.intent === "contract_usage_and_value") {
|
||||
return true;
|
||||
}
|
||||
return /(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin|year)/iu.test(String(input.reason ?? ""));
|
||||
}
|
||||
function composeLimitedReply(input) {
|
||||
const reason = normalizeLimitedReason(input.reason);
|
||||
const headingSeed = `${input.category}|${input.shape.shape}|${reason}`;
|
||||
const aggregateLimitedSignal = hasAggregateLimitedSignal({
|
||||
shape: input.shape,
|
||||
intent: input.intent,
|
||||
reason: input.reason
|
||||
});
|
||||
const heading = input.category === "empty_match"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||||
"В текущем срезе данных по этому запросу совпадения не найдены."
|
||||
"В текущем срезе данных по этому запросу совпадения не найдены.",
|
||||
"По заданным фильтрам в текущем срезе совпадений пока нет."
|
||||
])
|
||||
: input.category === "missing_anchor"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"Чтобы ответ был точным, нужно чуть сильнее заякорить запрос.",
|
||||
"Запрос понятен, но для надежного ответа не хватает опорного ориентира."
|
||||
"Запрос понятен, но для надежного ответа не хватает опорного ориентира.",
|
||||
"Вопрос по смыслу ясен, но пока не хватает конкретной опоры для выборки."
|
||||
])
|
||||
: input.category === "recipe_visibility_gap"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"Запрос понятен, но текущий сценарий выборки не дает нужной детализации.",
|
||||
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки."
|
||||
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки.",
|
||||
"Сценарий запроса корректный, но текущая витрина не дает нужной детализации."
|
||||
])
|
||||
: input.category === "unsupported"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"По этому вопросу в текущем адресном контуре пока нет надежного маршрута ответа.",
|
||||
"Сейчас в адресном режиме такой сценарий не закрыт без риска ошибочного вывода."
|
||||
"Сейчас не дам прямой адресный ответ, чтобы не ошибиться в выводах.",
|
||||
"В текущем адресном контуре этот запрос лучше не закрывать «в лоб» — риск неверной трактовки высок.",
|
||||
"Для такого формата запроса нужен более широкий аналитический контур, иначе ответ будет ненадежным."
|
||||
])
|
||||
: "Не удалось завершить проверку в адресном режиме.";
|
||||
const reasonSeed = `${headingSeed}|reason`;
|
||||
const reasonLine = input.category === "unsupported"
|
||||
? "Коротко: сценарий пока не покрыт текущими адресными маршрутами."
|
||||
? aggregateLimitedSignal
|
||||
? pickDeterministicVariant(reasonSeed, [
|
||||
"Это агрегатный/сравнительный вопрос: без расширенного анализа здесь легко дать ложную метрику.",
|
||||
"Запрос про сводную аналитику или ранжирование, поэтому в address-контуре ответ сейчас будет ненадежным.",
|
||||
"Нужна расширенная аналитическая обработка: адресный режим в этом кейсе не гарантирует корректный расчет."
|
||||
])
|
||||
: pickDeterministicVariant(reasonSeed, [
|
||||
"Сценарий пока не закрыт текущими адресными маршрутами без потери точности.",
|
||||
"Для этого запроса пока нет надежного ответа внутри текущего address-контура."
|
||||
])
|
||||
: input.category === "missing_anchor"
|
||||
? "Коротко: не хватает конкретного ориентира (контрагент, договор, счет или период)."
|
||||
? pickDeterministicVariant(reasonSeed, [
|
||||
"Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (контрагент, договор, счет или период).",
|
||||
"Для точного ответа нужен хотя бы один явный якорь: контрагент, договор, счет или период."
|
||||
])
|
||||
: input.category === "recipe_visibility_gap"
|
||||
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
|
||||
: `Коротко: ${reason}.`;
|
||||
? "Для уверенного ответа нужен более специализированный сценарий выборки."
|
||||
: `${reason}.`;
|
||||
const lines = [heading, reasonLine];
|
||||
const signalLine = buildLimitedIntentSignalLine({
|
||||
intent: input.intent,
|
||||
shape: input.shape
|
||||
});
|
||||
if (signalLine && !(input.category === "unsupported" && aggregateLimitedSignal)) {
|
||||
lines.push(signalLine);
|
||||
}
|
||||
const scopeLine = buildLimitedScopeLine(input.filters);
|
||||
if (scopeLine) {
|
||||
lines.push(scopeLine);
|
||||
|
|
@ -995,6 +1067,7 @@ function composeLimitedReply(input) {
|
|||
const offers = buildLimitedOffers({
|
||||
category: input.category,
|
||||
shape: input.shape,
|
||||
intent: input.intent,
|
||||
filters: input.filters,
|
||||
missingRequiredFilters: input.missingRequiredFilters,
|
||||
reason: input.reason,
|
||||
|
|
@ -1014,6 +1087,7 @@ function buildLimitedExecutionResult(input) {
|
|||
reason: input.reasonText,
|
||||
nextStep: input.nextStep,
|
||||
shape: input.shape,
|
||||
intent: input.intent.intent,
|
||||
filters: input.filters,
|
||||
missingRequiredFilters: input.missingRequiredFilters
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3613,6 +3613,53 @@ function renderPolicyReply(structure, context) {
|
|||
.filter(Boolean)
|
||||
.join("\n\n"));
|
||||
}
|
||||
function shouldUseSoftPolicyReply(input) {
|
||||
if (input.mode === "focused_grounded" || input.mode === "route_mismatch" || input.mode === "backend_error" || input.mode === "out_of_scope") {
|
||||
return false;
|
||||
}
|
||||
if (input.mode === "clarification_required" || input.mode === "no_grounded" || input.mode === "empty") {
|
||||
return true;
|
||||
}
|
||||
if (input.mode !== "broad_partial") {
|
||||
return false;
|
||||
}
|
||||
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
|
||||
input.coverageReport.requirements_partially_covered.length > 0 ||
|
||||
input.coverageReport.clarification_needed_for.length > 0 ||
|
||||
input.coverageReport.out_of_scope_requirements.length > 0;
|
||||
const weakEvidenceSignals = input.policySignals.broad_query_detected ||
|
||||
input.policySignals.broad_result_flag ||
|
||||
input.policySignals.minimum_evidence_failed ||
|
||||
input.aggregateEvidenceConfidence === "low" ||
|
||||
input.hasCriticalEvidenceLimitation ||
|
||||
input.limitationReasonCodes.includes("weak_source_mapping") ||
|
||||
input.limitationReasonCodes.includes("insufficient_detail") ||
|
||||
input.limitationReasonCodes.includes("missing_mechanism");
|
||||
return hasCoverageGaps || weakEvidenceSignals;
|
||||
}
|
||||
function renderSoftPolicyReply(input) {
|
||||
const questionType = input.context?.questionType ?? "unknown";
|
||||
const shortLine = ensureSentence(buildShortSectionLine(input.structure));
|
||||
const evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3);
|
||||
const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3);
|
||||
const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3);
|
||||
const clarificationLines = dedupeNarrativeLines(input.structure.next_step_block.clarification_questions ?? [], 2);
|
||||
const actionLines = dedupeNarrativeLines([...checkLines, ...(input.structure.next_step_block.recommended_actions ?? []), ...clarificationLines], 3);
|
||||
const modeLine = input.mode === "clarification_required"
|
||||
? "Чтобы дать точный ответ, нужно уточнить несколько ориентиров."
|
||||
: input.mode === "no_grounded" || input.mode === "empty"
|
||||
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
|
||||
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
|
||||
return sanitizeUserFacingReply([
|
||||
`Коротко: ${shortLine}`,
|
||||
modeLine,
|
||||
evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "",
|
||||
limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "",
|
||||
actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"));
|
||||
}
|
||||
function composeAssistantAnswerV11(input) {
|
||||
const fallbackType = fallbackFromSummary(input.routeSummary);
|
||||
const questionType = input.questionTypeHint ?? "unknown";
|
||||
|
|
@ -3856,6 +3903,24 @@ function composeAssistantAnswerV11(input) {
|
|||
missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
})
|
||||
: shouldUseSoftPolicyReply({
|
||||
mode: guardedDecision.mode,
|
||||
policySignals,
|
||||
limitationReasonCodes,
|
||||
aggregateEvidenceConfidence,
|
||||
coverageReport: input.coverageReport,
|
||||
hasCriticalEvidenceLimitation
|
||||
})
|
||||
? renderSoftPolicyReply({
|
||||
structure: answerStructure,
|
||||
context: {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage,
|
||||
userMessage: input.userMessage
|
||||
},
|
||||
mode: guardedDecision.mode
|
||||
})
|
||||
: renderPolicyReply(answerStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,27 @@ function countTokens(text) {
|
|||
function hasPeriodLiteral(text) {
|
||||
return /\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/.test(text);
|
||||
}
|
||||
function hasStandaloneAddressTopicSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasFollowupMarker(normalized) || hasReferentialPointer(normalized)) {
|
||||
return false;
|
||||
}
|
||||
const hasRequestCue = /(?:^|[\s,.;:!?()\-])(?:покажи|показать|выведи|дай|найди|список|какие|какой|какая|каких|сколько|где|show|list|find|which|what)/iu.test(normalized);
|
||||
if (!hasRequestCue) {
|
||||
return false;
|
||||
}
|
||||
const hasBusinessObject = /(?:договор|контракт|контрагент|поставщик|покупател|клиент|документ|платеж|оплат|сальдо|остатк|сч[её]т|оборот|выруч|доход|прибыл|ндс|дебитор|кредитор|организац|компан|контор|contract|counterparty|supplier|customer|document|payment|turnover|revenue|profit|balance|account|vat)/iu.test(normalized);
|
||||
if (!hasBusinessObject) {
|
||||
return false;
|
||||
}
|
||||
const hasStructuredAnchor = hasPeriodLiteral(normalized) ||
|
||||
/\b\d{2}(?:[.,]\d{1,2})?\b/.test(normalized) ||
|
||||
/(?:альтернатива|лайсвуд|райм|ооо\s+[a-zа-яё])/iu.test(normalized);
|
||||
return hasStructuredAnchor || countTokens(normalized) >= 6;
|
||||
}
|
||||
function extractNormalizedPeriodLiteral(text) {
|
||||
const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
|
||||
if (monthly) {
|
||||
|
|
@ -1350,6 +1371,35 @@ function buildAddressCoverageReport() {
|
|||
out_of_scope_requirements: []
|
||||
};
|
||||
}
|
||||
function buildAssistantBackendErrorDebugPayload(errorMessage) {
|
||||
return {
|
||||
trace_id: `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||||
prompt_version: "assistant_backend_error_fallback_v1",
|
||||
schema_version: "assistant_backend_error_fallback_v1",
|
||||
fallback_type: "unknown",
|
||||
route_summary: null,
|
||||
fragments: [],
|
||||
requirements_extracted: [],
|
||||
coverage_report: buildAddressCoverageReport(),
|
||||
routes: [],
|
||||
retrieval_status: [],
|
||||
retrieval_results: [],
|
||||
answer_grounding_check: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [
|
||||
`backend_error:${String(errorMessage ?? "unknown_error").slice(0, 280)}`
|
||||
],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
dropped_intent_segments: []
|
||||
};
|
||||
}
|
||||
function buildAssistantBackendErrorReply() {
|
||||
return "Сейчас не удалось завершить разбор из-за внутренней ошибки контуров LLM. Могу продолжить в адресном режиме: проверить документы, договоры и операции по нужному периоду или контрагенту.";
|
||||
}
|
||||
function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||||
const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded";
|
||||
const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null;
|
||||
|
|
@ -2214,6 +2264,9 @@ function hasAddressFollowupContextSignal(userMessage) {
|
|||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (hasStandaloneAddressTopicSignal(text)) {
|
||||
return false;
|
||||
}
|
||||
if (shouldHandleAsAssistantCapabilityMetaQuery(text)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2272,6 +2325,11 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
|||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage)
|
||||
: false;
|
||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal) {
|
||||
return null;
|
||||
}
|
||||
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2903,6 +2961,29 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
Boolean(sourceAnchorQuality.anchorValue) &&
|
||||
((candidateAnchorQuality.anchorType === "counterparty" &&
|
||||
candidateAnchorQuality.quality >= 2 &&
|
||||
Boolean(candidateAnchorQuality.anchorValue) &&
|
||||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? "")) ||
|
||||
(candidateAnchorQuality.quality < sourceAnchorQuality.quality &&
|
||||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidate)));
|
||||
if (counterpartyAnchorSubstitutedByCandidate) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_substitution",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage
|
||||
}, userMessage);
|
||||
}
|
||||
const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
|
|
@ -2920,27 +3001,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
sanitizedUserMessage
|
||||
}, userMessage);
|
||||
}
|
||||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
candidateAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
candidateAnchorQuality.quality >= 2 &&
|
||||
Boolean(sourceAnchorQuality.anchorValue) &&
|
||||
Boolean(candidateAnchorQuality.anchorValue) &&
|
||||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? "");
|
||||
if (counterpartyAnchorSubstitutedByCandidate) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_substitution",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage
|
||||
}, userMessage);
|
||||
}
|
||||
if (fallbackCandidate) {
|
||||
const fallbackAnchorQuality = evaluateAddressAnchorQuality(String(fallbackCandidate.candidate ?? ""));
|
||||
const fallbackPreferredForAnchorSafety = sameIntentForAnchorSafety &&
|
||||
|
|
@ -3123,6 +3183,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
|||
llmContractIntent === "unknown";
|
||||
const hasAnyAddressSignal = hasClassifierSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal;
|
||||
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
|
||||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||||
hasAccountingSignal(rawMessageForGate) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate);
|
||||
const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) ||
|
||||
|
|
@ -3149,7 +3210,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
|||
reason: "llm_predecompose_unsupported_mode"
|
||||
};
|
||||
}
|
||||
const hasMessageSignal = hasAnyAddressSignal;
|
||||
const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage;
|
||||
if (hasMessageSignal) {
|
||||
return {
|
||||
runAddressLane: true,
|
||||
|
|
@ -3234,7 +3295,9 @@ function hasDeepAnalysisPreferenceSignal(text) {
|
|||
}
|
||||
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 diagnosticsKeywordSignal = /(?:\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|audit|scan|\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 diagnosticsCheckVerbSignal = /(?:^|[\s,.;:!?()\-])\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c|\u043a\u0443|\u0438\u043c|\u043a\u0430)(?:$|[\s,.;:!?()\-])/iu.test(lower);
|
||||
const diagnosticsSignal = diagnosticsKeywordSignal || diagnosticsCheckVerbSignal;
|
||||
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);
|
||||
|
|
@ -3242,13 +3305,12 @@ function hasDeepAnalysisPreferenceSignal(text) {
|
|||
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 ||
|
||||
return lifecycleMismatchSignal ||
|
||||
(chainSignal && lifecycleTransitionGapSignal) ||
|
||||
expectedActualMismatchSignal ||
|
||||
(chainSignal && diagnosticsSignal) ||
|
||||
(riskOrAnomalySignal && (chainSignal || closureSignal || diagnosticsSignal || closureIntentSignal)) ||
|
||||
(diagnosticsSignal && closureIntentSignal) ||
|
||||
(riskOrAnomalySignal && (chainSignal || diagnosticsSignal || lifecycleTransitionGapSignal)) ||
|
||||
(diagnosticsSignal && (closureSignal || closureIntentSignal)) ||
|
||||
closureDiagnosticPhraseSignal ||
|
||||
signalVsNoiseDiagnostic;
|
||||
}
|
||||
|
|
@ -3257,7 +3319,7 @@ function hasDirectDeepAnalysisSignal(text) {
|
|||
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);
|
||||
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\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized);
|
||||
}
|
||||
function hasStrictDeepInvestigationCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
|
|
@ -3283,6 +3345,23 @@ function hasAggregateBusinessAnalyticsSignal(text) {
|
|||
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;
|
||||
}
|
||||
function hasOpenContractsAddressSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasContractCue = /(?:договор|контракт|contract)/iu.test(normalized);
|
||||
if (!hasContractCue) {
|
||||
return false;
|
||||
}
|
||||
const hasOpenCue = /(?:незакрыт|не\s+закрыт|открыт|open\s+contract|open\s+item|open)/iu.test(normalized);
|
||||
if (!hasOpenCue) {
|
||||
return false;
|
||||
}
|
||||
const hasRequestCue = /(?:покажи|показать|список|какие|какой|show|list|find|на\s+дату|as\s+of)/iu.test(normalized);
|
||||
const hasTemporalCue = hasPeriodLiteral(normalized) || /\b\d{4}[-/.]\d{2}[-/.]\d{2}\b/.test(normalized);
|
||||
return hasRequestCue || hasTemporalCue;
|
||||
}
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
|
|
@ -3320,10 +3399,21 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
|
||||
const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) ||
|
||||
hasStandaloneAddressTopicSignal(repairedRawUserMessage) ||
|
||||
hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) ||
|
||||
hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage);
|
||||
const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) ||
|
||||
hasOpenContractsAddressSignal(repairedRawUserMessage) ||
|
||||
hasOpenContractsAddressSignal(effectiveAddressUserMessage) ||
|
||||
hasOpenContractsAddressSignal(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 llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason);
|
||||
const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason &&
|
||||
/(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason));
|
||||
const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract &&
|
||||
typeof llmPreDecomposeMeta.semanticExtractionContract === "object"
|
||||
? llmPreDecomposeMeta.semanticExtractionContract
|
||||
|
|
@ -3339,7 +3429,8 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
|
||||
const keepAddressLaneByIntent = semanticApplyCanonicalRecommended &&
|
||||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent))) &&
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||
openContractsAddressSignal) &&
|
||||
!strictDeepInvestigationCueDetected;
|
||||
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
|
||||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
|
||||
|
|
@ -3431,7 +3522,8 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
|
||||
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
||||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)));
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||
openContractsAddressSignal);
|
||||
const semanticGuardHints = semanticExtractionContract?.guard_hints &&
|
||||
typeof semanticExtractionContract.guard_hints === "object"
|
||||
? semanticExtractionContract.guard_hints
|
||||
|
|
@ -3452,8 +3544,14 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
const unsupportedIntentOrMode = (modeDetection.mode !== "address_query" && intentResolution.intent === "unknown") ||
|
||||
llmContractMode === "unsupported";
|
||||
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
unsupportedIntentOrMode &&
|
||||
strongDataSignal &&
|
||||
(llmContractMode === "deep_analysis" ||
|
||||
!dataRetrievalSignal ||
|
||||
strictDeepInvestigationCueDetected ||
|
||||
semanticDeepInvestigationHintDetected ||
|
||||
aggregateBusinessAnalyticsSignal) &&
|
||||
!preserveAddressLaneSignal &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!supportedAddressIntentDetected &&
|
||||
|
|
@ -3470,21 +3568,25 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
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 &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!supportedAddressIntentDetected &&
|
||||
!vatExplainFollowupSignal &&
|
||||
(!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed));
|
||||
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
aggregateBusinessAnalyticsSignal &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(!followupContext ||
|
||||
llmContractMode === "unsupported" ||
|
||||
semanticAggregateShapeDetected ||
|
||||
!semanticApplyCanonicalRecommended));
|
||||
!semanticApplyCanonicalRecommended ||
|
||||
standaloneAddressTopicSignal));
|
||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
hasDeepSessionContinuationSignal({
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
|
|
@ -3591,24 +3693,29 @@ function resolveAssistantOrchestrationDecision(input) {
|
|||
}
|
||||
function hasStrongDataIntentSignal(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|mcp|bank|counterparty|contract|document|ledger|posting|account|организац|компан|контор|фирм)/i.test(lower);
|
||||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм)/i.test(lower);
|
||||
}
|
||||
function hasDataRetrievalRequestSignal(text) {
|
||||
const lower = compactWhitespace(String(text ?? "").toLowerCase());
|
||||
if (!lower) {
|
||||
return false;
|
||||
}
|
||||
const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower);
|
||||
const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover)/iu.test(lower);
|
||||
if (hasBroadInterrogative && hasBroadBusinessObject) {
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower);
|
||||
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
|
||||
return false;
|
||||
}
|
||||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|bank|counterparty|contract|document|account|balance|ledger|posting|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
|
||||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
|
||||
if (!hasRetrievalObject) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3769,6 +3876,10 @@ function hasAssistantDataScopeMetaQuestionSignal(text) {
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized);
|
||||
if (hasDirectSlangScopeLead) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
|
|
@ -4816,6 +4927,7 @@ class AssistantService {
|
|||
extractExecutionState
|
||||
}
|
||||
});
|
||||
try {
|
||||
const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({
|
||||
payload,
|
||||
runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)),
|
||||
|
|
@ -4825,5 +4937,46 @@ class AssistantService {
|
|||
});
|
||||
return turnRuntime.response;
|
||||
}
|
||||
catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const sessionId = String(payload?.session_id ?? payload?.sessionId ?? "").trim() || `asst-${(0, nanoid_1.nanoid)(10)}`;
|
||||
const ensuredSession = this.sessions.ensureSession(sessionId);
|
||||
const existingAssistant = [...ensuredSession.items].reverse().find((item) => item.role === "assistant") ?? null;
|
||||
if (existingAssistant) {
|
||||
return {
|
||||
ok: true,
|
||||
session_id: sessionId,
|
||||
assistant_reply: existingAssistant.text,
|
||||
reply_type: existingAssistant.reply_type ?? "backend_error",
|
||||
conversation_item: existingAssistant,
|
||||
debug: existingAssistant.debug ?? buildAssistantBackendErrorDebugPayload(errorMessage),
|
||||
conversation: cloneItems(ensuredSession.items)
|
||||
};
|
||||
}
|
||||
const createdAt = new Date().toISOString();
|
||||
const debugPayload = buildAssistantBackendErrorDebugPayload(errorMessage);
|
||||
const assistantItem = this.sessions.appendItem(sessionId, {
|
||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: buildAssistantBackendErrorReply(),
|
||||
reply_type: "backend_error",
|
||||
created_at: createdAt,
|
||||
trace_id: debugPayload.trace_id ?? null,
|
||||
debug: debugPayload
|
||||
});
|
||||
const sessionSnapshot = this.sessions.getSession(sessionId) ?? this.sessions.ensureSession(sessionId);
|
||||
this.sessionLogger.persistSession(sessionSnapshot);
|
||||
return {
|
||||
ok: true,
|
||||
session_id: sessionId,
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: "backend_error",
|
||||
conversation_item: assistantItem,
|
||||
debug: debugPayload,
|
||||
conversation: cloneItems(sessionSnapshot.items)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.AssistantService = AssistantService;
|
||||
|
|
|
|||
|
|
@ -202,6 +202,11 @@ export const __evalRouteTestUtils = {
|
|||
normalizeRuntimeQuestions
|
||||
};
|
||||
|
||||
export const __evalRouteAsyncTestUtils = {
|
||||
readReportedCaseIds,
|
||||
syncJobWithSessions
|
||||
};
|
||||
|
||||
function normalizeCaseIds(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
|
|
@ -365,10 +370,30 @@ function readSessionConversation(runId: string, caseId: string): EvalAsyncCaseIn
|
|||
}
|
||||
}
|
||||
|
||||
function readReportedCaseIds(job: EvalAsyncJob): Set<string> {
|
||||
const output = new Set<string>();
|
||||
const reportRecord = toRecord(job.report);
|
||||
const reportResults = toArray(reportRecord?.results)
|
||||
.map((item) => toRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => item !== null);
|
||||
|
||||
for (const result of reportResults) {
|
||||
const caseId = toStringSafe(result.case_id);
|
||||
if (!caseId) continue;
|
||||
output.add(caseId);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function isTerminalCaseStatus(status: EvalAsyncStatus): boolean {
|
||||
return status === "completed" || status === "failed";
|
||||
}
|
||||
|
||||
function syncJobWithSessions(job: EvalAsyncJob): void {
|
||||
if (!job.run_id || !job.eval_target.startsWith("assistant_")) {
|
||||
return;
|
||||
}
|
||||
const reportCaseIds = readReportedCaseIds(job);
|
||||
let completed = 0;
|
||||
let hasRunning = false;
|
||||
for (const item of job.cases) {
|
||||
|
|
@ -376,11 +401,16 @@ function syncJobWithSessions(job: EvalAsyncJob): void {
|
|||
item.messages = messages;
|
||||
const assistantMessages = messages.filter((entry) => entry.role === "assistant").length;
|
||||
const userMessages = messages.filter((entry) => entry.role === "user").length;
|
||||
if (assistantMessages >= item.turns_total && item.turns_total > 0) {
|
||||
const reportMarkedDone = reportCaseIds.has(item.case_id);
|
||||
if ((assistantMessages >= item.turns_total && item.turns_total > 0) || reportMarkedDone) {
|
||||
item.status = "completed";
|
||||
completed += 1;
|
||||
continue;
|
||||
}
|
||||
if (isTerminalCaseStatus(item.status) && isTerminalCaseStatus(job.status)) {
|
||||
completed += 1;
|
||||
continue;
|
||||
}
|
||||
if (userMessages > 0 || messages.length > 0) {
|
||||
item.status = "running";
|
||||
hasRunning = true;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,84 @@ const MONTH_PERIOD_NAME_YEAR_FIRST_PATTERN =
|
|||
/(?:^|[\s,.;:!?()\-])(?:за|for|на|in)?\s*(20\d{2})(?:\s*г(?:од|ода|\\.)?)?\s+([a-zа-яё]+)(?=$|[\s,.;:!?()\-])/iu;
|
||||
const DOC_SIGNAL_PATTERN =
|
||||
"(?:док(?:и|умент|ументы|ументов|умам|ума)|docs?|documents?|docy|doci|doki|dokument(?:y|ov|am|a)?)";
|
||||
const COUNTERPARTY_TOKEN_NOISE = new Set([
|
||||
"за",
|
||||
"с",
|
||||
"по",
|
||||
"у",
|
||||
"на",
|
||||
"и",
|
||||
"или",
|
||||
"в",
|
||||
"к",
|
||||
"год",
|
||||
"года",
|
||||
"г",
|
||||
"year",
|
||||
"кто",
|
||||
"что",
|
||||
"где",
|
||||
"когда",
|
||||
"сколько",
|
||||
"почему",
|
||||
"зачем",
|
||||
"какой",
|
||||
"какая",
|
||||
"какие",
|
||||
"каких",
|
||||
"каким",
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"есть",
|
||||
"можно",
|
||||
"могу",
|
||||
"можем",
|
||||
"нет",
|
||||
"покажи",
|
||||
"показать",
|
||||
"скажи",
|
||||
"выведи",
|
||||
"show",
|
||||
"list",
|
||||
"контра",
|
||||
"контре",
|
||||
"контрагент",
|
||||
"компания",
|
||||
"организация",
|
||||
"client",
|
||||
"customer",
|
||||
"supplier",
|
||||
"vendor",
|
||||
"partner",
|
||||
"company",
|
||||
"counterparty"
|
||||
]);
|
||||
|
||||
function isCounterpartyFillerToken(token: string): boolean {
|
||||
const normalized = String(token ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:пл[сз]|пж|пжлст|pls|please|пожалуйста)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:бл[яе]|блять|нах|нахуй|епт|ёпт|епта)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:док(?:и|ам|ами|умент(?:ы|ов)?)?|docs?|docy|doci|doki|dokument(?:y|ov|am|a)?)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:pokazh?|pokazhi|pokaji|pokezh|kakie|kakoi|kakaya|est|za|po|na|s|vse|all|poka)$/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCounterpartyNoiseToken(rawToken: string): boolean {
|
||||
const normalized = String(rawToken ?? "").trim().toLowerCase();
|
||||
return COUNTERPARTY_TOKEN_NOISE.has(normalized) || isCounterpartyFillerToken(normalized);
|
||||
}
|
||||
|
||||
function textMojibakeScore(value: string): number {
|
||||
const source = String(value ?? "");
|
||||
|
|
@ -488,101 +566,17 @@ function extractLooseByAnchorValue(text: string): string | undefined {
|
|||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = token.toLowerCase();
|
||||
const stopWords = new Set([
|
||||
"какой",
|
||||
"какая",
|
||||
"какие",
|
||||
"каких",
|
||||
"каким",
|
||||
"какими",
|
||||
"каком",
|
||||
"кто",
|
||||
"что",
|
||||
"мы",
|
||||
"видим",
|
||||
"контрагенту",
|
||||
"контрагента",
|
||||
"контрагентам",
|
||||
"контре",
|
||||
"компании",
|
||||
"компанию",
|
||||
"организации",
|
||||
"организацию",
|
||||
"поставщику",
|
||||
"поставщика",
|
||||
"поставщикам",
|
||||
"клиенту",
|
||||
"клиента",
|
||||
"клиентам",
|
||||
"покупателю",
|
||||
"покупателя",
|
||||
"покупателям",
|
||||
"заказчикам",
|
||||
"партнеру",
|
||||
"партнера",
|
||||
"договору",
|
||||
"договора",
|
||||
"контракту",
|
||||
"контракта",
|
||||
"счету",
|
||||
"счёту",
|
||||
"дате",
|
||||
"периоду",
|
||||
"период",
|
||||
"есть",
|
||||
"же",
|
||||
"сводные",
|
||||
"сводный",
|
||||
"сводная",
|
||||
"сводную",
|
||||
"сводном",
|
||||
"сводного",
|
||||
"сводному",
|
||||
"неуказанному",
|
||||
"неуказанный",
|
||||
"неуказанная",
|
||||
"неуказанное",
|
||||
"неуказанному",
|
||||
"указанному",
|
||||
"указанный",
|
||||
"указанная",
|
||||
"указанное",
|
||||
"объекту",
|
||||
"объект",
|
||||
"документам",
|
||||
"документами",
|
||||
"докам",
|
||||
"взаиморасчетам",
|
||||
"взаиморасчётам",
|
||||
"теперь",
|
||||
"сейчас",
|
||||
"вернись",
|
||||
"вернуться",
|
||||
"вернуть",
|
||||
"раскрой",
|
||||
"раскрыть",
|
||||
"раскройте",
|
||||
"связанный",
|
||||
"связанные",
|
||||
"связанных",
|
||||
"связанным",
|
||||
"связанному",
|
||||
"related",
|
||||
"linked",
|
||||
"нему",
|
||||
"ней",
|
||||
"нее",
|
||||
"ним",
|
||||
"этому",
|
||||
"тому",
|
||||
"этомуже",
|
||||
"томуже"
|
||||
]);
|
||||
if (stopWords.has(lowered)) {
|
||||
const normalizedToken = cleanupAnchorValue(token);
|
||||
if (!normalizedToken) {
|
||||
return undefined;
|
||||
}
|
||||
return token;
|
||||
if (!hasStrongCounterpartyTokenShape(normalizedToken)) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isLikelyCounterpartyToken(normalizedToken)) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizedToken;
|
||||
}
|
||||
|
||||
function extractContractTokenHeuristic(text: string): string | undefined {
|
||||
|
|
@ -610,207 +604,20 @@ function extractContractTokenHeuristic(text: string): string | undefined {
|
|||
|
||||
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||
const token = String(rawToken ?? "").trim();
|
||||
const lowered = token.toLowerCase();
|
||||
if (!token || token.length < 2) {
|
||||
if (!token || token.length < 3) {
|
||||
return false;
|
||||
}
|
||||
const lowered = token.toLowerCase();
|
||||
if (/^\d+$/.test(lowered)) {
|
||||
return false;
|
||||
}
|
||||
if (/^(?:19|20)\d{2}$/.test(lowered)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stopWords = new Set([
|
||||
"за",
|
||||
"с",
|
||||
"по",
|
||||
"у",
|
||||
"на",
|
||||
"и",
|
||||
"или",
|
||||
"какие",
|
||||
"какой",
|
||||
"какая",
|
||||
"какое",
|
||||
"каких",
|
||||
"каким",
|
||||
"какими",
|
||||
"каком",
|
||||
"какому",
|
||||
"какую",
|
||||
"кто",
|
||||
"что",
|
||||
"чего",
|
||||
"где",
|
||||
"когда",
|
||||
"почему",
|
||||
"зачем",
|
||||
"сколько",
|
||||
"чьи",
|
||||
"чья",
|
||||
"чей",
|
||||
"чью",
|
||||
"мы",
|
||||
"видим",
|
||||
"самый",
|
||||
"самая",
|
||||
"самое",
|
||||
"самые",
|
||||
"крупный",
|
||||
"крупная",
|
||||
"крупное",
|
||||
"крупные",
|
||||
"жирный",
|
||||
"жирная",
|
||||
"жирное",
|
||||
"жирные",
|
||||
"больше",
|
||||
"меньше",
|
||||
"платит",
|
||||
"платят",
|
||||
"прогноз",
|
||||
"forecast",
|
||||
"план",
|
||||
"плана",
|
||||
"ндс",
|
||||
"vat",
|
||||
"налог",
|
||||
"оплата",
|
||||
"оплаты",
|
||||
"платеж",
|
||||
"платёж",
|
||||
"платежа",
|
||||
"платежи",
|
||||
"денег",
|
||||
"деньги",
|
||||
"объем",
|
||||
"объём",
|
||||
"док",
|
||||
"доки",
|
||||
"документ",
|
||||
"документы",
|
||||
"документов",
|
||||
"документами",
|
||||
"документу",
|
||||
"документе",
|
||||
"документа",
|
||||
"документах",
|
||||
"докам",
|
||||
"доками",
|
||||
"банк",
|
||||
"банковские",
|
||||
"операции",
|
||||
"платежи",
|
||||
"платеж",
|
||||
"платёж",
|
||||
"контрагент",
|
||||
"контрагенту",
|
||||
"контрагента",
|
||||
"контрагентам",
|
||||
"компания",
|
||||
"компании",
|
||||
"организация",
|
||||
"организации",
|
||||
"поставщикам",
|
||||
"клиентам",
|
||||
"покупателям",
|
||||
"заказчикам",
|
||||
"аванс",
|
||||
"авансы",
|
||||
"проблемный",
|
||||
"проблемные",
|
||||
"проблемным",
|
||||
"закрытия",
|
||||
"закрыть",
|
||||
"закрыты",
|
||||
"год",
|
||||
"года",
|
||||
"г",
|
||||
"плс",
|
||||
"pls",
|
||||
"пж",
|
||||
"пжлст",
|
||||
"пожалуйста",
|
||||
"бля",
|
||||
"блять",
|
||||
"епт",
|
||||
"ёпт",
|
||||
"епта",
|
||||
"нах",
|
||||
"нахуй",
|
||||
"есть",
|
||||
"же",
|
||||
"сводные",
|
||||
"сводный",
|
||||
"сводная",
|
||||
"сводную",
|
||||
"сводном",
|
||||
"сводного",
|
||||
"сводному",
|
||||
"неуказанному",
|
||||
"неуказанный",
|
||||
"неуказанная",
|
||||
"неуказанное",
|
||||
"указанному",
|
||||
"указанный",
|
||||
"указанная",
|
||||
"указанное",
|
||||
"объекту",
|
||||
"объект",
|
||||
"покеж",
|
||||
"покажи",
|
||||
"скажи",
|
||||
"показать",
|
||||
"выведи",
|
||||
"show",
|
||||
"list",
|
||||
"please",
|
||||
"теперь",
|
||||
"сейчас",
|
||||
"вернись",
|
||||
"вернуться",
|
||||
"вернуть",
|
||||
"раскрой",
|
||||
"раскрыть",
|
||||
"раскройте",
|
||||
"нему",
|
||||
"ней",
|
||||
"ним",
|
||||
"этому",
|
||||
"тому",
|
||||
"этомуже",
|
||||
"томуже",
|
||||
"vse",
|
||||
"all",
|
||||
"kakie",
|
||||
"kakoi",
|
||||
"est",
|
||||
"za",
|
||||
"po",
|
||||
"na",
|
||||
"s",
|
||||
"poka",
|
||||
"pokaji",
|
||||
"skazhi",
|
||||
"pokazhi",
|
||||
"pokazh",
|
||||
"pokezh",
|
||||
"doki",
|
||||
"doky",
|
||||
"dokument",
|
||||
"dokumenty",
|
||||
"documents",
|
||||
"docs",
|
||||
"связанный",
|
||||
"связанные",
|
||||
"связанных",
|
||||
"связанным",
|
||||
"связанному",
|
||||
"related",
|
||||
"linked"
|
||||
]);
|
||||
return !stopWords.has(lowered);
|
||||
if (/^(?:(?:19|20)?\d{2})(?:-?й)?(?:г|год|года)?$/iu.test(lowered)) {
|
||||
return false;
|
||||
}
|
||||
return !isCounterpartyNoiseToken(lowered);
|
||||
}
|
||||
|
||||
function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
|
|
@ -892,64 +699,25 @@ function hasDocsOrBankSignal(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function extractCounterpartyFromFreeTextHeuristic(text: string): string | undefined {
|
||||
if (!hasDocsOrBankSignal(text)) {
|
||||
return undefined;
|
||||
function hasStrongCounterpartyTokenShape(token: string): boolean {
|
||||
const source = String(token ?? "").trim();
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokens = String(text ?? "")
|
||||
.split(/[^\p{L}\p{N}._-]+/u)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return undefined;
|
||||
if (/[0-9]/u.test(source) || /[._/-]/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const monthTokens = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"сент",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек",
|
||||
"january",
|
||||
"february",
|
||||
"march",
|
||||
"april",
|
||||
"may",
|
||||
"june",
|
||||
"july",
|
||||
"august",
|
||||
"september",
|
||||
"october",
|
||||
"november",
|
||||
"december"
|
||||
];
|
||||
for (const token of tokens) {
|
||||
const lowered = token.toLowerCase();
|
||||
if (!isLikelyCounterpartyToken(lowered)) {
|
||||
continue;
|
||||
if (/[A-ZА-ЯЁ]/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
|
||||
continue;
|
||||
// Keep only compact lowercase slang aliases (e.g. "svk"), not arbitrary words.
|
||||
if (/^[a-z]{2,6}$/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (monthTokens.some((prefix) => lowered.startsWith(prefix))) {
|
||||
continue;
|
||||
if (/^[а-яё]+$/iu.test(source) && source.length <= 4) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) {
|
||||
continue;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractImplicitCounterpartyValue(text: string): string | undefined {
|
||||
|
|
@ -961,7 +729,7 @@ function extractImplicitCounterpartyValue(text: string): string | undefined {
|
|||
const beforeDocsMatch = input.match(beforeDocsPattern);
|
||||
if (beforeDocsMatch) {
|
||||
const candidate = String(beforeDocsMatch[1] ?? "").trim();
|
||||
if (isLikelyCounterpartyToken(candidate)) {
|
||||
if (hasStrongCounterpartyTokenShape(candidate) && isLikelyCounterpartyToken(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
|
@ -973,7 +741,7 @@ function extractImplicitCounterpartyValue(text: string): string | undefined {
|
|||
const afterDocsMatch = input.match(afterDocsPattern);
|
||||
if (afterDocsMatch) {
|
||||
const candidate = String(afterDocsMatch[1] ?? "").trim();
|
||||
if (isLikelyCounterpartyToken(candidate)) {
|
||||
if (hasStrongCounterpartyTokenShape(candidate) && isLikelyCounterpartyToken(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
|
@ -1022,6 +790,9 @@ function extractLeadingCounterpartyTokenHeuristic(text: string): string | undefi
|
|||
];
|
||||
for (const token of tokens.slice(0, 3)) {
|
||||
const lowered = token.toLowerCase();
|
||||
if (!hasStrongCounterpartyTokenShape(token)) {
|
||||
continue;
|
||||
}
|
||||
if (!isLikelyCounterpartyToken(lowered)) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1154,18 +925,6 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
warnings.push("counterparty_anchor_derived_from_implicit_phrase");
|
||||
}
|
||||
}
|
||||
if (
|
||||
!filters.counterparty &&
|
||||
(intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty")
|
||||
) {
|
||||
const heuristicCounterparty = extractCounterpartyFromFreeTextHeuristic(text);
|
||||
if (heuristicCounterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(heuristicCounterparty);
|
||||
warnings.push("counterparty_anchor_derived_from_free_text_heuristic");
|
||||
}
|
||||
}
|
||||
if (
|
||||
!filters.counterparty &&
|
||||
(intent === "list_documents_by_counterparty" ||
|
||||
|
|
|
|||
|
|
@ -1111,6 +1111,7 @@ function buildLimitedScopeLine(filters: AddressFilterSet): string | null {
|
|||
function buildLimitedOffers(input: {
|
||||
category: AddressLimitedReasonCategory;
|
||||
shape: AddressQueryShapeDetection;
|
||||
intent: AddressIntent;
|
||||
filters: AddressFilterSet;
|
||||
missingRequiredFilters: string[];
|
||||
reason: string;
|
||||
|
|
@ -1139,6 +1140,14 @@ function buildLimitedOffers(input: {
|
|||
}
|
||||
}
|
||||
|
||||
if (input.intent === "list_receivables_counterparties") {
|
||||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||||
} else if (input.intent === "list_payables_counterparties") {
|
||||
offers.push("показать контрагентов с максимальными хвостами кредиторки по 60/76");
|
||||
} else if (input.intent === "open_items_by_counterparty_or_contract" || input.intent === "list_open_contracts") {
|
||||
offers.push("показать незакрытые договоры и хвосты взаиморасчетов на дату");
|
||||
}
|
||||
|
||||
if (counterparty) {
|
||||
offers.push(`показать документы и платежи по контрагенту ${counterparty}`);
|
||||
} else if (contract) {
|
||||
|
|
@ -1170,49 +1179,128 @@ function buildLimitedOffers(input: {
|
|||
return Array.from(new Set(offers)).slice(0, 3);
|
||||
}
|
||||
|
||||
function buildLimitedIntentSignalLine(input: {
|
||||
intent: AddressIntent;
|
||||
shape: AddressQueryShapeDetection;
|
||||
}): string | null {
|
||||
const byIntent: Partial<Record<AddressIntent, string>> = {
|
||||
list_documents_by_counterparty: "Сигнал запроса: нужен срез документов/платежей по контрагенту.",
|
||||
list_documents_by_contract: "Сигнал запроса: нужен срез документов/платежей по договору.",
|
||||
bank_operations_by_counterparty: "Сигнал запроса: нужен срез банковских операций по контрагенту.",
|
||||
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
||||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов."
|
||||
};
|
||||
|
||||
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
|
||||
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
||||
DOCUMENT_LIST: "Сигнал запроса: список документов/операций.",
|
||||
OBJECT_LOOKUP: "Сигнал запроса: поиск конкретных объектов.",
|
||||
VERIFY_FACTUAL: "Сигнал запроса: проверка фактического состояния по данным.",
|
||||
COMPOUND_FACTUAL_QUERY: "Сигнал запроса: комбинированная проверка взаимосвязанных фактов."
|
||||
};
|
||||
|
||||
return byIntent[input.intent] ?? byShape[input.shape.shape] ?? null;
|
||||
}
|
||||
|
||||
function hasAggregateLimitedSignal(input: {
|
||||
shape: AddressQueryShapeDetection;
|
||||
intent: AddressIntent;
|
||||
reason: string;
|
||||
}): boolean {
|
||||
if (input.shape.shape === "AGGREGATE_LOOKUP") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
input.intent === "counterparty_population_and_roles" ||
|
||||
input.intent === "counterparty_activity_lifecycle" ||
|
||||
input.intent === "contract_usage_overview" ||
|
||||
input.intent === "supplier_payouts_profile" ||
|
||||
input.intent === "customer_revenue_and_payments" ||
|
||||
input.intent === "contract_usage_and_value"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return /(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin|year)/iu.test(
|
||||
String(input.reason ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function composeLimitedReply(input: {
|
||||
category: AddressLimitedReasonCategory;
|
||||
reason: string;
|
||||
nextStep?: string;
|
||||
shape: AddressQueryShapeDetection;
|
||||
intent: AddressIntent;
|
||||
filters: AddressFilterSet;
|
||||
missingRequiredFilters: string[];
|
||||
}): string {
|
||||
const reason = normalizeLimitedReason(input.reason);
|
||||
const headingSeed = `${input.category}|${input.shape.shape}|${reason}`;
|
||||
const aggregateLimitedSignal = hasAggregateLimitedSignal({
|
||||
shape: input.shape,
|
||||
intent: input.intent,
|
||||
reason: input.reason
|
||||
});
|
||||
const heading =
|
||||
input.category === "empty_match"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||||
"В текущем срезе данных по этому запросу совпадения не найдены."
|
||||
"В текущем срезе данных по этому запросу совпадения не найдены.",
|
||||
"По заданным фильтрам в текущем срезе совпадений пока нет."
|
||||
])
|
||||
: input.category === "missing_anchor"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"Чтобы ответ был точным, нужно чуть сильнее заякорить запрос.",
|
||||
"Запрос понятен, но для надежного ответа не хватает опорного ориентира."
|
||||
"Запрос понятен, но для надежного ответа не хватает опорного ориентира.",
|
||||
"Вопрос по смыслу ясен, но пока не хватает конкретной опоры для выборки."
|
||||
])
|
||||
: input.category === "recipe_visibility_gap"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"Запрос понятен, но текущий сценарий выборки не дает нужной детализации.",
|
||||
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки."
|
||||
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки.",
|
||||
"Сценарий запроса корректный, но текущая витрина не дает нужной детализации."
|
||||
])
|
||||
: input.category === "unsupported"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"По этому вопросу в текущем адресном контуре пока нет надежного маршрута ответа.",
|
||||
"Сейчас в адресном режиме такой сценарий не закрыт без риска ошибочного вывода."
|
||||
"Сейчас не дам прямой адресный ответ, чтобы не ошибиться в выводах.",
|
||||
"В текущем адресном контуре этот запрос лучше не закрывать «в лоб» — риск неверной трактовки высок.",
|
||||
"Для такого формата запроса нужен более широкий аналитический контур, иначе ответ будет ненадежным."
|
||||
])
|
||||
: "Не удалось завершить проверку в адресном режиме.";
|
||||
|
||||
const reasonSeed = `${headingSeed}|reason`;
|
||||
const reasonLine =
|
||||
input.category === "unsupported"
|
||||
? "Коротко: сценарий пока не покрыт текущими адресными маршрутами."
|
||||
? aggregateLimitedSignal
|
||||
? pickDeterministicVariant(reasonSeed, [
|
||||
"Это агрегатный/сравнительный вопрос: без расширенного анализа здесь легко дать ложную метрику.",
|
||||
"Запрос про сводную аналитику или ранжирование, поэтому в address-контуре ответ сейчас будет ненадежным.",
|
||||
"Нужна расширенная аналитическая обработка: адресный режим в этом кейсе не гарантирует корректный расчет."
|
||||
])
|
||||
: pickDeterministicVariant(reasonSeed, [
|
||||
"Сценарий пока не закрыт текущими адресными маршрутами без потери точности.",
|
||||
"Для этого запроса пока нет надежного ответа внутри текущего address-контура."
|
||||
])
|
||||
: input.category === "missing_anchor"
|
||||
? "Коротко: не хватает конкретного ориентира (контрагент, договор, счет или период)."
|
||||
? pickDeterministicVariant(reasonSeed, [
|
||||
"Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (контрагент, договор, счет или период).",
|
||||
"Для точного ответа нужен хотя бы один явный якорь: контрагент, договор, счет или период."
|
||||
])
|
||||
: input.category === "recipe_visibility_gap"
|
||||
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
|
||||
: `Коротко: ${reason}.`;
|
||||
? "Для уверенного ответа нужен более специализированный сценарий выборки."
|
||||
: `${reason}.`;
|
||||
|
||||
const lines = [heading, reasonLine];
|
||||
const signalLine = buildLimitedIntentSignalLine({
|
||||
intent: input.intent,
|
||||
shape: input.shape
|
||||
});
|
||||
if (signalLine && !(input.category === "unsupported" && aggregateLimitedSignal)) {
|
||||
lines.push(signalLine);
|
||||
}
|
||||
const scopeLine = buildLimitedScopeLine(input.filters);
|
||||
if (scopeLine) {
|
||||
lines.push(scopeLine);
|
||||
|
|
@ -1221,6 +1309,7 @@ function composeLimitedReply(input: {
|
|||
const offers = buildLimitedOffers({
|
||||
category: input.category,
|
||||
shape: input.shape,
|
||||
intent: input.intent,
|
||||
filters: input.filters,
|
||||
missingRequiredFilters: input.missingRequiredFilters,
|
||||
reason: input.reason,
|
||||
|
|
@ -1275,6 +1364,7 @@ function buildLimitedExecutionResult(input: {
|
|||
reason: input.reasonText,
|
||||
nextStep: input.nextStep,
|
||||
shape: input.shape,
|
||||
intent: input.intent.intent,
|
||||
filters: input.filters,
|
||||
missingRequiredFilters: input.missingRequiredFilters
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -4306,6 +4306,74 @@ function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRender
|
|||
);
|
||||
}
|
||||
|
||||
function shouldUseSoftPolicyReply(input: {
|
||||
mode: PolicyMode;
|
||||
policySignals: PolicySignals;
|
||||
limitationReasonCodes: EvidenceLimitationReasonCode[];
|
||||
aggregateEvidenceConfidence: EvidenceConfidence;
|
||||
coverageReport: RequirementCoverageReport;
|
||||
hasCriticalEvidenceLimitation: boolean;
|
||||
}): boolean {
|
||||
if (input.mode === "focused_grounded" || input.mode === "route_mismatch" || input.mode === "backend_error" || input.mode === "out_of_scope") {
|
||||
return false;
|
||||
}
|
||||
if (input.mode === "clarification_required" || input.mode === "no_grounded" || input.mode === "empty") {
|
||||
return true;
|
||||
}
|
||||
if (input.mode !== "broad_partial") {
|
||||
return false;
|
||||
}
|
||||
const hasCoverageGaps =
|
||||
input.coverageReport.requirements_uncovered.length > 0 ||
|
||||
input.coverageReport.requirements_partially_covered.length > 0 ||
|
||||
input.coverageReport.clarification_needed_for.length > 0 ||
|
||||
input.coverageReport.out_of_scope_requirements.length > 0;
|
||||
const weakEvidenceSignals =
|
||||
input.policySignals.broad_query_detected ||
|
||||
input.policySignals.broad_result_flag ||
|
||||
input.policySignals.minimum_evidence_failed ||
|
||||
input.aggregateEvidenceConfidence === "low" ||
|
||||
input.hasCriticalEvidenceLimitation ||
|
||||
input.limitationReasonCodes.includes("weak_source_mapping") ||
|
||||
input.limitationReasonCodes.includes("insufficient_detail") ||
|
||||
input.limitationReasonCodes.includes("missing_mechanism");
|
||||
return hasCoverageGaps || weakEvidenceSignals;
|
||||
}
|
||||
|
||||
function renderSoftPolicyReply(input: {
|
||||
structure: AnswerStructureV11;
|
||||
context?: AnswerRenderContext;
|
||||
mode: PolicyMode;
|
||||
}): string {
|
||||
const questionType = input.context?.questionType ?? "unknown";
|
||||
const shortLine = ensureSentence(buildShortSectionLine(input.structure));
|
||||
const evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3);
|
||||
const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3);
|
||||
const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3);
|
||||
const clarificationLines = dedupeNarrativeLines(input.structure.next_step_block.clarification_questions ?? [], 2);
|
||||
const actionLines = dedupeNarrativeLines(
|
||||
[...checkLines, ...(input.structure.next_step_block.recommended_actions ?? []), ...clarificationLines],
|
||||
3
|
||||
);
|
||||
const modeLine =
|
||||
input.mode === "clarification_required"
|
||||
? "Чтобы дать точный ответ, нужно уточнить несколько ориентиров."
|
||||
: input.mode === "no_grounded" || input.mode === "empty"
|
||||
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
|
||||
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
`Коротко: ${shortLine}`,
|
||||
modeLine,
|
||||
evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "",
|
||||
limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "",
|
||||
actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutput {
|
||||
const fallbackType = fallbackFromSummary(input.routeSummary);
|
||||
const questionType: QuestionTypeClass = input.questionTypeHint ?? "unknown";
|
||||
|
|
@ -4596,6 +4664,24 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
|||
missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
})
|
||||
: shouldUseSoftPolicyReply({
|
||||
mode: guardedDecision.mode,
|
||||
policySignals,
|
||||
limitationReasonCodes,
|
||||
aggregateEvidenceConfidence,
|
||||
coverageReport: input.coverageReport,
|
||||
hasCriticalEvidenceLimitation
|
||||
})
|
||||
? renderSoftPolicyReply({
|
||||
structure: answerStructure,
|
||||
context: {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage,
|
||||
userMessage: input.userMessage
|
||||
},
|
||||
mode: guardedDecision.mode
|
||||
})
|
||||
: renderPolicyReply(answerStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
|
|
|
|||
|
|
@ -998,6 +998,27 @@ function countTokens(text) {
|
|||
function hasPeriodLiteral(text) {
|
||||
return /\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/.test(text);
|
||||
}
|
||||
function hasStandaloneAddressTopicSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasFollowupMarker(normalized) || hasReferentialPointer(normalized)) {
|
||||
return false;
|
||||
}
|
||||
const hasRequestCue = /(?:^|[\s,.;:!?()\-])(?:покажи|показать|выведи|дай|найди|список|какие|какой|какая|каких|сколько|где|show|list|find|which|what)/iu.test(normalized);
|
||||
if (!hasRequestCue) {
|
||||
return false;
|
||||
}
|
||||
const hasBusinessObject = /(?:договор|контракт|контрагент|поставщик|покупател|клиент|документ|платеж|оплат|сальдо|остатк|сч[её]т|оборот|выруч|доход|прибыл|ндс|дебитор|кредитор|организац|компан|контор|contract|counterparty|supplier|customer|document|payment|turnover|revenue|profit|balance|account|vat)/iu.test(normalized);
|
||||
if (!hasBusinessObject) {
|
||||
return false;
|
||||
}
|
||||
const hasStructuredAnchor = hasPeriodLiteral(normalized) ||
|
||||
/\b\d{2}(?:[.,]\d{1,2})?\b/.test(normalized) ||
|
||||
/(?:альтернатива|лайсвуд|райм|ооо\s+[a-zа-яё])/iu.test(normalized);
|
||||
return hasStructuredAnchor || countTokens(normalized) >= 6;
|
||||
}
|
||||
function extractNormalizedPeriodLiteral(text) {
|
||||
const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
|
||||
if (monthly) {
|
||||
|
|
@ -1304,6 +1325,35 @@ function buildAddressCoverageReport() {
|
|||
out_of_scope_requirements: []
|
||||
};
|
||||
}
|
||||
function buildAssistantBackendErrorDebugPayload(errorMessage) {
|
||||
return {
|
||||
trace_id: `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||||
prompt_version: "assistant_backend_error_fallback_v1",
|
||||
schema_version: "assistant_backend_error_fallback_v1",
|
||||
fallback_type: "unknown",
|
||||
route_summary: null,
|
||||
fragments: [],
|
||||
requirements_extracted: [],
|
||||
coverage_report: buildAddressCoverageReport(),
|
||||
routes: [],
|
||||
retrieval_status: [],
|
||||
retrieval_results: [],
|
||||
answer_grounding_check: {
|
||||
status: "no_grounded_answer",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [
|
||||
`backend_error:${String(errorMessage ?? "unknown_error").slice(0, 280)}`
|
||||
],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
dropped_intent_segments: []
|
||||
};
|
||||
}
|
||||
function buildAssistantBackendErrorReply() {
|
||||
return "Сейчас не удалось завершить разбор из-за внутренней ошибки контуров LLM. Могу продолжить в адресном режиме: проверить документы, договоры и операции по нужному периоду или контрагенту.";
|
||||
}
|
||||
function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||||
const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded";
|
||||
const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null;
|
||||
|
|
@ -2170,6 +2220,9 @@ function hasAddressFollowupContextSignal(userMessage) {
|
|||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (hasStandaloneAddressTopicSignal(text)) {
|
||||
return false;
|
||||
}
|
||||
if (shouldHandleAsAssistantCapabilityMetaQuery(text)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2228,6 +2281,11 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
|||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage)
|
||||
: false;
|
||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal) {
|
||||
return null;
|
||||
}
|
||||
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2859,6 +2917,29 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
Boolean(sourceAnchorQuality.anchorValue) &&
|
||||
((candidateAnchorQuality.anchorType === "counterparty" &&
|
||||
candidateAnchorQuality.quality >= 2 &&
|
||||
Boolean(candidateAnchorQuality.anchorValue) &&
|
||||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? "")) ||
|
||||
(candidateAnchorQuality.quality < sourceAnchorQuality.quality &&
|
||||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidate)));
|
||||
if (counterpartyAnchorSubstitutedByCandidate) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_substitution",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage
|
||||
}, userMessage);
|
||||
}
|
||||
const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
|
|
@ -2876,27 +2957,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
|||
sanitizedUserMessage
|
||||
}, userMessage);
|
||||
}
|
||||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
candidateAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
candidateAnchorQuality.quality >= 2 &&
|
||||
Boolean(sourceAnchorQuality.anchorValue) &&
|
||||
Boolean(candidateAnchorQuality.anchorValue) &&
|
||||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? "");
|
||||
if (counterpartyAnchorSubstitutedByCandidate) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_substitution",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage
|
||||
}, userMessage);
|
||||
}
|
||||
if (fallbackCandidate) {
|
||||
const fallbackAnchorQuality = evaluateAddressAnchorQuality(String(fallbackCandidate.candidate ?? ""));
|
||||
const fallbackPreferredForAnchorSafety = sameIntentForAnchorSafety &&
|
||||
|
|
@ -3079,6 +3139,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
|||
llmContractIntent === "unknown";
|
||||
const hasAnyAddressSignal = hasClassifierSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal;
|
||||
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
|
||||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||||
hasAccountingSignal(rawMessageForGate) ||
|
||||
hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate);
|
||||
const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) ||
|
||||
|
|
@ -3105,7 +3166,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
|||
reason: "llm_predecompose_unsupported_mode"
|
||||
};
|
||||
}
|
||||
const hasMessageSignal = hasAnyAddressSignal;
|
||||
const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage;
|
||||
if (hasMessageSignal) {
|
||||
return {
|
||||
runAddressLane: true,
|
||||
|
|
@ -3190,7 +3251,9 @@ function hasDeepAnalysisPreferenceSignal(text) {
|
|||
}
|
||||
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 diagnosticsKeywordSignal = /(?:\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|audit|scan|\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 diagnosticsCheckVerbSignal = /(?:^|[\s,.;:!?()\-])\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c|\u043a\u0443|\u0438\u043c|\u043a\u0430)(?:$|[\s,.;:!?()\-])/iu.test(lower);
|
||||
const diagnosticsSignal = diagnosticsKeywordSignal || diagnosticsCheckVerbSignal;
|
||||
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);
|
||||
|
|
@ -3198,13 +3261,12 @@ function hasDeepAnalysisPreferenceSignal(text) {
|
|||
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 ||
|
||||
return lifecycleMismatchSignal ||
|
||||
(chainSignal && lifecycleTransitionGapSignal) ||
|
||||
expectedActualMismatchSignal ||
|
||||
(chainSignal && diagnosticsSignal) ||
|
||||
(riskOrAnomalySignal && (chainSignal || closureSignal || diagnosticsSignal || closureIntentSignal)) ||
|
||||
(diagnosticsSignal && closureIntentSignal) ||
|
||||
(riskOrAnomalySignal && (chainSignal || diagnosticsSignal || lifecycleTransitionGapSignal)) ||
|
||||
(diagnosticsSignal && (closureSignal || closureIntentSignal)) ||
|
||||
closureDiagnosticPhraseSignal ||
|
||||
signalVsNoiseDiagnostic;
|
||||
}
|
||||
|
|
@ -3213,7 +3275,7 @@ function hasDirectDeepAnalysisSignal(text) {
|
|||
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);
|
||||
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\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized);
|
||||
}
|
||||
function hasStrictDeepInvestigationCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
|
|
@ -3239,6 +3301,23 @@ function hasAggregateBusinessAnalyticsSignal(text) {
|
|||
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;
|
||||
}
|
||||
function hasOpenContractsAddressSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasContractCue = /(?:договор|контракт|contract)/iu.test(normalized);
|
||||
if (!hasContractCue) {
|
||||
return false;
|
||||
}
|
||||
const hasOpenCue = /(?:незакрыт|не\s+закрыт|открыт|open\s+contract|open\s+item|open)/iu.test(normalized);
|
||||
if (!hasOpenCue) {
|
||||
return false;
|
||||
}
|
||||
const hasRequestCue = /(?:покажи|показать|список|какие|какой|show|list|find|на\s+дату|as\s+of)/iu.test(normalized);
|
||||
const hasTemporalCue = hasPeriodLiteral(normalized) || /\b\d{4}[-/.]\d{2}[-/.]\d{2}\b/.test(normalized);
|
||||
return hasRequestCue || hasTemporalCue;
|
||||
}
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
|
|
@ -3276,10 +3355,21 @@ export function resolveAssistantOrchestrationDecision(input) {
|
|||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
|
||||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
|
||||
const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) ||
|
||||
hasStandaloneAddressTopicSignal(repairedRawUserMessage) ||
|
||||
hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) ||
|
||||
hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage);
|
||||
const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) ||
|
||||
hasOpenContractsAddressSignal(repairedRawUserMessage) ||
|
||||
hasOpenContractsAddressSignal(effectiveAddressUserMessage) ||
|
||||
hasOpenContractsAddressSignal(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 llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason);
|
||||
const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason &&
|
||||
/(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason));
|
||||
const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract &&
|
||||
typeof llmPreDecomposeMeta.semanticExtractionContract === "object"
|
||||
? llmPreDecomposeMeta.semanticExtractionContract
|
||||
|
|
@ -3295,7 +3385,8 @@ export function resolveAssistantOrchestrationDecision(input) {
|
|||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
|
||||
const keepAddressLaneByIntent = semanticApplyCanonicalRecommended &&
|
||||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent))) &&
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||
openContractsAddressSignal) &&
|
||||
!strictDeepInvestigationCueDetected;
|
||||
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
|
||||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
|
||||
|
|
@ -3387,7 +3478,8 @@ export function resolveAssistantOrchestrationDecision(input) {
|
|||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
|
||||
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
||||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)));
|
||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||
openContractsAddressSignal);
|
||||
const semanticGuardHints = semanticExtractionContract?.guard_hints &&
|
||||
typeof semanticExtractionContract.guard_hints === "object"
|
||||
? semanticExtractionContract.guard_hints
|
||||
|
|
@ -3408,8 +3500,14 @@ export function resolveAssistantOrchestrationDecision(input) {
|
|||
const unsupportedIntentOrMode = (modeDetection.mode !== "address_query" && intentResolution.intent === "unknown") ||
|
||||
llmContractMode === "unsupported";
|
||||
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
unsupportedIntentOrMode &&
|
||||
strongDataSignal &&
|
||||
(llmContractMode === "deep_analysis" ||
|
||||
!dataRetrievalSignal ||
|
||||
strictDeepInvestigationCueDetected ||
|
||||
semanticDeepInvestigationHintDetected ||
|
||||
aggregateBusinessAnalyticsSignal) &&
|
||||
!preserveAddressLaneSignal &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!supportedAddressIntentDetected &&
|
||||
|
|
@ -3426,21 +3524,25 @@ export function resolveAssistantOrchestrationDecision(input) {
|
|||
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 &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!supportedAddressIntentDetected &&
|
||||
!vatExplainFollowupSignal &&
|
||||
(!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed));
|
||||
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
aggregateBusinessAnalyticsSignal &&
|
||||
!keepAddressLaneByIntent &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(!followupContext ||
|
||||
llmContractMode === "unsupported" ||
|
||||
semanticAggregateShapeDetected ||
|
||||
!semanticApplyCanonicalRecommended));
|
||||
!semanticApplyCanonicalRecommended ||
|
||||
standaloneAddressTopicSignal));
|
||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
hasDeepSessionContinuationSignal({
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
|
|
@ -3547,24 +3649,29 @@ export function resolveAssistantOrchestrationDecision(input) {
|
|||
}
|
||||
function hasStrongDataIntentSignal(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|mcp|bank|counterparty|contract|document|ledger|posting|account|организац|компан|контор|фирм)/i.test(lower);
|
||||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм)/i.test(lower);
|
||||
}
|
||||
function hasDataRetrievalRequestSignal(text) {
|
||||
const lower = compactWhitespace(String(text ?? "").toLowerCase());
|
||||
if (!lower) {
|
||||
return false;
|
||||
}
|
||||
const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower);
|
||||
const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover)/iu.test(lower);
|
||||
if (hasBroadInterrogative && hasBroadBusinessObject) {
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower);
|
||||
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
|
||||
return false;
|
||||
}
|
||||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|bank|counterparty|contract|document|account|balance|ledger|posting|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
|
||||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
|
||||
if (!hasRetrievalObject) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3725,6 +3832,10 @@ function hasAssistantDataScopeMetaQuestionSignal(text) {
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized);
|
||||
if (hasDirectSlangScopeLead) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
|
|
@ -4771,6 +4882,7 @@ export class AssistantService {
|
|||
extractExecutionState
|
||||
}
|
||||
});
|
||||
try {
|
||||
const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({
|
||||
payload,
|
||||
runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)),
|
||||
|
|
@ -4780,4 +4892,45 @@ export class AssistantService {
|
|||
});
|
||||
return turnRuntime.response;
|
||||
}
|
||||
catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const sessionId = String(payload?.session_id ?? payload?.sessionId ?? "").trim() || `asst-${(0, nanoid_1.nanoid)(10)}`;
|
||||
const ensuredSession = this.sessions.ensureSession(sessionId);
|
||||
const existingAssistant = [...ensuredSession.items].reverse().find((item) => item.role === "assistant") ?? null;
|
||||
if (existingAssistant) {
|
||||
return {
|
||||
ok: true,
|
||||
session_id: sessionId,
|
||||
assistant_reply: existingAssistant.text,
|
||||
reply_type: existingAssistant.reply_type ?? "backend_error",
|
||||
conversation_item: existingAssistant,
|
||||
debug: existingAssistant.debug ?? buildAssistantBackendErrorDebugPayload(errorMessage),
|
||||
conversation: cloneItems(ensuredSession.items)
|
||||
};
|
||||
}
|
||||
const createdAt = new Date().toISOString();
|
||||
const debugPayload = buildAssistantBackendErrorDebugPayload(errorMessage);
|
||||
const assistantItem = this.sessions.appendItem(sessionId, {
|
||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: buildAssistantBackendErrorReply(),
|
||||
reply_type: "backend_error",
|
||||
created_at: createdAt,
|
||||
trace_id: debugPayload.trace_id ?? null,
|
||||
debug: debugPayload
|
||||
});
|
||||
const sessionSnapshot = this.sessions.getSession(sessionId) ?? this.sessions.ensureSession(sessionId);
|
||||
this.sessionLogger.persistSession(sessionSnapshot);
|
||||
return {
|
||||
ok: true,
|
||||
session_id: sessionId,
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: "backend_error",
|
||||
conversation_item: assistantItem,
|
||||
debug: debugPayload,
|
||||
conversation: cloneItems(sessionSnapshot.items)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1883,6 +1883,15 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("does not capture narrative filler as counterparty in broad docs-vs-money question", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"В каких случаях мы видим ситуацию, когда документы есть, а денег нет и пока не предвидится?",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(String(extracted.extracted_filters.counterparty ?? "")).not.toContain("случая");
|
||||
});
|
||||
|
||||
it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
|
||||
|
|
@ -2070,10 +2079,7 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
it("extracts leading counterparty token for short bank phrase", () => {
|
||||
const result = extractAddressFilters("свк списания/поступления за 2020", "bank_operations_by_counterparty");
|
||||
expect(result.extracted_filters.counterparty).toBe("свк");
|
||||
expect(
|
||||
result.warnings.includes("counterparty_anchor_derived_from_leading_token") ||
|
||||
result.warnings.includes("counterparty_anchor_derived_from_free_text_heuristic")
|
||||
).toBe(true);
|
||||
expect(result.warnings).toContain("counterparty_anchor_derived_from_leading_token");
|
||||
});
|
||||
|
||||
it("treats 'за весь период' as all-time hint and does not force 90-day default", () => {
|
||||
|
|
@ -2119,7 +2125,7 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
expect(result.extracted_filters.period_to).toBe("2020-12-31");
|
||||
});
|
||||
|
||||
it("extracts free-text counterparty and relaxed short-year period from noisy phrase", () => {
|
||||
it("extracts compact counterparty and relaxed short-year period from noisy phrase", () => {
|
||||
const result = extractAddressFilters(
|
||||
"свк 20 год - покажи доки плс",
|
||||
"list_documents_by_counterparty"
|
||||
|
|
@ -2127,7 +2133,7 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
expect(result.extracted_filters.counterparty).toBe("свк");
|
||||
expect(result.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(result.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(result.warnings).toContain("counterparty_anchor_derived_from_free_text_heuristic");
|
||||
expect(result.warnings).toContain("counterparty_anchor_derived_from_leading_token");
|
||||
expect(result.warnings).toContain("period_derived_from_year_phrase");
|
||||
expect(result.extracted_filters.counterparty).not.toBe("плс");
|
||||
});
|
||||
|
|
@ -2172,8 +2178,8 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
expect(
|
||||
result.warnings.some(
|
||||
(warning) =>
|
||||
warning === "counterparty_anchor_derived_from_free_text_heuristic" ||
|
||||
warning === "counterparty_anchor_derived_from_implicit_phrase"
|
||||
warning === "counterparty_anchor_derived_from_implicit_phrase" ||
|
||||
warning === "counterparty_anchor_derived_from_leading_token"
|
||||
)
|
||||
).toBe(true);
|
||||
expect(result.warnings).toContain("period_derived_from_year_phrase");
|
||||
|
|
|
|||
|
|
@ -272,6 +272,28 @@ describe("assistant orchestration contract", () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
it("routes standalone aggregate query to deep even when stale followup context exists and LLM predecompose is unavailable", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "какие обороты по альтернативе за 2020 год",
|
||||
effectiveAddressUserMessage: "какие обороты по альтернативе за 2020 год",
|
||||
followupContext: {
|
||||
previous_intent: "list_documents_by_contract",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
},
|
||||
llmPreDecomposeMeta: null as any,
|
||||
useMock: true
|
||||
} 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("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?",
|
||||
|
|
@ -289,6 +311,47 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps unsupported retrieval query in address lane when LLM runtime is unavailable", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage:
|
||||
"\u0413\u0434\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u043a\u043e\u043f\u0438\u043b\u0438\u0441\u044c \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0430\u0432\u043d\u043e \u043f\u043e\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u044c?",
|
||||
effectiveAddressUserMessage:
|
||||
"\u0413\u0434\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u043a\u043e\u043f\u0438\u043b\u0438\u0441\u044c \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0430\u0432\u043d\u043e \u043f\u043e\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u044c?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: false,
|
||||
reason: "error:OpenAI API key is missing.",
|
||||
llmCanonicalCandidateDetected: false,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: false,
|
||||
apply_canonical_recommended: false,
|
||||
reason_codes: ["unsupported_low_confidence_contract", "deep_investigation_signal_detected"],
|
||||
guard_hints: {
|
||||
deep_investigation_signal_detected: true
|
||||
},
|
||||
extraction: {
|
||||
query_shape: "VERIFY_FACTUAL",
|
||||
aggregation_profile: "unknown"
|
||||
}
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
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.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps VAT explain follow-up in address lane when followup context is present", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "почему прогноз к уплате 0?",
|
||||
|
|
@ -367,6 +430,48 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||
});
|
||||
|
||||
it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
||||
effectiveAddressUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
||||
followupContext: {
|
||||
previous_intent: "month_close_costs_20_44"
|
||||
},
|
||||
llmPreDecomposeMeta: null as any,
|
||||
useMock: true
|
||||
} 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");
|
||||
});
|
||||
|
||||
it("routes 'по каким конторам можем общаться?' to chat/data-scope in orchestration contract", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "по каким конторам можем общаться?",
|
||||
effectiveAddressUserMessage: "по каким контрагентам у нас есть активность или договоры?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
mode_confidence: "medium",
|
||||
intent: "list_documents_by_contract",
|
||||
intent_confidence: "medium"
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("assistant_data_scope_query_detected");
|
||||
});
|
||||
|
||||
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { composeAssistantAnswer } from "../src/services/answerComposer";
|
||||
import type { UnifiedRetrievalResult } from "../src/types/assistant";
|
||||
|
||||
function routeSummary() {
|
||||
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: "none" as const,
|
||||
message: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe("assistant soft policy reply", () => {
|
||||
it("renders soft non-template reply for weak broad-partial envelope", () => {
|
||||
const retrieval: UnifiedRetrievalResult = {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "store_feature_risk",
|
||||
status: "partial",
|
||||
result_type: "summary",
|
||||
items: [{ note: "weak candidate" }],
|
||||
summary: {
|
||||
broad_query_detected: true,
|
||||
broad_result_flag: true,
|
||||
minimum_evidence_failed: true,
|
||||
narrowing_strength: "weak"
|
||||
},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: [],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "low",
|
||||
limitations: ["Weak evidence envelope"],
|
||||
errors: []
|
||||
};
|
||||
|
||||
const output = composeAssistantAnswer({
|
||||
userMessage: "Покажи общую картину рисков и аномалий по документам.",
|
||||
routeSummary: routeSummary(),
|
||||
retrievalResults: [retrieval],
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "общая картина рисков",
|
||||
subject_tokens: [],
|
||||
status: "partially_covered",
|
||||
route: "store_feature_risk"
|
||||
}
|
||||
],
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 0,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: ["R1"],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "partial",
|
||||
route_subject_match: true,
|
||||
missing_requirements: ["R1"],
|
||||
reasons: ["insufficient_detail"],
|
||||
why_included_summary: [],
|
||||
selection_reason_summary: []
|
||||
},
|
||||
enableAnswerPolicyV11: true
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("partial_coverage");
|
||||
expect(output.assistant_reply).toContain("Что могу сделать сейчас:");
|
||||
expect(output.assistant_reply).toContain("Что пока не доказано:");
|
||||
expect(output.assistant_reply).not.toContain("Что сломано:");
|
||||
});
|
||||
|
||||
it("keeps structured sectioned reply for focused grounded envelope", () => {
|
||||
const retrieval: UnifiedRetrievalResult = {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "store_feature_risk",
|
||||
status: "ok",
|
||||
result_type: "summary",
|
||||
items: [{ doc: "A-1" }],
|
||||
summary: {
|
||||
broad_query_detected: false,
|
||||
broad_result_flag: false,
|
||||
minimum_evidence_failed: false,
|
||||
narrowing_strength: "strong"
|
||||
},
|
||||
evidence: [
|
||||
{
|
||||
evidence_id: "ev-1",
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "A-1",
|
||||
period: "2020-06",
|
||||
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|A-1|2020-06"
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "store_feature_risk",
|
||||
source: {
|
||||
namespace: "snapshot_2020",
|
||||
entity: "document",
|
||||
id: "A-1",
|
||||
period: "2020-06"
|
||||
}
|
||||
},
|
||||
evidence_kind: "fact",
|
||||
mechanism_note: "Переход подтвержден документом и проводкой.",
|
||||
confidence: "high",
|
||||
payload: {
|
||||
amount: 100
|
||||
}
|
||||
}
|
||||
],
|
||||
why_included: ["релевантная запись"],
|
||||
selection_reason: ["сильное совпадение"],
|
||||
risk_factors: [],
|
||||
business_interpretation: [],
|
||||
confidence: "high",
|
||||
limitations: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
const output = composeAssistantAnswer({
|
||||
userMessage: "Проверь документ A-1 за июнь 2020.",
|
||||
routeSummary: routeSummary(),
|
||||
retrievalResults: [retrieval],
|
||||
requirements: [
|
||||
{
|
||||
requirement_id: "R1",
|
||||
source_fragment_id: "F1",
|
||||
requirement_text: "проверить документ",
|
||||
subject_tokens: [],
|
||||
status: "covered",
|
||||
route: "store_feature_risk"
|
||||
}
|
||||
],
|
||||
coverageReport: {
|
||||
requirements_total: 1,
|
||||
requirements_covered: 1,
|
||||
requirements_uncovered: [],
|
||||
requirements_partially_covered: [],
|
||||
clarification_needed_for: [],
|
||||
out_of_scope_requirements: []
|
||||
},
|
||||
groundingCheck: {
|
||||
status: "grounded",
|
||||
route_subject_match: true,
|
||||
missing_requirements: [],
|
||||
reasons: [],
|
||||
why_included_summary: ["релевантная запись"],
|
||||
selection_reason_summary: ["сильное совпадение"]
|
||||
},
|
||||
enableAnswerPolicyV11: true
|
||||
});
|
||||
|
||||
expect(output.reply_type).toBe("factual_with_explanation");
|
||||
expect(output.assistant_reply).toContain("Что сломано:");
|
||||
expect(output.assistant_reply).toContain("Ограничения:");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { __evalRouteAsyncTestUtils } from "../src/routes/eval";
|
||||
|
||||
describe("eval async job sync", () => {
|
||||
it("marks cases as completed when report already contains case results", () => {
|
||||
const job = {
|
||||
job_id: "job-sync-report",
|
||||
status: "running",
|
||||
created_at: "2026-04-11T00:00:00.000Z",
|
||||
updated_at: "2026-04-11T00:00:00.000Z",
|
||||
eval_target: "assistant_stage1",
|
||||
run_id: "assistant-stage1-ut-sync-report",
|
||||
case_set_file: "dummy.json",
|
||||
analysis_date: "2017-07-10",
|
||||
total_cases: 2,
|
||||
completed_cases: 0,
|
||||
cases: [
|
||||
{
|
||||
case_id: "AUTO-001",
|
||||
turns_total: 1,
|
||||
status: "queued",
|
||||
messages: []
|
||||
},
|
||||
{
|
||||
case_id: "AUTO-002",
|
||||
turns_total: 1,
|
||||
status: "queued",
|
||||
messages: []
|
||||
}
|
||||
],
|
||||
error: null,
|
||||
report: {
|
||||
results: [{ case_id: "AUTO-001" }, { case_id: "AUTO-002" }]
|
||||
}
|
||||
} as any;
|
||||
|
||||
__evalRouteAsyncTestUtils.syncJobWithSessions(job);
|
||||
|
||||
expect(job.completed_cases).toBe(2);
|
||||
expect(job.status).toBe("completed");
|
||||
expect(job.cases.map((item: any) => item.status)).toEqual(["completed", "completed"]);
|
||||
});
|
||||
|
||||
it("keeps terminal case state when job is already completed and session file is missing", () => {
|
||||
const job = {
|
||||
job_id: "job-sync-sticky",
|
||||
status: "completed",
|
||||
created_at: "2026-04-11T00:00:00.000Z",
|
||||
updated_at: "2026-04-11T00:00:00.000Z",
|
||||
eval_target: "assistant_stage1",
|
||||
run_id: "assistant-stage1-ut-sync-sticky",
|
||||
case_set_file: "dummy.json",
|
||||
analysis_date: null,
|
||||
total_cases: 1,
|
||||
completed_cases: 1,
|
||||
cases: [
|
||||
{
|
||||
case_id: "AUTO-001",
|
||||
turns_total: 1,
|
||||
status: "completed",
|
||||
messages: []
|
||||
}
|
||||
],
|
||||
error: null,
|
||||
report: null
|
||||
} as any;
|
||||
|
||||
__evalRouteAsyncTestUtils.syncJobWithSessions(job);
|
||||
|
||||
expect(job.completed_cases).toBe(1);
|
||||
expect(job.status).toBe("completed");
|
||||
expect(job.cases[0].status).toBe("completed");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
{
|
||||
"suite_id": "assistant_autogen_runtime_job-dpHXAu3U4-",
|
||||
"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": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
{
|
||||
"suite_id": "assistant_autogen_runtime_job-wH3kvBePAs",
|
||||
"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": "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное - это явный признак того, что нужно вмешаться."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"run_id": "eval-Mu-hfNaOlK",
|
||||
"timestamp": "2026-04-11T15:21:02.731Z",
|
||||
"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": 2
|
||||
},
|
||||
"cases_total": 2,
|
||||
"metrics": {
|
||||
"schema_validation_pass_rate": 100,
|
||||
"scope_detection_accuracy": null,
|
||||
"scope_in_scope_rate": 100,
|
||||
"multi_intent_detected_rate": 0,
|
||||
"clarification_required_rate": 0,
|
||||
"avg_fragments_per_message": 1,
|
||||
"out_of_scope_fragment_rate": 0,
|
||||
"routed_fragment_rate": 100,
|
||||
"no_route_fragment_rate": 0,
|
||||
"route_resolution_accuracy": null,
|
||||
"no_route_precision": null,
|
||||
"false_no_route_rate": null,
|
||||
"execution_state_consistency_rate": 100,
|
||||
"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": 2,
|
||||
"checks_passed": 2
|
||||
},
|
||||
"route_distribution": {
|
||||
"store_feature_risk": 1,
|
||||
"hybrid_store_plus_live": 1
|
||||
},
|
||||
"fallback_distribution": {
|
||||
"none": 2
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"case_id": "BQ-001",
|
||||
"raw_question": "Проверь счет 60 за июнь 2020",
|
||||
"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": "B_29wyGfEBZtW1",
|
||||
"request_count_for_case": 0
|
||||
},
|
||||
{
|
||||
"case_id": "BQ-002",
|
||||
"raw_question": "Покажи риски по счету 97",
|
||||
"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": "Tq98dzUzkLNd9e",
|
||||
"request_count_for_case": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -407,6 +407,15 @@ function CommentResolvedIcon({ resolved }: { resolved: boolean }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CopyOutlineIcon() {
|
||||
return (
|
||||
<svg className="autoruns-copy-icon-svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<rect x="9" y="9" width="11" height="11" rx="2.2" />
|
||||
<path d="M15 7V5.8a1.8 1.8 0 0 0-1.8-1.8H5.8A1.8 1.8 0 0 0 4 5.8v7.4A1.8 1.8 0 0 0 5.8 15H7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoRunsHistoryPanel({
|
||||
connection,
|
||||
prompts,
|
||||
|
|
@ -526,6 +535,38 @@ export function AutoRunsHistoryPanel({
|
|||
[onLog]
|
||||
);
|
||||
|
||||
const copyRunIdToClipboard = useCallback(
|
||||
async (event: React.SyntheticEvent, runId: string) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const value = String(runId ?? "").trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} else {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = value;
|
||||
textarea.setAttribute("readonly", "true");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
log(`run id copied: ${value}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setErrorText(`Копирование run id: ${message}`);
|
||||
log(`copy run id error: ${message}`);
|
||||
}
|
||||
},
|
||||
[log]
|
||||
);
|
||||
|
||||
function startAssistantLiveStatusTicker(): () => void {
|
||||
let index = 0;
|
||||
setAssistantLiveStatus(ASSISTANT_STAGES[0]);
|
||||
|
|
@ -1819,7 +1860,25 @@ export function AutoRunsHistoryPanel({
|
|||
<strong>{formatDateTime(run.run_timestamp)}</strong>
|
||||
<span>{formatShortTarget(run.eval_target)}</span>
|
||||
</div>
|
||||
<div className="autoruns-run-meta">{run.run_id}</div>
|
||||
<div className="autoruns-run-meta autoruns-run-id-row">
|
||||
<span>{run.run_id}</span>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="autoruns-copy-run-id-btn"
|
||||
onClick={(event) => void copyRunIdToClipboard(event, run.run_id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
void copyRunIdToClipboard(event, run.run_id);
|
||||
}
|
||||
}}
|
||||
title="Скопировать run id"
|
||||
aria-label={`Скопировать run id ${run.run_id}`}
|
||||
>
|
||||
<CopyOutlineIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div className="autoruns-run-meta">
|
||||
режим={run.mode ?? "нет данных"} | mock={String(run.use_mock)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -891,6 +891,60 @@ button:disabled {
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.autoruns-run-id-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.autoruns-run-id-row > span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.autoruns-copy-run-id-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgb(var(--rgb-text-main));
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
opacity: 0.92;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.autoruns-copy-run-id-btn:hover {
|
||||
color: rgb(var(--rgb-text-main));
|
||||
opacity: 1;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.autoruns-copy-run-id-btn:focus-visible {
|
||||
outline: 1px solid rgba(var(--rgb-text-main), 0.7);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.autoruns-copy-icon-svg {
|
||||
width: 0.82rem;
|
||||
height: 0.82rem;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.75;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.autoruns-dialog-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
@ -946,12 +1000,17 @@ button:disabled {
|
|||
padding: 8px 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.autoruns-msg header,
|
||||
.autoruns-msg footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
gap: 8px;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
|
|
@ -960,9 +1019,19 @@ button:disabled {
|
|||
.autoruns-msg-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-msg-head-actions > span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.autoruns-msg-case-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -979,6 +1048,14 @@ button:disabled {
|
|||
white-space: pre-wrap;
|
||||
line-height: 1.35;
|
||||
font-size: 0.84rem;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.autoruns-msg footer span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon {
|
||||
|
|
|
|||
Loading…
Reference in New Issue