Укрепить exact value-flow аналитику адресного контура
This commit is contained in:
parent
472d982486
commit
924f6fb0ea
|
|
@ -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_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 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 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 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 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/;
|
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
|
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) {
|
function extractAddressFilters(userMessage, intent) {
|
||||||
const rawText = String(userMessage ?? "").trim();
|
const rawText = String(userMessage ?? "").trim();
|
||||||
const text = normalizeMojibakeString(rawText);
|
const text = normalizeMojibakeString(rawText);
|
||||||
|
|
@ -1510,6 +1516,11 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
filters.limit = Math.min(200, Math.trunc(parsed));
|
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)) {
|
if (isInventoryItemAnchoredIntent(intent)) {
|
||||||
const itemAnchor = extractInventoryItemAnchor(text);
|
const itemAnchor = extractInventoryItemAnchor(text);
|
||||||
if (itemAnchor) {
|
if (itemAnchor) {
|
||||||
|
|
|
||||||
|
|
@ -1891,7 +1891,19 @@ function resolveAddressIntent(userMessage) {
|
||||||
const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
|
const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
|
||||||
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
|
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
|
||||||
if (unicodeAddressIntent) {
|
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) &&
|
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) &&
|
/(?:\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);
|
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||||
}
|
}
|
||||||
const visible = rankedByTotal.slice(0, limit);
|
const visible = rankedByTotal.slice(0, limit);
|
||||||
const heading = isSupplier
|
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
const heading = singleCandidateOnly
|
||||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
? isSupplier
|
||||||
|
? "Найденный поставщик по сумме выплат:"
|
||||||
|
: "Найденный заказчик по сумме поступлений:"
|
||||||
|
: isSupplier
|
||||||
|
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||||
|
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||||
const leadingCounterparty = visible[0] ?? null;
|
const leadingCounterparty = visible[0] ?? null;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
if (leadingCounterparty) {
|
if (leadingCounterparty) {
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = singleCandidateOnly
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${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} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
|
: `В выбранном срезе найден один клиент: ${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.unshift(directAnswerLine);
|
||||||
}
|
}
|
||||||
lines.push(...visible.map((item, index) => {
|
lines.push(...visible.map((item, index) => {
|
||||||
|
|
|
||||||
|
|
@ -968,6 +968,24 @@ function createAssistantRoutePolicy(deps) {
|
||||||
}
|
}
|
||||||
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
||||||
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
|
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 &&
|
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||||
llmPreDecomposeMeta?.applied &&
|
llmPreDecomposeMeta?.applied &&
|
||||||
llmContractMode === "address_query") ||
|
llmContractMode === "address_query") ||
|
||||||
|
|
@ -979,6 +997,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
|
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
|
||||||
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
|
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
|
||||||
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
|
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
|
||||||
|
customerValueRankingAddressSignal ||
|
||||||
hasAddressFollowupContextSignal(rawUserMessage) ||
|
hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
|
|
@ -1023,7 +1042,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
resolvedIntentResolution.intent === "unknown" &&
|
resolvedIntentResolution.intent === "unknown" &&
|
||||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||||
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
||||||
const protectAddressLaneFromFallback = laneProtectionArbitration.protectAddressLaneFromFallback;
|
const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal);
|
||||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
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}`)));
|
/(?:\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}`)));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[
|
||||||
const ACCOUNT_REVERSE_PATTERN =
|
const ACCOUNT_REVERSE_PATTERN =
|
||||||
/(?:^|[\s,.;:!?()\-])(\d{2}(?:[.,]\d{1,2})?)(?=\s*(?:сч[её]т|счет|account|acct))/iu;
|
/(?:^|[\s,.;:!?()\-])(\d{2}(?:[.,]\d{1,2})?)(?=\s*(?:сч[её]т|счет|account|acct))/iu;
|
||||||
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№#]*?(\d{1,3})/iu;
|
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№#]*?(\d{1,3})/iu;
|
||||||
|
const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000;
|
||||||
const COUNTERPARTY_PATTERN =
|
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;
|
/(?:по\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 =
|
const CONTRACT_PATTERN =
|
||||||
|
|
@ -1705,6 +1706,14 @@ function buildSemanticFrame(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldExpandSampleForValueAnalytics(intent: AddressIntent): boolean {
|
||||||
|
return (
|
||||||
|
intent === "customer_revenue_and_payments" ||
|
||||||
|
intent === "supplier_payouts_profile" ||
|
||||||
|
intent === "contract_usage_and_value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
||||||
const rawText = String(userMessage ?? "").trim();
|
const rawText = String(userMessage ?? "").trim();
|
||||||
const text = normalizeMojibakeString(rawText);
|
const text = normalizeMojibakeString(rawText);
|
||||||
|
|
@ -1753,6 +1762,12 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
filters.limit = Math.min(200, Math.trunc(parsed));
|
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)) {
|
if (isInventoryItemAnchoredIntent(intent)) {
|
||||||
const itemAnchor = extractInventoryItemAnchor(text);
|
const itemAnchor = extractInventoryItemAnchor(text);
|
||||||
|
|
|
||||||
|
|
@ -2768,7 +2768,23 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
|
|
||||||
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
|
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
|
||||||
if (unicodeAddressIntent) {
|
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 =
|
const hasLooseVatPayableBridge =
|
||||||
|
|
|
||||||
|
|
@ -787,15 +787,24 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
const visible = rankedByTotal.slice(0, limit);
|
const visible = rankedByTotal.slice(0, limit);
|
||||||
const heading = isSupplier
|
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||||
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
const heading = singleCandidateOnly
|
||||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
? isSupplier
|
||||||
|
? "Найденный поставщик по сумме выплат:"
|
||||||
|
: "Найденный заказчик по сумме поступлений:"
|
||||||
|
: isSupplier
|
||||||
|
? `Топ-${visible.length} поставщиков по сумме выплат:`
|
||||||
|
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||||
const leadingCounterparty = visible[0] ?? null;
|
const leadingCounterparty = visible[0] ?? null;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
if (leadingCounterparty) {
|
if (leadingCounterparty) {
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = singleCandidateOnly
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${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} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
|
: `В выбранном срезе найден один клиент: ${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.unshift(directAnswerLine);
|
||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|
|
||||||
|
|
@ -1053,6 +1053,24 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
}
|
}
|
||||||
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
||||||
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
|
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 &&
|
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||||
llmPreDecomposeMeta?.applied &&
|
llmPreDecomposeMeta?.applied &&
|
||||||
llmContractMode === "address_query") ||
|
llmContractMode === "address_query") ||
|
||||||
|
|
@ -1064,6 +1082,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
|
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
|
||||||
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
|
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
|
||||||
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
|
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
|
||||||
|
customerValueRankingAddressSignal ||
|
||||||
hasAddressFollowupContextSignal(rawUserMessage) ||
|
hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
|
|
@ -1108,7 +1127,9 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
resolvedIntentResolution.intent === "unknown" &&
|
resolvedIntentResolution.intent === "unknown" &&
|
||||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||||
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
||||||
const protectAddressLaneFromFallback = laneProtectionArbitration.protectAddressLaneFromFallback;
|
const protectAddressLaneFromFallback = Boolean(
|
||||||
|
laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal
|
||||||
|
);
|
||||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
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}`)));
|
/(?:\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}`)));
|
||||||
|
|
|
||||||
|
|
@ -2822,9 +2822,9 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(counterpartyProfile.extracted_filters.limit).toBeUndefined();
|
expect(counterpartyProfile.extracted_filters.limit).toBeUndefined();
|
||||||
expect(counterpartyLifecycle.extracted_filters.limit).toBeUndefined();
|
expect(counterpartyLifecycle.extracted_filters.limit).toBeUndefined();
|
||||||
expect(contractOverview.extracted_filters.limit).toBeUndefined();
|
expect(contractOverview.extracted_filters.limit).toBeUndefined();
|
||||||
expect(customerValue.extracted_filters.limit).toBe(20);
|
expect(customerValue.extracted_filters.limit).toBe(1000);
|
||||||
expect(supplierValue.extracted_filters.limit).toBe(20);
|
expect(supplierValue.extracted_filters.limit).toBe(1000);
|
||||||
expect(contractValue.extracted_filters.limit).toBe(20);
|
expect(contractValue.extracted_filters.limit).toBe(1000);
|
||||||
expect(vatForecast.extracted_filters.limit).toBeUndefined();
|
expect(vatForecast.extracted_filters.limit).toBeUndefined();
|
||||||
expect(periodProfile.extracted_filters.period_to).toBeDefined();
|
expect(periodProfile.extracted_filters.period_to).toBeDefined();
|
||||||
expect(docSectionProfile.extracted_filters.period_to).toBeDefined();
|
expect(docSectionProfile.extracted_filters.period_to).toBeDefined();
|
||||||
|
|
@ -2844,6 +2844,9 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(customerValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
|
expect(customerValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
|
||||||
expect(supplierValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
|
expect(supplierValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
|
||||||
expect(contractValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
|
expect(contractValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
|
||||||
|
expect(customerValue.warnings).toContain("value_analytics_sample_limit_expanded");
|
||||||
|
expect(supplierValue.warnings).toContain("value_analytics_sample_limit_expanded");
|
||||||
|
expect(contractValue.warnings).toContain("value_analytics_sample_limit_expanded");
|
||||||
expect(vatForecast.warnings).toContain("period_derived_from_month_phrase");
|
expect(vatForecast.warnings).toContain("period_derived_from_month_phrase");
|
||||||
expect(vatForecast.warnings).toContain("period_from_derived_from_quarter_for_vat_forecast");
|
expect(vatForecast.warnings).toContain("period_from_derived_from_quarter_for_vat_forecast");
|
||||||
expect(vatForecast.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
expect(vatForecast.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
||||||
|
|
@ -5066,6 +5069,17 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
|
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expands customer value analytics sample independently from visible ranking size", () => {
|
||||||
|
const filters = extractAddressFilters("какой у нас самый доходный год", "customer_revenue_and_payments");
|
||||||
|
const selected = selectAddressRecipe("customer_revenue_and_payments", filters.extracted_filters);
|
||||||
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters.extracted_filters);
|
||||||
|
|
||||||
|
expect(filters.extracted_filters.limit).toBe(1000);
|
||||||
|
expect(filters.warnings).toContain("value_analytics_sample_limit_expanded");
|
||||||
|
expect(plan.limit).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
it("selects supplier payouts recipe and keeps top-20 default", () => {
|
it("selects supplier payouts recipe and keeps top-20 default", () => {
|
||||||
const selected = selectAddressRecipe("supplier_payouts_profile", {});
|
const selected = selectAddressRecipe("supplier_payouts_profile", {});
|
||||||
expect(selected.selected_recipe).toBeTruthy();
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,16 @@ describe("address reply builders regressions", () => {
|
||||||
"customer_revenue_and_payments",
|
"customer_revenue_and_payments",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
counterparty: "Чапурнов",
|
||||||
amount: 250000,
|
amount: 250000,
|
||||||
period: "2020-03-31",
|
period: "2020-03-31",
|
||||||
registrator: "Поступление 1"
|
registrator: "Поступление 1"
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
counterparty: "Малый клиент",
|
||||||
|
amount: 100000,
|
||||||
|
period: "2020-04-30",
|
||||||
|
registrator: "Поступление 2"
|
||||||
} as any
|
} as any
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|
@ -31,7 +38,7 @@ describe("address reply builders regressions", () => {
|
||||||
detectContractValueFocus: () => "top_by_turnover",
|
detectContractValueFocus: () => "top_by_turnover",
|
||||||
detectMinOpsForAvgCheck: () => 1,
|
detectMinOpsForAvgCheck: () => 1,
|
||||||
extractRequestedYearFromQuestion: () => null,
|
extractRequestedYearFromQuestion: () => null,
|
||||||
extractCounterpartyName: () => "Чапурнов",
|
extractCounterpartyName: (row: any) => row.counterparty ?? "Чапурнов",
|
||||||
extractContractName: () => null,
|
extractContractName: () => null,
|
||||||
counterpartyLookupMatches: () => false,
|
counterpartyLookupMatches: () => false,
|
||||||
toUtcDayTimestamp: () => null,
|
toUtcDayTimestamp: () => null,
|
||||||
|
|
@ -44,6 +51,47 @@ describe("address reply builders regressions", () => {
|
||||||
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
|
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not overclaim a comparative top customer ranking when only one candidate is present", () => {
|
||||||
|
const result = composeCounterpartyAnalyticsReply(
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 250000,
|
||||||
|
period: "2021-03-31",
|
||||||
|
registrator: "Поступление 1"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "кто больше всего принес денег в 2021"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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("не полноценный сравнительный рейтинг");
|
||||||
|
expect(result?.text).not.toContain("Самый доходный клиент");
|
||||||
|
});
|
||||||
|
|
||||||
it("starts top year aggregate reply with a direct business answer", () => {
|
it("starts top year aggregate reply with a direct business answer", () => {
|
||||||
const result = composeCounterpartyAnalyticsReply(
|
const result = composeCounterpartyAnalyticsReply(
|
||||||
"customer_revenue_and_payments",
|
"customer_revenue_and_payments",
|
||||||
|
|
|
||||||
|
|
@ -761,6 +761,46 @@ describe("assistant orchestration contract", () => {
|
||||||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(false);
|
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps all-time top customer ranking in address lane instead of stale deep problem answer", () => {
|
||||||
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: "кто у нас самый доходный клиент за все время",
|
||||||
|
effectiveAddressUserMessage: "определить самого доходного клиента за весь период",
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "counterparty_activity_lifecycle",
|
||||||
|
previous_filters: {
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
llmPreDecomposeMeta: {
|
||||||
|
applied: true,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "address_query",
|
||||||
|
mode_confidence: "medium",
|
||||||
|
intent: "customer_revenue_and_payments",
|
||||||
|
intent_confidence: "medium"
|
||||||
|
},
|
||||||
|
semanticExtractionContract: {
|
||||||
|
valid: false,
|
||||||
|
apply_canonical_recommended: false,
|
||||||
|
extraction: {
|
||||||
|
query_shape: "AGGREGATE_LOOKUP",
|
||||||
|
aggregation_profile: "management_profile"
|
||||||
|
},
|
||||||
|
guard_hints: {},
|
||||||
|
reason_codes: ["ranking_semantic_guard_rejected"]
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
useMock: false
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(true);
|
||||||
|
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(decision.livingMode).toBe("address_data");
|
||||||
|
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||||
|
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps unsupported retrieval query in address lane when LLM runtime is unavailable", () => {
|
it("keeps unsupported retrieval query in address lane when LLM runtime is unavailable", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage:
|
rawUserMessage:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue