From 5e4cc0ed672cbf08823eac2dbae29cd7c3d2805a Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 11 Apr 2026 00:52:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=9B=D0=9E=D0=91=D0=90=D0=9B=D0=AC?= =?UTF-8?q?=D0=9D=D0=AB=D0=99=20=D0=A0=D0=95=D0=A4=D0=90=D0=9A=D0=A2=D0=9E?= =?UTF-8?q?=D0=A0=D0=98=D0=9D=D0=93=20=D0=90=D0=A0=D0=A5=D0=98=D0=A2=D0=95?= =?UTF-8?q?=D0=9A=D0=A2=D0=A3=D0=A0=D0=AB=20-=20=D0=A0=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D1=8D=D1=82=D0=B0?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=20=202.50:=20=D0=B2=D1=8B=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=20resolveSessionOrganizationScopeContext(...)=20+=20related=20?= =?UTF-8?q?scope=20sanitation=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20runtime-adapter,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=20=D0=B4=D0=BE=D0=B1=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=85=D0=BD=D0=B8=D0=B9=20=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20assistantService.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TECH/1CLLMARCH-FACT.md | 40 +++++++- ...ssistantOrganizationScopeRuntimeAdapter.js | 32 ++++++ .../backend/dist/services/assistantService.js | 37 +++---- ...ssistantOrganizationScopeRuntimeAdapter.ts | 61 ++++++++++++ .../backend/src/services/assistantService.ts | 37 +++---- ...antOrganizationScopeRuntimeAdapter.test.ts | 99 +++++++++++++++++++ 6 files changed, 261 insertions(+), 45 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantOrganizationScopeRuntimeAdapter.js create mode 100644 llm_normalizer/backend/src/services/assistantOrganizationScopeRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/tests/assistantOrganizationScopeRuntimeAdapter.test.ts diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index d540fb1..f78ce41 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -1572,7 +1572,45 @@ Validation: - `assistantDeepTurnPackagingRuntimeAdapter.test.ts` - `assistantWave10SettlementCorrectiveRegression.test.ts` -Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 + 2.43 + 2.44 + 2.45 + 2.46 + 2.47 + 2.48 + 2.49 completed)** +Implemented in current pass (Phase 2.50): +1. Extracted organization-scope runtime logic from `assistantService` into dedicated adapter: + - `assistantOrganizationScopeRuntimeAdapter.ts` + - introduced: + - `resolveSessionOrganizationScopeContextRuntime(...)` + - `mergeFollowupContextWithOrganizationScopeRuntime(...)` +2. Rewired `assistantService` scope helpers to delegate through the adapter (behavior-preserving): + - `resolveSessionOrganizationScopeContext(...)` now uses runtime adapter with existing extraction/scoring/sanitization helpers; + - `mergeFollowupContextWithOrganizationScope(...)` now uses runtime adapter while preserving existing normalization/toNonEmpty semantics. +3. Added focused unit tests: + - `assistantOrganizationScopeRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `assistantOrganizationScopeRuntimeAdapter.test.ts` + - `assistantTurnRuntimeDepsAdapter.test.ts` + - `assistantTurnRuntimeInputBuilder.test.ts` + - `assistantTurnAttemptRuntimeAdapter.test.ts` + - `assistantAddressAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisAttemptRuntimeAdapter.test.ts` + - `assistantDeepTurnAnalysisRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseAttemptRuntimeAdapter.test.ts` + - `assistantLivingChatAttemptRuntimeAdapter.test.ts` + - `assistantAddressLaneAttemptRuntimeAdapter.test.ts` + - `assistantUserTurnBootstrapRuntimeAdapter.test.ts` + - `assistantLivingChatLlmRuntimeAdapter.test.ts` + - `assistantLivingChatHandlerRuntimeAdapter.test.ts` + - `assistantLivingChatRuntimeAdapter.test.ts` + - `assistantAddressRuntimeAdapter.test.ts` + - `assistantAddressLaneResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnResponseRuntimeAdapter.test.ts` + - `assistantDeepTurnPackagingRuntimeAdapter.test.ts` + - `assistantWave10SettlementCorrectiveRegression.test.ts` + - `assistantLivingChatMode.test.ts` + +Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 + 2.43 + 2.44 + 2.45 + 2.46 + 2.47 + 2.48 + 2.49 + 2.50 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantOrganizationScopeRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantOrganizationScopeRuntimeAdapter.js new file mode 100644 index 0000000..525d947 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantOrganizationScopeRuntimeAdapter.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveSessionOrganizationScopeContextRuntime = resolveSessionOrganizationScopeContextRuntime; +exports.mergeFollowupContextWithOrganizationScopeRuntime = mergeFollowupContextWithOrganizationScopeRuntime; +function resolveSessionOrganizationScopeContextRuntime(input) { + const knownOrganizations = input.extractKnownOrganizationsFromHistory(input.items); + const selectedOrganization = input.resolveOrganizationSelectionFromMessage(input.userMessage, knownOrganizations); + const lastActiveOrganization = input.findLastAssistantActiveOrganization(input.items); + const activeOrganization = selectedOrganization ?? input.normalizeOrganizationScopeValue(lastActiveOrganization); + return { + knownOrganizations, + selectedOrganization, + activeOrganization + }; +} +function mergeFollowupContextWithOrganizationScopeRuntime(input) { + const normalizedOrganization = input.normalizeOrganizationScopeValue(input.organization); + const hasBase = input.followupContext && typeof input.followupContext === "object"; + const base = hasBase ? { ...input.followupContext } : {}; + if (!normalizedOrganization) { + return hasBase ? base : null; + } + const previousFiltersRaw = base.previous_filters; + const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" + ? { ...previousFiltersRaw } + : {}; + if (!input.toNonEmptyString(previousFilters.organization)) { + previousFilters.organization = normalizedOrganization; + } + base.previous_filters = previousFilters; + return base; +} diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index c1699e6..ac754ba 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -65,6 +65,7 @@ const assistantCanon_1 = __importStar(require("./assistantCanon")); const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter")); const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding")); const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter")); +const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter")); const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter")); const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter")); const assistantTurnRuntimeInputBuilder_1 = __importStar(require("./assistantTurnRuntimeInputBuilder")); @@ -3772,30 +3773,22 @@ function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations return best.organization; } function resolveSessionOrganizationScopeContext(userMessage, items) { - const knownOrganizations = extractKnownOrganizationsFromHistory(items); - const selectedOrganization = resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations); - const lastActiveOrganization = findLastAssistantActiveOrganization(items); - const activeOrganization = selectedOrganization ?? normalizeOrganizationScopeValue(lastActiveOrganization); - return { - knownOrganizations, - selectedOrganization, - activeOrganization - }; + return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({ + userMessage, + items, + extractKnownOrganizationsFromHistory, + resolveOrganizationSelectionFromMessage, + findLastAssistantActiveOrganization, + normalizeOrganizationScopeValue + }); } function mergeFollowupContextWithOrganizationScope(followupContext, organization) { - const normalizedOrganization = normalizeOrganizationScopeValue(organization); - const base = followupContext && typeof followupContext === "object" ? { ...followupContext } : {}; - if (!normalizedOrganization) { - return followupContext && typeof followupContext === "object" ? base : null; - } - const previousFilters = base.previous_filters && typeof base.previous_filters === "object" - ? { ...base.previous_filters } - : {}; - if (!toNonEmptyString(previousFilters.organization)) { - previousFilters.organization = normalizedOrganization; - } - base.previous_filters = previousFilters; - return base; + return (0, assistantOrganizationScopeRuntimeAdapter_1.mergeFollowupContextWithOrganizationScopeRuntime)({ + followupContext, + organization, + normalizeOrganizationScopeValue, + toNonEmptyString + }); } function resolveSessionOrganizationScopeContextForTests(userMessage, items) { return resolveSessionOrganizationScopeContext(userMessage, items); diff --git a/llm_normalizer/backend/src/services/assistantOrganizationScopeRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantOrganizationScopeRuntimeAdapter.ts new file mode 100644 index 0000000..a0ac02d --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantOrganizationScopeRuntimeAdapter.ts @@ -0,0 +1,61 @@ +export interface AssistantSessionOrganizationScopeContext { + knownOrganizations: string[]; + selectedOrganization: string | null; + activeOrganization: string | null; +} + +export interface ResolveSessionOrganizationScopeContextRuntimeInput { + userMessage: string; + items: ItemType[]; + extractKnownOrganizationsFromHistory: (items: ItemType[]) => string[]; + resolveOrganizationSelectionFromMessage: (userMessage: string, knownOrganizations: string[]) => string | null; + findLastAssistantActiveOrganization: (items: ItemType[]) => string | null; + normalizeOrganizationScopeValue: (value: unknown) => string | null; +} + +export interface MergeFollowupContextWithOrganizationScopeRuntimeInput { + followupContext: unknown; + organization: unknown; + normalizeOrganizationScopeValue: (value: unknown) => string | null; + toNonEmptyString: (value: unknown) => string | null; +} + +export function resolveSessionOrganizationScopeContextRuntime( + input: ResolveSessionOrganizationScopeContextRuntimeInput +): AssistantSessionOrganizationScopeContext { + const knownOrganizations = input.extractKnownOrganizationsFromHistory(input.items); + const selectedOrganization = input.resolveOrganizationSelectionFromMessage( + input.userMessage, + knownOrganizations + ); + const lastActiveOrganization = input.findLastAssistantActiveOrganization(input.items); + const activeOrganization = selectedOrganization ?? input.normalizeOrganizationScopeValue(lastActiveOrganization); + + return { + knownOrganizations, + selectedOrganization, + activeOrganization + }; +} + +export function mergeFollowupContextWithOrganizationScopeRuntime( + input: MergeFollowupContextWithOrganizationScopeRuntimeInput +): Record | null { + const normalizedOrganization = input.normalizeOrganizationScopeValue(input.organization); + const hasBase = input.followupContext && typeof input.followupContext === "object"; + const base = hasBase ? { ...(input.followupContext as Record) } : {}; + if (!normalizedOrganization) { + return hasBase ? base : null; + } + + const previousFiltersRaw = base.previous_filters; + const previousFilters = + previousFiltersRaw && typeof previousFiltersRaw === "object" + ? { ...(previousFiltersRaw as Record) } + : {}; + if (!input.toNonEmptyString(previousFilters.organization)) { + previousFilters.organization = normalizedOrganization; + } + base.previous_filters = previousFilters; + return base; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index ff134d8..b1abc96 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -19,6 +19,7 @@ import * as assistantCanon_1 from "./assistantCanon"; import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAttemptRuntimeAdapter"; import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter"; +import * as assistantOrganizationScopeRuntimeAdapter_1 from "./assistantOrganizationScopeRuntimeAdapter"; import * as assistantTurnAttemptRuntimeAdapter_1 from "./assistantTurnAttemptRuntimeAdapter"; import * as assistantTurnRuntimeDepsAdapter_1 from "./assistantTurnRuntimeDepsAdapter"; import * as assistantTurnRuntimeInputBuilder_1 from "./assistantTurnRuntimeInputBuilder"; @@ -3727,30 +3728,22 @@ function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations return best.organization; } function resolveSessionOrganizationScopeContext(userMessage, items) { - const knownOrganizations = extractKnownOrganizationsFromHistory(items); - const selectedOrganization = resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations); - const lastActiveOrganization = findLastAssistantActiveOrganization(items); - const activeOrganization = selectedOrganization ?? normalizeOrganizationScopeValue(lastActiveOrganization); - return { - knownOrganizations, - selectedOrganization, - activeOrganization - }; + return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({ + userMessage, + items, + extractKnownOrganizationsFromHistory, + resolveOrganizationSelectionFromMessage, + findLastAssistantActiveOrganization, + normalizeOrganizationScopeValue + }); } function mergeFollowupContextWithOrganizationScope(followupContext, organization) { - const normalizedOrganization = normalizeOrganizationScopeValue(organization); - const base = followupContext && typeof followupContext === "object" ? { ...followupContext } : {}; - if (!normalizedOrganization) { - return followupContext && typeof followupContext === "object" ? base : null; - } - const previousFilters = base.previous_filters && typeof base.previous_filters === "object" - ? { ...base.previous_filters } - : {}; - if (!toNonEmptyString(previousFilters.organization)) { - previousFilters.organization = normalizedOrganization; - } - base.previous_filters = previousFilters; - return base; + return (0, assistantOrganizationScopeRuntimeAdapter_1.mergeFollowupContextWithOrganizationScopeRuntime)({ + followupContext, + organization, + normalizeOrganizationScopeValue, + toNonEmptyString + }); } export function resolveSessionOrganizationScopeContextForTests(userMessage, items) { return resolveSessionOrganizationScopeContext(userMessage, items); diff --git a/llm_normalizer/backend/tests/assistantOrganizationScopeRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantOrganizationScopeRuntimeAdapter.test.ts new file mode 100644 index 0000000..af95d43 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantOrganizationScopeRuntimeAdapter.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it, vi } from "vitest"; +import { + mergeFollowupContextWithOrganizationScopeRuntime, + resolveSessionOrganizationScopeContextRuntime +} from "../src/services/assistantOrganizationScopeRuntimeAdapter"; + +describe("assistant organization scope runtime adapter", () => { + it("resolves selected organization from user message and promotes it to active scope", () => { + const extractKnownOrganizationsFromHistory = vi.fn(() => ["Org A", "Org B"]); + const resolveOrganizationSelectionFromMessage = vi.fn(() => "Org B"); + const findLastAssistantActiveOrganization = vi.fn(() => "Org A"); + const normalizeOrganizationScopeValue = vi.fn((value: unknown) => + typeof value === "string" && value.trim() ? value.trim() : null + ); + + const context = resolveSessionOrganizationScopeContextRuntime({ + userMessage: "по Org B покажи", + items: [] as any[], + extractKnownOrganizationsFromHistory, + resolveOrganizationSelectionFromMessage, + findLastAssistantActiveOrganization, + normalizeOrganizationScopeValue + }); + + expect(context).toEqual({ + knownOrganizations: ["Org A", "Org B"], + selectedOrganization: "Org B", + activeOrganization: "Org B" + }); + }); + + it("falls back active organization to last assistant scope when selection is absent", () => { + const normalizeOrganizationScopeValue = vi.fn((value: unknown) => + typeof value === "string" ? value.trim() : null + ); + + const context = resolveSessionOrganizationScopeContextRuntime({ + userMessage: "просто обсуждаем", + items: [] as any[], + extractKnownOrganizationsFromHistory: () => ["Org A"], + resolveOrganizationSelectionFromMessage: () => null, + findLastAssistantActiveOrganization: () => "Org A", + normalizeOrganizationScopeValue + }); + + expect(context).toEqual({ + knownOrganizations: ["Org A"], + selectedOrganization: null, + activeOrganization: "Org A" + }); + expect(normalizeOrganizationScopeValue).toHaveBeenCalledWith("Org A"); + }); + + it("merges organization into followup previous filters when organization is missing", () => { + const merged = mergeFollowupContextWithOrganizationScopeRuntime({ + followupContext: { + previous_filters: { + period: "2020-07" + } + }, + organization: " Org A ", + normalizeOrganizationScopeValue: (value: unknown) => + typeof value === "string" && value.trim() ? value.trim() : null, + toNonEmptyString: (value: unknown) => + typeof value === "string" && value.trim().length > 0 ? value.trim() : null + }); + + expect(merged).toEqual({ + previous_filters: { + period: "2020-07", + organization: "Org A" + } + }); + }); + + it("keeps existing organization in followup filters and returns null for empty context without org", () => { + const preserved = mergeFollowupContextWithOrganizationScopeRuntime({ + followupContext: { + previous_filters: { + organization: "Org Existing" + } + }, + organization: "Org A", + normalizeOrganizationScopeValue: (value: unknown) => + typeof value === "string" && value.trim() ? value.trim() : null, + toNonEmptyString: (value: unknown) => + typeof value === "string" && value.trim().length > 0 ? value.trim() : null + }); + const empty = mergeFollowupContextWithOrganizationScopeRuntime({ + followupContext: null, + organization: "", + normalizeOrganizationScopeValue: () => null, + toNonEmptyString: () => null + }); + + expect((preserved as any).previous_filters.organization).toBe("Org Existing"); + expect(empty).toBeNull(); + }); +});