Post-F: укрепить VAT, даты остатков и SVK follow-up

This commit is contained in:
2026-05-05 17:09:26 +03:00
parent 2ec2f81dd8
commit 98afdd39c4
15 changed files with 815 additions and 22 deletions
@@ -73,6 +73,15 @@ const COUNTERPARTY_TOKEN_NOISE = new Set([
"показать",
"скажи",
"выведи",
"видно",
"документам",
"документами",
"движение",
"движения",
"движениям",
"операциям",
"проверить",
"проверь",
"show",
"list",
"контра",
@@ -99,7 +108,10 @@ function isCounterpartyFillerToken(token) {
if (/^(?:бл[яе]|блять|нах|нахуй|епт|ёпт|епта)$/iu.test(normalized)) {
return true;
}
if (/^(?:док(?:и|ам|ами|умент(?:ы|ов)?)?|docs?|docy|doci|doki|dokument(?:y|ov|am|a)?)$/iu.test(normalized)) {
if (/^(?:док(?:и|ам|ами|умент(?:ы|ов|ам|ами)?)?|docs?|docy|doci|doki|dokument(?:y|ov|am|a)?)$/iu.test(normalized)) {
return true;
}
if (/^(?:движени[еяям]*|операци[яиюеям]*|проверить|проверь|видно)$/iu.test(normalized)) {
return true;
}
if (/^(?:pokazh?|pokazhi|pokaji|pokezh|kakie|kakoi|kakaya|est|za|po|na|s|vse|all|poka)$/iu.test(normalized)) {
@@ -1661,7 +1661,18 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(normalized);
const hasComparisonCue = /(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(normalized);
const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(normalized);
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && hasValueFlowCue;
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
}
function hasVatPeriodInspectionBridgeSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase();
if (!/(?:ндс|vat)/iu.test(normalized)) {
return false;
}
const hasPeriodCue = /(?:\b(?:19|20)\d{2}\b|за\s+(?:\d{4}|год|период|квартал|месяц|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)|\b[1-4]\s*(?:кв|квартал))/iu.test(normalized);
const hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized);
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue;
}
function resolveUnicodeAddressIntentBridge(text) {
const normalized = String(text ?? "").trim().toLowerCase();
@@ -1780,6 +1791,9 @@ function resolveUnicodeAddressIntentBridge(text) {
/(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) {
return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected");
}
if (hasVatPeriodInspectionBridgeSignal(normalized)) {
return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_period_inspection_bridge_signal_detected");
}
const inventoryBridgeIntent = (0, addressInventoryIntentSignals_1.resolveInventoryAddressIntent)(normalized);
if (inventoryBridgeIntent) {
if (inventoryBridgeIntent.intent === "inventory_aging_by_purchase_date") {
@@ -189,7 +189,15 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"сводную",
"сводном",
"сводного",
"сводному"
"сводному",
"видно",
"документам",
"документами",
"движение",
"движения",
"движениям",
"проверить",
"проверь"
]);
const FOLLOWUP_LOW_QUALITY_CONTRACT_TOKENS = new Set([
"за",
@@ -661,6 +669,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const currentHasExplicitTemporalScope = hasExplicitPeriodWindow(merged) ||
Boolean(toNonEmptyString(merged.as_of_date)) ||
hasExplicitPeriodInMessage ||
hasExplicitCurrentDateInMessage;
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
merged.organization = previousOrganization;
@@ -945,6 +957,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
}
if (!sameDateRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
!currentHasExplicitTemporalScope &&
hasOpenItemsHint(userMessage)) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
@@ -119,7 +119,28 @@ function isGarbageSemanticAnchorCandidate(value) {
"прокси",
"proxy",
"summary",
"overall"
"overall",
"деньги",
"денег",
"деньгам",
"деньгами",
"ооо",
"ип",
"ао",
"пао",
"зао",
"llc",
"inc",
"corp",
"документам",
"документами",
"движение",
"движения",
"движениям",
"операциям",
"проверить",
"проверь",
"видно"
]).has(compact)) {
return true;
}
@@ -756,6 +777,20 @@ function rawEntityResolutionCandidate(text) {
}
return null;
}
function rawScopedEntityCandidateFromText(text) {
const source = (0, addressTextRepair_1.repairAddressMojibakeText)(String(text ?? ""));
const patterns = [
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu,
/(?:документ(?:ам|ы)?|движени(?:ям|я)?|операци(?:ям|и)?|плат[её]ж(?:ам|и)?)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
];
for (const pattern of patterns) {
const candidate = normalizeEntityResolutionCandidate(source.match(pattern)?.[1] ?? "");
if (candidate.length >= 2 && !isInvalidEntityCandidate(candidate)) {
return candidate;
}
}
return null;
}
function resolveEntityResolutionAmbiguityChoice(text, candidates) {
const normalizedText = canonicalizeEntityResolutionCandidate(text);
if (!normalizedText || candidates.length <= 0) {
@@ -1013,6 +1048,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
!predecomposeEntities.organization &&
(rawValueFlowSignal || rawLifecycleSignal || rawMetadataSignal || rawBusinessOverviewSignal)
? rawScopedEntityCandidateFromText(rawEntitySourceText)
: null;
const entityResolutionClarificationCandidate = followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous"
? resolveEntityResolutionAmbiguityChoice(rawEntitySourceText, followupSeed.entityResolutionAmbiguityCandidates)
@@ -1379,8 +1419,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
});
const explicitCurrentCounterpartyCandidate = normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty)
? normalizedPredecomposeCounterparty
const explicitCurrentCounterpartyCandidate = (normalizedPredecomposeCounterparty ?? rawScopedEntityCandidate) &&
!isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty ?? rawScopedEntityCandidate ?? "")
? normalizedPredecomposeCounterparty ?? rawScopedEntityCandidate
: null;
const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean(explicitCurrentCounterpartyCandidate &&
(effectiveFollowupCounterparty || followupSeed.discoveryEntity) &&
@@ -1434,6 +1475,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
}
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
pushScopedEntityCandidate(entityCandidates, rawScopedEntityCandidate, groundedFollowupEntity);
if (!groundedFollowupEntity) {
if (!rawMetadataScopeOverridesFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null);
@@ -1452,7 +1494,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
pushUnique(entityCandidates, rawMetadataScopeHint);
}
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty;
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !explicitCurrentCounterpartyCandidate && !effectiveFollowupCounterparty;
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
Boolean(bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
@@ -1460,7 +1502,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable ||
followupSeed.organization);
const openScopeValueFlowWithoutResolvedCounterparty = Boolean(valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty);
const openScopeValueFlowWithoutResolvedCounterparty = Boolean(valueFlowSignal && !explicitCurrentCounterpartyCandidate && !effectiveFollowupCounterparty);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
@@ -1864,6 +1906,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
}
if (effectiveFollowupCounterparty &&
!rawEntitySearchOverridesStaleScope &&
!rawMetadataScopeOverridesFollowupEntity) {
+12 -3
View File
@@ -1429,13 +1429,22 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"dokumenty",
"документ",
"документы",
"документам",
"документами",
"документов",
"банк",
"банковские",
"операции",
"операциям",
"движение",
"движения",
"движениям",
"платеж",
"платёж",
"платежи",
"проверить",
"проверь",
"видно",
"контрагент",
"контрагенту",
"контрагента",
@@ -2615,10 +2624,10 @@ function hasAddressFollowupContextSignal(userMessage) {
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
return true;
}
const shortValueFlowRetargetCue = shortFollowup &&
const valueFlowRetargetFollowup = minTokens <= 14 &&
(hasMarker() || hasPointer() || hasAny(/^(?:Р°|a|Рё|i|also|then|now)(?=$|[\s,.;:!?])/iu)) &&
hasAny(/(?:нетто|сальдо|разниц|получил|заплатил|поступ|входящ|исходящ|оборот|выручк|денеж)/iu);
if (shortValueFlowRetargetCue) {
hasAny(/(?:нетто|сальдо|разниц|получил|заплатил|поступ|входящ|исходящ|оборот|выручк|денеж|нетто|сальдо|разниц|получил|заплатил|поступ|РІС…РѕРґСЏС‰|РёСЃС…РѕРґСЏС‰|РѕР±РѕСЂРѕС‚|выручк|денеж)/iu);
if (valueFlowRetargetFollowup) {
return true;
}
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
@@ -81,6 +81,15 @@ const COUNTERPARTY_TOKEN_NOISE = new Set([
"показать",
"скажи",
"выведи",
"видно",
"документам",
"документами",
"движение",
"движения",
"движениям",
"операциям",
"проверить",
"проверь",
"show",
"list",
"контра",
@@ -108,7 +117,10 @@ function isCounterpartyFillerToken(token: string): boolean {
if (/^(?:бл[яе]|блять|нах|нахуй|епт|ёпт|епта)$/iu.test(normalized)) {
return true;
}
if (/^(?:док(?:и|ам|ами|умент(?:ы|ов)?)?|docs?|docy|doci|doki|dokument(?:y|ov|am|a)?)$/iu.test(normalized)) {
if (/^(?:док(?:и|ам|ами|умент(?:ы|ов|ам|ами)?)?|docs?|docy|doci|doki|dokument(?:y|ov|am|a)?)$/iu.test(normalized)) {
return true;
}
if (/^(?:движени[еяям]*|операци[яиюеям]*|проверить|проверь|видно)$/iu.test(normalized)) {
return true;
}
if (/^(?:pokazh?|pokazhi|pokaji|pokezh|kakie|kakoi|kakaya|est|za|po|na|s|vse|all|poka)$/iu.test(normalized)) {
@@ -2147,8 +2147,26 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(
normalized
);
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && hasValueFlowCue;
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
}
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!/(?:ндс|vat)/iu.test(normalized)) {
return false;
}
const hasPeriodCue =
/(?:\b(?:19|20)\d{2}\b|за\s+(?:\d{4}|год|период|квартал|месяц|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)|\b[1-4]\s*(?:кв|квартал))/iu.test(
normalized
);
const hasInspectionCue =
/(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(
normalized
);
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue;
}
function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null {
@@ -2381,6 +2399,14 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
);
}
if (hasVatPeriodInspectionBridgeSignal(normalized)) {
return unicodeBridgeResolution(
"vat_liability_confirmed_for_tax_period",
"high",
"vat_period_inspection_bridge_signal_detected"
);
}
const inventoryBridgeIntent = resolveInventoryAddressIntent(normalized);
if (inventoryBridgeIntent) {
if (inventoryBridgeIntent.intent === "inventory_aging_by_purchase_date") {
@@ -283,7 +283,15 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"сводную",
"сводном",
"сводного",
"сводному"
"сводному",
"видно",
"документам",
"документами",
"движение",
"движения",
"движениям",
"проверить",
"проверь"
]);
const FOLLOWUP_LOW_QUALITY_CONTRACT_TOKENS = new Set([
@@ -858,6 +866,11 @@ function mergeFollowupFilters(
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const currentHasExplicitTemporalScope =
hasExplicitPeriodWindow(merged) ||
Boolean(toNonEmptyString(merged.as_of_date)) ||
hasExplicitPeriodInMessage ||
hasExplicitCurrentDateInMessage;
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
merged.organization = previousOrganization;
@@ -1196,6 +1209,7 @@ function mergeFollowupFilters(
if (
!sameDateRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
!currentHasExplicitTemporalScope &&
hasOpenItemsHint(userMessage)
) {
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
@@ -169,7 +169,28 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
"прокси",
"proxy",
"summary",
"overall"
"overall",
"деньги",
"денег",
"деньгам",
"деньгами",
"ооо",
"ип",
"ао",
"пао",
"зао",
"llc",
"inc",
"corp",
"документам",
"документами",
"движение",
"движения",
"движениям",
"операциям",
"проверить",
"проверь",
"видно"
]).has(compact)
) {
return true;
@@ -1099,6 +1120,21 @@ function rawEntityResolutionCandidate(text: string): string | null {
return null;
}
function rawScopedEntityCandidateFromText(text: string): string | null {
const source = repairAddressMojibakeText(String(text ?? ""));
const patterns = [
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu,
/(?:документ(?:ам|ы)?|движени(?:ям|я)?|операци(?:ям|и)?|плат[её]ж(?:ам|и)?)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
];
for (const pattern of patterns) {
const candidate = normalizeEntityResolutionCandidate(source.match(pattern)?.[1] ?? "");
if (candidate.length >= 2 && !isInvalidEntityCandidate(candidate)) {
return candidate;
}
}
return null;
}
function resolveEntityResolutionAmbiguityChoice(text: string, candidates: string[]): string | null {
const normalizedText = canonicalizeEntityResolutionCandidate(text);
if (!normalizedText || candidates.length <= 0) {
@@ -1419,6 +1455,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const rawScopedEntityCandidate =
!predecomposeEntities.counterparty &&
!predecomposeEntities.organization &&
(rawValueFlowSignal || rawLifecycleSignal || rawMetadataSignal || rawBusinessOverviewSignal)
? rawScopedEntityCandidateFromText(rawEntitySourceText)
: null;
const entityResolutionClarificationCandidate =
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous"
@@ -1870,8 +1912,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
!metadataGroundedMovementLaneApplicable
});
const explicitCurrentCounterpartyCandidate =
normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty)
? normalizedPredecomposeCounterparty
(normalizedPredecomposeCounterparty ?? rawScopedEntityCandidate) &&
!isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty ?? rawScopedEntityCandidate ?? "")
? normalizedPredecomposeCounterparty ?? rawScopedEntityCandidate
: null;
const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean(
explicitCurrentCounterpartyCandidate &&
@@ -1929,6 +1972,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
}
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
pushScopedEntityCandidate(entityCandidates, rawScopedEntityCandidate, groundedFollowupEntity);
if (!groundedFollowupEntity) {
if (!rawMetadataScopeOverridesFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null);
@@ -1950,7 +1994,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
pushUnique(entityCandidates, rawMetadataScopeHint);
}
const openScopeValueFlowWithoutCounterparty =
valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty;
valueFlowSignal && !explicitCurrentCounterpartyCandidate && !effectiveFollowupCounterparty;
const valueFlowOrganizationStaysScope =
openScopeValueFlowWithoutCounterparty &&
Boolean(
@@ -1962,7 +2006,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.organization
);
const openScopeValueFlowWithoutResolvedCounterparty = Boolean(
valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty
valueFlowSignal && !explicitCurrentCounterpartyCandidate && !effectiveFollowupCounterparty
);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
@@ -2409,6 +2453,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (rawScopedEntityCandidate && !normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_raw_scope");
}
if (
effectiveFollowupCounterparty &&
!rawEntitySearchOverridesStaleScope &&
@@ -1383,13 +1383,22 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"dokumenty",
"документ",
"документы",
"документам",
"документами",
"документов",
"банк",
"банковские",
"операции",
"операциям",
"движение",
"движения",
"движениям",
"платеж",
"платёж",
"платежи",
"проверить",
"проверь",
"видно",
"контрагент",
"контрагенту",
"контрагента",
@@ -2571,10 +2580,10 @@ function hasAddressFollowupContextSignal(userMessage) {
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
return true;
}
const shortValueFlowRetargetCue = shortFollowup &&
const valueFlowRetargetFollowup = minTokens <= 14 &&
(hasMarker() || hasPointer() || hasAny(/^(?:Р°|a|Рё|i|also|then|now)(?=$|[\s,.;:!?])/iu)) &&
hasAny(/(?:РЅРµССРѕ|сальдо|разниС|полуСРёР»|заплаСРёР»|РїРѕСЃССѓРї|РІСРѕРґСЏС|РёСЃСРѕРґСЏС|РѕР±РѕСЂРѕС|РІССЂСѓСРє|денеР)/iu);
if (shortValueFlowRetargetCue) {
hasAny(/(?:нетто|сальдо|разниц|получил|заплатил|поступ|входящ|исходящ|оборот|выручк|денеж|РЅРµССРѕ|сальдо|разниС|полуСРёР»|заплаСРёР»|РїРѕСЃССѓРї|РІСРѕРґСЏС|РёСЃСРѕРґСЏС|РѕР±РѕСЂРѕС|РІССЂСѓСРє|денеР)/iu);
if (valueFlowRetargetFollowup) {
return true;
}
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
@@ -23,4 +23,25 @@ describe("address filter extractor regressions", () => {
expect(extracted.extracted_filters.counterparty).toBe("\u0441\u0432\u043a");
expect(extracted.warnings).toContain("counterparty_anchor_derived_from_revenue_phrase");
});
it("drops document and movement service words as low-quality counterparty anchors", () => {
const documentNoise = extractAddressFilters(
"документы по контрагенту документам",
"list_documents_by_counterparty"
);
const movementNoise = extractAddressFilters(
"Проверить движение по счетам или документам",
"list_documents_by_counterparty"
);
const explicitCheckNoise = extractAddressFilters(
"документы по контрагенту Проверить",
"list_documents_by_counterparty"
);
expect(documentNoise.extracted_filters.counterparty).toBeUndefined();
expect(documentNoise.warnings).toContain("counterparty_anchor_dropped_low_quality");
expect(movementNoise.extracted_filters.counterparty).toBeUndefined();
expect(explicitCheckNoise.extracted_filters.counterparty).toBeUndefined();
expect(explicitCheckNoise.warnings).toContain("counterparty_anchor_dropped_low_quality");
});
});
@@ -144,6 +144,31 @@ describe("address follow-up temporal regressions", () => {
expect(result?.baseReasons).toContain("period_to_from_followup_context");
});
it("does not inherit stale inventory as-of date over an explicit fresh snapshot date", () => {
const result = runAddressDecomposeStage(
"Покажи складской срез Альтернативы Плюс на 2026-04-16: что есть в остатках, какие самые заметные позиции, и что это говорит о бизнесе.",
{
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31",
as_of_date: "2020-12-31"
},
previous_anchor_type: "organization",
previous_anchor_value: "ООО Альтернатива Плюс"
}
);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.as_of_date).toBe("2026-04-16");
expect(result?.filters.extracted_filters.period_from).toBe("2026-04-01");
expect(result?.filters.extracted_filters.period_to).toBe("2026-04-30");
expect(result?.baseReasons).not.toContain("as_of_date_from_open_items_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",
@@ -240,4 +265,30 @@ describe("address follow-up temporal regressions", () => {
expect(result?.filters.extracted_filters.period_from).toBeUndefined();
expect(result?.filters.extracted_filters.period_to).toBeUndefined();
});
it("replaces document and movement service-word anchors from counterparty follow-up context", () => {
const followupContext = {
previous_intent: "customer_revenue_and_payments" as const,
target_intent: "list_documents_by_counterparty" as const,
previous_filters: {
organization: "ООО Альтернатива Плюс",
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty" as const,
previous_anchor_value: "Группа СВК",
resolved_counterparty_from_display: true
};
const documents = runAddressDecomposeStage("документы по контрагенту документам", followupContext);
const movements = runAddressDecomposeStage("Проверить движение по счетам или документам", followupContext);
expect(documents?.intent.intent).toBe("list_documents_by_counterparty");
expect(documents?.filters.extracted_filters.counterparty).toBe("Группа СВК");
expect(documents?.baseReasons).toContain("counterparty_from_followup_context");
expect(movements?.intent.intent).toBe("list_documents_by_counterparty");
expect(movements?.filters.extracted_filters.counterparty).toBe("Группа СВК");
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
});
});
@@ -28,6 +28,15 @@ describe("vat payable confirmed as-of route", () => {
expect(result.reasons).toContain("vat_liability_colloquial_bridge_signal_detected");
});
it("keeps VAT period-inspection wording out of inventory snapshot arbitration", () => {
const result = resolveAddressIntent(
"Что с НДС за 2020 год по Альтернативе Плюс: какая позиция видна, на чем она основана и чего не хватает для налогового вывода?"
);
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result.reasons).toContain("vat_period_inspection_bridge_signal_detected");
});
it("keeps VAT forecast intent when explicit forecast wording is used", () => {
const result = resolveAddressIntent("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
expect(result.intent).toBe("vat_payable_forecast");
@@ -133,6 +133,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
});
it("extracts compact scoped counterparty from net follow-up wording when LLM entities are empty", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "А какое нетто по СВК: сколько получили минус сколько заплатили?",
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["СВК"],
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_raw_scope");
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
});
it("overrides a supported exact current-turn payout route when the question asks for a payment amount", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: