ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Систематизировать ответы по поставщику без поставок через анализ оплат и возвратов
This commit is contained in:
@@ -479,6 +479,12 @@ function detectValueRankingFocus(userMessage) {
|
||||
if (!text) {
|
||||
return "top_by_total";
|
||||
}
|
||||
const asksTotalMoneyEarned = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text) &&
|
||||
!/(?:клиент|заказчик|покупател|контрагент|customer|client|counterpart)/iu.test(text) &&
|
||||
!/(?:топ|top|сам(?:ый|ая|ое|ые)|наибольш|больше\s+всего|максимальн)/iu.test(text);
|
||||
if (asksTotalMoneyEarned) {
|
||||
return "total_flow";
|
||||
}
|
||||
const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
@@ -575,6 +581,13 @@ function extractCounterpartyName(row) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function hasCounterpartyItemFlowQuestion(userMessage) {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /(?:что\s+нам\s+(?:отгруж|постав|привоз|прод)|како(?:й|е|го|му)\s+товар|какую\s+услуг|какие\s+товар|какие\s+услуг|товар\s+или\s+услуг|позици(?:ю|и|ях)?)/iu.test(text);
|
||||
}
|
||||
function extractInventoryItemName(row) {
|
||||
const direct = String(row.item ?? "").trim();
|
||||
if (direct) {
|
||||
@@ -752,10 +765,13 @@ function looksLikeInventoryPartyToken(value) {
|
||||
}
|
||||
return normalized === normalized.toUpperCase() && normalized.length >= 4;
|
||||
}
|
||||
function extractInventoryCounterpartyCandidates(row) {
|
||||
function extractInventoryCounterpartyCandidates(row, excludedTokens = []) {
|
||||
const itemToken = normalizeEntityToken(extractInventoryItemName(row));
|
||||
const warehouseToken = normalizeEntityToken(extractInventoryWarehouseName(row));
|
||||
const organizationToken = normalizeEntityToken(extractInventoryOrganizationName(row));
|
||||
const excludedComparableTokens = excludedTokens
|
||||
.map((token) => normalizeEntityToken(token))
|
||||
.filter((token) => Boolean(token));
|
||||
const candidates = [];
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
@@ -763,14 +779,18 @@ function extractInventoryCounterpartyCandidates(row) {
|
||||
continue;
|
||||
}
|
||||
const comparable = normalizeEntityToken(normalized);
|
||||
if (!comparable || comparable === itemToken || comparable === warehouseToken || comparable === organizationToken) {
|
||||
if (!comparable ||
|
||||
comparable === itemToken ||
|
||||
comparable === warehouseToken ||
|
||||
comparable === organizationToken ||
|
||||
excludedComparableTokens.includes(comparable)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push(normalized);
|
||||
}
|
||||
return uniqueStrings(candidates);
|
||||
}
|
||||
function summarizeInventoryTraceRows(rows) {
|
||||
function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
|
||||
const items = uniqueStrings(rows
|
||||
.map((row) => extractInventoryItemName(row))
|
||||
.filter((item) => Boolean(item)));
|
||||
@@ -780,7 +800,7 @@ function summarizeInventoryTraceRows(rows) {
|
||||
const organizations = uniqueStrings(rows
|
||||
.map((row) => extractInventoryOrganizationName(row))
|
||||
.filter((item) => Boolean(item)));
|
||||
const counterparties = uniqueStrings(rows.flatMap((row) => extractInventoryCounterpartyCandidates(row)));
|
||||
const counterparties = uniqueStrings(rows.flatMap((row) => extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens)));
|
||||
const documents = uniqueStrings(rows
|
||||
.map((row) => String(row.registrator ?? "").trim())
|
||||
.filter((item) => item.length > 0 && item !== "(без названия)"));
|
||||
@@ -800,9 +820,9 @@ function summarizeInventoryTraceRows(rows) {
|
||||
totalAmount
|
||||
};
|
||||
}
|
||||
function formatInventoryTraceRows(rows, limit = 10) {
|
||||
function formatInventoryTraceRows(rows, limit = 10, excludedCounterpartyTokens = []) {
|
||||
return rows.slice(0, limit).map((row, index) => {
|
||||
const parties = extractInventoryCounterpartyCandidates(row);
|
||||
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
||||
const warehouse = extractInventoryWarehouseName(row);
|
||||
const organization = extractInventoryOrganizationName(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? formatMoneyRub(row.amount) : "сумма не указана";
|
||||
@@ -823,6 +843,33 @@ function formatInventoryTraceRows(rows, limit = 10) {
|
||||
return parts.join(" | ");
|
||||
});
|
||||
}
|
||||
function formatCounterpartyItemFlowRows(rows, limit = 12) {
|
||||
return rows.slice(0, limit).map((row, index) => {
|
||||
const item = extractInventoryItemName(row) ?? "позиция не указана";
|
||||
const contract = extractContractName(row);
|
||||
const warehouse = extractInventoryWarehouseName(row);
|
||||
const organization = extractInventoryOrganizationName(row);
|
||||
const quantity = extractInventoryQuantity(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? formatMoneyRub(row.amount) : "сумма не указана";
|
||||
const parts = [
|
||||
`${index + 1}. ${item}`,
|
||||
`договор: ${contract ?? "не указан"}`,
|
||||
`документ: ${row.registrator}`,
|
||||
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
||||
`сумма: ${amount}`
|
||||
];
|
||||
if (quantity !== null && quantity > 0) {
|
||||
parts.push(`количество: ${formatNumberWithDots(quantity, 3)}`);
|
||||
}
|
||||
if (warehouse) {
|
||||
parts.push(`склад: ${warehouse}`);
|
||||
}
|
||||
if (organization) {
|
||||
parts.push(`организация: ${organization}`);
|
||||
}
|
||||
return parts.join(" | ");
|
||||
});
|
||||
}
|
||||
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
|
||||
const byItem = new Map();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
@@ -2575,6 +2622,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
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));
|
||||
@@ -2606,6 +2655,31 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
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
|
||||
@@ -3344,8 +3418,11 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
if (intent === "inventory_sale_trace_for_item") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(saleRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const requestedItemHint = String(options.itemHint ?? "").trim();
|
||||
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
|
||||
const summary = summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
|
||||
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
||||
const excludedCounterpartyTokens = [itemLabel];
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
@@ -3367,7 +3444,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(saleRows, 12));
|
||||
lines.push(...formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
@@ -3964,8 +4041,11 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
if (intent === "open_items_by_counterparty_or_contract") {
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const accountLead = typeof options.accountHint === "string" && options.accountHint.trim().length > 0
|
||||
? `Проверил хвосты по счету ${options.accountHint.trim()}.`
|
||||
: "Собраны открытые позиции по взаиморасчетам.";
|
||||
const lines = [
|
||||
"Собраны открытые позиции по взаиморасчетам.",
|
||||
accountLead,
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
`Контрагентов с сигналом: ${counterparties.length}.`
|
||||
];
|
||||
@@ -4019,10 +4099,63 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
};
|
||||
}
|
||||
if (intent === "list_documents_by_counterparty") {
|
||||
const lines = [
|
||||
`Найдено документов по контрагенту: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
const resolvedCounterparty = (typeof options.counterpartyHint === "string" && options.counterpartyHint.trim().length > 0
|
||||
? options.counterpartyHint.trim()
|
||||
: null) ??
|
||||
(() => {
|
||||
const counterparties = uniqueStrings(rows
|
||||
.map((row) => extractCounterpartyName(row))
|
||||
.filter((item) => Boolean(item)));
|
||||
return counterparties.length === 1 ? counterparties[0] : null;
|
||||
})();
|
||||
const counterpartyLabel = typeof resolvedCounterparty === "string" && resolvedCounterparty.endsWith(".")
|
||||
? resolvedCounterparty
|
||||
: resolvedCounterparty
|
||||
? `${resolvedCounterparty}.`
|
||||
: null;
|
||||
const counterpartyInline = typeof counterpartyLabel === "string" ? counterpartyLabel.replace(/[.]+$/u, "") : resolvedCounterparty;
|
||||
const itemFlowQuestion = hasCounterpartyItemFlowQuestion(options.userMessage);
|
||||
const items = uniqueStrings(rows
|
||||
.map((row) => extractInventoryItemName(row))
|
||||
.filter((item) => Boolean(item)));
|
||||
const contracts = uniqueStrings(rows
|
||||
.map((row) => extractContractName(row))
|
||||
.filter((item) => Boolean(item)));
|
||||
const lines = [];
|
||||
if (itemFlowQuestion) {
|
||||
lines.push(counterpartyInline
|
||||
? `Контрагент: ${counterpartyInline}. Подтвержденных поставок товаров или услуг: ${rows.length}.`
|
||||
: `Подтвержденных поставок товаров или услуг по запрошенному контрагенту: ${rows.length}.`);
|
||||
}
|
||||
else {
|
||||
lines.push(counterpartyInline
|
||||
? `Контрагент: ${counterpartyInline}. Найдено документов: ${rows.length}.`
|
||||
: `Найдено документов по контрагенту: ${rows.length}.`);
|
||||
}
|
||||
if (counterpartyLabel) {
|
||||
lines.push(`Контрагент: ${counterpartyLabel}`);
|
||||
}
|
||||
if (itemFlowQuestion) {
|
||||
if (items.length > 0) {
|
||||
lines.push(`Позиции: ${items.slice(0, 8).join("; ")}.`);
|
||||
if (items.length > 8) {
|
||||
lines.push(`Показаны первые 8 из ${items.length} позиций.`);
|
||||
}
|
||||
}
|
||||
if (contracts.length === 1) {
|
||||
lines.push(`Договор: ${contracts[0]}.`);
|
||||
}
|
||||
else if (contracts.length > 1) {
|
||||
lines.push(`Договоры в выборке: ${contracts.slice(0, 3).join("; ")}.`);
|
||||
}
|
||||
lines.push(...formatCounterpartyItemFlowRows(rows));
|
||||
if (rows.length > 12) {
|
||||
lines.push(`Показаны первые 12 из ${rows.length} поставок.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push(...formatTopRows(rows, rows.length));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
||||
@@ -34,7 +34,7 @@ function hasSameDateHint(text) {
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|дат[ауеы],?\s+котор(?:ую|ая)\s+(?:до\s+этого|раньше|ранее)\s+(?:рассматривали|смотрели)|дат[ауеы],?\s+которая\s+был[ао]?\s+ранее\s+рассмотрен[ао]?|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date|date\s+we\s+looked\s+at\s+before|previously\s+considered\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasSamePeriodHint(text) {
|
||||
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
|
||||
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|на\s+этот\s+период|за\s+тот\s+период|на\s+тот\s+период|этот\s+период|тот\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitPeriodLiteral(text) {
|
||||
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
|
||||
@@ -521,6 +521,9 @@ function hasAddressFollowupContextSignal(text) {
|
||||
if (hasSameDateHint(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (hasSamePeriodHint(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const tokenCount = normalized.split(/\s+/).filter(Boolean).length;
|
||||
if (tokenCount <= 12 &&
|
||||
/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(normalized) &&
|
||||
@@ -668,7 +671,9 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_confirmed_as_of_date") {
|
||||
intent === "vat_payable_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period") {
|
||||
const hasFollowupSignalForConfirmed = hasAddressFollowupContextSignal(userMessage);
|
||||
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||
const currentContract = toNonEmptyString(merged.contract);
|
||||
@@ -739,6 +744,30 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (samePeriodRequested &&
|
||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date")) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
reasons.push("period_from_from_followup_context");
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
reasons.push("period_to_from_followup_context");
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date") {
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (samePeriodRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
@@ -921,6 +950,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
if (intent === "vat_payable_confirmed_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date") {
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
if (inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((intent === "list_open_contracts" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
@@ -962,7 +1000,7 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||
});
|
||||
}
|
||||
function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext) {
|
||||
if (!followupContext || !followupContext.previous_intent) {
|
||||
if (!followupContext || (!followupContext.previous_intent && !followupContext.target_intent)) {
|
||||
return detectedIntent;
|
||||
}
|
||||
const normalizedMessage = String(userMessage ?? "");
|
||||
@@ -970,7 +1008,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
if (!hasFollowupSignal) {
|
||||
return detectedIntent;
|
||||
}
|
||||
const previousIntent = followupContext.previous_intent;
|
||||
const sourceIntent = followupContext.previous_intent ?? null;
|
||||
const fallbackIntent = followupContext.target_intent ?? sourceIntent;
|
||||
if (!sourceIntent && !fallbackIntent) {
|
||||
return detectedIntent;
|
||||
}
|
||||
const previousFilters = followupContext.previous_filters ?? {};
|
||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||
const previousCounterparty = toNonEmptyString(previousFilters.counterparty);
|
||||
@@ -1000,7 +1042,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
reasons: [...detectedIntent.reasons, "open_items_from_followup_context"]
|
||||
};
|
||||
}
|
||||
const previousIsBalanceFamily = previousIntent === "account_balance_snapshot" || previousIntent === "documents_forming_balance";
|
||||
const previousIsBalanceFamily = sourceIntent === "account_balance_snapshot" || sourceIntent === "documents_forming_balance";
|
||||
if (previousIsBalanceFamily &&
|
||||
hasAccountSignal(normalizedMessage) &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
@@ -1015,7 +1057,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_balance_followup_context"]
|
||||
};
|
||||
}
|
||||
const previousIsInventoryFamily = isInventoryIntent(previousIntent);
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||
if (inventorySelectedObjectFollowup && hasInventorySupplierFollowupCue(normalizedMessage)) {
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
@@ -1024,7 +1066,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
detectedIntent.intent === "bank_operations_by_counterparty" ||
|
||||
detectedIntent.intent === "bank_operations_by_contract" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === previousIntent) {
|
||||
detectedIntent.intent === sourceIntent) {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
confidence: "low",
|
||||
@@ -1037,7 +1079,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
detectedIntent.intent === "list_documents_by_counterparty" ||
|
||||
detectedIntent.intent === "list_documents_by_contract" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === previousIntent) {
|
||||
detectedIntent.intent === sourceIntent) {
|
||||
return {
|
||||
intent: "inventory_purchase_documents_for_item",
|
||||
confidence: "low",
|
||||
@@ -1054,7 +1096,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
detectedIntent.intent === "bank_operations_by_contract" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === "inventory_sale_trace_for_item" ||
|
||||
detectedIntent.intent === previousIntent) {
|
||||
detectedIntent.intent === sourceIntent) {
|
||||
return {
|
||||
intent: "inventory_profitability_for_item",
|
||||
confidence: "low",
|
||||
@@ -1065,7 +1107,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
|
||||
detectedIntent.intent === previousIntent ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date") {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
@@ -1078,7 +1120,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === previousIntent) {
|
||||
detectedIntent.intent === sourceIntent) {
|
||||
return {
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
confidence: "low",
|
||||
@@ -1090,7 +1132,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "inventory_sale_trace_for_item" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === previousIntent) {
|
||||
detectedIntent.intent === sourceIntent) {
|
||||
return {
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
confidence: "low",
|
||||
@@ -1101,7 +1143,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
if (previousIsInventoryFamily &&
|
||||
hasFollowupSignal &&
|
||||
hasBareInventoryPurchaseDateFollowupCue(normalizedMessage) &&
|
||||
(detectedIntent.intent === "unknown" || detectedIntent.intent === previousIntent)) {
|
||||
(detectedIntent.intent === "unknown" || detectedIntent.intent === sourceIntent)) {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
confidence: "low",
|
||||
@@ -1158,7 +1200,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
return detectedIntent;
|
||||
}
|
||||
return {
|
||||
intent: previousIntent,
|
||||
intent: fallbackIntent ?? "unknown",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_from_followup_context"]
|
||||
};
|
||||
|
||||
@@ -72,6 +72,12 @@ function tokenizeAnchor(value) {
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token));
|
||||
}
|
||||
function tokenizeSearchableText(value) {
|
||||
return normalizeSearchText(value)
|
||||
.split(" ")
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
function anchorTokenVariants(token) {
|
||||
const source = String(token ?? "").trim().toLowerCase();
|
||||
if (!source) {
|
||||
@@ -90,9 +96,37 @@ function anchorTokenVariants(token) {
|
||||
}
|
||||
return Array.from(variants);
|
||||
}
|
||||
function normalizePartyTokenSkeleton(value) {
|
||||
return normalizeSearchText(value).replace(/\s+/g, "").replace(/[аеёиоуыэюяaeiouy]+/giu, "");
|
||||
}
|
||||
function fuzzyPartyTokenMatches(candidate, token) {
|
||||
const normalizedCandidate = normalizeSearchText(candidate);
|
||||
const normalizedToken = normalizeSearchText(token);
|
||||
if (!normalizedCandidate || !normalizedToken) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedCandidate === normalizedToken) {
|
||||
return true;
|
||||
}
|
||||
if (normalizedCandidate.length < 4 ||
|
||||
normalizedToken.length < 4 ||
|
||||
/\d/u.test(normalizedCandidate) ||
|
||||
/\d/u.test(normalizedToken)) {
|
||||
return false;
|
||||
}
|
||||
const candidateSkeleton = normalizePartyTokenSkeleton(normalizedCandidate);
|
||||
const tokenSkeleton = normalizePartyTokenSkeleton(normalizedToken);
|
||||
if (candidateSkeleton.length < 3 || tokenSkeleton.length < 3) {
|
||||
return false;
|
||||
}
|
||||
return (candidateSkeleton === tokenSkeleton ||
|
||||
candidateSkeleton.startsWith(tokenSkeleton) ||
|
||||
tokenSkeleton.startsWith(candidateSkeleton));
|
||||
}
|
||||
function matchesAnchorText(searchable, anchor) {
|
||||
const searchableNormalized = normalizeSearchText(searchable);
|
||||
const searchableLatin = transliterateCyrillicToLatin(searchableNormalized);
|
||||
const searchableTokens = tokenizeSearchableText(searchable);
|
||||
const tokens = tokenizeAnchor(anchor);
|
||||
if (tokens.length === 0) {
|
||||
const direct = normalizeSearchText(anchor);
|
||||
@@ -105,7 +139,9 @@ function matchesAnchorText(searchable, anchor) {
|
||||
const variants = anchorTokenVariants(token);
|
||||
return variants.some((variant) => {
|
||||
const tokenLatin = transliterateCyrillicToLatin(variant);
|
||||
return searchableNormalized.includes(variant) || searchableLatin.includes(tokenLatin);
|
||||
return (searchableNormalized.includes(variant) ||
|
||||
searchableLatin.includes(tokenLatin) ||
|
||||
searchableTokens.some((candidate) => fuzzyPartyTokenMatches(candidate, variant)));
|
||||
});
|
||||
});
|
||||
if (fullMatch) {
|
||||
|
||||
Reference in New Issue
Block a user