ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.6 Усилина оркестрация и детект data-scope для живого сленга. Убрана шаблонность soft-refusal в limited-ответах. Подрезана словарнаю жирность в фильтр-экстракторе
This commit is contained in:
@@ -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 (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
|
||||
continue;
|
||||
}
|
||||
if (monthTokens.some((prefix) => lowered.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) {
|
||||
continue;
|
||||
}
|
||||
return token;
|
||||
if (/[A-ZА-ЯЁ]/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
return undefined;
|
||||
// Keep only compact lowercase slang aliases (e.g. "svk"), not arbitrary words.
|
||||
if (/^[a-z]{2,6}$/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[а-яё]+$/iu.test(source) && source.length <= 4) {
|
||||
return true;
|
||||
}
|
||||
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,12 +4664,30 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
})
|
||||
: renderPolicyReply(answerStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
: 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,
|
||||
anchors: anchorUsage,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
|
||||
return {
|
||||
assistant_reply: finalAssistantReply,
|
||||
|
||||
@@ -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,13 +4882,55 @@ export class AssistantService {
|
||||
extractExecutionState
|
||||
}
|
||||
});
|
||||
const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({
|
||||
payload,
|
||||
runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)),
|
||||
resolveSessionOrganizationScopeContext: (runtimeUserMessage, sessionItems) => resolveSessionOrganizationScopeContext(runtimeUserMessage, sessionItems),
|
||||
runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantAddressAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)),
|
||||
runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantDeepTurnAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps))
|
||||
});
|
||||
return turnRuntime.response;
|
||||
try {
|
||||
const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({
|
||||
payload,
|
||||
runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)),
|
||||
resolveSessionOrganizationScopeContext: (runtimeUserMessage, sessionItems) => resolveSessionOrganizationScopeContext(runtimeUserMessage, sessionItems),
|
||||
runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantAddressAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)),
|
||||
runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantDeepTurnAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps))
|
||||
});
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user