АДРЕСНЫЙ РЕЖИМ -Step-5 - feat(assistant): стабилизация свободного LLM-роутинга, прическа маршрутов chat/address, прототип прогноза НДС
This commit is contained in:
@@ -169,13 +169,13 @@ function detectCounterpartyProfileFocus(userMessage) {
|
||||
const hasCustomerToken = /(?:заказчик(?:ов|а)?|клиент(?:ов|а)?|customer(?:s)?|client(?:s)?)/iu.test(text);
|
||||
const hasMixedToken = /(?:смешан|проч(?:их|ие)|mixed)/iu.test(text);
|
||||
const asksRoles = /(?:заказчик(?:ов|а)?|поставщик(?:ов|а)?|смешан|проч(?:их|ие)|типы?\s+контрагент|разбей|раздели|roles?|split)/iu.test(text);
|
||||
if (hasSupplierToken && !hasCustomerToken && !hasMixedToken && !asksTotal) {
|
||||
if (hasSupplierToken && !hasCustomerToken && !hasMixedToken) {
|
||||
return "suppliers_only";
|
||||
}
|
||||
if (hasCustomerToken && !hasSupplierToken && !hasMixedToken && !asksTotal) {
|
||||
if (hasCustomerToken && !hasSupplierToken && !hasMixedToken) {
|
||||
return "customers_only";
|
||||
}
|
||||
if (hasMixedToken && !hasSupplierToken && !hasCustomerToken && !asksTotal) {
|
||||
if (hasMixedToken && !hasSupplierToken && !hasCustomerToken) {
|
||||
return "mixed_only";
|
||||
}
|
||||
if (asksTotal && !asksRoles) {
|
||||
@@ -614,7 +614,17 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
const focus = detectCounterpartyProfileFocus(options.userMessage);
|
||||
const includeTotal = focus === "full_profile" || focus === "total_only";
|
||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead = focus === "suppliers_only"
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Смешанных контрагентов (и customer, и supplier): ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
? `Всего уникальных контрагентов в базе: ${totalCounterparties}.`
|
||||
: `Активных контрагентов по операциям: ${activeCounterparties}.`;
|
||||
const lines = [
|
||||
directLead,
|
||||
"Профиль контрагентов собран (catalog + bank-doc activity aggregate).",
|
||||
`Строк агрегата: ${rows.length}.`
|
||||
];
|
||||
@@ -693,17 +703,17 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
? `в ${requestedYear} году`
|
||||
: "в выбранном периоде";
|
||||
const lines = [
|
||||
`Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
|
||||
"Собран профиль активности заказчиков (bank-doc activity aggregate).",
|
||||
`Строк агрегата: ${rows.length}.`
|
||||
];
|
||||
if (counterparties.length === 0) {
|
||||
lines.push("Активных заказчиков по выбранному окну не найдено.");
|
||||
lines.push("По выбранному окну активности заказчики не найдены.");
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
lines.push(`Активные заказчики ${scopeLabel}: ${counterparties.length}.`);
|
||||
const visible = counterparties.slice(0, 120);
|
||||
lines.push(...visible.map((item, index) => {
|
||||
const suffix = item.lastPeriod ? ` | последняя активность: ${item.lastPeriod}` : "";
|
||||
@@ -734,7 +744,11 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
const usedContracts = sumMarker("CT_USED");
|
||||
const unusedContracts = totalContracts > 0 ? Math.max(0, totalContracts - Math.min(usedContracts, totalContracts)) : null;
|
||||
const usedShare = totalContracts > 0 ? formatPercent(Math.min(usedContracts, totalContracts), totalContracts) : null;
|
||||
const usageLead = totalContracts > 0
|
||||
? `Использованных договоров: ${usedContracts} из ${totalContracts}${usedShare ? ` (${usedShare})` : ""}.`
|
||||
: `Использованных договоров (есть factual связь с операциями): ${usedContracts}.`;
|
||||
const lines = [
|
||||
usageLead,
|
||||
"Профиль договорной базы собран (catalog + usage aggregate).",
|
||||
`Строк агрегата: ${rows.length}.`
|
||||
];
|
||||
@@ -830,9 +844,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "top_by_ops") {
|
||||
const visible = rankedByOps.slice(0, limit);
|
||||
lines.push(isSupplier
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} поставщиков по количеству исходящих платежных операций:`
|
||||
: `Топ-${visible.length} заказчиков по количеству входящих платежных операций:`);
|
||||
: `Топ-${visible.length} заказчиков по количеству входящих платежных операций:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | операций: ${item.ops} | сумма: ${item.total} | макс: ${item.maxSingle}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
@@ -841,9 +856,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "top_by_max_single") {
|
||||
const visible = rankedByMaxSingle.slice(0, limit);
|
||||
lines.push(isSupplier
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`);
|
||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | сумма: ${item.total} | операций: ${item.ops}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
@@ -852,9 +868,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "top_by_avg_check_min_ops") {
|
||||
const visible = rankedByAvgCheck.slice(0, limit);
|
||||
lines.push(isSupplier
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} поставщиков по среднему чеку (минимум ${minOpsForAvgCheck} операций):`
|
||||
: `Топ-${visible.length} заказчиков по среднему чеку (минимум ${minOpsForAvgCheck} входящих операций):`);
|
||||
: `Топ-${visible.length} заказчиков по среднему чеку (минимум ${minOpsForAvgCheck} входящих операций):`;
|
||||
lines.unshift(heading);
|
||||
if (visible.length === 0) {
|
||||
lines.push(`Контрагентов с минимум ${minOpsForAvgCheck} операций не найдено.`);
|
||||
}
|
||||
@@ -868,9 +885,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "top_deals") {
|
||||
const visible = rankedDealsTop.slice(0, limit);
|
||||
lines.push(isSupplier
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
|
||||
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`);
|
||||
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.period ?? "n/a"} | ${item.counterparty} | ${item.registrator} | ${item.amount}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
@@ -879,9 +897,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "bottom_deals") {
|
||||
const visible = rankedDealsBottom.slice(0, limit);
|
||||
lines.push(isSupplier
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} самых маленьких разовых выплат:`
|
||||
: `Топ-${visible.length} самых маленьких разовых сделок по поступлениям:`);
|
||||
: `Топ-${visible.length} самых маленьких разовых сделок по поступлениям:`;
|
||||
lines.unshift(heading);
|
||||
if (activeOnlyForBottomDeals) {
|
||||
lines.push("Фильтр: только активные контрагенты (минимум 3 операции).");
|
||||
}
|
||||
@@ -892,9 +911,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
};
|
||||
}
|
||||
const visible = rankedByTotal.slice(0, limit);
|
||||
lines.push(isSupplier
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`);
|
||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => {
|
||||
const avgCheck = item.ops > 0 ? (item.total / item.ops).toFixed(2) : "0";
|
||||
return `${index + 1}. ${item.name} | сумма: ${item.total} | операций: ${item.ops} | средний чек: ${avgCheck} | макс: ${item.maxSingle}`;
|
||||
@@ -943,9 +963,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
.filter((item) => item.docs > 0 && item.turnover > 0)
|
||||
.sort((a, b) => a.turnover - b.turnover || b.docs - a.docs || a.contract.localeCompare(b.contract));
|
||||
const lines = [
|
||||
`Активных договоров: ${contractRows.length}.`,
|
||||
"Собран профиль договоров по обороту/бюджету (bank-doc contract aggregate).",
|
||||
`Строк источника: ${rows.length}.`,
|
||||
`Активных договоров: ${contractRows.length}.`
|
||||
`Договорных агрегатов: ${contractRows.length}.`
|
||||
];
|
||||
if (contractRows.length === 0) {
|
||||
lines.push("В выбранном окне не найдено операций, связанных с договорами.");
|
||||
@@ -956,7 +977,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "top_by_docs") {
|
||||
const visible = rankedByDocs.slice(0, limit);
|
||||
lines.push(`Топ-${visible.length} договоров по количеству операций:`);
|
||||
const heading = `Топ-${visible.length} договоров по количеству операций:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | операций: ${item.docs} | оборот: ${item.turnover} | контрагентов: ${item.counterparties.size}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
@@ -965,7 +987,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (focus === "bottom_by_turnover_active") {
|
||||
const visible = rankedBottomActive.slice(0, limit);
|
||||
lines.push(`Топ-${visible.length} активных договоров с минимальным бюджетом (оборотом):`);
|
||||
const heading = `Топ-${visible.length} активных договоров с минимальным бюджетом (оборотом):`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${item.turnover} | операций: ${item.docs} | последняя активность: ${item.lastPeriod ?? "n/a"}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
@@ -973,7 +996,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
};
|
||||
}
|
||||
const visible = rankedByTurnover.slice(0, limit);
|
||||
lines.push(`Топ-${visible.length} договоров по сумме оборота:`);
|
||||
const heading = `Топ-${visible.length} договоров по сумме оборота:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${item.turnover} | операций: ${item.docs} | контрагентов: ${item.counterparties.size} | последняя активность: ${item.lastPeriod ?? "n/a"}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
@@ -1073,8 +1097,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "list_documents_by_counterparty") {
|
||||
const lines = [
|
||||
"Собран список документов по контрагенту (live address lane).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
`Найдено документов по контрагенту: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
return {
|
||||
@@ -1084,6 +1107,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "list_documents_by_contract") {
|
||||
const lines = [
|
||||
`Найдено документов по договору: ${rows.length}.`,
|
||||
"Собран список документов по договору (live address lane).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
@@ -1095,6 +1119,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "bank_operations_by_counterparty") {
|
||||
const lines = [
|
||||
`Найдено банковских операций по контрагенту: ${rows.length}.`,
|
||||
"Собран список банковских операций по контрагенту (live address lane).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
@@ -1106,6 +1131,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "bank_operations_by_contract") {
|
||||
const lines = [
|
||||
`Найдено банковских операций по договору: ${rows.length}.`,
|
||||
"Собран список банковских операций по договору (live address lane).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
|
||||
@@ -24,6 +24,9 @@ function hasAllTimeHint(text) {
|
||||
function hasSameDateHint(text) {
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitPeriodLiteral(text) {
|
||||
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
|
||||
}
|
||||
function hasOpenItemsHint(text) {
|
||||
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
||||
}
|
||||
@@ -93,6 +96,29 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"что",
|
||||
"все",
|
||||
"всё",
|
||||
"кроме",
|
||||
"помимо",
|
||||
"этого",
|
||||
"этот",
|
||||
"эта",
|
||||
"эту",
|
||||
"этом",
|
||||
"это",
|
||||
"эти",
|
||||
"этих",
|
||||
"документ",
|
||||
"документа",
|
||||
"документы",
|
||||
"документов",
|
||||
"договор",
|
||||
"договора",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"еще",
|
||||
"ещё",
|
||||
"другие",
|
||||
"другое",
|
||||
"остальное",
|
||||
"год",
|
||||
"года",
|
||||
"году",
|
||||
@@ -313,6 +339,17 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
}
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
if (intent === "counterparty_activity_lifecycle" &&
|
||||
hasAddressFollowupContextSignal(userMessage) &&
|
||||
!hasExplicitPeriodLiteral(userMessage)) {
|
||||
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
||||
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
if (!currentPeriodFrom && currentPeriodTo === todayIso) {
|
||||
delete merged.period_to;
|
||||
reasons.push("period_to_cleared_for_lifecycle_followup");
|
||||
}
|
||||
}
|
||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||
if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) {
|
||||
@@ -439,23 +476,29 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
}
|
||||
function runAddressDecomposeStage(userMessage, followupContext) {
|
||||
const detectedMode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage);
|
||||
const mode = detectedMode.mode === "address_query"
|
||||
? detectedMode
|
||||
: followupContext && hasAddressFollowupContextSignal(userMessage)
|
||||
? {
|
||||
mode: "address_query",
|
||||
confidence: "medium",
|
||||
reasons: [...detectedMode.reasons, "address_mode_from_followup_context"]
|
||||
}
|
||||
: detectedMode;
|
||||
if (mode.mode !== "address_query") {
|
||||
return null;
|
||||
}
|
||||
const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage);
|
||||
if (shape.shape === "EXPLAIN_OR_REASON") {
|
||||
return null;
|
||||
}
|
||||
const detectedIntent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage);
|
||||
const mode = detectedMode.mode === "address_query"
|
||||
? detectedMode
|
||||
: detectedIntent.intent !== "unknown"
|
||||
? {
|
||||
mode: "address_query",
|
||||
confidence: "medium",
|
||||
reasons: [...detectedMode.reasons, "address_mode_from_resolved_intent"]
|
||||
}
|
||||
: followupContext && hasAddressFollowupContextSignal(userMessage)
|
||||
? {
|
||||
mode: "address_query",
|
||||
confidence: "medium",
|
||||
reasons: [...detectedMode.reasons, "address_mode_from_followup_context"]
|
||||
}
|
||||
: detectedMode;
|
||||
if (mode.mode !== "address_query") {
|
||||
return null;
|
||||
}
|
||||
const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext);
|
||||
const extractedFilters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent);
|
||||
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext);
|
||||
|
||||
Reference in New Issue
Block a user