gadget/docs/archive/services/chat.service.ts

371 lines
9.4 KiB
TypeScript

// src/services/chat.service.ts
// Copyright (C) 2026 DTP Technologies, LLC
// All Rights Reserved
import { Types } from "mongoose";
import {
ChatSession,
ChatSessionType,
ChatSessionMode,
} from "../models/chat-session.js";
import {
ChatHistory,
IChatHistory as IChatHistoryModel,
ChatHistoryStatus,
} from "../models/chat-history.js";
import { DtpService } from "../lib/service.js";
export interface IChatSessionListItem {
_id: string;
name: string;
lastMessageAt: Date | null;
turnCount: number;
toolCallCount: number;
inputTokens: number;
outputTokens: number;
createdAt: Date;
mode: ChatSessionMode;
}
export interface IChatSessionDetail extends IChatSessionListItem {
user: string;
project?: string;
type: ChatSessionType;
pins: Array<{ _id?: string; content: string }>;
}
export interface IChatHistoryEntry {
_id: string;
sessionId: string;
prompt: string;
response: {
thinking?: string;
message?: string;
};
status: ChatHistoryStatus;
toolCalls: Array<{
tool: {
name: string;
callId: string;
parameters: Array<{ name: string; value: string }>;
};
response?: string;
fileOperation?: {
type: "read" | "write" | "edit" | "shell";
path?: string;
diff?: string;
linesAdded?: number;
linesRemoved?: number;
isBinary?: boolean;
};
subagentStats?: {
inputTokens: number;
outputTokens: number;
toolCallCount: number;
};
}>;
fileOperations: Array<{
type: "read" | "write" | "edit" | "shell";
path?: string;
diff?: string;
linesAdded?: number;
linesRemoved?: number;
isBinary?: boolean;
}>;
inputTokens: number;
outputTokens: number;
createdAt: Date;
mode: ChatSessionMode;
isSubagent: boolean;
error?: {
message: string;
stack?: string;
timestamp: Date;
};
}
export class ChatService extends DtpService {
get name(): string {
return "ChatService";
}
get slug(): string {
return "chat";
}
async start(): Promise<void> {
this.log.info("service started");
}
async stop(): Promise<void> {
this.log.info("service stopped");
}
// ==================== Session Management ====================
async listByProject(
projectId: string,
userId: string,
): Promise<IChatSessionListItem[]> {
const sessions = await ChatSession.find({
user: userId,
project: projectId,
})
.sort({ lastMessageAt: -1, createdAt: -1 })
.lean();
return sessions.map((s) => ({
_id: s._id.toString(),
name: s.name,
lastMessageAt: s.lastMessageAt ?? null,
turnCount: s.stats.turnCount,
toolCallCount: s.stats.toolCallCount,
inputTokens: s.stats.inputTokens,
outputTokens: s.stats.outputTokens,
createdAt: s.createdAt,
mode: s.mode,
}));
}
async listAll(userId: string): Promise<IChatSessionListItem[]> {
const sessions = await ChatSession.find({
user: userId,
})
.sort({ lastMessageAt: -1, createdAt: -1 })
.limit(50)
.lean();
return sessions.map((s) => ({
_id: s._id.toString(),
name: s.name,
lastMessageAt: s.lastMessageAt ?? null,
turnCount: s.stats.turnCount,
toolCallCount: s.stats.toolCallCount,
inputTokens: s.stats.inputTokens,
outputTokens: s.stats.outputTokens,
createdAt: s.createdAt,
mode: s.mode,
}));
}
async create(
userId: string,
projectId?: string,
name?: string,
): Promise<IChatSessionListItem> {
const session = new ChatSession({
user: userId,
project: projectId,
name: name ?? "New Session",
type: ChatSessionType.Desktop,
mode: ChatSessionMode.Build,
});
await session.save();
this.log.info("ChatSession created", {
sessionId: session._id,
userId,
projectId,
});
return {
_id: session._id.toString(),
name: session.name,
lastMessageAt: null,
turnCount: 0,
toolCallCount: 0,
inputTokens: 0,
outputTokens: 0,
createdAt: session.createdAt,
mode: session.mode,
};
}
async findById(sessionId: string): Promise<IChatSessionDetail | null> {
const session = await ChatSession.findById(sessionId).lean();
if (!session) {
return null;
}
const sessionWithProject = await ChatSession.findById(sessionId)
.populate("project", "name")
.lean() as any;
const projectName = (sessionWithProject?.project as any)?.name;
const userId =
session.user instanceof Types.ObjectId
? session.user.toString()
: String(session.user);
return {
_id: session._id.toString(),
name: session.name,
lastMessageAt: session.lastMessageAt ?? null,
turnCount: session.stats.turnCount,
toolCallCount: session.stats.toolCallCount,
inputTokens: session.stats.inputTokens,
outputTokens: session.stats.outputTokens,
createdAt: session.createdAt,
mode: session.mode,
user: userId,
project: projectName,
type: session.type,
pins: session.pins.map((p) => ({
_id: p._id?.toString(),
content: p.content,
})),
};
}
async updateName(sessionId: string, name: string): Promise<void> {
await ChatSession.findByIdAndUpdate(sessionId, { name });
this.log.info("ChatSession name updated", { sessionId, name });
}
async updateMode(sessionId: string, mode: ChatSessionMode): Promise<void> {
await ChatSession.findByIdAndUpdate(sessionId, { mode });
this.log.info("ChatSession mode updated", { sessionId, mode });
}
async delete(sessionId: string, userId: string): Promise<void> {
const result = await ChatSession.deleteOne({
_id: sessionId,
user: userId,
});
if (result.deletedCount === 0) {
throw new Error("ChatSession not found or not owned by user");
}
this.log.info("ChatSession deleted", { sessionId, userId });
}
// ==================== History Management ====================
async getHistory(sessionId: string): Promise<IChatHistoryEntry[]> {
const history = await ChatHistory.find({ session: sessionId })
.sort({ createdAt: 1 })
.lean();
return history.map((h) => ({
_id: h._id.toString(),
sessionId: h.session.toString(),
prompt: h.prompt,
response: {
thinking: h.response?.thinking,
message: h.response?.message,
},
status: h.status,
toolCalls: h.toolCalls.map((tc) => ({
tool: {
name: tc.tool.name,
callId: tc.tool.callId,
parameters: tc.tool.parameters.map((p) => ({
name: p.name,
value: p.value,
})),
},
response: tc.response,
fileOperation: tc.fileOperation,
subagentStats: tc.subagentStats,
})),
fileOperations: h.fileOperations,
inputTokens: h.inputTokens,
outputTokens: h.outputTokens,
createdAt: h.createdAt,
mode: h.mode,
isSubagent: h.isSubagent ?? false,
subagentHistory: h.subagentHistory || [],
error: h.error,
}));
}
async createHistoryEntry(
sessionId: string,
userId: string,
prompt: string,
mode: ChatSessionMode,
): Promise<IChatHistoryEntry> {
const entry = new ChatHistory({
session: sessionId,
user: userId,
prompt,
mode,
status: ChatHistoryStatus.Processing,
});
await entry.save();
// Update session stats
await ChatSession.findByIdAndUpdate(sessionId, {
$inc: {
"stats.turnCount": 1,
"stats.inputTokens": entry.inputTokens,
"stats.outputTokens": entry.outputTokens,
},
lastMessageAt: new Date(),
});
this.log.info("ChatHistory entry created", {
historyId: entry._id,
sessionId,
userId,
});
return {
_id: entry._id.toString(),
sessionId,
prompt: entry.prompt,
response: {
thinking: entry.response?.thinking,
message: entry.response?.message,
},
status: entry.status,
toolCalls: [],
fileOperations: [],
inputTokens: 0,
outputTokens: 0,
createdAt: entry.createdAt,
mode: entry.mode,
isSubagent: entry.isSubagent ?? false,
};
}
async updateHistoryEntry(
historyId: string,
updates: Partial<{
response: { thinking?: string; message?: string };
status: ChatHistoryStatus;
toolCalls: IChatHistoryModel["toolCalls"];
fileOperations: IChatHistoryModel["fileOperations"];
inputTokens: number;
outputTokens: number;
error: { message: string; stack?: string; timestamp: Date };
}>,
): Promise<void> {
const updateData: any = {};
if (updates.response !== undefined) {
updateData.response = updates.response;
}
if (updates.status !== undefined) {
updateData.status = updates.status;
}
if (updates.toolCalls !== undefined) {
updateData.toolCalls = updates.toolCalls;
}
if (updates.fileOperations !== undefined) {
updateData.fileOperations = updates.fileOperations;
}
if (updates.inputTokens !== undefined) {
updateData.inputTokens = updates.inputTokens;
}
if (updates.outputTokens !== undefined) {
updateData.outputTokens = updates.outputTokens;
}
if (updates.error !== undefined) {
updateData.error = updates.error;
}
await ChatHistory.findByIdAndUpdate(historyId, updateData);
this.log.info("ChatHistory entry updated", { historyId });
}
}
export default new ChatService();