Укрепить exact value-flow аналитику адресного контура

This commit is contained in:
2026-05-01 22:37:57 +03:00
parent 472d982486
commit 924f6fb0ea
11 changed files with 234 additions and 20 deletions
@@ -11,6 +11,7 @@ const iconv_lite_1 = __importDefault(require("iconv-lite"));
const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i;
const ACCOUNT_REVERSE_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{2}(?:[.,]\d{1,2})?)(?=\s*(?:сч[её]т|счет|account|acct))/iu;
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№#]*?(\d{1,3})/iu;
const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000;
const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu;
const CONTRACT_PATTERN = /(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
@@ -1465,6 +1466,11 @@ function buildSemanticFrame(text, filters, warnings) {
selected_object_scope_detected: selectedObjectScopeDetected
};
}
function shouldExpandSampleForValueAnalytics(intent) {
return (intent === "customer_revenue_and_payments" ||
intent === "supplier_payouts_profile" ||
intent === "contract_usage_and_value");
}
function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim();
const text = normalizeMojibakeString(rawText);
@@ -1510,6 +1516,11 @@ function extractAddressFilters(userMessage, intent) {
filters.limit = Math.min(200, Math.trunc(parsed));
}
}
if (shouldExpandSampleForValueAnalytics(intent)) {
const currentLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit) ? Math.max(1, Math.trunc(filters.limit)) : 0;
filters.limit = Math.max(currentLimit, VALUE_ANALYTICS_SAMPLE_LIMIT);
warnings.push("value_analytics_sample_limit_expanded");
}
if (isInventoryItemAnchoredIntent(intent)) {
const itemAnchor = extractInventoryItemAnchor(text);
if (itemAnchor) {
@@ -1891,7 +1891,19 @@ function resolveAddressIntent(userMessage) {
const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
if (unicodeAddressIntent) {
return unicodeAddressIntent;
const reasons = [...unicodeAddressIntent.reasons];
if (currentTurnBridgeText !== bridgeText && !reasons.includes("current_turn_noise_normalized")) {
reasons.push("current_turn_noise_normalized");
}
if (unicodeAddressIntent.intent === "customer_revenue_and_payments" &&
[text, repairedText, turnNoiseNormalizedBridgeText, currentTurnBridgeText].some((sample) => hasSpecificCounterpartyRevenueBridgeSignal(sample)) &&
!reasons.includes("specific_counterparty_revenue_bridge_signal_detected")) {
reasons.push("specific_counterparty_revenue_bridge_signal_detected");
}
return {
...unicodeAddressIntent,
reasons
};
}
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\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|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) &&
@@ -593,15 +593,24 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
return (0, replyContracts_1.buildFactualListReply)(lines);
}
const visible = rankedByTotal.slice(0, limit);
const heading = isSupplier
? `Топ-${visible.length} поставщиков по сумме выплат:`
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
const singleCandidateOnly = rankedByTotal.length === 1;
const heading = singleCandidateOnly
? isSupplier
? "Найденный поставщик по сумме выплат:"
: "Найденный заказчик по сумме поступлений:"
: 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} операциям). Это денежный поток, а не чистая прибыль.`;
const directAnswerLine = singleCandidateOnly
? isSupplier
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
: 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) => {
@@ -968,6 +968,24 @@ function createAssistantRoutePolicy(deps) {
}
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
const customerValueRankingAddressSignal = [
rawUserMessage,
effectiveAddressUserMessage,
repairedRawUserMessage,
repairedEffectiveAddressUserMessage
].some((value) => {
const normalized = compactWhitespace(repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
if (!normalized) {
return false;
}
if (capabilityMetaQuery || dataScopeMetaQuery) {
return false;
}
const hasRankingCue = /(?:сам(?:ый|ая|ое|ые)|топ|рейтинг|больше\s+всего|максимальн|лидер|highest|top|best)/iu.test(normalized);
const hasValueCue = /(?:доход|выруч|оборот|денег|принес|поступлен|revenue|turnover|value|money)/iu.test(normalized);
const hasCustomerCue = /(?:клиент|покупател|контрагент|customer|counterparty|кто\s+у\s+нас|кто\s+нам|кто\s+больше)/iu.test(normalized);
return hasRankingCue && hasValueCue && hasCustomerCue;
});
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
llmContractMode === "address_query") ||
@@ -979,6 +997,7 @@ function createAssistantRoutePolicy(deps) {
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
customerValueRankingAddressSignal ||
hasAddressFollowupContextSignal(rawUserMessage) ||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
@@ -1023,7 +1042,7 @@ function createAssistantRoutePolicy(deps) {
resolvedIntentResolution.intent === "unknown" &&
(!llmContractIntent || llmContractIntent === "unknown"));
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
const protectAddressLaneFromFallback = laneProtectionArbitration.protectAddressLaneFromFallback;
const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal);
const vatExplainFollowupSignal = Boolean(followupContext &&
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));