diff --git a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js index 7434e86..06bcb1b 100644 --- a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js +++ b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js @@ -645,7 +645,7 @@ __WHERE_CLAUSE__ `; const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = ` ВЫБРАТЬ - ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период, + __PERIOD_TO_EXPR__ КАК Период, "VAT_BOOK_SALES" КАК Регистратор, "68.02" КАК СчетДт, "" КАК СчетКт, @@ -655,7 +655,7 @@ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = ` __WHERE_CLAUSE__ ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ - ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период, + __PERIOD_TO_EXPR__ КАК Период, "VAT_BOOK_PURCHASES" КАК Регистратор, "19" КАК СчетДт, "" КАК СчетКт, @@ -1395,7 +1395,18 @@ function buildAddressRecipePlan(recipe, filters) { .replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES)) .replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES)) : recipe.query_template === "vat_liability_confirmed_tax_period_profile" - ? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период")) + ? (() => { + const periodToExpr = (typeof filters.period_to === "string" && filters.period_to.trim().length > 0 + ? toDateTimeExpr(filters.period_to, true) + : null) ?? + (typeof filters.period_from === "string" && filters.period_from.trim().length > 0 + ? toDateTimeExpr(filters.period_from, true) + : null) ?? + "ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)"; + return VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE + .replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период")) + .replaceAll("__PERIOD_TO_EXPR__", periodToExpr); + })() : recipe.query_template === "vat_payable_confirmed_as_of_balance_profile" ? (() => { const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0 diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 63b0998..6e69bc6 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -1120,17 +1120,31 @@ function buildAssistantMcpDiscoveryTurnInput(input) { !metadataGroundedDocumentLaneApplicable && !metadataGroundedMovementLaneApplicable }); - const metadataLaneScopeHint = rawMetadataScopeHint ?? - followupSeed.metadataScopeHint ?? - followupSeed.discoveryEntity ?? - followupSeed.metadataSelectedEntitySet ?? - null; + const explicitCurrentCounterpartyCandidate = normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty) + ? normalizedPredecomposeCounterparty + : null; + const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean(explicitCurrentCounterpartyCandidate && + (followupSeed.counterparty || followupSeed.discoveryEntity) && + !sameScopedName(explicitCurrentCounterpartyCandidate, followupSeed.counterparty ?? followupSeed.discoveryEntity) && + (valueFlowSignal || + lifecycleSignal || + metadataGroundedDocumentLaneApplicable || + metadataGroundedMovementLaneApplicable)); + const metadataLaneScopeHint = explicitCurrentCounterpartyOverridesFollowupEntity + ? null + : rawMetadataScopeHint ?? + followupSeed.metadataScopeHint ?? + followupSeed.discoveryEntity ?? + followupSeed.metadataSelectedEntitySet ?? + null; const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) && !followupSeed.counterparty && metadataLaneCarryoverAvailable); const groundedFollowupEntity = metadataScopedLaneWithoutSubject ? null - : followupSeed.counterparty ?? followupSeed.discoveryEntity; + : explicitCurrentCounterpartyOverridesFollowupEntity + ? null + : followupSeed.counterparty ?? followupSeed.discoveryEntity; const entityCandidates = entityResolutionSignal ? [] : []; if (entityResolutionSignal) { pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate); @@ -1179,6 +1193,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty ? currentTurnOrganizationScope ?? followupSeed.organization : null; + if (explicitCurrentCounterpartyCandidate && + (valueFlowSignal || lifecycleSignal || metadataGroundedDocumentLaneApplicable || metadataGroundedMovementLaneApplicable)) { + for (let index = entityCandidates.length - 1; index >= 0; index -= 1) { + const candidate = entityCandidates[index]; + if (!candidate || sameScopedName(candidate, explicitCurrentCounterpartyCandidate)) { + continue; + } + if ((metadataLaneScopeHint && sameScopedName(candidate, metadataLaneScopeHint)) || + (explicitOrganizationScope && sameScopedName(candidate, explicitOrganizationScope)) || + (followupSeed.organization && sameScopedName(candidate, followupSeed.organization)) || + (followupSeed.counterparty && + !sameScopedName(followupSeed.counterparty, explicitCurrentCounterpartyCandidate) && + sameScopedName(candidate, followupSeed.counterparty))) { + entityCandidates.splice(index, 1); + } + } + } if (valueFlowOrganizationStaysScope && explicitOrganizationScope) { for (let index = entityCandidates.length - 1; index >= 0; index -= 1) { if (entityCandidates[index] === explicitOrganizationScope) { diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index 4c316a5..f101791 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -668,7 +668,7 @@ __WHERE_CLAUSE__ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = ` ВЫБРАТЬ - ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период, + __PERIOD_TO_EXPR__ КАК Период, "VAT_BOOK_SALES" КАК Регистратор, "68.02" КАК СчетДт, "" КАК СчетКт, @@ -678,7 +678,7 @@ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = ` __WHERE_CLAUSE__ ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ - ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период, + __PERIOD_TO_EXPR__ КАК Период, "VAT_BOOK_PURCHASES" КАК Регистратор, "19" КАК СчетДт, "" КАК СчетКт, @@ -1553,10 +1553,19 @@ export function buildAddressRecipePlan( .replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES)) .replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES)) : recipe.query_template === "vat_liability_confirmed_tax_period_profile" - ? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll( - "__WHERE_CLAUSE__", - buildManagementWhereClause(filters, "Движения.Период") - ) + ? (() => { + const periodToExpr = + (typeof filters.period_to === "string" && filters.period_to.trim().length > 0 + ? toDateTimeExpr(filters.period_to, true) + : null) ?? + (typeof filters.period_from === "string" && filters.period_from.trim().length > 0 + ? toDateTimeExpr(filters.period_from, true) + : null) ?? + "ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)"; + return VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE + .replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период")) + .replaceAll("__PERIOD_TO_EXPR__", periodToExpr); + })() : recipe.query_template === "vat_payable_confirmed_as_of_balance_profile" ? (() => { const asOfExpr = diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index b49108a..2c5494c 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -1474,16 +1474,31 @@ export function buildAssistantMcpDiscoveryTurnInput( valueFlowSignal, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, entityResolutionSignal: - entityResolutionSignal && - !metadataGroundedDocumentLaneApplicable && - !metadataGroundedMovementLaneApplicable + entityResolutionSignal && + !metadataGroundedDocumentLaneApplicable && + !metadataGroundedMovementLaneApplicable }); + const explicitCurrentCounterpartyCandidate = + normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty) + ? normalizedPredecomposeCounterparty + : null; + const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean( + explicitCurrentCounterpartyCandidate && + (followupSeed.counterparty || followupSeed.discoveryEntity) && + !sameScopedName(explicitCurrentCounterpartyCandidate, followupSeed.counterparty ?? followupSeed.discoveryEntity) && + (valueFlowSignal || + lifecycleSignal || + metadataGroundedDocumentLaneApplicable || + metadataGroundedMovementLaneApplicable) + ); const metadataLaneScopeHint = - rawMetadataScopeHint ?? - followupSeed.metadataScopeHint ?? - followupSeed.discoveryEntity ?? - followupSeed.metadataSelectedEntitySet ?? - null; + explicitCurrentCounterpartyOverridesFollowupEntity + ? null + : rawMetadataScopeHint ?? + followupSeed.metadataScopeHint ?? + followupSeed.discoveryEntity ?? + followupSeed.metadataSelectedEntitySet ?? + null; const metadataScopedLaneWithoutSubject = Boolean( (metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) && !followupSeed.counterparty && @@ -1491,7 +1506,9 @@ export function buildAssistantMcpDiscoveryTurnInput( ); const groundedFollowupEntity = metadataScopedLaneWithoutSubject ? null - : followupSeed.counterparty ?? followupSeed.discoveryEntity; + : explicitCurrentCounterpartyOverridesFollowupEntity + ? null + : followupSeed.counterparty ?? followupSeed.discoveryEntity; const entityCandidates = entityResolutionSignal ? [] : []; if (entityResolutionSignal) { pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate); @@ -1548,6 +1565,27 @@ export function buildAssistantMcpDiscoveryTurnInput( valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty ? currentTurnOrganizationScope ?? followupSeed.organization : null; + if ( + explicitCurrentCounterpartyCandidate && + (valueFlowSignal || lifecycleSignal || metadataGroundedDocumentLaneApplicable || metadataGroundedMovementLaneApplicable) + ) { + for (let index = entityCandidates.length - 1; index >= 0; index -= 1) { + const candidate = entityCandidates[index]; + if (!candidate || sameScopedName(candidate, explicitCurrentCounterpartyCandidate)) { + continue; + } + if ( + (metadataLaneScopeHint && sameScopedName(candidate, metadataLaneScopeHint)) || + (explicitOrganizationScope && sameScopedName(candidate, explicitOrganizationScope)) || + (followupSeed.organization && sameScopedName(candidate, followupSeed.organization)) || + (followupSeed.counterparty && + !sameScopedName(followupSeed.counterparty, explicitCurrentCounterpartyCandidate) && + sameScopedName(candidate, followupSeed.counterparty)) + ) { + entityCandidates.splice(index, 1); + } + } + } if (valueFlowOrganizationStaysScope && explicitOrganizationScope) { for (let index = entityCandidates.length - 1; index >= 0; index -= 1) { if (entityCandidates[index] === explicitOrganizationScope) { diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index b875350..22d922f 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -5177,6 +5177,8 @@ describe("address recipe catalog counterparty filtering", () => { expect(plan.query).toContain("РегистрНакопления.НДСЗаписиКнигиПокупок"); expect(plan.query).toContain("VAT_BOOK_SALES"); expect(plan.query).toContain("VAT_BOOK_PURCHASES"); + expect(plan.query).toContain("ДАТАВРЕМЯ(2019, 12, 31, 23, 59, 59)"); + expect(plan.query).not.toContain("ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период"); }); it("keeps inventory-on-hand phrasing in address lane", () => { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 007e02c..7616e01 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -133,6 +133,50 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected"); }); + it("prefers the explicit current-turn counterparty over stale organization-scoped carryover in net follow-up discovery", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?", + assistantTurnMeaning: { + asked_domain_family: "counterparty_value", + asked_action_family: "net_value_flow", + explicit_entity_candidates: ["Альтернатива Плюс"], + explicit_organization_scope: "ООО Альтернатива Плюс", + explicit_date_scope: "2020", + unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting", + stale_replay_forbidden: true + }, + predecomposeContract: { + entities: { counterparty: "Группа СВК" }, + period: { period_from: "2020-01-01", period_to: "2020-12-31" } + }, + followupContext: { + previous_intent: "counterparty_activity_lifecycle", + previous_filters: { + organization: "ООО Альтернатива Плюс", + counterparty: "Альтернатива Плюс", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "organization", + previous_anchor_value: "ООО Альтернатива Плюс" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "counterparty_value", + asked_action_family: "net_value_flow", + explicit_entity_candidates: ["Группа СВК"], + explicit_organization_scope: "ООО Альтернатива Плюс", + explicit_date_scope: "2020", + unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting", + stale_replay_forbidden: true + }); + expect(result.turn_meaning_ref?.metadata_scope_hint).toBeUndefined(); + expect(result.data_need_graph?.subject_candidates).toEqual(["Группа СВК"]); + }); + it("captures monthly aggregation as part of bidirectional value-flow meaning", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "какое нетто по деньгам с Группа СВК за 2020 год по месяцам: сколько получили и сколько заплатили помесячно?",