АРЧ АП11 - Архитектура после регресса: + Архитектура: восстановить bridge от provenance выбранной позиции к VAT-периоду и закрыть phase10 replay
This commit is contained in:
@@ -1598,7 +1598,11 @@ function extractAddressFilters(userMessage, intent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intent === "vat_liability_confirmed_for_tax_period" && !periodRange.period_from && !periodRange.period_to) {
|
||||
const monthPeriodWasDerived = warnings.includes("period_derived_from_month_phrase");
|
||||
if (intent === "vat_liability_confirmed_for_tax_period" &&
|
||||
!periodRange.period_from &&
|
||||
!periodRange.period_to &&
|
||||
!monthPeriodWasDerived) {
|
||||
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||
if (periodToForQuarter) {
|
||||
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.hasCompactAccountCodeToken = hasCompactAccountCodeToken;
|
||||
exports.hasAccountNumberAnchor = hasAccountNumberAnchor;
|
||||
exports.resolveAddressIntent = resolveAddressIntent;
|
||||
const addressCounterpartyIntentSignals_1 = require("./addressCounterpartyIntentSignals");
|
||||
const addressInventoryIntentSignals_1 = require("./addressInventoryIntentSignals");
|
||||
@@ -1449,11 +1451,21 @@ function hasCustomerRevenueRankingBridgeSignal(text) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasSupplierCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|supplier|vendor)/iu.test(normalized);
|
||||
if (hasSupplierCue) {
|
||||
return false;
|
||||
}
|
||||
const hasDirectRankingCue = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0430\s+)?(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438(?:\s+\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f)?|(?:\u0430\s+)?(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438\s+\u0437\u0430\s+\d{4}|(?:\u043e\u0431\u0449\u0430\u044f\s+)?\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(normalized);
|
||||
if (hasDirectRankingCue) {
|
||||
return true;
|
||||
}
|
||||
const hasMoneyCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(normalized);
|
||||
if (!hasMoneyCue) {
|
||||
return false;
|
||||
}
|
||||
return /(?:\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(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(normalized);
|
||||
const hasCustomerRankingCue = /(?:\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(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money|(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)))/iu.test(normalized);
|
||||
const hasRevenueAggregateCue = /(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u043a\w*\s+\u0437\u0430\s+\d{4})/iu.test(normalized);
|
||||
return hasCustomerRankingCue || hasRevenueAggregateCue;
|
||||
}
|
||||
function hasInventoryProvenanceBridgeSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
@@ -1481,6 +1493,10 @@ function hasColloquialInventoryOnHandBridgeSignal(text) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasInventoryAgingCue = /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440\u044b\u0439\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(normalized);
|
||||
if (hasInventoryAgingCue) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
|
||||
const hasWarehouseCue = /(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(normalized);
|
||||
if (!hasWarehouseCue) {
|
||||
@@ -1489,15 +1505,74 @@ function hasColloquialInventoryOnHandBridgeSignal(text) {
|
||||
const hasQuestionCue = /(?:\u0447\u0442\u043e|\u0447\u0435|\u0447\u0451|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|show|list|what)/iu.test(normalized);
|
||||
return hasQuestionCue && tokenCount <= 8;
|
||||
}
|
||||
function repairLikelyUtf8Mojibake(text) {
|
||||
const raw = String(text ?? "");
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const repaired = Buffer.from(raw, "latin1").toString("utf8");
|
||||
return repaired || raw;
|
||||
}
|
||||
catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
function resolveAddressIntent(userMessage) {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
if (hasCustomerRevenueRankingBridgeSignal(text)) {
|
||||
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
|
||||
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
|
||||
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:\u043d\u0430\u043c\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e)|\u043d\u0430\u043c\s+\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0430\u043c\s+\u043d\u0443\u0436\u043d\u043e\s+\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) &&
|
||||
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text);
|
||||
if (hasLooseVatPayableBridge) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "high",
|
||||
reasons: ["vat_liability_colloquial_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasExplicitReceivablesSnapshotBridge = /(?:\u043d\u0430\u043c\s+\u043a\u0442\u043e-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\w+\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043d\u0430\u043c|receivables?)/iu.test(text);
|
||||
if (hasExplicitReceivablesSnapshotBridge) {
|
||||
return {
|
||||
intent: "receivables_confirmed_as_of_date",
|
||||
confidence: "high",
|
||||
reasons: ["receivables_snapshot_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasExplicitPayablesSnapshotBridge = /(?:\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|payables?)/iu.test(text);
|
||||
if (hasExplicitPayablesSnapshotBridge) {
|
||||
return {
|
||||
intent: "payables_confirmed_as_of_date",
|
||||
confidence: "high",
|
||||
reasons: ["payables_snapshot_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasDirectInventoryAgingBridge = /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(bridgeText);
|
||||
if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) {
|
||||
return {
|
||||
intent: "inventory_aging_by_purchase_date",
|
||||
confidence: "high",
|
||||
reasons: ["inventory_aging_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasDirectRevenueAggregateBridge = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u043a\u043b\u0438\u0435\u043d\u0442|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a).*(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u0438\u043b\u0438)|\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(bridgeText);
|
||||
if (hasDirectRevenueAggregateBridge || hasCustomerRevenueRankingBridgeSignal(bridgeText)) {
|
||||
return {
|
||||
intent: "customer_revenue_and_payments",
|
||||
confidence: "medium",
|
||||
reasons: ["customer_revenue_ranking_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasHistoricalInventorySnapshotBridge = [text, repairedText, bridgeText].some((sample) => /(?:\u043e\u0441\u0442\u0430\u0442|inventory|stock|\u0441\u043a\u043b\u0430\u0434|остат|склад)/iu.test(sample) &&
|
||||
/(?:(?:\u043d\u0430|\u0437\u0430|на|за)\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|(?:\u043d\u0430|\u0437\u0430|на|за)\s+\d{4}|\b(?:19|20)\d{2}\b)/iu.test(sample));
|
||||
if (hasHistoricalInventorySnapshotBridge) {
|
||||
return {
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
confidence: "medium",
|
||||
reasons: ["inventory_historical_snapshot_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
|
||||
return {
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
|
||||
+16
-4
@@ -487,14 +487,19 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
}
|
||||
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 {
|
||||
const strongestYear = visible[0];
|
||||
const directAnswerLine = isSupplier
|
||||
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} лет по сумме выплат:`
|
||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
lines.unshift(directAnswerLine);
|
||||
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);
|
||||
@@ -556,7 +561,14 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
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)}`;
|
||||
|
||||
@@ -7,6 +7,7 @@ exports.hasInventoryPurchaseDateFollowupCue = hasInventoryPurchaseDateFollowupCu
|
||||
exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFollowupCue;
|
||||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||
@@ -537,6 +538,14 @@ function hasInventorySaleFollowupCue(text) {
|
||||
function hasInventoryPurchaseToSaleChainFollowupCue(text) {
|
||||
return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||
}
|
||||
function hasAddressFollowupContextSignal(text) {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
@@ -563,6 +572,9 @@ function hasAddressFollowupContextSignal(text) {
|
||||
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (tokenCount <= 14 && hasInventoryPurchaseDateVatBridgeCue(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
||||
if (tokenCount <= 8 && hasPeriodLiteral) {
|
||||
return true;
|
||||
@@ -1056,6 +1068,17 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const inventorySelectedObjectFollowup = hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
if (inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date")) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_purchase_date_vat_bridge"]
|
||||
};
|
||||
}
|
||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
|
||||
+2
-2
@@ -209,8 +209,8 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine = agingItems.length > 0
|
||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
||||
const lines = [directAnswerLine];
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
|
||||
@@ -683,6 +683,7 @@ function createAssistantRoutePolicy(deps) {
|
||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
!protectAddressLaneFromFallback &&
|
||||
hasDeepSessionContinuationSignal({
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
|
||||
+29
-5
@@ -2642,6 +2642,9 @@ function hasAddressFollowupContextSignal(userMessage) {
|
||||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
||||
return true;
|
||||
}
|
||||
if (shortFollowup && samples.some((sample) => (0, decomposeStage_1.hasInventoryPurchaseDateVatBridgeCue)(sample))) {
|
||||
return true;
|
||||
}
|
||||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
||||
return true;
|
||||
}
|
||||
@@ -3458,6 +3461,31 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
}, userMessage);
|
||||
}
|
||||
const candidatePredecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||
sourceMessage: String(userMessage ?? ""),
|
||||
canonicalMessage: candidate,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
});
|
||||
const sourceHasExplicitAccountAnchor = (0, addressIntentResolver_1.hasAccountNumberAnchor)(repairedSourceMessage || userMessage) ||
|
||||
(0, addressIntentResolver_1.hasCompactAccountCodeToken)(repairedSourceMessage || userMessage);
|
||||
const candidateInjectsAccountAnchor = Boolean(toNonEmptyString(candidatePredecomposeContract?.entities?.account));
|
||||
if (sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_injection",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
}, userMessage);
|
||||
}
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
@@ -3554,11 +3582,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||||
sourceMessage: String(userMessage ?? ""),
|
||||
canonicalMessage: candidate,
|
||||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||
sourceMessage: String(userMessage ?? ""),
|
||||
canonicalMessage: candidate,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
})
|
||||
predecomposeContract: candidatePredecomposeContract
|
||||
});
|
||||
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
||||
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
||||
|
||||
@@ -3,6 +3,101 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAssistantTransitionPolicy = createAssistantTransitionPolicy;
|
||||
function createAssistantTransitionPolicy(deps) {
|
||||
function parseDmyDateToIso(value) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||
}
|
||||
function computeMonthWindowFromIso(isoDate) {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||
return null;
|
||||
}
|
||||
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
const mm = String(month).padStart(2, "0");
|
||||
const dd = String(lastDay).padStart(2, "0");
|
||||
return {
|
||||
purchase_date: isoDate,
|
||||
period_from: `${year}-${mm}-01`,
|
||||
period_to: `${year}-${mm}-${dd}`,
|
||||
as_of_date: `${year}-${mm}-${dd}`
|
||||
};
|
||||
}
|
||||
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
||||
const dates = [];
|
||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||
const value = deps.toNonEmptyString(entityRef?.value);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const matches = String(value).match(/\b(\d{2}\.\d{2}\.\d{4})\b/g);
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
for (const token of matches) {
|
||||
const isoDate = parseDmyDateToIso(token);
|
||||
if (isoDate) {
|
||||
dates.push(isoDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return dates.sort()[0] ?? null;
|
||||
}
|
||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||
if (replyText) {
|
||||
const repairedText = deps.repairAddressMojibake(replyText);
|
||||
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||
if (explicitFirstDateIso) {
|
||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
||||
}
|
||||
}
|
||||
const sessionContext = addressNavigationState &&
|
||||
typeof addressNavigationState === "object" &&
|
||||
addressNavigationState.session_context &&
|
||||
typeof addressNavigationState.session_context === "object"
|
||||
? addressNavigationState.session_context
|
||||
: null;
|
||||
const focusObject = sessionContext && typeof sessionContext.active_focus_object === "object"
|
||||
? sessionContext.active_focus_object
|
||||
: null;
|
||||
const preferredResultSetId = deps.toNonEmptyString(focusObject?.provenance_result_set_id) ??
|
||||
deps.toNonEmptyString(sessionContext?.active_result_set_id);
|
||||
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
||||
const preferredResultSet = (preferredResultSetId
|
||||
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
||||
: null) ??
|
||||
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||
null;
|
||||
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
||||
}
|
||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
||||
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||
!hasInventoryItemFocusHint &&
|
||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)) {
|
||||
return false;
|
||||
}
|
||||
const samples = [userMessage, alternateMessage]
|
||||
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()))
|
||||
.filter((item) => item.length > 0);
|
||||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return samples.some((sample) => /(?:ндс|vat)/iu.test(sample) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(sample));
|
||||
}
|
||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||
if (!hasInventoryRootFrame) {
|
||||
return false;
|
||||
@@ -159,6 +254,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
@@ -172,11 +268,15 @@ function createAssistantTransitionPolicy(deps) {
|
||||
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
inventoryShortFollowupAlternate
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
@@ -206,6 +306,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
hasInventoryRootRestatementPrimary ||
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
@@ -340,11 +441,13 @@ function createAssistantTransitionPolicy(deps) {
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
@@ -356,6 +459,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
@@ -435,6 +539,13 @@ function createAssistantTransitionPolicy(deps) {
|
||||
if (!deps.toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||
previousFilters.organization = organizationClarificationSelection;
|
||||
}
|
||||
if (inventoryPurchaseDateVatBridge) {
|
||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState);
|
||||
if (purchaseBridgeWindow) {
|
||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||
}
|
||||
}
|
||||
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
@@ -461,7 +572,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||
previousFilters.period_to = deps.toNonEmptyString(navigationDateScope?.period_to);
|
||||
}
|
||||
const rootContextOnlyPivot = Boolean((deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage) &&
|
||||
!inventoryPurchaseDateVatBridge);
|
||||
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) ||
|
||||
deps.isInventoryRootFrameIntent(sourceIntentHint) ||
|
||||
@@ -567,11 +679,13 @@ function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate));
|
||||
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
|
||||
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
||||
: explicitInventorySameDatePivot
|
||||
? "inventory_on_hand_as_of_date"
|
||||
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
|
||||
const carryoverTargetIntent = inventoryPurchaseDateVatBridge
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
: followupSelectionMode === "carry_root_context"
|
||||
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
||||
: explicitInventorySameDatePivot
|
||||
? "inventory_on_hand_as_of_date"
|
||||
: displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined;
|
||||
return {
|
||||
followupContext: {
|
||||
previous_intent: previousIntent ?? undefined,
|
||||
|
||||
@@ -1848,7 +1848,13 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intent === "vat_liability_confirmed_for_tax_period" && !periodRange.period_from && !periodRange.period_to) {
|
||||
const monthPeriodWasDerived = warnings.includes("period_derived_from_month_phrase");
|
||||
if (
|
||||
intent === "vat_liability_confirmed_for_tax_period" &&
|
||||
!periodRange.period_from &&
|
||||
!periodRange.period_to &&
|
||||
!monthPeriodWasDerived
|
||||
) {
|
||||
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||
if (periodToForQuarter) {
|
||||
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
||||
|
||||
@@ -534,7 +534,7 @@ function hasFuzzyLexeme(text: string, lexemeRoots: string[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasCompactAccountCodeToken(text: string): boolean {
|
||||
export function hasCompactAccountCodeToken(text: string): boolean {
|
||||
// Match compact account tokens while reducing false positives on short-year literals like "22 РіРѕРґ".
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
@@ -1599,7 +1599,7 @@ function hasGenericAddressLookupSignal(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasAccountNumberAnchor(text: string): boolean {
|
||||
export function hasAccountNumberAnchor(text: string): boolean {
|
||||
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
|
||||
}
|
||||
|
||||
@@ -1790,6 +1790,20 @@ function hasCustomerRevenueRankingBridgeSignal(text: string): boolean {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasSupplierCue =
|
||||
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|supplier|vendor)/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (hasSupplierCue) {
|
||||
return false;
|
||||
}
|
||||
const hasDirectRankingCue =
|
||||
/(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0430\s+)?(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438(?:\s+\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f)?|(?:\u0430\s+)?(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438\s+\u0437\u0430\s+\d{4}|(?:\u043e\u0431\u0449\u0430\u044f\s+)?\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (hasDirectRankingCue) {
|
||||
return true;
|
||||
}
|
||||
const hasMoneyCue =
|
||||
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(
|
||||
normalized
|
||||
@@ -1797,9 +1811,15 @@ function hasCustomerRevenueRankingBridgeSignal(text: string): boolean {
|
||||
if (!hasMoneyCue) {
|
||||
return false;
|
||||
}
|
||||
return /(?:\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(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasCustomerRankingCue =
|
||||
/(?:\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(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money|(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)))/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasRevenueAggregateCue =
|
||||
/(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u043a\w*\s+\u0437\u0430\s+\d{4})/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasCustomerRankingCue || hasRevenueAggregateCue;
|
||||
}
|
||||
|
||||
function hasInventoryProvenanceBridgeSignal(text: string): boolean {
|
||||
@@ -1845,6 +1865,13 @@ function hasColloquialInventoryOnHandBridgeSignal(text: string): boolean {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasInventoryAgingCue =
|
||||
/(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440\u044b\u0439\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (hasInventoryAgingCue) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
|
||||
const hasWarehouseCue =
|
||||
/(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(
|
||||
@@ -1860,10 +1887,81 @@ function hasColloquialInventoryOnHandBridgeSignal(text: string): boolean {
|
||||
return hasQuestionCue && tokenCount <= 8;
|
||||
}
|
||||
|
||||
function repairLikelyUtf8Mojibake(text: string): string {
|
||||
const raw = String(text ?? "");
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const repaired = Buffer.from(raw, "latin1").toString("utf8");
|
||||
return repaired || raw;
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
|
||||
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
|
||||
|
||||
if (hasCustomerRevenueRankingBridgeSignal(text)) {
|
||||
const hasLooseVatPayableBridge =
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:\u043d\u0430\u043c\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e)|\u043d\u0430\u043c\s+\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0430\u043c\s+\u043d\u0443\u0436\u043d\u043e\s+\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasLooseVatPayableBridge) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "high",
|
||||
reasons: ["vat_liability_colloquial_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
const hasExplicitReceivablesSnapshotBridge =
|
||||
/(?:\u043d\u0430\u043c\s+\u043a\u0442\u043e-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\w+\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043d\u0430\u043c|receivables?)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasExplicitReceivablesSnapshotBridge) {
|
||||
return {
|
||||
intent: "receivables_confirmed_as_of_date",
|
||||
confidence: "high",
|
||||
reasons: ["receivables_snapshot_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
const hasExplicitPayablesSnapshotBridge =
|
||||
/(?:\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|payables?)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasExplicitPayablesSnapshotBridge) {
|
||||
return {
|
||||
intent: "payables_confirmed_as_of_date",
|
||||
confidence: "high",
|
||||
reasons: ["payables_snapshot_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
const hasDirectInventoryAgingBridge =
|
||||
/(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(
|
||||
bridgeText
|
||||
);
|
||||
if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) {
|
||||
return {
|
||||
intent: "inventory_aging_by_purchase_date",
|
||||
confidence: "high",
|
||||
reasons: ["inventory_aging_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
const hasDirectRevenueAggregateBridge =
|
||||
/(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u043a\u043b\u0438\u0435\u043d\u0442|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a).*(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u0438\u043b\u0438)|\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(
|
||||
bridgeText
|
||||
);
|
||||
if (hasDirectRevenueAggregateBridge || hasCustomerRevenueRankingBridgeSignal(bridgeText)) {
|
||||
return {
|
||||
intent: "customer_revenue_and_payments",
|
||||
confidence: "medium",
|
||||
@@ -1871,6 +1969,19 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
};
|
||||
}
|
||||
|
||||
const hasHistoricalInventorySnapshotBridge = [text, repairedText, bridgeText].some(
|
||||
(sample) =>
|
||||
/(?:\u043e\u0441\u0442\u0430\u0442|inventory|stock|\u0441\u043a\u043b\u0430\u0434|остат|склад)/iu.test(sample) &&
|
||||
/(?:(?:\u043d\u0430|\u0437\u0430|на|за)\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|(?:\u043d\u0430|\u0437\u0430|на|за)\s+\d{4}|\b(?:19|20)\d{2}\b)/iu.test(sample)
|
||||
);
|
||||
if (hasHistoricalInventorySnapshotBridge) {
|
||||
return {
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
confidence: "medium",
|
||||
reasons: ["inventory_historical_snapshot_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
|
||||
return {
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
|
||||
+16
-4
@@ -639,13 +639,18 @@ export function composeCounterpartyAnalyticsReply(
|
||||
|
||||
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 {
|
||||
const strongestYear = visible[0];
|
||||
const directAnswerLine = isSupplier
|
||||
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} лет по сумме выплат:`
|
||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
lines.unshift(directAnswerLine);
|
||||
lines.push(
|
||||
...visible.map(
|
||||
(item, index) =>
|
||||
@@ -742,7 +747,14 @@ export function composeCounterpartyAnalyticsReply(
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
lines.push(
|
||||
...visible.map((item, index) => {
|
||||
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
||||
|
||||
@@ -691,6 +691,19 @@ export function hasInventoryPurchaseToSaleChainFollowupCue(text: string): boolea
|
||||
);
|
||||
}
|
||||
|
||||
export function hasInventoryPurchaseDateVatBridgeCue(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/(?:ндс|vat)/iu.test(normalized) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
normalized
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
@@ -728,6 +741,9 @@ export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (tokenCount <= 14 && hasInventoryPurchaseDateVatBridgeCue(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
||||
if (tokenCount <= 8 && hasPeriodLiteral) {
|
||||
return true;
|
||||
@@ -1314,6 +1330,21 @@ function deriveIntentWithFollowupContext(
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const inventorySelectedObjectFollowup =
|
||||
hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal);
|
||||
const inventoryPurchaseDateVatBridge =
|
||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
|
||||
if (
|
||||
inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date")
|
||||
) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_purchase_date_vat_bridge"]
|
||||
};
|
||||
}
|
||||
|
||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||
const vatIntent: AddressIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||
|
||||
@@ -316,8 +316,8 @@ export function composeInventoryReply(
|
||||
.join("; ");
|
||||
const directAnswerLine =
|
||||
agingItems.length > 0
|
||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
||||
const lines: string[] = [directAnswerLine];
|
||||
appendInventoryBulletSection(lines, "Сводка:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
|
||||
@@ -719,6 +719,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
!protectAddressLaneFromFallback &&
|
||||
hasDeepSessionContinuationSignal({
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
|
||||
@@ -2597,6 +2597,9 @@ function hasAddressFollowupContextSignal(userMessage) {
|
||||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
||||
return true;
|
||||
}
|
||||
if (shortFollowup && samples.some((sample) => (0, decomposeStage_1.hasInventoryPurchaseDateVatBridgeCue)(sample))) {
|
||||
return true;
|
||||
}
|
||||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
||||
return true;
|
||||
}
|
||||
@@ -3413,6 +3416,31 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
}, userMessage);
|
||||
}
|
||||
const candidatePredecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||
sourceMessage: String(userMessage ?? ""),
|
||||
canonicalMessage: candidate,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
});
|
||||
const sourceHasExplicitAccountAnchor = (0, addressIntentResolver_1.hasAccountNumberAnchor)(repairedSourceMessage || userMessage) ||
|
||||
(0, addressIntentResolver_1.hasCompactAccountCodeToken)(repairedSourceMessage || userMessage);
|
||||
const candidateInjectsAccountAnchor = Boolean(toNonEmptyString(candidatePredecomposeContract?.entities?.account));
|
||||
if (sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_injection",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
}, userMessage);
|
||||
}
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
@@ -3509,11 +3537,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||||
sourceMessage: String(userMessage ?? ""),
|
||||
canonicalMessage: candidate,
|
||||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||
sourceMessage: String(userMessage ?? ""),
|
||||
canonicalMessage: candidate,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
})
|
||||
predecomposeContract: candidatePredecomposeContract
|
||||
});
|
||||
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
||||
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
||||
|
||||
@@ -1,6 +1,118 @@
|
||||
// @ts-nocheck
|
||||
|
||||
export function createAssistantTransitionPolicy(deps) {
|
||||
function parseDmyDateToIso(value) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||
}
|
||||
|
||||
function computeMonthWindowFromIso(isoDate) {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||
return null;
|
||||
}
|
||||
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
const mm = String(month).padStart(2, "0");
|
||||
const dd = String(lastDay).padStart(2, "0");
|
||||
return {
|
||||
purchase_date: isoDate,
|
||||
period_from: `${year}-${mm}-01`,
|
||||
period_to: `${year}-${mm}-${dd}`,
|
||||
as_of_date: `${year}-${mm}-${dd}`
|
||||
};
|
||||
}
|
||||
|
||||
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
||||
const dates = [];
|
||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||
const value = deps.toNonEmptyString(entityRef?.value);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const matches = String(value).match(/\b(\d{2}\.\d{2}\.\d{4})\b/g);
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
for (const token of matches) {
|
||||
const isoDate = parseDmyDateToIso(token);
|
||||
if (isoDate) {
|
||||
dates.push(isoDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return dates.sort()[0] ?? null;
|
||||
}
|
||||
|
||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||
if (replyText) {
|
||||
const repairedText = deps.repairAddressMojibake(replyText);
|
||||
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||
if (explicitFirstDateIso) {
|
||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionContext =
|
||||
addressNavigationState &&
|
||||
typeof addressNavigationState === "object" &&
|
||||
addressNavigationState.session_context &&
|
||||
typeof addressNavigationState.session_context === "object"
|
||||
? addressNavigationState.session_context
|
||||
: null;
|
||||
const focusObject =
|
||||
sessionContext && typeof sessionContext.active_focus_object === "object"
|
||||
? sessionContext.active_focus_object
|
||||
: null;
|
||||
const preferredResultSetId =
|
||||
deps.toNonEmptyString(focusObject?.provenance_result_set_id) ??
|
||||
deps.toNonEmptyString(sessionContext?.active_result_set_id);
|
||||
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
||||
const preferredResultSet =
|
||||
(preferredResultSetId
|
||||
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
||||
: null) ??
|
||||
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||
null;
|
||||
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
||||
if (
|
||||
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||
!hasInventoryItemFocusHint &&
|
||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const samples = [userMessage, alternateMessage]
|
||||
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()))
|
||||
.filter((item) => item.length > 0);
|
||||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return samples.some(
|
||||
(sample) =>
|
||||
/(?:ндс|vat)/iu.test(sample) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
sample
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||
if (!hasInventoryRootFrame) {
|
||||
return false;
|
||||
@@ -197,6 +309,12 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
deps.isInventorySelectedObjectIntent(sourceIntentHint))
|
||||
);
|
||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(
|
||||
userMessage,
|
||||
alternateMessage,
|
||||
sourceIntentHint,
|
||||
hasNavigationInventoryItemFocusHint
|
||||
);
|
||||
let inventoryShortFollowupPrimary =
|
||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
@@ -214,11 +332,15 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
let hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
inventoryShortFollowupAlternate
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
@@ -265,6 +387,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
hasInventoryRootRestatementPrimary ||
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
@@ -418,11 +541,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
@@ -434,6 +559,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
@@ -522,6 +648,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
if (!deps.toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||
previousFilters.organization = organizationClarificationSelection;
|
||||
}
|
||||
if (inventoryPurchaseDateVatBridge) {
|
||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState);
|
||||
if (purchaseBridgeWindow) {
|
||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||
}
|
||||
}
|
||||
const shouldBackfillPreviousDateScopeFromNavigation =
|
||||
sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
@@ -556,7 +689,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
}
|
||||
const rootContextOnlyPivot = Boolean(
|
||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage)
|
||||
deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage) &&
|
||||
!inventoryPurchaseDateVatBridge
|
||||
);
|
||||
const inventoryRootTemporalPivot = Boolean(
|
||||
inventoryRootFrame &&
|
||||
@@ -681,7 +815,9 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasSelectedObjectInventorySignalAlternate)
|
||||
);
|
||||
const carryoverTargetIntent =
|
||||
followupSelectionMode === "carry_root_context"
|
||||
inventoryPurchaseDateVatBridge
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
: followupSelectionMode === "carry_root_context"
|
||||
? inventoryRootFrame?.intent ?? displayedEntityTargetIntent ?? explicitIntent ?? previousIntent ?? undefined
|
||||
: explicitInventorySameDatePivot
|
||||
? "inventory_on_hand_as_of_date"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
||||
|
||||
describe("address filter extractor regressions", () => {
|
||||
it("keeps explicit month window for confirmed VAT tax-period intent", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
||||
"vat_liability_confirmed_for_tax_period"
|
||||
);
|
||||
|
||||
expect(extracted.extracted_filters.period_from).toBe("2019-12-01");
|
||||
expect(extracted.extracted_filters.period_to).toBe("2019-12-31");
|
||||
expect(extracted.warnings).toContain("period_derived_from_month_phrase");
|
||||
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
});
|
||||
});
|
||||
@@ -55,7 +55,7 @@ describe("address follow-up temporal regressions", () => {
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-05-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-05-31");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31");
|
||||
@@ -76,7 +76,7 @@ describe("address follow-up temporal regressions", () => {
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-05-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-05-31");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31");
|
||||
@@ -116,4 +116,26 @@ describe("address follow-up temporal regressions", () => {
|
||||
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
|
||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
||||
});
|
||||
|
||||
it("retargets inventory purchase-date VAT bridge into confirmed VAT period with inherited purchase month", () => {
|
||||
const result = runAddressDecomposeStage("ндс можешь прикинуть на дату покупки рабочей станции?", {
|
||||
previous_intent: "inventory_purchase_provenance_for_item",
|
||||
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||
previous_filters: {
|
||||
item: "Рабочая станция универсального специалиста",
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
period_from: "2015-02-01",
|
||||
period_to: "2015-02-28",
|
||||
as_of_date: "2016-03-31"
|
||||
},
|
||||
previous_anchor_type: "item",
|
||||
previous_anchor_value: "Рабочая станция универсального специалиста"
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
||||
expect(result?.baseReasons).toContain("period_from_followup_context");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||
|
||||
describe("addressIntentResolver regression bridges", () => {
|
||||
it("detects colloquial VAT liability for a month period", () => {
|
||||
const result = resolveAddressIntent("прикинь какой ндс нам надо заплатить на февраль 2017");
|
||||
|
||||
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
});
|
||||
|
||||
it("detects payables snapshot wording in plain human form", () => {
|
||||
const result = resolveAddressIntent("мы должны комуто денег на сегодня?");
|
||||
|
||||
expect(result.intent).toBe("payables_confirmed_as_of_date");
|
||||
});
|
||||
|
||||
it("detects top customer all-time revenue wording", () => {
|
||||
const result = resolveAddressIntent("кто у нас самый доходный клиент за все время");
|
||||
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("detects top-year company revenue wording", () => {
|
||||
const result = resolveAddressIntent("какой у нас самый доходный год");
|
||||
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("does not collapse very old stock request into generic inventory snapshot", () => {
|
||||
const result = resolveAddressIntent("Есть ли остатки товара, которые закупались очень давно");
|
||||
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
it("detects bare historical inventory root with explicit month-year", () => {
|
||||
const result = resolveAddressIntent("остатки на март 2016");
|
||||
|
||||
expect(result.intent).toBe("inventory_on_hand_as_of_date");
|
||||
});
|
||||
});
|
||||
@@ -2747,14 +2747,15 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
||||
});
|
||||
|
||||
it("derives full tax quarter window for confirmed VAT tax-period intent from month phrase", () => {
|
||||
it("keeps explicit month window for confirmed VAT tax-period intent from month phrase", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
||||
"vat_liability_confirmed_for_tax_period"
|
||||
);
|
||||
expect(extracted.extracted_filters.period_from).toBe("2019-10-01");
|
||||
expect(extracted.extracted_filters.period_from).toBe("2019-12-01");
|
||||
expect(extracted.extracted_filters.period_to).toBe("2019-12-31");
|
||||
expect(extracted.warnings).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
expect(extracted.warnings).toContain("period_derived_from_month_phrase");
|
||||
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
});
|
||||
|
||||
it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { composeCounterpartyAnalyticsReply } from "../src/services/address_runtime/counterpartyAnalyticsReplyBuilders";
|
||||
import { composeInventoryReply } from "../src/services/address_runtime/inventoryReplyBuilders";
|
||||
|
||||
describe("address reply builders regressions", () => {
|
||||
it("starts top customer aggregate reply with a direct business answer", () => {
|
||||
const result = composeCounterpartyAnalyticsReply(
|
||||
"customer_revenue_and_payments",
|
||||
[
|
||||
{
|
||||
amount: 250000,
|
||||
period: "2020-03-31",
|
||||
registrator: "Поступление 1"
|
||||
} as any
|
||||
],
|
||||
{
|
||||
userMessage: "кто у нас самый доходный клиент за все время?"
|
||||
},
|
||||
{
|
||||
formatPercent: () => null,
|
||||
formatDateRu: (value: string) => value,
|
||||
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
|
||||
detectCounterpartyProfileFocus: () => "full_profile",
|
||||
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
|
||||
hasCounterpartyLifecycleLongevityQuestion: () => false,
|
||||
hasCounterpartyActivityAgeQuestion: () => false,
|
||||
detectRankingLimit: () => 5,
|
||||
detectValueRankingFocus: () => "top_by_total",
|
||||
detectContractValueFocus: () => "top_by_turnover",
|
||||
detectMinOpsForAvgCheck: () => 1,
|
||||
extractRequestedYearFromQuestion: () => null,
|
||||
extractCounterpartyName: () => "Чапурнов",
|
||||
extractContractName: () => null,
|
||||
counterpartyLookupMatches: () => false,
|
||||
toUtcDayTimestamp: () => null,
|
||||
formatAgeYearsMonthsDays: () => "0 дней",
|
||||
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
|
||||
}
|
||||
);
|
||||
|
||||
expect(result?.text.split("\n")[0]).toContain("Самый доходный клиент");
|
||||
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
|
||||
});
|
||||
|
||||
it("starts top year aggregate reply with a direct business answer", () => {
|
||||
const result = composeCounterpartyAnalyticsReply(
|
||||
"customer_revenue_and_payments",
|
||||
[
|
||||
{
|
||||
amount: 320000,
|
||||
period: "2021-05-12",
|
||||
registrator: "Поступление 2"
|
||||
} as any
|
||||
],
|
||||
{
|
||||
userMessage: "какой у нас самый доходный год"
|
||||
},
|
||||
{
|
||||
formatPercent: () => null,
|
||||
formatDateRu: (value: string) => value,
|
||||
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
|
||||
detectCounterpartyProfileFocus: () => "full_profile",
|
||||
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
|
||||
hasCounterpartyLifecycleLongevityQuestion: () => false,
|
||||
hasCounterpartyActivityAgeQuestion: () => false,
|
||||
detectRankingLimit: () => 5,
|
||||
detectValueRankingFocus: () => "top_years_by_total",
|
||||
detectContractValueFocus: () => "top_by_turnover",
|
||||
detectMinOpsForAvgCheck: () => 1,
|
||||
extractRequestedYearFromQuestion: () => null,
|
||||
extractCounterpartyName: () => "Чапурнов",
|
||||
extractContractName: () => null,
|
||||
counterpartyLookupMatches: () => false,
|
||||
toUtcDayTimestamp: () => null,
|
||||
formatAgeYearsMonthsDays: () => "0 дней",
|
||||
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
|
||||
}
|
||||
);
|
||||
|
||||
expect(result?.text.split("\n")[0]).toContain("Самый доходный год");
|
||||
expect(result?.text.split("\n")[0]).toContain("2021");
|
||||
});
|
||||
|
||||
it("keeps very old stock answer free of explicit as-of date in the first line", () => {
|
||||
const result = composeInventoryReply(
|
||||
"inventory_aging_by_purchase_date",
|
||||
[
|
||||
{
|
||||
amount: 1000,
|
||||
period: "2015-02-05",
|
||||
registrator: "Поступление 3"
|
||||
} as any
|
||||
],
|
||||
{
|
||||
userMessage: "Есть ли остатки товара, которые закупались очень давно",
|
||||
asOfDate: "2026-04-18"
|
||||
},
|
||||
{
|
||||
resolvePayablesAsOfDate: () => "2026-04-18",
|
||||
buildInventoryOnHandAggregate: () => [],
|
||||
uniqueStrings: (values: string[]) => values,
|
||||
formatDateRu: (value: string) => value,
|
||||
formatNumberWithDots: (value: number) => String(value),
|
||||
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||
isInventoryPurchaseMovement: () => true,
|
||||
summarizeInventoryTraceRows: () => ({
|
||||
item: "Рабочая станция",
|
||||
warehouses: ["Основной склад"],
|
||||
organizations: ["ООО Альтернатива Плюс"],
|
||||
counterparties: ["Чапурнов"],
|
||||
documents: ["Поступление 3"],
|
||||
firstPeriod: "2015-02-05",
|
||||
lastPeriod: "2015-02-05",
|
||||
totalAmount: 1000
|
||||
}),
|
||||
formatInventoryTraceRows: () => [],
|
||||
hasInventoryPurchaseDateActionFocus: () => false,
|
||||
inventoryTraceDateLabel: (value: string | null) => value ?? "дата не указана",
|
||||
extractInventoryCounterpartyCandidates: () => ["Чапурнов"],
|
||||
buildInventoryAgingByItemAggregate: () => [
|
||||
{
|
||||
item: "Рабочая станция",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
firstPurchasePeriod: "2015-02-05",
|
||||
lastPurchasePeriod: "2015-02-05",
|
||||
operations: 1,
|
||||
documentCount: 1,
|
||||
counterparties: ["Чапурнов"],
|
||||
ageDays: 4089
|
||||
}
|
||||
],
|
||||
formatInventoryAgingRows: () => ["1. Рабочая станция | первая закупка: 2015-02-05"],
|
||||
isInventorySaleMovement: () => false
|
||||
}
|
||||
);
|
||||
|
||||
const firstLine = result?.text.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("К самым старым закупкам");
|
||||
expect(firstLine).not.toContain("2026-04-18");
|
||||
});
|
||||
});
|
||||
@@ -157,6 +157,53 @@ describe("assistantRoutePolicy", () => {
|
||||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||||
});
|
||||
|
||||
it("does not let deep session continuation override an exact VAT period route", () => {
|
||||
const policy = buildPolicy({
|
||||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||||
resolveAddressIntent: () => ({ intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }),
|
||||
resolveAddressToolGateDecision: () => ({
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: "address_mode_classifier_detected"
|
||||
}),
|
||||
hasDeepSessionContinuationSignal: () => true
|
||||
});
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а какой НДС мы должны сгрузить на март 2020?",
|
||||
effectiveAddressUserMessage: "а какой НДС мы должны сгрузить на март 2020?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
mode_confidence: "high",
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
intent_confidence: "high"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: true,
|
||||
apply_canonical_recommended: true,
|
||||
extraction: {
|
||||
query_shape: "UNKNOWN",
|
||||
aggregation_profile: "unknown"
|
||||
},
|
||||
guard_hints: {
|
||||
deep_investigation_signal_detected: false
|
||||
}
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.orchestrationContract?.deep_session_continuation_fallback_to_deep).toBe(false);
|
||||
});
|
||||
|
||||
it("routes memory recap follow-up over grounded answer to chat", () => {
|
||||
const policy = buildPolicy({
|
||||
resolveRouteMemorySignals: () => ({
|
||||
|
||||
@@ -254,6 +254,133 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||||
});
|
||||
|
||||
it("bridges selected-item purchase provenance into a VAT period follow-up", () => {
|
||||
const item = "Рабочая станция универсального специалиста";
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: [
|
||||
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||||
"Подтверждение:",
|
||||
"- Первая найденная дата закупки: 05.02.2015.",
|
||||
"- Последняя найденная дата закупки: 22.07.2015."
|
||||
].join("\n"),
|
||||
debug: {
|
||||
detected_intent: "inventory_purchase_provenance_for_item",
|
||||
extracted_filters: {
|
||||
item,
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2016-03-31"
|
||||
},
|
||||
anchor_type: "item",
|
||||
anchor_value_resolved: item
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => false,
|
||||
findRecentInventoryRootFrame: () => null,
|
||||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
{
|
||||
session_context: {
|
||||
active_focus_object: {
|
||||
object_type: "item",
|
||||
label: item,
|
||||
provenance_result_set_id: "rs-provenance"
|
||||
},
|
||||
active_result_set_id: "rs-provenance"
|
||||
},
|
||||
result_sets: [
|
||||
{
|
||||
result_set_id: "rs-provenance",
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
entity_refs: [
|
||||
{
|
||||
index: 1,
|
||||
entity_type: "item",
|
||||
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
item,
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
period_from: "2015-02-01",
|
||||
period_to: "2015-02-28"
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps selected-object continuity for purchase-date VAT bridge even when an inventory root frame exists", () => {
|
||||
const item = "Рабочая станция универсального специалиста";
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: [
|
||||
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||||
"Подтверждение:",
|
||||
"- Первая найденная дата закупки: 05.02.2015."
|
||||
].join("\n"),
|
||||
debug: {
|
||||
detected_intent: "inventory_purchase_provenance_for_item",
|
||||
extracted_filters: {
|
||||
item,
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2016-03-31"
|
||||
},
|
||||
anchor_type: "item",
|
||||
anchor_value_resolved: item
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => false,
|
||||
hasForeignAccountingPivotOverInventoryMessage: () => true,
|
||||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
{
|
||||
session_context: {
|
||||
active_focus_object: {
|
||||
object_type: "item",
|
||||
label: item,
|
||||
provenance_result_set_id: "rs-provenance"
|
||||
},
|
||||
active_result_set_id: "rs-provenance"
|
||||
},
|
||||
result_sets: [
|
||||
{
|
||||
result_set_id: "rs-provenance",
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
entity_refs: [
|
||||
{
|
||||
index: 1,
|
||||
entity_type: "item",
|
||||
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
});
|
||||
|
||||
it("drops stale carryover for a fresh standalone topic from another intent family", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
|
||||
Reference in New Issue
Block a user