ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.26 вынос runtime-блок living chat из assistantService в отдельный adapter
This commit is contained in:
parent
205daeccc5
commit
828c0ef378
|
|
@ -895,7 +895,34 @@ Validation:
|
|||
- `assistantMcpRuntimeBridge.test.ts`
|
||||
- `assistantAddressFollowupContext.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 completed)**
|
||||
Implemented in current pass (Phase 2.26):
|
||||
1. Extracted living-chat finalization/response tail from `assistantService` into dedicated runtime adapter:
|
||||
- `assistantLivingChatTurnFinalizeRuntimeAdapter.ts`
|
||||
- introduced:
|
||||
- `finalizeAssistantLivingChatTurn(...)`
|
||||
2. Centralized living-chat finalization runtime sequence (behavior-preserving):
|
||||
- assistant item creation for chat lane;
|
||||
- structured `assistant_message_chat` processed-event payload build;
|
||||
- turn commit/persist/log via shared commit runtime adapter;
|
||||
- API success response assembly from committed conversation state.
|
||||
3. Rewired `assistantService` `tryHandleLivingChat(...)` finalize path to consume adapter output (behavior-preserving).
|
||||
4. Added focused unit tests:
|
||||
- `assistantLivingChatTurnFinalizeRuntimeAdapter.test.ts`
|
||||
|
||||
Validation:
|
||||
1. `npm run build` passed.
|
||||
2. Targeted living/address/deep finalize pack passed:
|
||||
- `assistantLivingChatTurnFinalizeRuntimeAdapter.test.ts`
|
||||
- `assistantAddressTurnFinalizeRuntimeAdapter.test.ts`
|
||||
- `assistantDeepTurnFinalizeRuntimeAdapter.test.ts`
|
||||
- `assistantLivingChatMode.test.ts`
|
||||
- `assistantLivingRouter.test.ts`
|
||||
3. Additional safety regressions passed:
|
||||
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||
- `assistantMcpRuntimeBridge.test.ts`
|
||||
- `assistantAddressFollowupContext.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 completed)**
|
||||
|
||||
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
||||
|
||||
|
|
|
|||
63
llm_normalizer/backend/dist/services/assistantLivingChatTurnFinalizeRuntimeAdapter.js
vendored
Normal file
63
llm_normalizer/backend/dist/services/assistantLivingChatTurnFinalizeRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.finalizeAssistantLivingChatTurn = finalizeAssistantLivingChatTurn;
|
||||
const nanoid_1 = require("nanoid");
|
||||
const assistantTurnCommitRuntimeAdapter_1 = require("./assistantTurnCommitRuntimeAdapter");
|
||||
function toTraceId(debug) {
|
||||
const value = debug?.trace_id;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
function finalizeAssistantLivingChatTurn(input) {
|
||||
const nowIso = input.nowIso ?? (() => new Date().toISOString());
|
||||
const messageIdFactory = input.messageIdFactory ?? (() => `msg-${(0, nanoid_1.nanoid)(10)}`);
|
||||
const commitSafe = input.commitFn ?? assistantTurnCommitRuntimeAdapter_1.commitAssistantTurnAndLog;
|
||||
const assistantItem = {
|
||||
message_id: messageIdFactory(),
|
||||
session_id: input.sessionId,
|
||||
role: "assistant",
|
||||
text: input.assistantReply,
|
||||
reply_type: input.replyType,
|
||||
created_at: nowIso(),
|
||||
trace_id: toTraceId(input.debug),
|
||||
debug: input.debug
|
||||
};
|
||||
const logDetails = {
|
||||
session_id: input.sessionId,
|
||||
message_id: assistantItem.message_id,
|
||||
user_message: input.userMessage,
|
||||
living_router_mode: input.modeDecision?.mode ?? "chat",
|
||||
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
trace_id: assistantItem.trace_id
|
||||
};
|
||||
const commitResult = commitSafe({
|
||||
sessionId: input.sessionId,
|
||||
assistantItem,
|
||||
eventType: "assistant_message_chat",
|
||||
logDetails,
|
||||
appendItem: input.appendItem,
|
||||
getSession: input.getSession,
|
||||
persistSession: input.persistSession,
|
||||
cloneConversation: input.cloneConversation,
|
||||
logEvent: input.logEvent
|
||||
});
|
||||
const response = {
|
||||
ok: true,
|
||||
session_id: input.sessionId,
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
conversation_item: assistantItem,
|
||||
debug: input.debug,
|
||||
conversation: commitResult.conversation
|
||||
};
|
||||
return {
|
||||
assistantItem,
|
||||
commitResult,
|
||||
response
|
||||
};
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@ const assistantDeepTurnGroundingRuntimeAdapter_1 = __importStar(require("./assis
|
|||
const assistantDeepTurnPackagingRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPackagingRuntimeAdapter"));
|
||||
const assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPlanRuntimeAdapter"));
|
||||
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
||||
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
||||
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||
|
|
@ -4604,49 +4605,21 @@ class AssistantService {
|
|||
normalized: null,
|
||||
normalizer_output: null
|
||||
};
|
||||
const assistantItem = {
|
||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: chatText,
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: debug.trace_id,
|
||||
debug
|
||||
};
|
||||
this.sessions.appendItem(sessionId, assistantItem);
|
||||
const current = this.sessions.getSession(sessionId);
|
||||
if (current) {
|
||||
this.sessionLogger.persistSession(current);
|
||||
}
|
||||
const conversation = cloneItems(current?.items ?? []);
|
||||
(0, log_1.logJson)({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
service: "assistant_loop",
|
||||
message: "assistant_message_processed",
|
||||
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
||||
sessionId,
|
||||
eventType: "assistant_message_chat",
|
||||
details: {
|
||||
session_id: sessionId,
|
||||
message_id: assistantItem.message_id,
|
||||
user_message: userMessage,
|
||||
living_router_mode: modeDecision?.mode ?? "chat",
|
||||
living_router_reason: modeDecision?.reason ?? "living_chat_signal_detected",
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
trace_id: assistantItem.trace_id
|
||||
}
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
session_id: sessionId,
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
conversation_item: assistantItem,
|
||||
userMessage,
|
||||
assistantReply: chatText,
|
||||
replyType: "factual_with_explanation",
|
||||
debug,
|
||||
conversation
|
||||
};
|
||||
modeDecision,
|
||||
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
||||
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
|
||||
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
|
||||
cloneConversation: (items) => cloneItems(items),
|
||||
logEvent: (payload) => (0, log_1.logJson)(payload),
|
||||
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
|
||||
});
|
||||
return finalization.response;
|
||||
}
|
||||
catch (error) {
|
||||
(0, log_1.logJson)({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import { nanoid } from "nanoid";
|
||||
import type {
|
||||
AssistantConversationItem,
|
||||
AssistantDebugPayload,
|
||||
AssistantMessageResponsePayload,
|
||||
AssistantReplyType
|
||||
} from "../types/assistant";
|
||||
import type { CommitAssistantTurnAndLogOutput } from "./assistantTurnCommitRuntimeAdapter";
|
||||
import { commitAssistantTurnAndLog } from "./assistantTurnCommitRuntimeAdapter";
|
||||
|
||||
export interface LivingModeDecisionForFinalize {
|
||||
mode?: string | null;
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
export interface FinalizeAssistantLivingChatTurnInput {
|
||||
sessionId: string;
|
||||
userMessage: string;
|
||||
assistantReply: string;
|
||||
replyType: AssistantReplyType;
|
||||
debug: AssistantDebugPayload | Record<string, unknown>;
|
||||
modeDecision?: LivingModeDecisionForFinalize | null;
|
||||
appendItem: Parameters<typeof commitAssistantTurnAndLog>[0]["appendItem"];
|
||||
getSession: Parameters<typeof commitAssistantTurnAndLog>[0]["getSession"];
|
||||
persistSession: Parameters<typeof commitAssistantTurnAndLog>[0]["persistSession"];
|
||||
cloneConversation: Parameters<typeof commitAssistantTurnAndLog>[0]["cloneConversation"];
|
||||
logEvent: Parameters<typeof commitAssistantTurnAndLog>[0]["logEvent"];
|
||||
messageIdFactory?: () => string;
|
||||
nowIso?: () => string;
|
||||
commitFn?: typeof commitAssistantTurnAndLog;
|
||||
}
|
||||
|
||||
export interface FinalizeAssistantLivingChatTurnOutput {
|
||||
assistantItem: AssistantConversationItem;
|
||||
commitResult: CommitAssistantTurnAndLogOutput;
|
||||
response: AssistantMessageResponsePayload;
|
||||
}
|
||||
|
||||
function toTraceId(debug: AssistantDebugPayload | Record<string, unknown>): string | null {
|
||||
const value = (debug as Record<string, unknown> | null | undefined)?.trace_id;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
export function finalizeAssistantLivingChatTurn(
|
||||
input: FinalizeAssistantLivingChatTurnInput
|
||||
): FinalizeAssistantLivingChatTurnOutput {
|
||||
const nowIso = input.nowIso ?? (() => new Date().toISOString());
|
||||
const messageIdFactory = input.messageIdFactory ?? (() => `msg-${nanoid(10)}`);
|
||||
const commitSafe = input.commitFn ?? commitAssistantTurnAndLog;
|
||||
const assistantItem: AssistantConversationItem = {
|
||||
message_id: messageIdFactory(),
|
||||
session_id: input.sessionId,
|
||||
role: "assistant",
|
||||
text: input.assistantReply,
|
||||
reply_type: input.replyType,
|
||||
created_at: nowIso(),
|
||||
trace_id: toTraceId(input.debug),
|
||||
debug: input.debug as AssistantDebugPayload
|
||||
};
|
||||
const logDetails = {
|
||||
session_id: input.sessionId,
|
||||
message_id: assistantItem.message_id,
|
||||
user_message: input.userMessage,
|
||||
living_router_mode: input.modeDecision?.mode ?? "chat",
|
||||
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
trace_id: assistantItem.trace_id
|
||||
};
|
||||
const commitResult = commitSafe({
|
||||
sessionId: input.sessionId,
|
||||
assistantItem,
|
||||
eventType: "assistant_message_chat",
|
||||
logDetails,
|
||||
appendItem: input.appendItem,
|
||||
getSession: input.getSession,
|
||||
persistSession: input.persistSession,
|
||||
cloneConversation: input.cloneConversation,
|
||||
logEvent: input.logEvent
|
||||
});
|
||||
const response: AssistantMessageResponsePayload = {
|
||||
ok: true,
|
||||
session_id: input.sessionId,
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type as AssistantReplyType,
|
||||
conversation_item: assistantItem,
|
||||
debug: input.debug as AssistantDebugPayload,
|
||||
conversation: commitResult.conversation
|
||||
};
|
||||
return {
|
||||
assistantItem,
|
||||
commitResult,
|
||||
response
|
||||
};
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import * as assistantDeepTurnGroundingRuntimeAdapter_1 from "./assistantDeepTurn
|
|||
import * as assistantDeepTurnPackagingRuntimeAdapter_1 from "./assistantDeepTurnPackagingRuntimeAdapter";
|
||||
import * as assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanRuntimeAdapter";
|
||||
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
||||
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
||||
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
||||
import iconv from "iconv-lite";
|
||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||
|
|
@ -4559,49 +4560,21 @@ export class AssistantService {
|
|||
normalized: null,
|
||||
normalizer_output: null
|
||||
};
|
||||
const assistantItem = {
|
||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: chatText,
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: debug.trace_id,
|
||||
debug
|
||||
};
|
||||
this.sessions.appendItem(sessionId, assistantItem);
|
||||
const current = this.sessions.getSession(sessionId);
|
||||
if (current) {
|
||||
this.sessionLogger.persistSession(current);
|
||||
}
|
||||
const conversation = cloneItems(current?.items ?? []);
|
||||
(0, log_1.logJson)({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
service: "assistant_loop",
|
||||
message: "assistant_message_processed",
|
||||
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
||||
sessionId,
|
||||
eventType: "assistant_message_chat",
|
||||
details: {
|
||||
session_id: sessionId,
|
||||
message_id: assistantItem.message_id,
|
||||
user_message: userMessage,
|
||||
living_router_mode: modeDecision?.mode ?? "chat",
|
||||
living_router_reason: modeDecision?.reason ?? "living_chat_signal_detected",
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
trace_id: assistantItem.trace_id
|
||||
}
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
session_id: sessionId,
|
||||
assistant_reply: assistantItem.text,
|
||||
reply_type: assistantItem.reply_type,
|
||||
conversation_item: assistantItem,
|
||||
userMessage,
|
||||
assistantReply: chatText,
|
||||
replyType: "factual_with_explanation",
|
||||
debug,
|
||||
conversation
|
||||
};
|
||||
modeDecision,
|
||||
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
||||
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
|
||||
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
|
||||
cloneConversation: (items) => cloneItems(items),
|
||||
logEvent: (payload) => (0, log_1.logJson)(payload),
|
||||
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
|
||||
});
|
||||
return finalization.response;
|
||||
}
|
||||
catch (error) {
|
||||
(0, log_1.logJson)({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { finalizeAssistantLivingChatTurn } from "../src/services/assistantLivingChatTurnFinalizeRuntimeAdapter";
|
||||
|
||||
describe("assistant living chat turn finalize runtime adapter", () => {
|
||||
it("builds assistant chat item and emits expected living chat log envelope", () => {
|
||||
const commitCalls: Array<Record<string, unknown>> = [];
|
||||
const output = finalizeAssistantLivingChatTurn({
|
||||
sessionId: "asst-chat-1",
|
||||
userMessage: "что ты умеешь?",
|
||||
assistantReply: "Могу помочь с анализом данных 1С.",
|
||||
replyType: "factual_with_explanation",
|
||||
debug: { trace_id: "chat-trace-1", detected_mode: "chat" } as any,
|
||||
modeDecision: {
|
||||
mode: "chat",
|
||||
reason: "assistant_capability_query_detected"
|
||||
},
|
||||
appendItem: () => {},
|
||||
getSession: () => null,
|
||||
persistSession: () => {},
|
||||
cloneConversation: () => [],
|
||||
logEvent: () => {},
|
||||
messageIdFactory: () => "msg-chat-1",
|
||||
nowIso: () => "2026-04-10T13:00:00.000Z",
|
||||
commitFn: ((input: Record<string, unknown>) => {
|
||||
commitCalls.push(input);
|
||||
return {
|
||||
currentSession: null,
|
||||
conversation: []
|
||||
};
|
||||
}) as any
|
||||
});
|
||||
|
||||
expect(output.assistantItem.message_id).toBe("msg-chat-1");
|
||||
expect(output.assistantItem.created_at).toBe("2026-04-10T13:00:00.000Z");
|
||||
expect(output.assistantItem.trace_id).toBe("chat-trace-1");
|
||||
expect(commitCalls).toHaveLength(1);
|
||||
expect(commitCalls[0]?.["eventType"]).toBe("assistant_message_chat");
|
||||
const logDetails = commitCalls[0]?.["logDetails"] as Record<string, unknown>;
|
||||
expect(logDetails?.["session_id"]).toBe("asst-chat-1");
|
||||
expect(logDetails?.["living_router_mode"]).toBe("chat");
|
||||
expect(logDetails?.["living_router_reason"]).toBe("assistant_capability_query_detected");
|
||||
expect(logDetails?.["assistant_reply"]).toBe("Могу помочь с анализом данных 1С.");
|
||||
expect(output.response.reply_type).toBe("factual_with_explanation");
|
||||
});
|
||||
|
||||
it("uses default commit runtime and returns persisted conversation", () => {
|
||||
let appendCalls = 0;
|
||||
let persistCalls = 0;
|
||||
let logCalls = 0;
|
||||
let storedSession: any = null;
|
||||
const output = finalizeAssistantLivingChatTurn({
|
||||
sessionId: "asst-chat-2",
|
||||
userMessage: "какие есть данные?",
|
||||
assistantReply: "Доступны данные по нескольким организациям.",
|
||||
replyType: "factual_with_explanation",
|
||||
debug: { trace_id: "chat-trace-2" } as any,
|
||||
appendItem: (_sessionId, item) => {
|
||||
appendCalls += 1;
|
||||
storedSession = {
|
||||
session_id: "asst-chat-2",
|
||||
updated_at: "2026-04-10T13:00:00.000Z",
|
||||
items: [item],
|
||||
investigation_state: null
|
||||
};
|
||||
},
|
||||
getSession: () => storedSession,
|
||||
persistSession: () => {
|
||||
persistCalls += 1;
|
||||
},
|
||||
cloneConversation: (items) => items.map((item) => ({ ...item })),
|
||||
logEvent: () => {
|
||||
logCalls += 1;
|
||||
},
|
||||
messageIdFactory: () => "msg-chat-2",
|
||||
nowIso: () => "2026-04-10T13:05:00.000Z"
|
||||
});
|
||||
|
||||
expect(appendCalls).toBe(1);
|
||||
expect(persistCalls).toBe(1);
|
||||
expect(logCalls).toBe(1);
|
||||
expect(output.response.ok).toBe(true);
|
||||
expect(output.response.session_id).toBe("asst-chat-2");
|
||||
expect(output.response.reply_type).toBe("factual_with_explanation");
|
||||
expect(output.response.conversation).toHaveLength(1);
|
||||
expect(output.response.conversation_item.message_id).toBe("msg-chat-2");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue