/** * ChatEngine - Manages AI chat sessions and message persistence * * Responsible for: * - Creating, updating, and deleting chat conversations * - Storing and retrieving chat messages * - Managing conversation state (titles, models, etc.) */ import { v4 as uuidv4 } from 'uuid'; import { DatabaseConnection } from '../database/connection'; export interface ChatConversationData { id: string; title: string; model?: string; createdAt: Date; updatedAt: Date; } export interface ChatMessageData { id?: number; conversationId: string; role: 'system' | 'user' | 'assistant' | 'tool'; content?: string; toolCallId?: string; toolCalls?: string; // JSON array of tool calls createdAt: Date; } export interface CreateConversationInput { title?: string; model?: string; systemPrompt?: string; } export class ChatEngine { private db: DatabaseConnection; constructor(database: DatabaseConnection) { this.db = database; } /** * Create a new chat conversation */ async createConversation(input: CreateConversationInput = {}): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const id = `chat_${uuidv4()}`; const title = input.title || 'New Chat'; const model = input.model || 'claude-sonnet-4'; const now = Date.now(); await client.execute({ sql: `INSERT INTO chat_conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`, args: [id, title, model, now, now], }); // Add system prompt as first message if provided if (input.systemPrompt) { await this.addMessage({ conversationId: id, role: 'system', content: input.systemPrompt, createdAt: new Date(now), }); } return { id, title, model, createdAt: new Date(now), updatedAt: new Date(now), }; } /** * Get a conversation by ID with all messages */ async getConversation(id: string): Promise<(ChatConversationData & { messages: ChatMessageData[] }) | null> { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const convResult = await client.execute({ sql: `SELECT * FROM chat_conversations WHERE id = ?`, args: [id], }); if (convResult.rows.length === 0) { return null; } const row = convResult.rows[0]; const conversation: ChatConversationData = { id: row.id as string, title: row.title as string, model: row.model as string | undefined, createdAt: new Date(row.created_at as number), updatedAt: new Date(row.updated_at as number), }; const messagesResult = await client.execute({ sql: `SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY created_at ASC`, args: [id], }); const messages: ChatMessageData[] = messagesResult.rows.map(r => ({ id: r.id as number, conversationId: r.conversation_id as string, role: r.role as 'system' | 'user' | 'assistant' | 'tool', content: r.content as string | undefined, toolCallId: r.tool_call_id as string | undefined, toolCalls: r.tool_calls as string | undefined, createdAt: new Date(r.created_at as number), })); return { ...conversation, messages }; } /** * Get all conversations, sorted by most recently updated */ async getRecentConversations(limit: number = 50): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const result = await client.execute({ sql: `SELECT * FROM chat_conversations ORDER BY updated_at DESC LIMIT ?`, args: [limit], }); return result.rows.map(row => ({ id: row.id as string, title: row.title as string, model: row.model as string | undefined, createdAt: new Date(row.created_at as number), updatedAt: new Date(row.updated_at as number), })); } /** * Update a conversation's metadata */ async updateConversation(id: string, updates: Partial>): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const setClauses: string[] = ['updated_at = ?']; const args: (string | number | null)[] = [Date.now()]; if (updates.title !== undefined) { setClauses.push('title = ?'); args.push(updates.title); } if (updates.model !== undefined) { setClauses.push('model = ?'); args.push(updates.model); } args.push(id); await client.execute({ sql: `UPDATE chat_conversations SET ${setClauses.join(', ')} WHERE id = ?`, args, }); } /** * Delete a conversation and all its messages */ async deleteConversation(id: string): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } // Messages are deleted via CASCADE, but let's be explicit await client.execute({ sql: `DELETE FROM chat_messages WHERE conversation_id = ?`, args: [id], }); await client.execute({ sql: `DELETE FROM chat_conversations WHERE id = ?`, args: [id], }); } /** * Add a message to a conversation */ async addMessage(message: Omit): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const createdAt = message.createdAt?.getTime() || Date.now(); const result = await client.execute({ sql: `INSERT INTO chat_messages (conversation_id, role, content, tool_call_id, tool_calls, created_at) VALUES (?, ?, ?, ?, ?, ?)`, args: [ message.conversationId, message.role, message.content || null, message.toolCallId || null, message.toolCalls || null, createdAt, ], }); // Update conversation's updated_at timestamp await client.execute({ sql: `UPDATE chat_conversations SET updated_at = ? WHERE id = ?`, args: [createdAt, message.conversationId], }); return { id: Number(result.lastInsertRowid), conversationId: message.conversationId, role: message.role, content: message.content, toolCallId: message.toolCallId, toolCalls: message.toolCalls, createdAt: new Date(createdAt), }; } /** * Get messages for a conversation */ async getMessages(conversationId: string): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const result = await client.execute({ sql: `SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY created_at ASC`, args: [conversationId], }); return result.rows.map(r => ({ id: r.id as number, conversationId: r.conversation_id as string, role: r.role as 'system' | 'user' | 'assistant' | 'tool', content: r.content as string | undefined, toolCallId: r.tool_call_id as string | undefined, toolCalls: r.tool_calls as string | undefined, createdAt: new Date(r.created_at as number), })); } /** * Clear all messages from a conversation (but keep the conversation) */ async clearMessages(conversationId: string): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } await client.execute({ sql: `DELETE FROM chat_messages WHERE conversation_id = ?`, args: [conversationId], }); } /** * Get default system prompt for new conversations */ async getDefaultSystemPrompt(): Promise { const client = this.db.getLocalClient(); if (!client) { return this.getBuiltInSystemPrompt(); } const result = await client.execute({ sql: `SELECT value FROM settings WHERE key = 'chat_system_prompt'`, args: [], }); // Return saved prompt if it exists and is non-empty if (result.rows.length > 0 && result.rows[0].value) { return result.rows[0].value as string; } return this.getBuiltInSystemPrompt(); } /** * Set default system prompt for new conversations. * Pass empty string to reset to built-in default. */ async setDefaultSystemPrompt(prompt: string): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } // If empty string, delete the setting to use built-in default if (!prompt || prompt.trim() === '') { await client.execute({ sql: `DELETE FROM settings WHERE key = ?`, args: ['chat_system_prompt'], }); return; } const now = Date.now(); await client.execute({ sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`, args: ['chat_system_prompt', prompt, now], }); } /** * Get the built-in default system prompt */ private getBuiltInSystemPrompt(): string { return `You are an AI assistant integrated into the Blogging Desktop Server (bDS) application. Your role is to help users manage their blog posts and media files using ONLY the tools provided to you. IMPORTANT: You do NOT have access to the internet, real-time data, or any external services. You can ONLY access information through the tools listed below. Do not claim otherwise. Available Tools: - search_posts: Search blog posts using full-text search. Supports category/tag filters. - read_post: Read the full content and metadata of a specific post by ID. - list_posts: List posts with optional filtering by status, category, or tags. - get_media: Get information about a specific media file by ID. - list_media: List media files with optional MIME type filtering. - view_image: View an image to analyze its visual content. Use this when you need to describe or analyze what an image looks like. - update_post_metadata: Update a post's title, excerpt, tags, or categories. - update_media_metadata: Update a media file's alt text, caption, or tags. - list_tags: List all tags with post counts. - list_categories: List all categories with post counts. When answering questions: 1. USE THE TOOLS to find information. Never make up data about posts or media. 2. If asked about something outside your tools (weather, news, websites), explain that you can only access the user's local blog content. 3. Be concise and helpful. Format post information clearly when displaying it. 4. If a search returns no results, suggest alternative queries or filters. 5. When asked to describe or analyze an image, use the view_image tool to see the actual image content.`; } /** * Get a setting by key */ async getSetting(key: string): Promise { const client = this.db.getLocalClient(); if (!client) return null; const result = await client.execute({ sql: `SELECT value FROM settings WHERE key = ?`, args: [key], }); if (result.rows.length > 0) { return result.rows[0].value as string; } return null; } /** * Set a setting by key */ async setSetting(key: string, value: string): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const now = Date.now(); await client.execute({ sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`, args: [key, value, now], }); } /** * Get selected model for new conversations */ async getSelectedModel(): Promise { const client = this.db.getLocalClient(); if (!client) { return 'claude-sonnet-4'; } const result = await client.execute({ sql: `SELECT value FROM settings WHERE key = 'chat_model'`, args: [], }); if (result.rows.length > 0) { return result.rows[0].value as string; } return 'claude-sonnet-4'; } /** * Set selected model for new conversations */ async setSelectedModel(model: string): Promise { const client = this.db.getLocalClient(); if (!client) { throw new Error('Database not initialized'); } const now = Date.now(); await client.execute({ sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`, args: ['chat_model', model, now], }); } }