АРЧ АП11 - Архитектура после ге :

This commit is contained in:
2026-04-17 23:49:21 +03:00
parent 8f9364e7c9
commit a5ea9adf53
72 changed files with 7353 additions and 4027 deletions
@@ -1,7 +1,112 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveCounterpartyAddressIntent = resolveCounterpartyAddressIntent;
function hasUnicodeOpenItemsAccountSignal(text) {
const normalized = String(text ?? "").toLowerCase();
if (!normalized) {
return false;
}
return (/(?:\u0445\u0432\u043e\u0441\u0442|\u0434\u043e\u043b\u0433|\u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442|\u0432\u0438\u0441)/iu.test(normalized) &&
/(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:\u0430|\u0443|\u043e\u043c|\u043e\u0432)?\s*(?:\u2116|#)?\s*(?:60|62|76)(?:\.\d{2})?|(?:60|62|76)(?:\.\d{2})?\s*(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:\u0430|\u0443|\u043e\u043c|\u043e\u0432)?))/iu.test(normalized));
}
function hasUnicodeCounterpartyShipmentItemFlowSignal(text) {
return /(?:\u043e\u0442\u0433\u0440\u0443\u0436\u0430\u043b)/iu.test(text) &&
/(?:\u0442\u043e\u0432\u0430\u0440|\u0443\u0441\u043b\u0443\u0433|\u043f\u043e\u0437\u0438\u0446\u0438|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440)/iu.test(text);
}
function hasUnicodePassiveShipmentByCounterpartySignal(text) {
return (/(?:\u043e\u0442\u0433\u0440\u0443\u0436\u0435\u043d(?:\u044b|\u043e|\u0430)?)/iu.test(text) &&
/(?:\u0442\u043e\u0432\u0430\u0440|\u0443\u0441\u043b\u0443\u0433|\u043f\u043e\u0437\u0438\u0446\u0438|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440)/iu.test(text) &&
/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u043c)/iu.test(text));
}
function hasUnicodeDocumentSignal(text) {
return /(?:\u0434\u043e\u043a(?:\u0443\u043c\u0435\u043d\u0442(?:\u044b|\u043e\u0432|\u0430\u043c|\u0430\u043c\u0438|\u0430\u0445)?|\u0438)?|\u0434\u043e\u043a\u0438)(?=$|[\s,.;:!?()])/iu.test(text);
}
function hasUnicodeLikelyCounterpartyAfterBy(text) {
const match = text.match(/(?:^|[\s(])\u043f\u043e\s+([\p{L}\d][\p{L}\d._-]{1,})(?=$|[\s,.;:!?()])/iu);
if (!match) {
return false;
}
const token = String(match[1] ?? "").toLowerCase();
if (!token) {
return false;
}
const stopWords = new Set([
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443",
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430",
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u043c",
"\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438",
"\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044e",
"\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438",
"\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
"\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443",
"\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u043e\u043c",
"\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0443",
"\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043e\u043c",
"\u0441\u0447\u0435\u0442\u0443",
"\u0441\u0447\u0451\u0442\u0443",
"\u0441\u0447\u0435\u0442\u043e\u043c",
"\u0441\u0447\u0451\u0442\u043e\u043c",
"\u0434\u0430\u0442\u0435",
"\u0434\u0430\u0442\u0443",
"\u043f\u0435\u0440\u0438\u043e\u0434\u0443",
"\u043f\u0435\u0440\u0438\u043e\u0434",
"\u0441\u043a\u043b\u0430\u0434\u0443",
"\u0441\u043a\u043b\u0430\u0434\u0435",
"\u0431\u0430\u043d\u043a\u0443",
"\u0431\u0430\u043d\u043a\u0435",
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c",
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0443"
]);
return !stopWords.has(token);
}
function resolveCounterpartyAddressIntent(text, deps) {
if (hasUnicodeOpenItemsAccountSignal(text)) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
reasons: ["open_items_signal_detected"]
};
}
if (/(?:хвост|долг|незакрыт|вис)/iu.test(text) &&
/(?:счету?|сч[её]ту?)\s*60|60\s*(?:счет|сч[её]т)/iu.test(text)) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
reasons: ["open_items_signal_detected"]
};
}
if (/отгружал/iu.test(text) &&
/(?:товар|услуг|позици|номенклатур)/iu.test(text)) {
return {
intent: "list_documents_by_counterparty",
confidence: "medium",
reasons: ["counterparty_item_flow_signal_detected"]
};
}
if (/отгружен(?:ы|о|а)?/iu.test(text) &&
/(?:товар|услуг|позици|номенклатур)/iu.test(text) &&
/контрагентом/iu.test(text)) {
return {
intent: "list_documents_by_counterparty",
confidence: "medium",
reasons: ["counterparty_item_flow_signal_detected"]
};
}
if (hasUnicodeCounterpartyShipmentItemFlowSignal(text)) {
return {
intent: "list_documents_by_counterparty",
confidence: "medium",
reasons: ["counterparty_item_flow_signal_detected"]
};
}
if (hasUnicodePassiveShipmentByCounterpartySignal(text)) {
return {
intent: "list_documents_by_counterparty",
confidence: "medium",
reasons: ["counterparty_item_flow_signal_detected"]
};
}
if (deps.hasOpenContractsListSignal(text)) {
return {
intent: "open_contracts_confirmed_as_of_date",
@@ -124,6 +229,13 @@ function resolveCounterpartyAddressIntent(text, deps) {
reasons: ["bank_ops_by_counterparty_signal_detected"]
};
}
if (hasUnicodeDocumentSignal(text) && hasUnicodeLikelyCounterpartyAfterBy(text)) {
return {
intent: "list_documents_by_counterparty",
confidence: "medium",
reasons: ["documents_by_counterparty_signal_detected"]
};
}
if ((deps.hasAny(text, deps.documentsByCounterpartyHints) || deps.hasCounterpartyShipmentItemFlowSignal(text)) &&
(deps.hasPartyAnchorMention(text) ||
deps.hasLooseByAnchorMention(text) ||
@@ -844,6 +844,17 @@ function extractShipmentCounterpartyValue(text) {
}
return candidate;
}
function extractInstrumentalCounterpartyValue(text) {
const match = String(text ?? "").match(/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
return undefined;
}
const candidate = cleanupAnchorValue(String(match[1] ?? ""));
if (!candidate || !isLikelyCounterpartyToken(candidate)) {
return undefined;
}
return candidate;
}
function hasExplicitAccountCue(text) {
return /(?:сч[её]т|счет|account|acct)/iu.test(String(text ?? ""));
}
@@ -1508,6 +1519,17 @@ function extractAddressFilters(userMessage, intent) {
warnings.push("counterparty_anchor_derived_from_shipment_phrase");
}
}
if (!filters.counterparty &&
allowGenericCounterpartyAnchor &&
(intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_contracts_by_counterparty")) {
const instrumentalCounterparty = extractInstrumentalCounterpartyValue(text);
if (instrumentalCounterparty) {
filters.counterparty = cleanupAnchorValue(instrumentalCounterparty);
warnings.push("counterparty_anchor_derived_from_instrumental_phrase");
}
}
if (!filters.counterparty &&
allowGenericCounterpartyAnchor &&
(intent === "list_documents_by_counterparty" ||
File diff suppressed because it is too large Load Diff
@@ -8,12 +8,26 @@ function hasInventoryAccount41Anchor(text) {
function hasInventoryAsOfCue(text) {
return /(?:сейчас|текущ|на\s+дату|по\s+состоянию|срез|на\s+конец|date|as\s+of|current|now|today)/iu.test(text);
}
function hasPlainRussianInventoryOnHandSignal(text) {
const normalized = String(text ?? "").toLowerCase().replace(/ё/g, "е");
if (!normalized) {
return false;
}
const hasStockCue = /(?:остатк|склад|товар(?:ы|ов)?|номенклатур|позици|на\s+складе|по\s+складу)/iu.test(normalized);
if (!hasStockCue) {
return false;
}
const hasRequestCue = /(?:какие|какой|какая|что|покажи|показать|получить|дай|выведи|чекни|посмотри|есть\s+ли)/iu.test(normalized);
const hasSnapshotCue = /(?:на\s+(?:дату|сегодня|сейчас|март|апрел|май|мая|июн|июл|август|сентябр|октябр|ноябр|декабр|январ|феврал)|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return hasRequestCue && (hasSnapshotCue || /остатк/iu.test(normalized));
}
function hasInventoryOnHandSignal(text) {
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
const hasStockStateCue = /(?:(?:что|ч[еёо])\s+там\s+на\s+склад(?:е|у|ом|ах)|(?:что|ч[еёо]).*происход(?:ит|ило|ящее).*(?:на\s+)?склад(?:е|у|ом|ах)|происход(?:ит|ило|ящее)\s+на\s+склад(?:е|у|ом|ах)|ситуац(?:ия|ии)\s+на\s+склад(?:е|у|ом|ах)|обстановк(?:а|и)\s+на\s+склад(?:е|у|ом|ах)|what(?:'s| is)?\s+(?:there\s+)?(?:on|in)\s+(?:the\s+)?(?:warehouse|stock)|what(?:'s| is)?\s+happening\s+(?:on|in)\s+(?:the\s+)?(?:warehouse|stock))/iu.test(text);
const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
const hasStockLexeme = /(?:склад(?:е|у|ом|ы|ов)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme && !hasAccount41Anchor) {
const hasPlainRussianRootCue = hasPlainRussianInventoryOnHandSignal(text);
if (!hasStockLexeme && !hasAccount41Anchor && !hasPlainRussianRootCue) {
return false;
}
if (hasInventoryProvenanceSignalV2(text) ||
@@ -29,6 +43,9 @@ function hasInventoryOnHandSignal(text) {
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true;
}
if (hasPlainRussianRootCue) {
return true;
}
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
}
@@ -171,6 +188,14 @@ function resolveInventoryAddressIntent(text) {
reasons: ["inventory_purchase_date_signal_detected"]
};
}
if ((text.includes("по этой позиции") || text.includes("по этому товару")) &&
(text.includes("когда была закупка") || text.includes("когда купили"))) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_purchase_date_signal_detected"]
};
}
if (hasSelectedObjectInventoryPurchaseDocumentsSignal(text)) {
return {
intent: "inventory_purchase_documents_for_item",
@@ -1461,6 +1461,24 @@ function isOrganizationScopedInventoryIntent(intent) {
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
function shouldDeferInventoryOrganizationClarification(intent, filters, semanticFrame) {
if (!isOrganizationScopedInventoryIntent(intent)) {
return false;
}
const hasItemScope = Boolean(toNonEmptyFilterValue(filters.item)) ||
semanticFrame?.anchor_kind === "item" ||
semanticFrame?.anchor_kind === "selected_object" ||
semanticFrame?.selected_object_scope_detected === true;
if (hasItemScope) {
return true;
}
return (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
function collectOrganizationCandidatesFromRows(rows) {
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(rows.map((row) => row.organization).filter((value) => Boolean(value)));
}
@@ -2584,6 +2602,7 @@ class AddressQueryService {
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
!activeOrganization &&
!resolvedOrganizationFromMessage &&
!shouldDeferInventoryOrganizationClarification(intent.intent, filters.extracted_filters, semanticFrame) &&
knownOrganizations.length > 1) {
return buildOrganizationClarificationExecutionResult({
mode,
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.contractCandidatesFromRows = contractCandidatesFromRows;
exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
const replyPackaging_1 = require("./replyPackaging");
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
function uniqueStrings(values) {
return Array.from(new Set(values
@@ -2135,6 +2135,30 @@ function composeFactualReplyBody(intent, rows, options = {}) {
if (inventoryReply) {
return inventoryReply;
}
const counterpartyAnalyticsReply = (0, counterpartyAnalyticsReplyBuilders_1.composeCounterpartyAnalyticsReply)(intent, rows, options, {
formatPercent,
formatDateRu,
formatMoneyRub,
extractYearFromIso,
detectCounterpartyProfileFocus,
detectCounterpartyLifecycleFocus,
hasCounterpartyLifecycleLongevityQuestion,
hasCounterpartyActivityAgeQuestion,
detectRankingLimit,
detectValueRankingFocus,
detectContractValueFocus,
detectMinOpsForAvgCheck,
extractRequestedYearFromQuestion,
extractCounterpartyName,
extractContractName,
counterpartyLookupMatches,
toUtcDayTimestamp,
formatAgeYearsMonthsDays,
normalizeQuestionText
});
if (counterpartyAnalyticsReply) {
return counterpartyAnalyticsReply;
}
if (intent === "document_type_and_account_section_profile") {
const rowsByMarker = new Map();
for (const row of rows) {
@@ -2366,692 +2390,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
text: lines.join("\n")
};
}
if (intent === "counterparty_population_and_roles") {
const rowsByMarker = new Map();
for (const row of rows) {
const marker = String(row.registrator ?? "").trim().toUpperCase();
if (!marker) {
continue;
}
if (!rowsByMarker.has(marker)) {
rowsByMarker.set(marker, []);
}
rowsByMarker.get(marker).push(row);
}
const sumMarker = (marker) => (rowsByMarker.get(marker) ?? []).reduce((sum, row) => sum + (row.amount ?? 0), 0);
const totalCounterparties = sumMarker("CP_TOTAL");
const customerActive = sumMarker("CP_CUSTOMER_ACTIVE");
const supplierActive = sumMarker("CP_SUPPLIER_ACTIVE");
const mixedActive = sumMarker("CP_MIXED_ACTIVE");
const activeUnion = sumMarker("CP_ACTIVE_UNION");
const customerOnly = Math.max(0, customerActive - mixedActive);
const supplierOnly = Math.max(0, supplierActive - mixedActive);
const resolvedActive = customerOnly + supplierOnly + mixedActive;
const activeCounterparties = Math.max(activeUnion, resolvedActive);
const otherCounterparties = totalCounterparties > 0 ? Math.max(0, totalCounterparties - resolvedActive) : null;
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}.`
];
if (includeTotal) {
if (totalCounterparties > 0) {
lines.push(`Всего уникальных контрагентов в базе: ${totalCounterparties}.`);
}
else if (activeCounterparties > 0) {
lines.push(`Total из справочника не получен, оценка по активности в документах: ${activeCounterparties} контрагентов.`);
}
else {
lines.push("По количеству контрагентов агрегатных строк не найдено.");
}
}
if (includeRoles) {
if (resolvedActive > 0 || activeCounterparties > 0) {
lines.push("Роли контрагентов по активности:");
lines.push(`1. Заказчики (только customer-роль): ${customerOnly}.`);
lines.push(`2. Поставщики (только supplier-роль): ${supplierOnly}.`);
lines.push(`3. Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
lines.push(`4. Активные контрагенты (union ролей): ${activeCounterparties}.`);
if (otherCounterparties !== null) {
lines.push(`5. Прочие/неактивные в выбранном окне: ${otherCounterparties}.`);
}
}
else {
lines.push("По role-split контрагентов агрегатных строк не найдено.");
}
}
if (focus === "suppliers_only") {
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
}
if (focus === "customers_only") {
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
}
if (focus === "mixed_only") {
lines.push(`Смешанных контрагентов (и customer, и supplier): ${mixedActive}.`);
}
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
if (intent === "counterparty_activity_lifecycle") {
const activityFirstRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST");
const activityRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY");
const activityYearRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_YEAR");
const byCounterparty = new Map();
for (const row of activityFirstRows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
}
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
current.firstObservedActivity = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityYearRows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: null,
years: new Set(year !== null ? [year] : [])
});
continue;
}
current.opsCount += opsCount;
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityRows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const current = byCounterparty.get(name);
if (!current) {
const year = extractYearFromIso(row.period);
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
}
if (activityYearRows.length === 0 && opsCount > current.opsCount) {
current.opsCount = opsCount;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
const year = extractYearFromIso(row.period);
if (year !== null) {
current.years.add(year);
}
}
const counterpartiesRaw = Array.from(byCounterparty.values());
const focus = detectCounterpartyLifecycleFocus(options.userMessage);
const requestedYear = extractRequestedYearFromQuestion(options.userMessage);
const longevityQuestion = hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
const activityAgeQuestion = hasCounterpartyActivityAgeQuestion(options.userMessage);
const rankingLimit = detectRankingLimit(options.userMessage, 10);
const counterparties = counterpartiesRaw.sort((left, right) => {
if (longevityQuestion) {
const yearsDiff = right.years.size - left.years.size;
if (yearsDiff !== 0) {
return yearsDiff;
}
}
if (right.opsCount !== left.opsCount) {
return right.opsCount - left.opsCount;
}
return (right.lastPeriod ?? "").localeCompare(left.lastPeriod ?? "");
});
const scopeLabel = focus === "active_customers_all_time"
? "за все время"
: requestedYear
? `в ${requestedYear} году`
: "в выбранном периоде";
if (activityAgeQuestion) {
const focusedCounterparty = counterparties.find((item) => counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
if (focusedCounterparty) {
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
const lastObservedActivity = focusedCounterparty.lastPeriod;
const firstTimestamp = toUtcDayTimestamp(firstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(lastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: focusedCounterparty.years.size > 0
? `${focusedCounterparty.years.size} г.`
: null;
const directLine = observedAgeLabel && firstObservedActivity && lastObservedActivity
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`;
const lines = [directLine];
if (firstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(firstObservedActivity)}.`);
}
if (lastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(lastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
if (focusedCounterparty.years.size > 0) {
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
const organizationHint = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint ?? null);
if (organizationHint && counterparties.length > 0) {
const organizationFirstObservedActivity = counterparties.reduce((earliest, item) => {
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
if (!candidate) {
return earliest;
}
if (!earliest || candidate < earliest) {
return candidate;
}
return earliest;
}, null);
const organizationLastObservedActivity = counterparties.reduce((latest, item) => {
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
if (!candidate) {
return latest;
}
if (!latest || candidate > latest) {
return candidate;
}
return latest;
}, null);
const organizationYears = new Set();
let organizationOpsCount = 0;
for (const item of counterparties) {
organizationOpsCount += item.opsCount;
for (const year of item.years) {
organizationYears.add(year);
}
}
const firstTimestamp = toUtcDayTimestamp(organizationFirstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(organizationLastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: organizationYears.size > 0
? `${organizationYears.size} г.`
: null;
const lines = [
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
];
if (organizationFirstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(organizationFirstObservedActivity)}.`);
}
if (organizationLastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(organizationLastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
if (organizationYears.size > 0) {
const years = Array.from(organizationYears).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
}
const lines = longevityQuestion
? [
`Заказчиков с самым длинным горизонтом сотрудничества (по годам): ${counterparties.length}.`,
"Собран lifecycle-профиль заказчиков: ранжирование по числу лет и частоте активности.",
`Строк агрегата: ${rows.length}.`
]
: [
`Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
"Собран профиль активности заказчиков (bank-doc activity aggregate).",
`Строк агрегата: ${rows.length}.`
];
if (counterparties.length === 0) {
lines.push(longevityQuestion
? "По доступному окну не удалось выделить заказчиков с подтвержденной длительностью сотрудничества по годам."
: "По выбранному окну активности заказчики не найдены.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
const visible = counterparties.slice(0, longevityQuestion ? rankingLimit : 120);
if (longevityQuestion) {
lines.push(`Топ-${visible.length} заказчиков по охвату лет и частоте операций:`);
}
lines.push(...visible.map((item, index) => {
const years = Array.from(item.years).sort((a, b) => a - b);
const yearsLabel = years.length > 0 ? ` | лет в базе: ${years.length} | годы: ${years.join(", ")}` : "";
const periodSpan = item.firstPeriod && item.lastPeriod ? ` | период: ${item.firstPeriod}..${item.lastPeriod}` : "";
if (longevityQuestion) {
return `${index + 1}. ${item.name} | операций: ${item.opsCount}${yearsLabel}${periodSpan}`;
}
const suffix = item.lastPeriod ? ` | последняя активность: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name} | операций: ${item.opsCount}${suffix}${years.length > 0 ? ` | лет в базе: ${years.length}` : ""}`;
}));
if (counterparties.length > visible.length) {
lines.push(longevityQuestion
? `Показаны первые ${visible.length} из ${counterparties.length} заказчиков (полный список можно выгрузить отдельно).`
: `Показаны первые ${visible.length} из ${counterparties.length} заказчиков.`);
}
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "contract_usage_overview") {
const rowsByMarker = new Map();
for (const row of rows) {
const marker = String(row.registrator ?? "").trim().toUpperCase();
if (!marker) {
continue;
}
if (!rowsByMarker.has(marker)) {
rowsByMarker.set(marker, []);
}
rowsByMarker.get(marker).push(row);
}
const sumMarker = (marker) => (rowsByMarker.get(marker) ?? []).reduce((sum, row) => sum + (row.amount ?? 0), 0);
const totalContracts = sumMarker("CT_TOTAL");
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}.`
];
if (totalContracts > 0) {
lines.push(`Всего договоров в базе: ${totalContracts}.`);
}
else {
lines.push("Общее количество договоров не получено (пустой/недоступный срез справочника).");
}
lines.push(`Использованных договоров (есть factual связь с операциями): ${usedContracts}.`);
if (unusedContracts !== null) {
lines.push(`Неиспользуемых договоров: ${unusedContracts}.`);
}
if (usedShare) {
lines.push(`Доля используемых договоров: ${usedShare}.`);
}
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
if (intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile") {
const isSupplier = intent === "supplier_payouts_profile";
const focus = detectValueRankingFocus(options.userMessage);
const limit = detectRankingLimit(options.userMessage, 20);
const minOpsForAvgCheck = detectMinOpsForAvgCheck(options.userMessage);
const normalizedQuestion = normalizeQuestionText(options.userMessage);
const byCounterparty = new Map();
const byYear = new Map();
const deals = [];
for (const row of rows) {
const counterparty = extractCounterpartyName(row);
const amount = row.amount ?? 0;
if (!counterparty || !Number.isFinite(amount) || amount <= 0) {
continue;
}
const current = byCounterparty.get(counterparty);
if (!current) {
byCounterparty.set(counterparty, {
name: counterparty,
total: amount,
ops: 1,
maxSingle: amount,
minSingle: amount,
lastPeriod: row.period
});
}
else {
current.total += amount;
current.ops += 1;
current.maxSingle = Math.max(current.maxSingle, amount);
current.minSingle = Math.min(current.minSingle, amount);
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
}
deals.push({
period: row.period,
registrator: row.registrator,
counterparty,
amount
});
const year = extractYearFromIso(row.period);
if (year !== null) {
const yearBucket = byYear.get(year);
if (!yearBucket) {
byYear.set(year, {
year,
total: amount,
ops: 1,
maxSingle: amount,
counterparties: new Set([counterparty])
});
}
else {
yearBucket.total += amount;
yearBucket.ops += 1;
yearBucket.maxSingle = Math.max(yearBucket.maxSingle, amount);
yearBucket.counterparties.add(counterparty);
}
}
}
const profileRows = Array.from(byCounterparty.values());
const yearRows = Array.from(byYear.values());
const totalFlow = profileRows.reduce((sum, item) => sum + item.total, 0);
const totalOperations = profileRows.reduce((sum, item) => sum + item.ops, 0);
const rankedByTotal = [...profileRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.name.localeCompare(b.name));
const rankedByYearTotal = [...yearRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.year - b.year);
const rankedByOps = [...profileRows].sort((a, b) => b.ops - a.ops || b.total - a.total || a.name.localeCompare(b.name));
const rankedByMaxSingle = [...profileRows].sort((a, b) => b.maxSingle - a.maxSingle || b.total - a.total || a.name.localeCompare(b.name));
const rankedByAvgCheck = [...profileRows]
.filter((item) => item.ops >= minOpsForAvgCheck)
.map((item) => ({
...item,
avgCheck: item.total / item.ops
}))
.sort((a, b) => b.avgCheck - a.avgCheck || b.total - a.total || a.name.localeCompare(b.name));
const rankedDealsTop = [...deals].sort((a, b) => b.amount - a.amount || (b.period ?? "").localeCompare(a.period ?? ""));
const activeOnlyForBottomDeals = /(?:активн|active)/iu.test(normalizedQuestion);
const activeCounterpartiesForBottom = new Set(profileRows.filter((item) => item.ops >= Math.max(3, minOpsForAvgCheck)).map((item) => item.name));
const rankedDealsBottom = [...deals]
.filter((item) => !activeOnlyForBottomDeals || activeCounterpartiesForBottom.has(item.counterparty))
.sort((a, b) => a.amount - b.amount || (a.period ?? "").localeCompare(b.period ?? ""));
const lines = [
isSupplier
? "Собран профиль выплат поставщикам (bank-doc value aggregate)."
: "Собран профиль поступлений от заказчиков (bank-doc value aggregate).",
`Строк источника: ${rows.length}.`,
`Уникальных контрагентов: ${profileRows.length}.`
];
if (profileRows.length === 0) {
lines.push("По выбранному окну данных платежные строки не найдены.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
if (focus === "total_flow") {
const periodLine = options.periodFrom && options.periodTo
? `За период ${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)} подтверждено ${formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
: `За все доступное время подтверждено ${formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
const directAnswerLine = isSupplier
? periodLine
: `${periodLine} Это сумма денег, полученных от клиентов, а не чистая прибыль.`;
const summaryLines = [
directAnswerLine,
"",
"Подтверждение:",
`- Операций в выборке: ${totalOperations}.`,
`- Контрагентов в выборке: ${profileRows.length}.`
];
if (rankedByYearTotal.length > 0) {
summaryLines.push(`- Самый сильный год по поступлениям: ${rankedByYearTotal[0].year} (${formatMoneyRub(rankedByYearTotal[0].total)}).`);
}
if (rankedByTotal.length > 0) {
summaryLines.push(`- Крупнейший контрагент по потоку: ${rankedByTotal[0].name} (${formatMoneyRub(rankedByTotal[0].total)}).`);
}
return {
responseType: "FACTUAL_SUMMARY",
text: summaryLines.join("\n")
};
}
if (focus === "top_years_by_total") {
const visible = rankedByYearTotal.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} лет по сумме выплат:`
: `Топ-${visible.length} лет по сумме поступлений:`;
lines.unshift(heading);
if (visible.length === 0) {
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
}
else {
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${item.total} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | макс: ${item.maxSingle}`));
}
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (focus === "top_by_ops") {
const visible = rankedByOps.slice(0, limit);
const heading = isSupplier
? `Топ-${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",
text: lines.join("\n")
};
}
if (focus === "top_by_max_single") {
const visible = rankedByMaxSingle.slice(0, limit);
const heading = isSupplier
? `Топ-${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",
text: lines.join("\n")
};
}
if (focus === "top_by_avg_check_min_ops") {
const visible = rankedByAvgCheck.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} поставщиков по среднему чеку (минимум ${minOpsForAvgCheck} операций):`
: `Топ-${visible.length} заказчиков по среднему чеку (минимум ${minOpsForAvgCheck} входящих операций):`;
lines.unshift(heading);
if (visible.length === 0) {
lines.push(`Контрагентов с минимум ${minOpsForAvgCheck} операций не найдено.`);
}
else {
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | средний чек: ${item.avgCheck.toFixed(2)} | операций: ${item.ops} | сумма: ${item.total}`));
}
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (focus === "top_deals") {
const visible = rankedDealsTop.slice(0, limit);
const heading = isSupplier
? `Топ-${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",
text: lines.join("\n")
};
}
if (focus === "bottom_deals") {
const visible = rankedDealsBottom.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} самых маленьких разовых выплат:`
: `Топ-${visible.length} самых маленьких разовых сделок по поступлениям:`;
lines.unshift(heading);
if (activeOnlyForBottomDeals) {
lines.push("Фильтр: только активные контрагенты (минимум 3 операции).");
}
lines.push(...visible.map((item, index) => `${index + 1}. ${item.period ?? "n/a"} | ${item.counterparty} | ${item.registrator} | ${item.amount}`));
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
const visible = rankedByTotal.slice(0, limit);
const heading = isSupplier
? `Топ-${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}`;
}));
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "contract_usage_and_value") {
const focus = detectContractValueFocus(options.userMessage);
const limit = detectRankingLimit(options.userMessage, 20);
const byContract = new Map();
for (const row of rows) {
const contract = extractContractName(row);
const amount = row.amount ?? 0;
if (!contract || !Number.isFinite(amount) || amount <= 0) {
continue;
}
const counterparty = extractCounterpartyName(row);
const current = byContract.get(contract);
if (!current) {
byContract.set(contract, {
contract,
turnover: amount,
docs: 1,
lastPeriod: row.period,
counterparties: new Set(counterparty ? [counterparty] : [])
});
}
else {
current.turnover += amount;
current.docs += 1;
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if (counterparty) {
current.counterparties.add(counterparty);
}
}
}
const contractRows = Array.from(byContract.values());
const rankedByTurnover = [...contractRows].sort((a, b) => b.turnover - a.turnover || b.docs - a.docs || a.contract.localeCompare(b.contract));
const rankedByDocs = [...contractRows].sort((a, b) => b.docs - a.docs || b.turnover - a.turnover || a.contract.localeCompare(b.contract));
const rankedBottomActive = [...contractRows]
.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}.`
];
if (contractRows.length === 0) {
lines.push("В выбранном окне не найдено операций, связанных с договорами.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
if (focus === "top_by_docs") {
const visible = rankedByDocs.slice(0, limit);
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",
text: lines.join("\n")
};
}
if (focus === "bottom_by_turnover_active") {
const visible = rankedBottomActive.slice(0, limit);
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",
text: lines.join("\n")
};
}
const visible = rankedByTurnover.slice(0, limit);
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",
text: lines.join("\n")
};
}
if (intent === "vat_payable_forecast") {
const rowsByMarker = new Map();
for (const row of rows) {
@@ -0,0 +1,632 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.composeCounterpartyAnalyticsReply = composeCounterpartyAnalyticsReply;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
const replyContracts_1 = require("./replyContracts");
function groupRowsByMarker(rows) {
const rowsByMarker = new Map();
for (const row of rows) {
const marker = String(row.registrator ?? "").trim().toUpperCase();
if (!marker) {
continue;
}
if (!rowsByMarker.has(marker)) {
rowsByMarker.set(marker, []);
}
rowsByMarker.get(marker).push(row);
}
return rowsByMarker;
}
function formatOptionalDate(value, formatDateRu) {
return value ? formatDateRu(value) : "дата не указана";
}
function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
if (intent === "counterparty_population_and_roles") {
const rowsByMarker = groupRowsByMarker(rows);
const sumMarker = (marker) => (rowsByMarker.get(marker) ?? []).reduce((sum, row) => sum + (row.amount ?? 0), 0);
const totalCounterparties = sumMarker("CP_TOTAL");
const customerActive = sumMarker("CP_CUSTOMER_ACTIVE");
const supplierActive = sumMarker("CP_SUPPLIER_ACTIVE");
const mixedActive = sumMarker("CP_MIXED_ACTIVE");
const activeUnion = sumMarker("CP_ACTIVE_UNION");
const customerOnly = Math.max(0, customerActive - mixedActive);
const supplierOnly = Math.max(0, supplierActive - mixedActive);
const resolvedActive = customerOnly + supplierOnly + mixedActive;
const activeCounterparties = Math.max(activeUnion, resolvedActive);
const otherCounterparties = totalCounterparties > 0 ? Math.max(0, totalCounterparties - resolvedActive) : null;
const focus = deps.detectCounterpartyProfileFocus(options.userMessage);
const includeTotal = focus === "full_profile" || focus === "total_only";
const includeRoles = focus === "full_profile" || focus === "roles_only";
const directLead = focus === "suppliers_only"
? `Контрагентов только в роли поставщика: ${supplierOnly}.`
: focus === "customers_only"
? `Контрагентов только в роли заказчика: ${customerOnly}.`
: focus === "mixed_only"
? `Контрагентов со смешанной ролью: ${mixedActive}.`
: includeTotal && totalCounterparties > 0
? `Всего уникальных контрагентов в базе: ${totalCounterparties}.`
: `Активных контрагентов по документальной активности: ${activeCounterparties}.`;
const lines = [
directLead,
"Профиль контрагентов собран по справочнику и документальной активности.",
`Строк агрегата: ${rows.length}.`
];
if (includeTotal) {
if (totalCounterparties > 0) {
lines.push(`Всего уникальных контрагентов в базе: ${totalCounterparties}.`);
}
else if (activeCounterparties > 0) {
lines.push(`Полный итог по справочнику не получен, поэтому даю оценку по документальной активности: ${activeCounterparties}.`);
}
else {
lines.push("По количеству контрагентов агрегатных строк не найдено.");
}
}
if (includeRoles) {
if (resolvedActive > 0 || activeCounterparties > 0) {
lines.push("Распределение ролей по активности:");
lines.push(`1. Только заказчики: ${customerOnly}.`);
lines.push(`2. Только поставщики: ${supplierOnly}.`);
lines.push(`3. И заказчики, и поставщики: ${mixedActive}.`);
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
if (otherCounterparties !== null) {
lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`);
}
}
else {
lines.push("По распределению ролей агрегатных строк не найдено.");
}
}
if (focus === "suppliers_only") {
lines.push(`Контрагентов только в роли поставщика: ${supplierOnly}.`);
}
if (focus === "customers_only") {
lines.push(`Контрагентов только в роли заказчика: ${customerOnly}.`);
}
if (focus === "mixed_only") {
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
if (intent === "counterparty_activity_lifecycle") {
const activityFirstRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST");
const activityRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY");
const activityYearRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_YEAR");
const byCounterparty = new Map();
for (const row of activityFirstRows) {
const name = deps.extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = deps.extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
}
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
current.firstObservedActivity = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityYearRows) {
const name = deps.extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = deps.extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: null,
years: new Set(year !== null ? [year] : [])
});
continue;
}
current.opsCount += opsCount;
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityRows) {
const name = deps.extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const current = byCounterparty.get(name);
if (!current) {
const year = deps.extractYearFromIso(row.period);
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
}
if (activityYearRows.length === 0 && opsCount > current.opsCount) {
current.opsCount = opsCount;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
const year = deps.extractYearFromIso(row.period);
if (year !== null) {
current.years.add(year);
}
}
const counterpartiesRaw = Array.from(byCounterparty.values());
const focus = deps.detectCounterpartyLifecycleFocus(options.userMessage);
const requestedYear = deps.extractRequestedYearFromQuestion(options.userMessage);
const longevityQuestion = deps.hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
const activityAgeQuestion = deps.hasCounterpartyActivityAgeQuestion(options.userMessage);
const rankingLimit = deps.detectRankingLimit(options.userMessage, 10);
const counterparties = counterpartiesRaw.sort((left, right) => {
if (longevityQuestion) {
const yearsDiff = right.years.size - left.years.size;
if (yearsDiff !== 0) {
return yearsDiff;
}
}
if (right.opsCount !== left.opsCount) {
return right.opsCount - left.opsCount;
}
return (right.lastPeriod ?? "").localeCompare(left.lastPeriod ?? "");
});
const scopeLabel = focus === "active_customers_all_time"
? "за все время"
: requestedYear
? `в ${requestedYear} году`
: "в выбранном периоде";
if (activityAgeQuestion) {
const focusedCounterparty = counterparties.find((item) => deps.counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
if (focusedCounterparty) {
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
const lastObservedActivity = focusedCounterparty.lastPeriod;
const firstTimestamp = deps.toUtcDayTimestamp(firstObservedActivity);
const lastTimestamp = deps.toUtcDayTimestamp(lastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? deps.formatAgeYearsMonthsDays(observedDays)
: focusedCounterparty.years.size > 0
? `${focusedCounterparty.years.size} г.`
: null;
const lines = [
observedAgeLabel && firstObservedActivity && lastObservedActivity
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`
];
if (firstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${deps.formatDateRu(firstObservedActivity)}.`);
}
if (lastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${deps.formatDateRu(lastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
if (focusedCounterparty.years.size > 0) {
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
const organizationHint = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint ?? null);
if (organizationHint && counterparties.length > 0) {
const organizationFirstObservedActivity = counterparties.reduce((earliest, item) => {
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
if (!candidate) {
return earliest;
}
if (!earliest || candidate < earliest) {
return candidate;
}
return earliest;
}, null);
const organizationLastObservedActivity = counterparties.reduce((latest, item) => {
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
if (!candidate) {
return latest;
}
if (!latest || candidate > latest) {
return candidate;
}
return latest;
}, null);
const organizationYears = new Set();
let organizationOpsCount = 0;
for (const item of counterparties) {
organizationOpsCount += item.opsCount;
for (const year of item.years) {
organizationYears.add(year);
}
}
const firstTimestamp = deps.toUtcDayTimestamp(organizationFirstObservedActivity);
const lastTimestamp = deps.toUtcDayTimestamp(organizationLastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? deps.formatAgeYearsMonthsDays(observedDays)
: organizationYears.size > 0
? `${organizationYears.size} г.`
: null;
const lines = [
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
];
if (organizationFirstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${deps.formatDateRu(organizationFirstObservedActivity)}.`);
}
if (organizationLastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${deps.formatDateRu(organizationLastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
if (organizationYears.size > 0) {
const years = Array.from(organizationYears).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
}
const lines = longevityQuestion
? [
`Заказчиков с самым длинным горизонтом сотрудничества: ${counterparties.length}.`,
"Собран профиль длительности сотрудничества по годам и частоте активности.",
`Строк агрегата: ${rows.length}.`
]
: [
`Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
"Собран профиль активности заказчиков по платежным документам.",
`Строк агрегата: ${rows.length}.`
];
if (counterparties.length === 0) {
lines.push(longevityQuestion
? "По доступному окну не удалось выделить заказчиков с подтвержденной длительностью сотрудничества."
: "По выбранному окну активные заказчики не найдены.");
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
const visible = counterparties.slice(0, longevityQuestion ? rankingLimit : 120);
if (longevityQuestion) {
lines.push(`Топ-${visible.length} заказчиков по охвату лет и частоте операций:`);
}
lines.push(...visible.map((item, index) => {
const years = Array.from(item.years).sort((a, b) => a - b);
const yearsLabel = years.length > 0 ? ` | лет в базе: ${years.length} | годы: ${years.join(", ")}` : "";
const periodSpan = item.firstPeriod && item.lastPeriod
? ` | период: ${formatOptionalDate(item.firstPeriod, deps.formatDateRu)}..${formatOptionalDate(item.lastPeriod, deps.formatDateRu)}`
: "";
if (longevityQuestion) {
return `${index + 1}. ${item.name} | операций: ${item.opsCount}${yearsLabel}${periodSpan}`;
}
const suffix = item.lastPeriod ? ` | последняя активность: ${deps.formatDateRu(item.lastPeriod)}` : "";
return `${index + 1}. ${item.name} | операций: ${item.opsCount}${suffix}${years.length > 0 ? ` | лет в базе: ${years.length}` : ""}`;
}));
if (counterparties.length > visible.length) {
lines.push(`Показаны первые ${visible.length} из ${counterparties.length} заказчиков.`);
}
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (intent === "contract_usage_overview") {
const rowsByMarker = groupRowsByMarker(rows);
const sumMarker = (marker) => (rowsByMarker.get(marker) ?? []).reduce((sum, row) => sum + (row.amount ?? 0), 0);
const totalContracts = sumMarker("CT_TOTAL");
const usedContracts = sumMarker("CT_USED");
const unusedContracts = totalContracts > 0 ? Math.max(0, totalContracts - Math.min(usedContracts, totalContracts)) : null;
const usedShare = totalContracts > 0 ? deps.formatPercent(Math.min(usedContracts, totalContracts), totalContracts) : null;
const usageLead = totalContracts > 0
? `Использованных договоров: ${usedContracts} из ${totalContracts}${usedShare ? ` (${usedShare})` : ""}.`
: `Использованных договоров с подтвержденной связью с операциями: ${usedContracts}.`;
const lines = [
usageLead,
"Профиль договорной базы собран по справочнику и подтвержденным операциям.",
`Строк агрегата: ${rows.length}.`
];
if (totalContracts > 0) {
lines.push(`Всего договоров в базе: ${totalContracts}.`);
}
else {
lines.push("Общее количество договоров не получено из доступного среза справочника.");
}
lines.push(`Использованных договоров с подтвержденной связью с операциями: ${usedContracts}.`);
if (unusedContracts !== null) {
lines.push(`Неиспользуемых договоров: ${unusedContracts}.`);
}
if (usedShare) {
lines.push(`Доля используемых договоров: ${usedShare}.`);
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
if (intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile") {
const isSupplier = intent === "supplier_payouts_profile";
const focus = deps.detectValueRankingFocus(options.userMessage);
const limit = deps.detectRankingLimit(options.userMessage, 20);
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
const byCounterparty = new Map();
const byYear = new Map();
const deals = [];
for (const row of rows) {
const counterparty = deps.extractCounterpartyName(row);
const amount = row.amount ?? 0;
if (!counterparty || !Number.isFinite(amount) || amount <= 0) {
continue;
}
const current = byCounterparty.get(counterparty);
if (!current) {
byCounterparty.set(counterparty, {
name: counterparty,
total: amount,
ops: 1,
maxSingle: amount,
minSingle: amount,
lastPeriod: row.period
});
}
else {
current.total += amount;
current.ops += 1;
current.maxSingle = Math.max(current.maxSingle, amount);
current.minSingle = Math.min(current.minSingle, amount);
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
}
deals.push({
period: row.period,
registrator: row.registrator,
counterparty,
amount
});
const year = deps.extractYearFromIso(row.period);
if (year !== null) {
const yearBucket = byYear.get(year);
if (!yearBucket) {
byYear.set(year, {
year,
total: amount,
ops: 1,
maxSingle: amount,
counterparties: new Set([counterparty])
});
}
else {
yearBucket.total += amount;
yearBucket.ops += 1;
yearBucket.maxSingle = Math.max(yearBucket.maxSingle, amount);
yearBucket.counterparties.add(counterparty);
}
}
}
const profileRows = Array.from(byCounterparty.values());
const yearRows = Array.from(byYear.values());
const totalFlow = profileRows.reduce((sum, item) => sum + item.total, 0);
const totalOperations = profileRows.reduce((sum, item) => sum + item.ops, 0);
const rankedByTotal = [...profileRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.name.localeCompare(b.name));
const rankedByYearTotal = [...yearRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.year - b.year);
const rankedByOps = [...profileRows].sort((a, b) => b.ops - a.ops || b.total - a.total || a.name.localeCompare(b.name));
const rankedByMaxSingle = [...profileRows].sort((a, b) => b.maxSingle - a.maxSingle || b.total - a.total || a.name.localeCompare(b.name));
const rankedByAvgCheck = [...profileRows]
.filter((item) => item.ops >= minOpsForAvgCheck)
.map((item) => ({
...item,
avgCheck: item.total / item.ops
}))
.sort((a, b) => b.avgCheck - a.avgCheck || b.total - a.total || a.name.localeCompare(b.name));
const rankedDealsTop = [...deals].sort((a, b) => b.amount - a.amount || (b.period ?? "").localeCompare(a.period ?? ""));
const activeOnlyForBottomDeals = /(?:активн|active)/iu.test(normalizedQuestion);
const activeCounterpartiesForBottom = new Set(profileRows.filter((item) => item.ops >= Math.max(3, minOpsForAvgCheck)).map((item) => item.name));
const rankedDealsBottom = [...deals]
.filter((item) => !activeOnlyForBottomDeals || activeCounterpartiesForBottom.has(item.counterparty))
.sort((a, b) => a.amount - b.amount || (a.period ?? "").localeCompare(b.period ?? ""));
const lines = [
isSupplier
? "Собран профиль выплат поставщикам по платежным документам."
: "Собран профиль поступлений от заказчиков по платежным документам.",
`Строк источника: ${rows.length}.`,
`Уникальных контрагентов: ${profileRows.length}.`
];
if (profileRows.length === 0) {
lines.push("По выбранному окну данных платежные строки не найдены.");
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
if (focus === "total_flow") {
const periodLine = options.periodFrom && options.periodTo
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
const directAnswerLine = isSupplier
? periodLine
: `${periodLine} Это денежный поток от клиентов, а не чистая прибыль.`;
const summaryLines = [
directAnswerLine,
"",
"Подтверждение:",
`- Операций в выборке: ${totalOperations}.`,
`- Контрагентов в выборке: ${profileRows.length}.`
];
if (rankedByYearTotal.length > 0) {
summaryLines.push(`- Самый сильный год по поступлениям: ${rankedByYearTotal[0].year} (${deps.formatMoneyRub(rankedByYearTotal[0].total)}).`);
}
if (rankedByTotal.length > 0) {
summaryLines.push(`- Крупнейший контрагент по потоку: ${rankedByTotal[0].name} (${deps.formatMoneyRub(rankedByTotal[0].total)}).`);
}
return (0, replyContracts_1.buildFactualSummaryReply)(summaryLines);
}
if (focus === "top_years_by_total") {
const visible = rankedByYearTotal.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} лет по сумме выплат:`
: `Топ-${visible.length} лет по сумме поступлений:`;
lines.unshift(heading);
if (visible.length === 0) {
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
}
else {
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`));
}
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (focus === "top_by_ops") {
const visible = rankedByOps.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} поставщиков по количеству исходящих платежных операций:`
: `Топ-${visible.length} заказчиков по количеству входящих платежных операций:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | операций: ${item.ops} | сумма: ${deps.formatMoneyRub(item.total)} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (focus === "top_by_max_single") {
const visible = rankedByMaxSingle.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (focus === "top_by_avg_check_min_ops") {
const visible = rankedByAvgCheck.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} поставщиков по среднему чеку (минимум ${minOpsForAvgCheck} операций):`
: `Топ-${visible.length} заказчиков по среднему чеку (минимум ${minOpsForAvgCheck} входящих операций):`;
lines.unshift(heading);
if (visible.length === 0) {
lines.push(`Контрагентов с минимум ${minOpsForAvgCheck} операций не найдено.`);
}
else {
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | средний чек: ${deps.formatMoneyRub(item.avgCheck)} | операций: ${item.ops} | сумма: ${deps.formatMoneyRub(item.total)}`));
}
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (focus === "top_deals") {
const visible = rankedDealsTop.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
: `Топ-${visible.length} самых крупных разовых поступлений:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (focus === "bottom_deals") {
const visible = rankedDealsBottom.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} самых маленьких разовых выплат:`
: `Топ-${visible.length} самых маленьких разовых поступлений:`;
lines.unshift(heading);
if (activeOnlyForBottomDeals) {
lines.push("Фильтр: только активные контрагенты с минимум 3 операциями.");
}
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
const visible = rankedByTotal.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} поставщиков по сумме выплат:`
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => {
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
return `${index + 1}. ${item.name} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | средний чек: ${deps.formatMoneyRub(avgCheck)} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`;
}));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (intent === "contract_usage_and_value") {
const focus = deps.detectContractValueFocus(options.userMessage);
const limit = deps.detectRankingLimit(options.userMessage, 20);
const byContract = new Map();
for (const row of rows) {
const contract = deps.extractContractName(row);
const amount = row.amount ?? 0;
if (!contract || !Number.isFinite(amount) || amount <= 0) {
continue;
}
const counterparty = deps.extractCounterpartyName(row);
const current = byContract.get(contract);
if (!current) {
byContract.set(contract, {
contract,
turnover: amount,
docs: 1,
lastPeriod: row.period,
counterparties: new Set(counterparty ? [counterparty] : [])
});
}
else {
current.turnover += amount;
current.docs += 1;
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if (counterparty) {
current.counterparties.add(counterparty);
}
}
}
const contractRows = Array.from(byContract.values());
const rankedByTurnover = [...contractRows].sort((a, b) => b.turnover - a.turnover || b.docs - a.docs || a.contract.localeCompare(b.contract));
const rankedByDocs = [...contractRows].sort((a, b) => b.docs - a.docs || b.turnover - a.turnover || a.contract.localeCompare(b.contract));
const rankedBottomActive = [...contractRows]
.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}.`,
"Собран профиль договоров по обороту и подтвержденным операциям.",
`Строк источника: ${rows.length}.`,
`Договорных агрегатов: ${contractRows.length}.`
];
if (contractRows.length === 0) {
lines.push("В выбранном окне не найдено операций, связанных с договорами.");
return (0, replyContracts_1.buildFactualSummaryReply)(lines);
}
if (focus === "top_by_docs") {
const visible = rankedByDocs.slice(0, limit);
lines.unshift(`Топ-${visible.length} договоров по количеству операций:`);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | операций: ${item.docs} | оборот: ${deps.formatMoneyRub(item.turnover)} | контрагентов: ${item.counterparties.size}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
if (focus === "bottom_by_turnover_active") {
const visible = rankedBottomActive.slice(0, limit);
lines.unshift(`Топ-${visible.length} активных договоров с минимальным оборотом:`);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${deps.formatMoneyRub(item.turnover)} | операций: ${item.docs} | последняя активность: ${formatOptionalDate(item.lastPeriod, deps.formatDateRu)}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
const visible = rankedByTurnover.slice(0, limit);
lines.unshift(`Топ-${visible.length} договоров по сумме оборота:`);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${deps.formatMoneyRub(item.turnover)} | операций: ${item.docs} | контрагентов: ${item.counterparties.size} | последняя активность: ${formatOptionalDate(item.lastPeriod, deps.formatDateRu)}`));
return (0, replyContracts_1.buildFactualListReply)(lines);
}
return null;
}
@@ -421,12 +421,17 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
const rootContextOnly = followupContext.root_context_only === true;
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
const normalized = String(userMessage ?? "");
const hasExplicitInventoryRootSnapshotCue = /(?:склад|остат(?:ок|ки)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized);
const hasInventoryRootRestatementCue = /(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) &&
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
(/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized) ||
hasSameDateHint(normalized) ||
hasSamePeriodHint(normalized));
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
rootContextOnly ||
(currentFrameKind === "inventory_root" && (hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue)) ||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
(currentFrameKind === "generic" &&
hasExplicitInventoryRootSnapshotCue &&
(hasSameDateHint(normalized) || hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue));
if (!canReenterInventoryRoot) {
return false;
}
@@ -791,15 +796,13 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
!hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage)) {
if (intent === "inventory_aging_by_purchase_date") {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
const todayIso = new Date().toISOString().slice(0, 10);
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
}
if ((Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) &&
@@ -1025,6 +1028,8 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
const isVatFollowup = hasVatCue(normalizedMessage);
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
if (detectedIntent.intent === "unknown" && isVatFollowup) {
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
? "vat_liability_confirmed_for_tax_period"
@@ -1038,7 +1043,10 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
};
}
const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup;
if (allowOpenItemsFollowupFallback && hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
if (allowOpenItemsFollowupFallback &&
!inventorySelectedObjectFollowup &&
hasOpenItemsHint(normalizedMessage) &&
hasAnyPartyAnchor) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "low",
@@ -1060,8 +1068,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
reasons: [...detectedIntent.reasons, "intent_adjusted_to_balance_followup_context"]
};
}
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
if (inventorySelectedObjectFollowup && hasInventorySupplierFollowupCue(normalizedMessage)) {
if (detectedIntent.intent === "unknown" ||
detectedIntent.intent === "list_documents_by_counterparty" ||
@@ -19,9 +19,7 @@ function containsLetterLikeChars(text) {
return /[A-Za-z\u0400-\u04FF]/u.test(source);
}
function createAssistantBoundaryPolicy(deps) {
const defaultChannel = String(deps.activeMcpChannel ?? "default");
function buildAssistantDataScopeContractReply(scopeProbe = null) {
const channel = String(scopeProbe?.channel ?? defaultChannel);
const organizations = Array.isArray(scopeProbe?.organizations)
? scopeProbe.organizations
.map((item) => String(item ?? "").trim())
@@ -29,27 +27,27 @@ function createAssistantBoundaryPolicy(deps) {
: [];
if (organizations.length === 1) {
return [
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
`Сейчас доступна организация: ${organizations[0]}.`,
"Могу сразу показать по ней документы, операции, договоры или остатки."
].join(" ");
}
if (organizations.length > 1) {
const preview = organizations.slice(0, 10).join(", ");
return [
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
`Сейчас доступны организации (${organizations.length}): ${preview}.`,
"Скажите, по какой организации смотреть данные."
].join(" ");
}
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
return [
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контур."
"Сейчас не удалось определить список организаций из подключенной базы.",
`Техническая причина: ${scopeProbe.error}.`,
"Проверьте подключение, и я сразу назову доступный контур."
].join(" ");
}
return [
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
"Сейчас вижу только данные текущего подключенного контура.",
"Если в нем несколько организаций, скажите, по какой смотреть данные."
].join(" ");
}
function buildAssistantDataScopeSelectionReply(organization) {
@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatIsoDateForReply = formatIsoDateForReply;
exports.readAddressDebugFilters = readAddressDebugFilters;
exports.readAddressDebugItem = readAddressDebugItem;
exports.readAddressDebugOrganization = readAddressDebugOrganization;
exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
exports.isGroundedAddressDebug = isGroundedAddressDebug;
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
function fallbackToNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function formatIsoDateForReply(value) {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
return `${match[3]}.${match[2]}.${match[1]}`;
}
function readAddressDebugFilters(debug) {
return toRecordObject(debug?.extracted_filters);
}
function readAddressDebugItem(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
return (toNonEmptyString(extractedFilters?.item) ??
(String(debug?.anchor_type ?? "") === "item"
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
: null));
}
function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
}
function readAddressDebugScopedDate(debug) {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
return (formatIsoDateForReply(extractedFilters?.as_of_date) ??
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.period_to));
}
function isGroundedAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
if (!debug || typeof debug !== "object") {
return false;
}
const executionLane = toNonEmptyString(debug.execution_lane);
if (executionLane !== "address_query") {
return false;
}
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
return groundingStatus === "grounded";
}
function isGroundedInventoryContextDebug(debug, toNonEmptyString) {
if (!isGroundedAddressDebug(debug, toNonEmptyString)) {
return false;
}
const detectedIntent = toNonEmptyString(debug?.detected_intent);
const capabilityId = toNonEmptyString(debug?.capability_id);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
const rootIntent = toNonEmptyString(rootFrameContext?.root_intent);
return (detectedIntent === "inventory_on_hand_as_of_date" ||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
rootIntent === "inventory_on_hand_as_of_date");
}
function resolveAssistantContinuitySnapshot(input) {
const toNonEmptyString = input.toNonEmptyString ?? fallbackToNonEmptyString;
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
let lastGroundedAddressDebug = null;
let lastGroundedItemAddressDebug = null;
let lastGroundedInventoryAddressDebug = null;
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
const item = sessionItems[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (!isGroundedAddressDebug(debug, toNonEmptyString)) {
continue;
}
if (!lastGroundedAddressDebug) {
lastGroundedAddressDebug = debug;
}
if (!lastGroundedItemAddressDebug && readAddressDebugItem(debug, toNonEmptyString)) {
lastGroundedItemAddressDebug = debug;
}
if (!lastGroundedInventoryAddressDebug && isGroundedInventoryContextDebug(debug, toNonEmptyString)) {
lastGroundedInventoryAddressDebug = debug;
}
if (lastGroundedAddressDebug && lastGroundedItemAddressDebug && lastGroundedInventoryAddressDebug) {
break;
}
}
const primaryDebug = lastGroundedItemAddressDebug ?? lastGroundedAddressDebug;
return {
lastGroundedAddressDebug,
lastGroundedItemAddressDebug,
lastGroundedInventoryAddressDebug,
activeItem: readAddressDebugItem(primaryDebug, toNonEmptyString),
activeOrganization: readAddressDebugOrganization(primaryDebug, toNonEmptyString),
activeScopedDate: readAddressDebugScopedDate(primaryDebug),
hasGroundedAddressContext: Boolean(lastGroundedAddressDebug),
hasGroundedItemContext: Boolean(lastGroundedItemAddressDebug),
hasGroundedInventoryContext: Boolean(lastGroundedInventoryAddressDebug)
};
}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.createAssistantDataScopePolicy = createAssistantDataScopePolicy;
// @ts-nocheck
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
function normalizeScopeLabel(value) {
return String(value ?? "")
@@ -235,6 +236,12 @@ function createAssistantDataScopePolicy(deps) {
if (selected) {
return selected;
}
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug)) {
const groundedOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)((0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug));
if (groundedOrganization) {
return groundedOrganization;
}
}
}
return null;
}
@@ -226,6 +226,7 @@ async function runAssistantLivingChatRuntime(input) {
chatText = (0, assistantMemoryRecapPolicy_1.buildAddressMemoryRecapReply)({
organization: scopedOrganization,
addressDebug: lastMemoryAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
@@ -108,7 +108,7 @@ function createAssistantLivingModePolicy(deps) {
const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample));
const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample));
const hasExplicitRecapPrompt = samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|напомни\s+что\s+мы|what\s+we\s+already\s+(?:discussed|figured\s+out))/iu.test(sample));
if (!hasMemoryCue || !(hasDiscussionCue || hasExplicitRecapPrompt)) {
if (!(hasExplicitRecapPrompt || (hasMemoryCue && hasDiscussionCue))) {
return false;
}
if (hasExplicitRecapPrompt) {
@@ -5,13 +5,13 @@ exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapa
exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply;
exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext;
exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy;
function formatIsoDateForReply(value) {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
return `${match[3]}.${match[2]}.${match[1]}`;
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function collectMessageSamples(input) {
const values = [
@@ -30,89 +30,17 @@ function hasSignalAcrossSamples(samples, detector) {
function hasExplicitRecapPromptSignal(samples) {
return samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|что\s+уже\s+поняли|напомни\s+что\s+мы)/iu.test(sample));
}
function findLastGroundedInventoryAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
const answerGroundingCheck = debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check
: null;
const groundingStatus = String(answerGroundingCheck?.status ?? "");
const detectedIntent = String(debug.detected_intent ?? "");
const capabilityId = String(debug.capability_id ?? "");
const rootFrameContext = debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
? debug.address_root_frame_context
: null;
const rootIntent = String(rootFrameContext?.root_intent ?? "");
const isInventoryContext = detectedIntent === "inventory_on_hand_as_of_date" ||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
rootIntent === "inventory_on_hand_as_of_date";
if (groundingStatus === "grounded" && isInventoryContext) {
return debug;
}
}
return null;
}
function findLastAddressDebugWithItem(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (String(debug.execution_lane ?? "") !== "address_query") {
continue;
}
const extractedFilters = debug.extracted_filters && typeof debug.extracted_filters === "object"
? debug.extracted_filters
: null;
const itemLabel = String(extractedFilters?.item ?? "").trim() ||
(String(debug.anchor_type ?? "") === "item"
? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim()
: "");
if (itemLabel) {
return debug;
}
}
return null;
}
function findLastAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
if (String(item.debug.execution_lane ?? "") === "address_query") {
return item.debug;
}
}
return null;
}
function buildInventoryHistoryCapabilityFollowupReply(input) {
const rootFrameContext = input.addressDebug?.address_root_frame_context &&
typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
: null;
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? input.addressDebug.extracted_filters
: null;
const extractedFilters = (0, assistantContinuityPolicy_1.readAddressDebugFilters)(input.addressDebug);
const organization = input.organization ??
input.toNonEmptyString(rootFrameContext?.organization) ??
input.toNonEmptyString(extractedFilters?.organization);
const lastAsOfDate = formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.as_of_date);
const lastAsOfDate = (0, assistantContinuityPolicy_1.formatIsoDateForReply)(rootFrameContext?.as_of_date) ??
(0, assistantContinuityPolicy_1.formatIsoDateForReply)(extractedFilters?.as_of_date);
const organizationPart = organization ? ` по компании «${organization}»` : "";
const referenceLine = lastAsOfDate
? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.`
@@ -128,6 +56,93 @@ function buildInventoryHistoryCapabilityFollowupReply(input) {
"Если хочешь, сразу покажу нужный исторический период."
].join("\n");
}
function normalizeRecapIdentity(value) {
return String(value ?? "")
.trim()
.toLowerCase()
.replace(/[«»"'`]/g, "")
.replace(/\s+/g, " ");
}
function buildRecapFactLine(input) {
const detectedIntent = String(input.debug?.detected_intent ?? "");
const scopedDate = (0, assistantContinuityPolicy_1.readAddressDebugScopedDate)(input.debug);
const itemPart = input.item ? `по позиции «${input.item}»` : null;
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
const datePart = scopedDate ? ` на ${scopedDate}` : "";
if (detectedIntent === "inventory_on_hand_as_of_date") {
return `смотрели остатки${organizationPart ? ` ${organizationPart}` : ""}${datePart}`.trim();
}
if (detectedIntent === "inventory_purchase_provenance_for_item" && itemPart) {
return `разобрали, кто поставлял ${itemPart}${datePart}`.trim();
}
if (detectedIntent === "inventory_purchase_documents_for_item" && itemPart) {
return `подняли документы закупки ${itemPart}${datePart}`.trim();
}
if (detectedIntent === "inventory_sale_trace_for_item" && itemPart) {
return `разобрали, кому продавали ${itemPart}${datePart}`.trim();
}
if (detectedIntent === "inventory_purchase_to_sale_chain" && itemPart) {
return `проследили цепочку от закупки до продажи ${itemPart}${datePart}`.trim();
}
if (detectedIntent === "inventory_profitability_for_item" && itemPart) {
return `смотрели рентабельность ${itemPart}${datePart}`.trim();
}
if (detectedIntent === "inventory_aging_by_purchase_date" && itemPart) {
return `смотрели возраст остатков ${itemPart}${datePart}`.trim();
}
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
return `смотрели активность в базе 1С ${organizationPart}`.trim();
}
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
return `поднимали документы ${organizationPart}${datePart}`.trim();
}
return null;
}
function collectRecentRecapFacts(input) {
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
if (sessionItems.length === 0) {
return [];
}
const currentItemKey = normalizeRecapIdentity(input.item);
const currentOrganizationKey = normalizeRecapIdentity(input.organization);
const facts = [];
const seen = new Set();
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
const item = sessionItems[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
if (!(0, assistantContinuityPolicy_1.isGroundedAddressDebug)(item.debug, input.toNonEmptyString)) {
continue;
}
const debugItem = (0, assistantContinuityPolicy_1.readAddressDebugItem)(item.debug, input.toNonEmptyString);
const debugOrganization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(item.debug, input.toNonEmptyString);
const itemMatches = currentItemKey ? normalizeRecapIdentity(debugItem) === currentItemKey : false;
const organizationMatches = currentOrganizationKey
? normalizeRecapIdentity(debugOrganization) === currentOrganizationKey
: false;
if (currentItemKey && !itemMatches) {
continue;
}
if (!currentItemKey && currentOrganizationKey && !organizationMatches) {
continue;
}
const fact = buildRecapFactLine({
debug: item.debug,
item: debugItem,
organization: debugOrganization
});
if (!fact || seen.has(fact)) {
continue;
}
seen.add(fact);
facts.push(fact);
if (facts.length >= 3) {
break;
}
}
return facts.reverse();
}
function buildAddressMemoryRecapReply(input) {
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? input.addressDebug.extracted_filters
@@ -136,18 +151,27 @@ function buildAddressMemoryRecapReply(input) {
typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
: null;
const item = input.toNonEmptyString(extractedFilters?.item) ??
(String(input.addressDebug?.anchor_type ?? "") === "item"
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
: null);
const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(input.addressDebug, input.toNonEmptyString);
const organization = input.organization ??
input.toNonEmptyString(extractedFilters?.organization) ??
input.toNonEmptyString(rootFrameContext?.organization);
const scopedDate = formatIsoDateForReply(extractedFilters?.as_of_date) ??
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.period_to);
const scopedDate = (0, assistantContinuityPolicy_1.readAddressDebugScopedDate)(input.addressDebug);
const recapFacts = collectRecentRecapFacts({
sessionItems: input.sessionItems,
item,
organization,
toNonEmptyString: input.toNonEmptyString
});
if (item) {
if (recapFacts.length > 0) {
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
const organizationPart = organization ? ` по компании «${organization}»` : "";
return [
`Да, помню. По позиции «${item}»${organizationPart}${datePart} мы уже выяснили:`,
...recapFacts.map((fact) => `- ${fact}.`),
"Могу сразу продолжить по ней: поставщик, закупка, документы или продажа."
].join("\n");
}
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
const organizationPart = organization ? ` по компании «${organization}»` : "";
return [
@@ -168,21 +192,29 @@ function buildAddressMemoryRecapReply(input) {
function resolveAssistantLivingChatMemoryContext(input) {
const contextualInventoryHistoryCapabilityFollowup = String(input.modeDecisionReason ?? "") === "inventory_history_capability_followup_detected";
const contextualMemoryRecapFollowup = String(input.modeDecisionReason ?? "") === "memory_recap_followup_detected";
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
const continuity = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({
sessionItems: input.sessionItems,
toNonEmptyString
});
return {
contextualInventoryHistoryCapabilityFollowup,
contextualMemoryRecapFollowup,
lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup
? findLastGroundedInventoryAddressDebug(sessionItems)
? continuity.lastGroundedInventoryAddressDebug
: null,
lastMemoryAddressDebug: contextualMemoryRecapFollowup
? findLastAddressDebugWithItem(sessionItems) ?? findLastAddressDebug(sessionItems)
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
: null
};
}
function createAssistantMemoryRecapPolicy(deps) {
function resolveRouteMemorySignals(input) {
const samples = collectMessageSamples(input);
const continuity = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({
sessionItems: input.sessionItems,
toNonEmptyString
});
const groundedInventoryContext = continuity.lastGroundedInventoryAddressDebug ?? input.lastGroundedAddressDebug;
const historicalCapabilitySignal = hasSignalAcrossSamples(samples, deps.hasHistoricalCapabilityFollowupSignal);
const memoryRecapSignal = hasSignalAcrossSamples(samples, deps.hasConversationMemoryRecallFollowupSignal);
const explicitRecapPromptSignal = hasExplicitRecapPromptSignal(samples);
@@ -191,13 +223,13 @@ function createAssistantMemoryRecapPolicy(deps) {
!input.dataScopeMetaQuery &&
!input.dataRetrievalSignal &&
historicalCapabilitySignal &&
deps.isGroundedInventoryContextDebug(input.lastGroundedAddressDebug)),
deps.isGroundedInventoryContextDebug(groundedInventoryContext)),
contextualMemoryRecapFollowupDetected: Boolean(!input.dataScopeMetaQuery &&
!input.capabilityMetaQuery &&
!input.aggregateBusinessAnalyticsSignal &&
memoryRecapSignal &&
(explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) &&
input.hasPriorAddressDebug)
continuity.hasGroundedAddressContext)
};
}
return {
+176 -10
View File
@@ -102,10 +102,16 @@ function createAssistantRoutePolicy(deps) {
hasDataRetrievalRequestSignal(repairedRawUserMessage) ||
hasDataRetrievalRequestSignal(effectiveAddressUserMessage) ||
hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage);
const analyticsSample = compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`.toLowerCase());
const colloquialCustomerValueSignal = /(?:\u043a\u0442\u043e|\u043a\u043e\u043c\u0443|customer|client|counterparty)/iu.test(analyticsSample) &&
/(?:(?:\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0451\u0441|\u0437\u0430\u043d\u0435\u0441|\u0437\u0430\u043d\u0451\u0441).*(?:\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u044c\u0433)|(?:\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u044c\u0433).*(?:\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0451\u0441|\u0437\u0430\u043d\u0435\u0441|\u0437\u0430\u043d\u0451\u0441)|(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?).*(?:\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u044c\u0433))/iu.test(analyticsSample);
const turnoverAggregateSignal = /(?:\u043e\u0431\u043e\u0440\u043e\u0442(?:\u044b)?|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u043e\u0445\u043e\u0434|turnover|revenue).*(?:\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|year|month|quarter)/iu.test(analyticsSample);
const aggregateBusinessAnalyticsBridgeDetected = colloquialCustomerValueSignal || turnoverAggregateSignal;
const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) ||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage) ||
aggregateBusinessAnalyticsBridgeDetected;
const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) ||
hasStandaloneAddressTopicSignal(repairedRawUserMessage) ||
hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) ||
@@ -195,15 +201,20 @@ function createAssistantRoutePolicy(deps) {
!capabilityMetaQuery &&
!dataRetrievalSignal);
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal &&
!aggregateBusinessAnalyticsSignal &&
!effectiveAddressFollowupSignal &&
resolvedModeDetection.mode === "unsupported" &&
resolvedIntentResolution.intent === "unknown");
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected", "llm_canonical_data_signal_detected"].includes(String(baseToolGate?.reason ?? "")));
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
!baseToolGatePreservesAddressLane &&
!protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected);
const lastAddressAssistantDebug = sessionItems
@@ -220,7 +231,8 @@ function createAssistantRoutePolicy(deps) {
strongDataSignal,
aggregateBusinessAnalyticsSignal,
lastGroundedAddressDebug,
hasPriorAddressDebug: Boolean(lastGroundedAddressDebug || lastAddressAssistantDebug)
hasPriorAddressDebug: Boolean(lastGroundedAddressDebug || lastAddressAssistantDebug),
sessionItems
});
const contextualHistoricalCapabilityFollowupDetected = memorySignals.contextualHistoricalCapabilityFollowupDetected;
const contextualMemoryRecapFollowupDetected = memorySignals.contextualMemoryRecapFollowupDetected;
@@ -392,6 +404,143 @@ function createAssistantRoutePolicy(deps) {
}
};
}
const supportedExactInvestigativeAddressBypass = Boolean(llmContractMode === "deep_analysis" &&
semanticApplyCanonicalRecommended &&
strictDeepInvestigationBypassAllowed &&
llmContractIntent &&
ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent));
if (supportedExactInvestigativeAddressBypass) {
return {
runAddressLane: true,
toolGateDecision: "run_address_lane",
toolGateReason: "address_signal_detected",
livingMode: "address_data",
livingReason: "address_lane_triggered",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
semantic_contract_valid: semanticContractValid,
semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended,
semantic_reason_codes: semanticReasonCodes,
semantic_route_arbitration: {
supported_address_intent_detected: true,
strict_deep_investigation_bypass_allowed: true,
semantic_deep_investigation_hint_detected: semanticExtractionContract?.guard_hints?.deep_investigation_signal_detected === true,
semantic_aggregate_shape_detected: false,
followup_semantic_override_to_deep_allowed: false
},
followup_context_detected: Boolean(followupContext),
unsupported_address_intent_fallback_to_deep: false,
deep_analysis_signal_fallback_to_deep: false,
aggregate_analytics_signal_fallback_to_deep: false,
deep_session_continuation_fallback_to_deep: false,
final_decision: {
run_address_lane: true,
tool_gate_decision: "run_address_lane",
tool_gate_reason: "address_signal_detected",
living_mode: "address_data",
living_reason: "address_lane_triggered"
}
}
};
}
if (colloquialCustomerValueSignal) {
return {
runAddressLane: true,
toolGateDecision: "run_address_lane",
toolGateReason: "address_signal_detected",
livingMode: "address_data",
livingReason: "address_lane_triggered",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: true,
data_retrieval_signal_detected: dataRetrievalSignal,
semantic_contract_valid: semanticContractValid,
semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended,
semantic_reason_codes: semanticReasonCodes,
semantic_route_arbitration: {
supported_address_intent_detected: true,
strict_deep_investigation_bypass_allowed: false,
semantic_deep_investigation_hint_detected: false,
semantic_aggregate_shape_detected: true,
followup_semantic_override_to_deep_allowed: false
},
followup_context_detected: Boolean(followupContext),
unsupported_address_intent_fallback_to_deep: false,
deep_analysis_signal_fallback_to_deep: false,
aggregate_analytics_signal_fallback_to_deep: false,
deep_session_continuation_fallback_to_deep: false,
final_decision: {
run_address_lane: true,
tool_gate_decision: "run_address_lane",
tool_gate_reason: "address_signal_detected",
living_mode: "address_data",
living_reason: "address_lane_triggered"
}
}
};
}
const standaloneAggregateAnalyticsFollowupFallback = Boolean(followupContext &&
aggregateBusinessAnalyticsBridgeDetected &&
llmContractMode === null &&
!((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
openContractsAddressSignal));
if (standaloneAggregateAnalyticsFollowupFallback) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "aggregate_analytics_signal_fallback_to_deep",
livingMode: "deep_analysis",
livingReason: "aggregate_analytics_signal_fallback_to_deep",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
semantic_contract_valid: semanticContractValid,
semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended,
semantic_reason_codes: semanticReasonCodes,
semantic_route_arbitration: {
supported_address_intent_detected: false,
strict_deep_investigation_bypass_allowed: false,
semantic_deep_investigation_hint_detected: false,
semantic_aggregate_shape_detected: true,
followup_semantic_override_to_deep_allowed: true
},
followup_context_detected: true,
unsupported_address_intent_fallback_to_deep: false,
deep_analysis_signal_fallback_to_deep: false,
aggregate_analytics_signal_fallback_to_deep: true,
deep_session_continuation_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "aggregate_analytics_signal_fallback_to_deep",
living_mode: "deep_analysis",
living_reason: "aggregate_analytics_signal_fallback_to_deep"
}
}
};
}
if (nonDomainQueryIndexed) {
return {
runAddressLane: false,
@@ -422,7 +571,6 @@ function createAssistantRoutePolicy(deps) {
};
}
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
llmContractMode === "address_query") ||
@@ -447,6 +595,12 @@ function createAssistantRoutePolicy(deps) {
Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
openContractsAddressSignal);
const supportedAddressRouteCandidateDetected = Boolean(supportedAddressIntentDetected ||
keepAddressLaneByIntent ||
(baseToolGate?.runAddressLane &&
llmContractMode !== "deep_analysis" &&
!strictDeepInvestigationCueDetected &&
["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected", "llm_canonical_data_signal_detected"].includes(String(baseToolGate?.reason ?? ""))));
const semanticGuardHints = semanticExtractionContract?.guard_hints &&
typeof semanticExtractionContract.guard_hints === "object"
? semanticExtractionContract.guard_hints
@@ -481,8 +635,7 @@ function createAssistantRoutePolicy(deps) {
semanticDeepInvestigationHintDetected ||
aggregateBusinessAnalyticsSignal) &&
!preserveAddressLaneSignal &&
!keepAddressLaneByIntent &&
!supportedAddressIntentDetected &&
!supportedAddressRouteCandidateDetected &&
(!followupContext || followupSemanticOverrideToDeepAllowed));
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
@@ -492,6 +645,19 @@ function createAssistantRoutePolicy(deps) {
hasDirectDeepAnalysisSignal(repairedRawUserMessage) ||
hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) ||
hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage));
const exactAddressIntentProtectedFromSemanticDeepHint = Boolean(supportedAddressRouteCandidateDetected &&
llmContractMode === "address_query" &&
llmContractIntent &&
ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent) &&
semanticApplyCanonicalRecommended &&
!deepAnalysisPreferenceDetected &&
!strictDeepInvestigationCueDetected &&
!semanticAggregateShapeDetected);
const protectAddressLaneFromFallback = Boolean(supportedAddressRouteCandidateDetected &&
!deepAnalysisPreferenceDetected &&
(exactAddressIntentProtectedFromSemanticDeepHint ||
!semanticDeepInvestigationHintDetected ||
strictDeepInvestigationBypassAllowed));
const vatExplainFollowupSignal = Boolean(followupContext &&
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
@@ -501,17 +667,16 @@ function createAssistantRoutePolicy(deps) {
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
!llmRuntimeUnavailableDetected &&
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
!keepAddressLaneByIntent &&
!supportedAddressIntentDetected &&
!protectAddressLaneFromFallback &&
!vatExplainFollowupSignal &&
(!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed));
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
!llmRuntimeUnavailableDetected &&
aggregateBusinessAnalyticsSignal &&
!keepAddressLaneByIntent &&
!supportedAddressIntentDetected &&
!protectAddressLaneFromFallback &&
(!followupContext ||
llmContractMode === "unsupported" ||
llmContractMode === null ||
semanticAggregateShapeDetected ||
!semanticApplyCanonicalRecommended ||
standaloneAddressTopicSignal));
@@ -631,10 +796,11 @@ function createAssistantRoutePolicy(deps) {
semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended,
semantic_reason_codes: semanticReasonCodes,
semantic_route_arbitration: {
supported_address_intent_detected: supportedAddressIntentDetected,
supported_address_intent_detected: supportedAddressRouteCandidateDetected,
strict_deep_investigation_bypass_allowed: strictDeepInvestigationBypassAllowed,
semantic_deep_investigation_hint_detected: semanticDeepInvestigationHintDetected,
semantic_aggregate_shape_detected: semanticAggregateShapeDetected,
exact_address_intent_protected_from_semantic_deep_hint: exactAddressIntentProtectedFromSemanticDeepHint,
followup_semantic_override_to_deep_allowed: followupSemanticOverrideToDeepAllowed
},
followup_context_detected: Boolean(followupContext),
@@ -174,7 +174,7 @@ function intersectsSpan(start, end, spans) {
function hasAccountContextAround(text, start, end) {
const left = text.slice(Math.max(0, start - 28), start);
const right = text.slice(end, Math.min(text.length, end + 28));
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
return /(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
}
function extractAccountsFromTextDetailed(text, options) {
const lower = String(text ?? "").toLowerCase();
@@ -184,8 +184,8 @@ function extractAccountsFromTextDetailed(text, options) {
const percentSpans = collectPercentLikeSpans(lower);
const contractSpans = collectContractLikeSpans(lower);
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans, ...contractSpans];
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(lower);
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
const hasAccountingLexeme = /(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(lower);
const contextualPattern = /(?:^|[^\p{L}\d])(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)[\s#:№]*((?:\d{2})(?:\.\d{2})?)/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const token = String(contextualMatch[1] ?? "").trim();
@@ -811,7 +811,15 @@ function resolveDomainPolarityGuard(input) {
const repairedMessage = repairRuntimeGuardsMojibake(String(input.userMessage ?? ""));
const lower = repairedMessage.toLowerCase();
const accountExtraction = extractAccountsFromTextDetailed(lower);
const accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...accountExtraction.resolved_account_anchors]);
let accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...accountExtraction.resolved_account_anchors]);
if (accounts.length === 0) {
const directAccountMentions = Array.from(repairedMessage.matchAll(/(?:^|[^\p{L}\d])(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)[\s#:№]*((?:60|62|76)(?:\.\d{2})?)/giu))
.map((match) => String(match[1] ?? "").trim())
.filter((token) => token.length > 0);
if (directAccountMentions.length > 0) {
accounts = uniqueStrings([...accounts, ...directAccountMentions]);
}
}
const prefixes = new Set(accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item)));
const settlementSignal = input.focusDomainHint === "settlements_60_62" ||
prefixes.has("60") ||
+16 -2
View File
@@ -3701,6 +3701,9 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
? llmPreDecomposeMeta.semanticExtractionContract
: null;
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
semanticCanonicalRecommended;
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
const hasLlmCanonicalSignal = semanticCanonicalRecommended &&
@@ -3713,6 +3716,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
Boolean(llmPreDecomposeMeta?.applied) &&
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
hasStrongDataIntentSignal(repairedInputMessage);
const hasBusinessRankingAddressSignal = /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?).*(?:\u0434\u0435\u043d\u0435\u0433)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) ||
hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage);
const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
@@ -3721,11 +3725,12 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
hasAccountingSignal(repairedInputMessage) ||
hasShortDebtMirrorFollowupSignal(rawMessageForGate) ||
hasShortDebtMirrorFollowupSignal(repairedInputMessage) ||
hasBusinessRankingAddressSignal ||
sameDateAccountFollowupSignal;
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
llmContractIntent === "unknown";
const hasAnyAddressSignal = hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal;
const hasAnyAddressSignal = hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal || llmSupportedDeepAddressIntentSignal;
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
hasAccountingSignal(rawMessageForGate) ||
@@ -3739,6 +3744,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
!hasClassifierSignal &&
!hasIntentSignal &&
!hasLexicalAddressSignal &&
!llmSupportedDeepAddressIntentSignal &&
!strongDataSignalFromRawMessage &&
!strongDataSignalFromEffectiveMessage) {
return {
@@ -3749,6 +3755,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
}
if (hasUnsupportedLowConfidencePredecomposeSignal && !followupContext &&
!hasAnyAddressSignal &&
!llmSupportedDeepAddressIntentSignal &&
!strongDataSignalFromRawMessage &&
!strongDataSignalFromEffectiveMessage) {
return {
@@ -3770,7 +3777,9 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
? "llm_canonical_candidate_detected"
: hasLlmCanonicalDataSignal
? "llm_canonical_data_signal_detected"
: "address_signal_detected"
: llmSupportedDeepAddressIntentSignal
? "address_signal_detected"
: "address_signal_detected"
};
}
if (followupContext) {
@@ -3886,6 +3895,11 @@ function hasAggregateBusinessAnalyticsSignal(text) {
if (!normalized) {
return false;
}
const hasTurnoverByEntityBridgeSignal = /(?:\u043e\u0431\u043e\u0440\u043e\u0442(?:\u044b)?|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u043e\u0445\u043e\u0434|turnover|revenue).*(?:\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|year|month|quarter)/iu.test(normalized) &&
/(?:\b\u043f\u043e\s+[\p{L}\d._-]{3,}|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|customer|supplier|counterparty|company)/iu.test(normalized);
if (hasTurnoverByEntityBridgeSignal) {
return true;
}
const hasMetricCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u043c\u0430\u0440\u0436|\u0440\u0435\u043d\u0442\u0430\u0431\u0435\u043b|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b|turnover|revenue|profit|margin)/iu.test(normalized);
if (!hasMetricCue) {
return false;
@@ -3,6 +3,38 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAssistantTransitionPolicy = createAssistantTransitionPolicy;
function createAssistantTransitionPolicy(deps) {
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
if (!hasInventoryRootFrame) {
return false;
}
const normalized = deps
.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase())
.replace(/ё/g, "е");
if (!normalized) {
return false;
}
if (deps.countTokens(normalized) > 10) {
return false;
}
const hasInventoryLexeme = /(?:остат|склад|товар|номенклатур|позиц)/iu.test(normalized);
const hasRestatementCue = /(?:еще\s+раз|снова|повтори|повтори\s+еще\s+раз|верни|покажи)/iu.test(normalized) &&
/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|тот\s+же\s+период|этот\s+же\s+период)/iu.test(normalized);
const hasBareSnapshotSameDateCue = hasInventoryLexeme &&
/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|тот\s+же\s+период|этот\s+же\s+период)/iu.test(normalized);
const bareSnapshotSameDatePhrases = [
"\u043d\u0430 \u0442\u0443 \u0436\u0435 \u0434\u0430\u0442",
"\u043d\u0430 \u044d\u0442\u0443 \u0436\u0435 \u0434\u0430\u0442",
"\u043d\u0430 \u044d\u0442\u0443 \u0434\u0430\u0442",
"\u044d\u0442\u0443 \u0434\u0430\u0442",
"\u0442\u0430 \u0436\u0435 \u0434\u0430\u0442\u0430",
"\u0442\u043e\u0442 \u0436\u0435 \u043f\u0435\u0440\u0438\u043e\u0434",
"\u044d\u0442\u043e\u0442 \u0436\u0435 \u043f\u0435\u0440\u0438\u043e\u0434"
];
const hasBareSnapshotSameDatePhraseCue = hasInventoryLexeme && bareSnapshotSameDatePhrases.some((phrase) => normalized.includes(phrase));
return (hasInventoryLexeme &&
(hasRestatementCue || hasBareSnapshotSameDateCue || hasBareSnapshotSameDatePhraseCue) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(normalized));
}
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized || deps.countTokens(normalized) > 4) {
@@ -16,6 +48,62 @@ function createAssistantTransitionPolicy(deps) {
}
return /^(?:Р°|Рё|РЅСѓ)?\s*РїРѕ\s+[a-zР°-СЏС‘0-9._-]{2,}(?:\s+[a-zР°-СЏС‘0-9._-]{2,})?$/iu.test(normalized);
}
function shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized || deps.countTokens(normalized) > 4) {
return false;
}
if (sourceIntent !== "list_documents_by_counterparty" && sourceIntent !== "list_documents_by_contract") {
return false;
}
if (/(?:bank|payment|wire|statement)/iu.test(normalized)) {
return false;
}
return /^(?:а|и|ну)?\s*(?:покажи\s+)?по\s+[a-zа-яё0-9._-]{2,}(?:\s+[a-zа-яё0-9._-]{2,})?$/iu.test(normalized);
}
function inferStandaloneAddressTopicFamily(userMessage) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized) {
return null;
}
if (/(?:ндс|vat)/iu.test(normalized)) {
return "vat";
}
if (/(?:остат|склад|товар|номенклатур|позици)/iu.test(normalized)) {
return "inventory";
}
return null;
}
function resolveDisplayedEntityRetargetIntent(userMessage, entityType) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized) {
return null;
}
if (entityType === "counterparty") {
if (/(?:договор|контракт)/iu.test(normalized)) {
return "list_contracts_by_counterparty";
}
if (/(?:банк|выписк|плат[её]ж|оплат|statement|payment|wire)/iu.test(normalized)) {
return "bank_operations_by_counterparty";
}
if (/(?:документ|накладн|счет|сч[её]т|акт|реализац|поступл)/iu.test(normalized)) {
return "list_documents_by_counterparty";
}
if (/(?:сколько\s+денег|сколько\s+принес|выручк|сумм[аы]?|оплатил|продаж)/iu.test(normalized)) {
return "customer_revenue_and_payments";
}
return null;
}
if (entityType === "contract") {
if (/(?:банк|выписк|плат[её]ж|оплат|statement|payment|wire)/iu.test(normalized)) {
return "bank_operations_by_contract";
}
if (/(?:документ|накладн|счет|сч[её]т|акт|реализац|поступл)/iu.test(normalized)) {
return "list_documents_by_contract";
}
}
return null;
}
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
const previousAddressItem = deps.findLastAddressAssistantItem(items);
const previousAddressDebug = previousAddressItem?.debug ?? null;
@@ -78,6 +166,10 @@ function createAssistantTransitionPolicy(deps) {
const hasInventoryRootTemporalFollowupAlternate = deps.toNonEmptyString(alternateMessage)
? deps.hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
: false;
const hasInventoryRootRestatementPrimary = hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
const hasInventoryRootRestatementAlternate = deps.toNonEmptyString(alternateMessage)
? hasInventoryRootRestatementLikeSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
: false;
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
@@ -86,6 +178,8 @@ function createAssistantTransitionPolicy(deps) {
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
Boolean(debtRoleSwapIntent) ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
@@ -100,6 +194,8 @@ function createAssistantTransitionPolicy(deps) {
!hasAlternateFollowupSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
@@ -109,6 +205,8 @@ function createAssistantTransitionPolicy(deps) {
!hasAlternateFollowupSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
@@ -131,8 +229,38 @@ function createAssistantTransitionPolicy(deps) {
? resolvedAlternateIntent
: null;
const sourceIntentFamily = deps.resolveAddressIntentFamily(sourceIntent);
const explicitIntentFamily = deps.resolveAddressIntentFamily(explicitIntent);
if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) {
const explicitIntentFamily = deps.resolveAddressIntentFamily(explicitIntent) ??
inferStandaloneAddressTopicFamily(userMessage) ??
(deps.toNonEmptyString(alternateMessage) ? inferStandaloneAddressTopicFamily(String(alternateMessage ?? "")) : null);
const foreignAccountingPivotOverInventory = deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage);
const familyMismatchStandaloneTopic = Boolean(hasStandaloneAddressTopic &&
sourceIntentFamily &&
explicitIntentFamily &&
sourceIntentFamily !== explicitIntentFamily &&
!hasOrganizationClarificationContinuation &&
!hasImplicitContinuationSignal &&
!hasIndexReferenceSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!inventoryShortFollowupPrimary &&
!inventoryShortFollowupAlternate &&
!foreignAccountingPivotOverInventory &&
!deps.hasFollowupMarker(userMessage) &&
!deps.hasReferentialPointer(userMessage) &&
(!deps.toNonEmptyString(alternateMessage)
? true
: !deps.hasFollowupMarker(String(alternateMessage ?? "")) &&
!deps.hasReferentialPointer(String(alternateMessage ?? ""))));
if (familyMismatchStandaloneTopic) {
return null;
}
if (sourceIntentFamily &&
explicitIntentFamily &&
sourceIntentFamily !== explicitIntentFamily &&
!hasStrongFollowupReference &&
!foreignAccountingPivotOverInventory) {
return null;
}
let previousIntent = sourceIntent;
@@ -144,7 +272,7 @@ function createAssistantTransitionPolicy(deps) {
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
? deps.toNonEmptyString(followupOffer.suggested_intents[0])
: null;
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent);
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
if (suggestedIntent && !keepPreviousIntent) {
previousIntent = suggestedIntent;
followupSelectionMode = "switch_to_suggested_intent";
@@ -248,6 +376,7 @@ function createAssistantTransitionPolicy(deps) {
: "generic"
: null;
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
const previousFiltersRaw = previousAddressDebug.extracted_filters;
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" ? { ...previousFiltersRaw } : {};
const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" ||
@@ -314,7 +443,15 @@ function createAssistantTransitionPolicy(deps) {
currentFrameKind === "inventory_root") &&
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
const inventoryRootRestatementPivot = Boolean(inventoryRootFrame &&
(deps.isInventorySelectedObjectIntent(sourceIntentHint) ||
deps.isInventoryRootFrameIntent(sourceIntentHint) ||
currentFrameKind === "inventory_drilldown" ||
currentFrameKind === "inventory_root" ||
currentFrameKind === "generic") &&
(hasInventoryRootRestatementPrimary || hasInventoryRootRestatementAlternate) &&
!deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot || inventoryRootRestatementPivot;
if (rootScopedPivot) {
previousIntent = null;
previousAnchorType = null;
@@ -330,6 +467,7 @@ function createAssistantTransitionPolicy(deps) {
? deps.resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
: null);
if (resolvedEntityFromFollowup && !rootScopedPivot) {
displayedEntityTargetIntent = resolveDisplayedEntityRetargetIntent(userMessage, resolvedEntityFromFollowup.entityType);
if (resolvedEntityFromFollowup.entityType === "counterparty") {
previousFilters.counterparty = resolvedEntityFromFollowup.value;
previousAnchorType = "counterparty";
@@ -396,11 +534,13 @@ function createAssistantTransitionPolicy(deps) {
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate));
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
? inventoryRootFrame?.intent ?? explicitIntent ?? previousIntent ?? undefined
: explicitIntent ?? previousIntent ?? undefined;
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
@@ -430,14 +570,15 @@ function createAssistantTransitionPolicy(deps) {
const previousIntent = deps.toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
const selectionMode = deps.toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
const rootContextOnly = selectionMode === "carry_root_context";
const carryoverTargetIntent = deps.toNonEmptyString(carryoverMeta?.followupContext?.target_intent) ?? null;
const explicitIntentRaw = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
const rootIntent = deps.toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
const targetIntent = selectionMode === "switch_to_suggested_intent"
? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
: rootContextOnly
? rootIntent ?? explicitIntent ?? null
: explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
const rewrittenByPredecompose = deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
const hasExplicitIntent = Boolean(explicitIntent);