From 5520dbccbc3206bd2a59245f0492c8aa94d8fc6f Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 10 Apr 2026 23:09:23 +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=202.39:=20=D0=B2=D1=8B=D0=BD=D0=BE=D1=81=20b?= =?UTF-8?q?ootstrap=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20(=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=B0=20+=20append/persist=20user-item)=20=D0=B8=D0=B7?= =?UTF-8?q?=20handleMessage=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-=D0=B0=D0=B4=D0=B0=D0=BF?= =?UTF-8?q?=D1=82=D0=B5=D1=80.=20=D0=AD=D1=82=D0=BE=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=20=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B0=D0=B7=D0=B3=D1=80=D1=83=D0=B6=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20assistantService.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/TECH/1CLLMARCH-FACT.md | 28 +++++++- .../backend/dist/services/assistantService.js | 34 ++++----- ...ssistantUserTurnBootstrapRuntimeAdapter.js | 34 +++++++++ .../backend/src/services/assistantService.ts | 34 ++++----- ...ssistantUserTurnBootstrapRuntimeAdapter.ts | 64 +++++++++++++++++ ...antUserTurnBootstrapRuntimeAdapter.test.ts | 72 +++++++++++++++++++ 6 files changed, 223 insertions(+), 43 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantUserTurnBootstrapRuntimeAdapter.js create mode 100644 llm_normalizer/backend/src/services/assistantUserTurnBootstrapRuntimeAdapter.ts create mode 100644 llm_normalizer/backend/tests/assistantUserTurnBootstrapRuntimeAdapter.test.ts diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 810552d..cd7fedb 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -1235,7 +1235,33 @@ 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 completed)** +Implemented in current pass (Phase 2.39): +1. Extracted user-turn bootstrap sequence from `assistantService` into dedicated runtime adapter: + - `assistantUserTurnBootstrapRuntimeAdapter.ts` + - introduced: + - `runAssistantUserTurnBootstrapRuntime(...)` +2. Centralized user-turn bootstrap flow (behavior-preserving): + - session ensure + user message normalization/repair; + - user item append + session persistence; + - runtime analysis context projection. +3. Rewired `assistantService.handleMessage(...)` to consume bootstrap runtime output and preserve downstream `questionId` contract usage. +4. Added focused unit tests: + - `assistantUserTurnBootstrapRuntimeAdapter.test.ts` + +Validation: +1. `npm run build` passed. +2. Targeted living/address/deep followup pack passed: + - `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` + +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 completed)** ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index d6a1f25..6ce9fce 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -81,6 +81,7 @@ const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assis const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter")); const assistantLivingChatHandlerRuntimeAdapter_1 = __importStar(require("./assistantLivingChatHandlerRuntimeAdapter")); const assistantLivingChatLlmRuntimeAdapter_1 = __importStar(require("./assistantLivingChatLlmRuntimeAdapter")); +const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter")); const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning")); const iconv_lite_1 = __importDefault(require("iconv-lite")); const DATA_SCOPE_CACHE_TTL_MS = 60_000; @@ -4384,27 +4385,18 @@ class AssistantService { return this.sessions.getSession(sessionId); } async handleMessage(payload) { - const session = this.sessions.ensureSession(payload.session_id); - const sessionId = session.session_id; - const userMessageRaw = String(payload.user_message ?? payload.message ?? "").trim(); - const repairedUserMessage = compactWhitespace(repairAddressMojibake(userMessageRaw)); - const userMessage = repairedUserMessage || userMessageRaw; - const runtimeAnalysisContext = resolveRuntimeAnalysisContext(payload?.context); - const userItem = { - message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, - session_id: sessionId, - role: "user", - text: userMessage, - reply_type: null, - created_at: new Date().toISOString(), - trace_id: null, - debug: null - }; - this.sessions.appendItem(sessionId, userItem); - const sessionAfterUserAppend = this.sessions.getSession(sessionId); - if (sessionAfterUserAppend) { - this.sessionLogger.persistSession(sessionAfterUserAppend); - } + const { session, sessionId, userMessage, runtimeAnalysisContext, userItem } = (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)({ + payload, + ensureSession: (targetSessionId) => this.sessions.ensureSession(targetSessionId), + appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), + getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), + persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), + compactWhitespace, + repairAddressMojibake, + resolveRuntimeAnalysisContext, + messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, + nowIso: () => new Date().toISOString() + }); const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items); const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { const runtime = (0, assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime)({ diff --git a/llm_normalizer/backend/dist/services/assistantUserTurnBootstrapRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantUserTurnBootstrapRuntimeAdapter.js new file mode 100644 index 0000000..857be6e --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantUserTurnBootstrapRuntimeAdapter.js @@ -0,0 +1,34 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runAssistantUserTurnBootstrapRuntime = runAssistantUserTurnBootstrapRuntime; +function runAssistantUserTurnBootstrapRuntime(input) { + const session = input.ensureSession(String(input.payload.session_id ?? "")); + const sessionId = session.session_id; + const userMessageRaw = String(input.payload.user_message ?? input.payload.message ?? "").trim(); + const repairedUserMessage = input.compactWhitespace(input.repairAddressMojibake(userMessageRaw)); + const userMessage = repairedUserMessage || userMessageRaw; + const runtimeAnalysisContext = input.resolveRuntimeAnalysisContext(input.payload?.context); + const userItem = { + message_id: (input.messageIdFactory ?? (() => "msg-unknown"))(), + session_id: sessionId, + role: "user", + text: userMessage, + reply_type: null, + created_at: (input.nowIso ?? (() => new Date().toISOString()))(), + trace_id: null, + debug: null + }; + input.appendItem(sessionId, userItem); + const sessionAfterUserAppend = input.getSession(sessionId); + if (sessionAfterUserAppend) { + input.persistSession(sessionAfterUserAppend); + } + return { + session, + sessionId, + userMessageRaw, + userMessage, + runtimeAnalysisContext, + userItem + }; +} diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index d421265..473d385 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -35,6 +35,7 @@ import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurn import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter"; import * as assistantLivingChatHandlerRuntimeAdapter_1 from "./assistantLivingChatHandlerRuntimeAdapter"; import * as assistantLivingChatLlmRuntimeAdapter_1 from "./assistantLivingChatLlmRuntimeAdapter"; +import * as assistantUserTurnBootstrapRuntimeAdapter_1 from "./assistantUserTurnBootstrapRuntimeAdapter"; import * as assistantQueryPlanning_1 from "./assistantQueryPlanning"; import iconv from "iconv-lite"; const DATA_SCOPE_CACHE_TTL_MS = 60_000; @@ -4339,27 +4340,18 @@ export class AssistantService { return this.sessions.getSession(sessionId); } async handleMessage(payload) { - const session = this.sessions.ensureSession(payload.session_id); - const sessionId = session.session_id; - const userMessageRaw = String(payload.user_message ?? payload.message ?? "").trim(); - const repairedUserMessage = compactWhitespace(repairAddressMojibake(userMessageRaw)); - const userMessage = repairedUserMessage || userMessageRaw; - const runtimeAnalysisContext = resolveRuntimeAnalysisContext(payload?.context); - const userItem = { - message_id: `msg-${(0, nanoid_1.nanoid)(10)}`, - session_id: sessionId, - role: "user", - text: userMessage, - reply_type: null, - created_at: new Date().toISOString(), - trace_id: null, - debug: null - }; - this.sessions.appendItem(sessionId, userItem); - const sessionAfterUserAppend = this.sessions.getSession(sessionId); - if (sessionAfterUserAppend) { - this.sessionLogger.persistSession(sessionAfterUserAppend); - } + const { session, sessionId, userMessage, runtimeAnalysisContext, userItem } = (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)({ + payload, + ensureSession: (targetSessionId) => this.sessions.ensureSession(targetSessionId), + appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), + getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), + persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), + compactWhitespace, + repairAddressMojibake, + resolveRuntimeAnalysisContext, + messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, + nowIso: () => new Date().toISOString() + }); const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items); const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => { const runtime = (0, assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime)({ diff --git a/llm_normalizer/backend/src/services/assistantUserTurnBootstrapRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantUserTurnBootstrapRuntimeAdapter.ts new file mode 100644 index 0000000..022a728 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantUserTurnBootstrapRuntimeAdapter.ts @@ -0,0 +1,64 @@ +import type { AssistantConversationItem, AssistantSessionState } from "../types/assistant"; + +export interface AssistantUserTurnBootstrapPayloadLike { + session_id?: unknown; + user_message?: unknown; + message?: unknown; + context?: unknown; +} + +export interface RunAssistantUserTurnBootstrapRuntimeInput { + payload: AssistantUserTurnBootstrapPayloadLike; + ensureSession: (sessionId: string) => AssistantSessionState; + appendItem: (sessionId: string, item: AssistantConversationItem) => void; + getSession: (sessionId: string) => AssistantSessionState | null; + persistSession: (session: AssistantSessionState) => void; + compactWhitespace: (value: unknown) => string; + repairAddressMojibake: (value: unknown) => string; + resolveRuntimeAnalysisContext: (value: unknown) => { as_of_date: string | null }; + messageIdFactory?: () => string; + nowIso?: () => string; +} + +export interface RunAssistantUserTurnBootstrapRuntimeOutput { + session: AssistantSessionState; + sessionId: string; + userMessageRaw: string; + userMessage: string; + runtimeAnalysisContext: { as_of_date: string | null }; + userItem: AssistantConversationItem; +} + +export function runAssistantUserTurnBootstrapRuntime( + input: RunAssistantUserTurnBootstrapRuntimeInput +): RunAssistantUserTurnBootstrapRuntimeOutput { + const session = input.ensureSession(String(input.payload.session_id ?? "")); + const sessionId = session.session_id; + const userMessageRaw = String(input.payload.user_message ?? input.payload.message ?? "").trim(); + const repairedUserMessage = input.compactWhitespace(input.repairAddressMojibake(userMessageRaw)); + const userMessage = repairedUserMessage || userMessageRaw; + const runtimeAnalysisContext = input.resolveRuntimeAnalysisContext(input.payload?.context); + const userItem: AssistantConversationItem = { + message_id: (input.messageIdFactory ?? (() => "msg-unknown"))(), + session_id: sessionId, + role: "user", + text: userMessage, + reply_type: null, + created_at: (input.nowIso ?? (() => new Date().toISOString()))(), + trace_id: null, + debug: null + }; + input.appendItem(sessionId, userItem); + const sessionAfterUserAppend = input.getSession(sessionId); + if (sessionAfterUserAppend) { + input.persistSession(sessionAfterUserAppend); + } + return { + session, + sessionId, + userMessageRaw, + userMessage, + runtimeAnalysisContext, + userItem + }; +} diff --git a/llm_normalizer/backend/tests/assistantUserTurnBootstrapRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantUserTurnBootstrapRuntimeAdapter.test.ts new file mode 100644 index 0000000..8b6270d --- /dev/null +++ b/llm_normalizer/backend/tests/assistantUserTurnBootstrapRuntimeAdapter.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from "vitest"; +import { runAssistantUserTurnBootstrapRuntime } from "../src/services/assistantUserTurnBootstrapRuntimeAdapter"; + +describe("assistant user turn bootstrap runtime adapter", () => { + it("normalizes user message, appends user turn and persists session", () => { + const session = { + session_id: "asst-1", + items: [] + } as any; + const appendItem = vi.fn(); + const getSession = vi.fn(() => session); + const persistSession = vi.fn(); + + const output = runAssistantUserTurnBootstrapRuntime({ + payload: { + session_id: "asst-1", + user_message: " source " + }, + ensureSession: () => session, + appendItem, + getSession, + persistSession, + compactWhitespace: (value: unknown) => String(value ?? "").replace(/\s+/g, " ").trim(), + repairAddressMojibake: () => " fixed user text ", + resolveRuntimeAnalysisContext: () => ({ as_of_date: "2020-07-31" }), + messageIdFactory: () => "msg-fixed", + nowIso: () => "2026-04-10T00:00:00.000Z" + }); + + expect(output.session).toBe(session); + expect(output.sessionId).toBe("asst-1"); + expect(output.userMessageRaw).toBe("source"); + expect(output.userMessage).toBe("fixed user text"); + expect(output.runtimeAnalysisContext).toEqual({ as_of_date: "2020-07-31" }); + expect(appendItem).toHaveBeenCalledWith( + "asst-1", + expect.objectContaining({ + message_id: "msg-fixed", + role: "user", + text: "fixed user text", + created_at: "2026-04-10T00:00:00.000Z" + }) + ); + expect(getSession).toHaveBeenCalledWith("asst-1"); + expect(persistSession).toHaveBeenCalledWith(session); + }); + + it("falls back to raw message when repaired text is empty", () => { + const session = { + session_id: "asst-2", + items: [] + } as any; + + const output = runAssistantUserTurnBootstrapRuntime({ + payload: { + session_id: "asst-2", + message: " raw fallback " + }, + ensureSession: () => session, + appendItem: () => undefined, + getSession: () => null, + persistSession: () => undefined, + compactWhitespace: () => "", + repairAddressMojibake: () => "", + resolveRuntimeAnalysisContext: () => ({ as_of_date: null }) + }); + + expect(output.userMessageRaw).toBe("raw fallback"); + expect(output.userMessage).toBe("raw fallback"); + expect(output.runtimeAnalysisContext).toEqual({ as_of_date: null }); + }); +});