ARCH: включить оборот конкретного контрагента в revenue-контур
This commit is contained in:
parent
b339a8f8ca
commit
f75be32e41
|
|
@ -528,6 +528,25 @@ function extractLooseByAnchorValue(text) {
|
|||
}
|
||||
return normalizedToken;
|
||||
}
|
||||
function extractSpecificCounterpartyRevenueAnchor(text) {
|
||||
const source = String(text ?? "");
|
||||
const normalized = source.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
const hasRevenueCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)/iu.test(normalized);
|
||||
const hasExcludedCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|supplier|vendor|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0438|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0431\u0430\u043d\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|docs?|documents?|contract|bank|payment)/iu.test(normalized);
|
||||
if (!hasRevenueCue || hasExcludedCue) {
|
||||
return undefined;
|
||||
}
|
||||
const match = source.match(/(?:^|[\s,.;:!?])(?:\u043f\u043e|by|for)\s+([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu) ??
|
||||
source.match(/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)\s+(?:(?:\u0431\u044b\u043b(?:\u0430|\u043e)?|was)\s+)?(?:(?:\u0443|\u043f\u043e|by|for)\s+)?([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu);
|
||||
const token = cleanupAnchorValue(String(match?.[1] ?? ""));
|
||||
if (!token || !hasStrongCounterpartyTokenShape(token) || !isLikelyCounterpartyToken(token)) {
|
||||
return undefined;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
function extractContractTokenHeuristic(text) {
|
||||
const source = String(text ?? "");
|
||||
const explicit = source.match(/(?:№|#|n)\s*([a-zа-яё0-9][a-zа-яё0-9./_-]{1,})/iu);
|
||||
|
|
@ -1494,6 +1513,13 @@ function extractAddressFilters(userMessage, intent) {
|
|||
if (counterpartyMatch && !filters.counterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
|
||||
}
|
||||
if (!filters.counterparty && allowGenericCounterpartyAnchor && intent === "customer_revenue_and_payments") {
|
||||
const revenueCounterparty = extractSpecificCounterpartyRevenueAnchor(text);
|
||||
if (revenueCounterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(revenueCounterparty);
|
||||
warnings.push("counterparty_anchor_derived_from_revenue_phrase");
|
||||
}
|
||||
}
|
||||
if (!filters.counterparty &&
|
||||
allowGenericCounterpartyAnchor &&
|
||||
(intent === "list_documents_by_counterparty" ||
|
||||
|
|
|
|||
|
|
@ -1467,6 +1467,39 @@ function hasCustomerRevenueRankingBridgeSignal(text) {
|
|||
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 hasSpecificCounterpartyRevenueBridgeSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
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);
|
||||
const hasNonRevenueEntityCue = /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0438|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0431\u0430\u043d\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|docs?|documents?|contract|bank|payment)/iu.test(normalized);
|
||||
if (hasSupplierCue || hasNonRevenueEntityCue) {
|
||||
return false;
|
||||
}
|
||||
const hasRevenueCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)/iu.test(normalized);
|
||||
if (!hasRevenueCue) {
|
||||
return false;
|
||||
}
|
||||
const explicitEntityMatch = normalized.match(/(?:^|[\s,.;:!?])(?:\u043f\u043e|by|for)\s+([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu) ??
|
||||
normalized.match(/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)\s+(?:(?:\u0431\u044b\u043b(?:\u0430|\u043e)?|was)\s+)?(?:(?:\u0443|\u043f\u043e|by|for)\s+)?([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu);
|
||||
const entity = explicitEntityMatch?.[1] ? String(explicitEntityMatch[1]).toLowerCase() : null;
|
||||
if (!entity || /^\d+$/.test(entity)) {
|
||||
return false;
|
||||
}
|
||||
const ignoredEntityTails = new Set([
|
||||
"\u043d\u0430\u043c",
|
||||
"\u043d\u0430\u0441",
|
||||
"\u0432\u0441\u0435",
|
||||
"\u0432\u0441\u0435\u043c",
|
||||
"\u0433\u043e\u0434",
|
||||
"\u0433\u043e\u0434\u0430",
|
||||
"\u043c\u0435\u0441\u044f\u0446",
|
||||
"year",
|
||||
"month"
|
||||
]);
|
||||
return !ignoredEntityTails.has(entity);
|
||||
}
|
||||
function hasInventoryProvenanceBridgeSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
|
|
@ -1565,11 +1598,18 @@ function resolveAddressIntent(userMessage) {
|
|||
};
|
||||
}
|
||||
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)) {
|
||||
const hasSpecificCounterpartyRevenueBridge = [text, repairedText, turnNoiseNormalizedBridgeText, currentTurnBridgeText].some((sample) => hasSpecificCounterpartyRevenueBridgeSignal(sample));
|
||||
if (hasDirectRevenueAggregateBridge ||
|
||||
hasCustomerRevenueRankingBridgeSignal(bridgeText) ||
|
||||
hasSpecificCounterpartyRevenueBridge) {
|
||||
return {
|
||||
intent: "customer_revenue_and_payments",
|
||||
confidence: "medium",
|
||||
reasons: ["customer_revenue_ranking_bridge_signal_detected"]
|
||||
reasons: [
|
||||
hasSpecificCounterpartyRevenueBridge
|
||||
? "specific_counterparty_revenue_bridge_signal_detected"
|
||||
: "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) &&
|
||||
|
|
|
|||
|
|
@ -400,6 +400,30 @@ function createAssistantTransitionPolicy(deps) {
|
|||
? deps.hasFollowupMarker(String(alternateMessage ?? "")) ||
|
||||
deps.hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
hasInventoryRootRestatementPrimary ||
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasFollowupMarker(String(alternateMessage ?? "")) ||
|
||||
deps.hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasCurrentTurnExplicitEntity = Array.isArray(assistantTurnMeaning?.explicit_entity_candidates) &&
|
||||
assistantTurnMeaning.explicit_entity_candidates.length > 0;
|
||||
if (assistantTurnMeaning?.intent_override_strength === "explicit_current_turn_intent" &&
|
||||
hasCurrentTurnExplicitEntity &&
|
||||
!hasConcreteFollowupReference) {
|
||||
return null;
|
||||
}
|
||||
const hasStandaloneAddressTopic = deps.hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage) ? deps.hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic &&
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
|||
"receivables_confirmed_as_of_date",
|
||||
"payables_confirmed_as_of_date",
|
||||
"list_documents_by_counterparty",
|
||||
"customer_revenue_and_payments",
|
||||
"inventory_on_hand_as_of_date"
|
||||
]);
|
||||
function fallbackCompactWhitespace(value) {
|
||||
|
|
|
|||
|
|
@ -600,6 +600,35 @@ function extractLooseByAnchorValue(text: string): string | undefined {
|
|||
return normalizedToken;
|
||||
}
|
||||
|
||||
function extractSpecificCounterpartyRevenueAnchor(text: string): string | undefined {
|
||||
const source = String(text ?? "");
|
||||
const normalized = source.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
const hasRevenueCue =
|
||||
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasExcludedCue =
|
||||
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|supplier|vendor|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0438|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0431\u0430\u043d\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|docs?|documents?|contract|bank|payment)/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (!hasRevenueCue || hasExcludedCue) {
|
||||
return undefined;
|
||||
}
|
||||
const match =
|
||||
source.match(/(?:^|[\s,.;:!?])(?:\u043f\u043e|by|for)\s+([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu) ??
|
||||
source.match(
|
||||
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)\s+(?:(?:\u0431\u044b\u043b(?:\u0430|\u043e)?|was)\s+)?(?:(?:\u0443|\u043f\u043e|by|for)\s+)?([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu
|
||||
);
|
||||
const token = cleanupAnchorValue(String(match?.[1] ?? ""));
|
||||
if (!token || !hasStrongCounterpartyTokenShape(token) || !isLikelyCounterpartyToken(token)) {
|
||||
return undefined;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function extractContractTokenHeuristic(text: string): string | undefined {
|
||||
const source = String(text ?? "");
|
||||
const explicit = source.match(/(?:№|#|n)\s*([a-zа-яё0-9][a-zа-яё0-9./_-]{1,})/iu);
|
||||
|
|
@ -1728,6 +1757,13 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
if (counterpartyMatch && !filters.counterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
|
||||
}
|
||||
if (!filters.counterparty && allowGenericCounterpartyAnchor && intent === "customer_revenue_and_payments") {
|
||||
const revenueCounterparty = extractSpecificCounterpartyRevenueAnchor(text);
|
||||
if (revenueCounterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(revenueCounterparty);
|
||||
warnings.push("counterparty_anchor_derived_from_revenue_phrase");
|
||||
}
|
||||
}
|
||||
if (
|
||||
!filters.counterparty &&
|
||||
allowGenericCounterpartyAnchor &&
|
||||
|
|
|
|||
|
|
@ -1822,6 +1822,52 @@ function hasCustomerRevenueRankingBridgeSignal(text: string): boolean {
|
|||
return hasCustomerRankingCue || hasRevenueAggregateCue;
|
||||
}
|
||||
|
||||
function hasSpecificCounterpartyRevenueBridgeSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
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
|
||||
);
|
||||
const hasNonRevenueEntityCue =
|
||||
/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0438|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0431\u0430\u043d\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|docs?|documents?|contract|bank|payment)/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (hasSupplierCue || hasNonRevenueEntityCue) {
|
||||
return false;
|
||||
}
|
||||
const hasRevenueCue =
|
||||
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)/iu.test(
|
||||
normalized
|
||||
);
|
||||
if (!hasRevenueCue) {
|
||||
return false;
|
||||
}
|
||||
const explicitEntityMatch =
|
||||
normalized.match(/(?:^|[\s,.;:!?])(?:\u043f\u043e|by|for)\s+([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu) ??
|
||||
normalized.match(
|
||||
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)\s+(?:(?:\u0431\u044b\u043b(?:\u0430|\u043e)?|was)\s+)?(?:(?:\u0443|\u043f\u043e|by|for)\s+)?([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu
|
||||
);
|
||||
const entity = explicitEntityMatch?.[1] ? String(explicitEntityMatch[1]).toLowerCase() : null;
|
||||
if (!entity || /^\d+$/.test(entity)) {
|
||||
return false;
|
||||
}
|
||||
const ignoredEntityTails = new Set([
|
||||
"\u043d\u0430\u043c",
|
||||
"\u043d\u0430\u0441",
|
||||
"\u0432\u0441\u0435",
|
||||
"\u0432\u0441\u0435\u043c",
|
||||
"\u0433\u043e\u0434",
|
||||
"\u0433\u043e\u0434\u0430",
|
||||
"\u043c\u0435\u0441\u044f\u0446",
|
||||
"year",
|
||||
"month"
|
||||
]);
|
||||
return !ignoredEntityTails.has(entity);
|
||||
}
|
||||
|
||||
function hasInventoryProvenanceBridgeSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
|
|
@ -1972,11 +2018,22 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
/(?:\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)) {
|
||||
const hasSpecificCounterpartyRevenueBridge = [text, repairedText, turnNoiseNormalizedBridgeText, currentTurnBridgeText].some(
|
||||
(sample) => hasSpecificCounterpartyRevenueBridgeSignal(sample)
|
||||
);
|
||||
if (
|
||||
hasDirectRevenueAggregateBridge ||
|
||||
hasCustomerRevenueRankingBridgeSignal(bridgeText) ||
|
||||
hasSpecificCounterpartyRevenueBridge
|
||||
) {
|
||||
return {
|
||||
intent: "customer_revenue_and_payments",
|
||||
confidence: "medium",
|
||||
reasons: ["customer_revenue_ranking_bridge_signal_detected"]
|
||||
reasons: [
|
||||
hasSpecificCounterpartyRevenueBridge
|
||||
? "specific_counterparty_revenue_bridge_signal_detected"
|
||||
: "customer_revenue_ranking_bridge_signal_detected"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -536,6 +536,34 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
? deps.hasFollowupMarker(String(alternateMessage ?? "")) ||
|
||||
deps.hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasConcreteFollowupReference =
|
||||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
hasInventoryRootRestatementPrimary ||
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasFollowupMarker(String(alternateMessage ?? "")) ||
|
||||
deps.hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasCurrentTurnExplicitEntity =
|
||||
Array.isArray(assistantTurnMeaning?.explicit_entity_candidates) &&
|
||||
assistantTurnMeaning.explicit_entity_candidates.length > 0;
|
||||
if (
|
||||
assistantTurnMeaning?.intent_override_strength === "explicit_current_turn_intent" &&
|
||||
hasCurrentTurnExplicitEntity &&
|
||||
!hasConcreteFollowupReference
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const hasStandaloneAddressTopic =
|
||||
deps.hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage) ? deps.hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
|||
"receivables_confirmed_as_of_date",
|
||||
"payables_confirmed_as_of_date",
|
||||
"list_documents_by_counterparty",
|
||||
"customer_revenue_and_payments",
|
||||
"inventory_on_hand_as_of_date"
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,4 +14,13 @@ describe("address filter extractor regressions", () => {
|
|||
expect(extracted.warnings).toContain("period_derived_from_month_phrase");
|
||||
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
});
|
||||
it("extracts a compact counterparty tail for customer revenue profile", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||||
"customer_revenue_and_payments"
|
||||
);
|
||||
|
||||
expect(extracted.extracted_filters.counterparty).toBe("\u0441\u0432\u043a");
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_derived_from_revenue_phrase");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,23 @@ describe("addressIntentResolver regression bridges", () => {
|
|||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("detects specific counterparty turnover wording as revenue profile", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
expect(result.reasons).toContain("specific_counterparty_revenue_bridge_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps documents by counterparty wording out of revenue bridge", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u0441\u0432\u043a"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("list_documents_by_counterparty");
|
||||
});
|
||||
|
||||
it("does not collapse very old stock request into generic inventory snapshot", () => {
|
||||
const result = resolveAddressIntent("Есть ли остатки товара, которые закупались очень давно");
|
||||
|
||||
|
|
|
|||
|
|
@ -965,4 +965,52 @@ describe("assistantTransitionPolicy", () => {
|
|||
|
||||
expect(carryover).toBeNull();
|
||||
});
|
||||
|
||||
it("drops carryover for a supported current-turn counterparty revenue pivot with a new entity", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "Documents by previous counterparty",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: { status: "grounded" },
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
extracted_filters: {
|
||||
counterparty: "Previous Counterparty",
|
||||
organization: "Org Alt"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_resolved: "Previous Counterparty"
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
isImplicitAddressContinuationByLlm: () => true,
|
||||
resolveAssistantTurnMeaning: () => ({
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "counterparty_value_or_turnover",
|
||||
explicit_intent_candidate: "customer_revenue_and_payments",
|
||||
intent_override_strength: "explicit_current_turn_intent",
|
||||
explicit_entity_candidates: [
|
||||
{
|
||||
type: "counterparty",
|
||||
value: "svk",
|
||||
source: "current_turn_loose_entity_tail"
|
||||
}
|
||||
],
|
||||
stale_replay_forbidden: false
|
||||
}),
|
||||
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments" }),
|
||||
resolveAddressIntentFamily: () => "counterparty"
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ describe("assistantTurnMeaningPolicy", () => {
|
|||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||||
});
|
||||
|
||||
it("marks unsupported counterparty turnover as understood and forbids stale replay", () => {
|
||||
it("promotes specific counterparty turnover to the supported revenue intent", () => {
|
||||
const policy = buildPolicy();
|
||||
|
||||
const meaning = policy.resolveAssistantTurnMeaning({
|
||||
|
|
@ -41,11 +41,12 @@ describe("assistantTurnMeaningPolicy", () => {
|
|||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a"
|
||||
});
|
||||
|
||||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||
expect(meaning.explicit_intent_candidate).toBe("customer_revenue_and_payments");
|
||||
expect(meaning.asked_domain_family).toBe("counterparty");
|
||||
expect(meaning.asked_action_family).toBe("counterparty_value_or_turnover");
|
||||
expect(meaning.unsupported_but_understood_family).toBe("counterparty_value_or_turnover");
|
||||
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||
expect(meaning.unsupported_but_understood_family).toBeNull();
|
||||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||||
expect(meaning.carryover_budget).toBe("matching_family_only");
|
||||
expect(meaning.explicit_entity_candidates).toEqual([
|
||||
{
|
||||
type: "counterparty",
|
||||
|
|
|
|||
Loading…
Reference in New Issue