From af4b01d954db1bb99c05bf1270fa1290cf51b1ce Mon Sep 17 00:00:00 2001 From: dctouch Date: Mon, 25 May 2026 23:15:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=9D=D0=94=D0=A1=20fol?= =?UTF-8?q?low-up=20=D0=B7=D0=B0=20=D0=BC=D0=B5=D1=81=D1=8F=D1=86=20=D0=B2?= =?UTF-8?q?=20=D0=BD=D0=B0=D0=BB=D0=BE=D0=B3=D0=BE=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BA=D0=B2=D0=B0=D1=80=D1=82=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/services/addressQueryService.js | 22 ++++++++++++++- .../src/services/addressQueryService.ts | 24 ++++++++++++++++- .../tests/addressQueryRuntimeM23.test.ts | 27 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/llm_normalizer/backend/dist/services/addressQueryService.js b/llm_normalizer/backend/dist/services/addressQueryService.js index c0848ac..db1b341 100644 --- a/llm_normalizer/backend/dist/services/addressQueryService.js +++ b/llm_normalizer/backend/dist/services/addressQueryService.js @@ -211,6 +211,22 @@ function deriveTaxQuarterWindowForDate(value) { period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}` }; } +function isFullCalendarMonthWindow(periodFrom, periodTo) { + const fromIso = normalizeIsoDateForQuery(periodFrom); + const toIso = normalizeIsoDateForQuery(periodTo); + if (!fromIso || !toIso) { + return false; + } + const fromMatch = fromIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + const toMatch = toIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!fromMatch || !toMatch || fromMatch[1] !== toMatch[1] || fromMatch[2] !== toMatch[2] || fromMatch[3] !== "01") { + return false; + } + const year = Number(fromMatch[1]); + const month = Number(fromMatch[2]); + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return toMatch[3] === String(lastDay).padStart(2, "0"); +} function deriveMonthWindowForDate(value) { const isoDate = normalizeIsoDateForQuery(value); if (!isoDate) { @@ -3003,8 +3019,12 @@ class AddressQueryService { }) }); } + const vatTaxPeriodFromCarriedMonth = intent.intent === "vat_liability_confirmed_for_tax_period" && + (filters.warnings.includes("period_from_from_followup_context") || + filters.warnings.includes("period_to_from_followup_context")) && + isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to); if (intent.intent === "vat_liability_confirmed_for_tax_period" && - filters.warnings.includes("period_derived_from_month_phrase")) { + (filters.warnings.includes("period_derived_from_month_phrase") || vatTaxPeriodFromCarriedMonth)) { const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to); if (taxQuarterWindow) { filters.extracted_filters = { diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index b49e6a4..43099eb 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -355,6 +355,23 @@ function deriveTaxQuarterWindowForDate(value: unknown): { period_from: string; p }; } +function isFullCalendarMonthWindow(periodFrom: unknown, periodTo: unknown): boolean { + const fromIso = normalizeIsoDateForQuery(periodFrom); + const toIso = normalizeIsoDateForQuery(periodTo); + if (!fromIso || !toIso) { + return false; + } + const fromMatch = fromIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + const toMatch = toIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!fromMatch || !toMatch || fromMatch[1] !== toMatch[1] || fromMatch[2] !== toMatch[2] || fromMatch[3] !== "01") { + return false; + } + const year = Number(fromMatch[1]); + const month = Number(fromMatch[2]); + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return toMatch[3] === String(lastDay).padStart(2, "0"); +} + function deriveMonthWindowForDate(value: unknown): { period_from: string; period_to: string } | null { const isoDate = normalizeIsoDateForQuery(value); if (!isoDate) { @@ -3716,9 +3733,14 @@ export class AddressQueryService { }) }); } + const vatTaxPeriodFromCarriedMonth = + intent.intent === "vat_liability_confirmed_for_tax_period" && + (filters.warnings.includes("period_from_from_followup_context") || + filters.warnings.includes("period_to_from_followup_context")) && + isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to); if ( intent.intent === "vat_liability_confirmed_for_tax_period" && - filters.warnings.includes("period_derived_from_month_phrase") + (filters.warnings.includes("period_derived_from_month_phrase") || vatTaxPeriodFromCarriedMonth) ) { const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to); if (taxQuarterWindow) { diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index a417c41..e5f0de0 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -4102,6 +4102,33 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 9000 expect(result?.debug.route_expectation_status).toBe("matched"); }); + it("normalizes carried monthly debt period into VAT tax quarter for same-period follow-up", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("а какой ндс мы должны примерно заплатить за этот период?", { + followupContext: { + previous_intent: "receivables_confirmed_as_of_date", + previous_filters: { + organization: 'ООО "Альтернатива Плюс"', + period_from: "2017-05-01", + period_to: "2017-05-31", + as_of_date: "2017-05-31" + }, + previous_anchor_type: "organization", + previous_anchor_value: 'ООО "Альтернатива Плюс"', + target_intent: "vat_liability_confirmed_for_tax_period" + } + }); + + expect(result?.handled).toBe(true); + expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period"); + expect(result?.debug.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1"); + expect(result?.debug.extracted_filters.period_from).toBe("2017-04-01"); + expect(result?.debug.extracted_filters.period_to).toBe("2017-06-30"); + expect(result?.debug.reasons).toContain("period_from_from_followup_context"); + expect(result?.debug.reasons).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability"); + expect(String(result?.reply_text ?? "")).toContain("01.04.2017..30.06.2017"); + }, 35000); + it("routes customer lifecycle question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?");