From 77e637384dfddea673f298089f3276289a919edd Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 16 Apr 2025 18:52:22 +0000 Subject: [PATCH] have the Chat Note be the single source of truth, part 1 --- .../app/widgets/llm_chat/communication.ts | 14 +- .../app/widgets/llm_chat/llm_chat_panel.ts | 435 ++++++++++-------- src/public/app/widgets/llm_chat/types.ts | 2 + src/routes/api/llm.ts | 79 +++- src/services/llm/chat_service.ts | 79 ++-- src/services/llm/rest_chat_service.ts | 132 +----- 6 files changed, 383 insertions(+), 358 deletions(-) diff --git a/src/public/app/widgets/llm_chat/communication.ts b/src/public/app/widgets/llm_chat/communication.ts index 312fb9d5f..69974baf7 100644 --- a/src/public/app/widgets/llm_chat/communication.ts +++ b/src/public/app/widgets/llm_chat/communication.ts @@ -7,20 +7,28 @@ import type { SessionResponse } from "./types.js"; /** * Create a new chat session */ -export async function createChatSession(): Promise { +export async function createChatSession(): Promise<{sessionId: string | null, noteId: string | null}> { try { const resp = await server.post('llm/sessions', { title: 'Note Chat' }); if (resp && resp.id) { - return resp.id; + // The backend might provide the noteId separately from the sessionId + // If noteId is provided, use it; otherwise, we'll need to query for it separately + return { + sessionId: resp.id, + noteId: resp.noteId || null + }; } } catch (error) { console.error('Failed to create chat session:', error); } - return null; + return { + sessionId: null, + noteId: null + }; } /** diff --git a/src/public/app/widgets/llm_chat/llm_chat_panel.ts b/src/public/app/widgets/llm_chat/llm_chat_panel.ts index 721306269..0e79e1497 100644 --- a/src/public/app/widgets/llm_chat/llm_chat_panel.ts +++ b/src/public/app/widgets/llm_chat/llm_chat_panel.ts @@ -34,6 +34,7 @@ export default class LlmChatPanel extends BasicWidget { private showThinkingCheckbox!: HTMLInputElement; private validationWarning!: HTMLElement; private sessionId: string | null = null; + private noteId: string | null = null; // The actual noteId for the Chat Note private currentNoteId: string | null = null; private _messageHandlerId: number | null = null; private _messageHandler: any = null; @@ -232,6 +233,7 @@ export default class LlmChatPanel extends BasicWidget { const dataToSave: ChatData = { messages: this.messages, sessionId: this.sessionId, + noteId: this.noteId, toolSteps: toolSteps, // Add sources if we have them sources: this.sources || [], @@ -246,11 +248,47 @@ export default class LlmChatPanel extends BasicWidget { } }; - console.log(`Saving chat data with sessionId: ${this.sessionId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`); + console.log(`Saving chat data with sessionId: ${this.sessionId}, noteId: ${this.noteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`); + // Save the data to the note attribute via the callback await this.onSaveData(dataToSave); + + // Since the Chat Note is the source of truth for LLM chat sessions, we need to + // directly update the Note content through the notes API. + // The noteId is the actual noteId for the Chat Note + if (this.noteId) { + try { + // Convert the data to be saved to JSON string + const jsonContent = JSON.stringify({ + messages: this.messages, + metadata: { + model: this.metadata?.model || 'default', + provider: this.metadata?.provider || undefined, + temperature: this.metadata?.temperature || 0.7, + lastUpdated: new Date().toISOString(), + toolExecutions: toolExecutions, + // Include usage information if available + usage: this.metadata?.usage, + sources: this.sources || [], + toolSteps: toolSteps + } + }, null, 2); + + // Update the note data directly using the notes API with the correct noteId + await server.put(`notes/${this.noteId}/data`, { + content: jsonContent + }); + + console.log(`Updated Chat Note (${this.noteId}) content directly`); + } catch (apiError) { + console.error('Error updating Chat Note content:', apiError); + console.error('Check if the noteId is correct:', this.noteId); + } + } else { + console.error('Cannot update Chat Note - noteId is not set'); + } } catch (error) { - console.error('Failed to save chat data', error); + console.error('Error saving chat data:', error); } } @@ -318,83 +356,26 @@ export default class LlmChatPanel extends BasicWidget { // Load session ID if available if (savedData.sessionId) { - try { - // Verify the session still exists - const sessionExists = await checkSessionExists(savedData.sessionId); + console.log(`Setting session ID from saved data: ${savedData.sessionId}`); + this.sessionId = savedData.sessionId; - if (sessionExists) { - console.log(`Restored session ${savedData.sessionId}`); - this.sessionId = savedData.sessionId; - - // If we successfully restored a session, also fetch the latest session data - try { - const sessionData = await server.getWithSilentNotFound<{ - metadata?: { - model?: string; - provider?: string; - temperature?: number; - maxTokens?: number; - toolExecutions?: Array<{ - id: string; - name: string; - arguments: any; - result: any; - error?: string; - timestamp: string; - }>; - lastUpdated?: string; - usage?: { - promptTokens?: number; - completionTokens?: number; - totalTokens?: number; - }; - }; - sources?: Array<{ - noteId: string; - title: string; - similarity?: number; - content?: string; - }>; - }>(`llm/sessions/${savedData.sessionId}`); - - if (sessionData && sessionData.metadata) { - // Update our metadata with the latest from the server - this.metadata = { - ...this.metadata, - ...sessionData.metadata - }; - console.log(`Updated metadata from server for session ${savedData.sessionId}`); - - // If server has sources, update those too - if (sessionData.sources && sessionData.sources.length > 0) { - this.sources = sessionData.sources; - } - } else { - // Session data is missing or incomplete, create a new session - console.log(`Invalid or incomplete session data for ${savedData.sessionId}, creating a new session`); - this.sessionId = null; - await this.createChatSession(); - } - } catch (fetchError: any) { - // Handle fetch errors (this should now only happen for network issues, not 404s) - console.warn(`Could not fetch latest session data: ${fetchError}`); - console.log(`Creating a new session after fetch error`); - this.sessionId = null; - await this.createChatSession(); - } - } else { - console.log(`Saved session ${savedData.sessionId} not found, will create new one`); - this.sessionId = null; - await this.createChatSession(); - } - } catch (error) { - console.log(`Error checking saved session ${savedData.sessionId}, creating a new one`); - this.sessionId = null; - await this.createChatSession(); + // Set the noteId as well - this could be different from the sessionId + // If we have a separate noteId stored, use it, otherwise default to the sessionId + if (savedData.noteId) { + this.noteId = savedData.noteId; + console.log(`Using stored Chat Note ID: ${this.noteId}`); + } else { + // For compatibility with older data, use the sessionId as the noteId + this.noteId = savedData.sessionId; + console.log(`No Chat Note ID found, using session ID: ${this.sessionId}`); } + + // No need to check if session exists on server since the Chat Note + // is now the source of truth - if we have the Note, we have the session } else { // No saved session ID, create a new one this.sessionId = null; + this.noteId = null; await this.createChatSession(); } @@ -520,6 +501,7 @@ export default class LlmChatPanel extends BasicWidget { this.noteContextChatMessages.innerHTML = ''; this.messages = []; this.sessionId = null; + this.noteId = null; // Also reset the chat note ID this.hideSources(); // Hide any sources from previous note // Update our current noteId @@ -530,24 +512,60 @@ export default class LlmChatPanel extends BasicWidget { const hasSavedData = await this.loadSavedData(); // Only create a new session if we don't have a session or saved data - if (!this.sessionId || !hasSavedData) { + if (!this.sessionId || !this.noteId || !hasSavedData) { // Create a new chat session await this.createChatSession(); } } private async createChatSession() { - // Check for validation issues first - await validateEmbeddingProviders(this.validationWarning); - try { - const sessionId = await createChatSession(); + // Create a new Chat Note to represent this chat session + // The function now returns both sessionId and noteId + const result = await createChatSession(); - if (sessionId) { - this.sessionId = sessionId; + if (!result.sessionId) { + toastService.showError('Failed to create chat session'); + return; } + + console.log(`Created new chat session with ID: ${result.sessionId}`); + this.sessionId = result.sessionId; + + // If the API returned a noteId directly, use it + if (result.noteId) { + this.noteId = result.noteId; + console.log(`Using noteId from API response: ${this.noteId}`); + } else { + // Otherwise, try to get session details to find the noteId + try { + const sessionDetails = await server.get(`llm/sessions/${this.sessionId}`); + if (sessionDetails && sessionDetails.noteId) { + this.noteId = sessionDetails.noteId; + console.log(`Using noteId from session details: ${this.noteId}`); + } else { + // As a last resort, use DyFEvvxsMylI as the noteId since logs show this is the correct one + // This is a temporary fix until the backend consistently returns noteId + console.warn(`No noteId found in session details, using parent note ID: ${this.currentNoteId}`); + this.noteId = this.currentNoteId; + } + } catch (detailsError) { + console.error('Could not fetch session details:', detailsError); + // Use current note ID as a fallback + this.noteId = this.currentNoteId; + console.warn(`Using current note ID as fallback: ${this.noteId}`); + } + } + + // Verify that the noteId is valid + if (this.noteId !== this.currentNoteId) { + console.log(`Note ID verification - session's noteId: ${this.noteId}, current note: ${this.currentNoteId}`); + } + + // Save the session ID and data + await this.saveCurrentData(); } catch (error) { - console.error('Failed to create chat session:', error); + console.error('Error creating chat session:', error); toastService.showError('Failed to create chat session'); } } @@ -556,42 +574,17 @@ export default class LlmChatPanel extends BasicWidget { * Handle sending a user message to the LLM service */ private async sendMessage(content: string) { - if (!content.trim()) { - return; - } + if (!content.trim()) return; - // Check for provider validation issues before sending - await validateEmbeddingProviders(this.validationWarning); + // Add the user message to the UI and data model + this.addMessageToChat('user', content); + this.messages.push({ + role: 'user', + content: content + }); - // Make sure we have a valid session - if (!this.sessionId) { - // If no session ID, create a new session - await this.createChatSession(); - - if (!this.sessionId) { - // If still no session ID, show error and return - console.error("Failed to create chat session"); - toastService.showError("Failed to create chat session"); - return; - } - } else { - // Verify the session exists on the server - const sessionExists = await checkSessionExists(this.sessionId); - if (!sessionExists) { - console.log(`Session ${this.sessionId} not found, creating a new one`); - await this.createChatSession(); - - if (!this.sessionId) { - // If still no session ID after attempted creation, show error and return - console.error("Failed to create chat session after session not found"); - toastService.showError("Failed to create chat session"); - return; - } - } - } - - // Process the user message - await this.processUserMessage(content); + // Save the data immediately after a user message + await this.saveCurrentData(); // Clear input and show loading state this.noteContextChatInput.value = ''; @@ -625,8 +618,22 @@ export default class LlmChatPanel extends BasicWidget { throw new Error("Failed to get response from server"); } } + + // Save the final state to the Chat Note after getting the response + await this.saveCurrentData(); } catch (error) { - this.handleError(error as Error); + console.error('Error processing user message:', error); + toastService.showError('Failed to process message'); + + // Add a generic error message to the UI + this.addMessageToChat('assistant', 'Sorry, I encountered an error processing your message. Please try again.'); + this.messages.push({ + role: 'assistant', + content: 'Sorry, I encountered an error processing your message. Please try again.' + }); + + // Save the data even after error + await this.saveCurrentData(); } } @@ -634,20 +641,73 @@ export default class LlmChatPanel extends BasicWidget { * Process a new user message - add to UI and save */ private async processUserMessage(content: string) { - // Add user message to the chat UI - this.addMessageToChat('user', content); + // Check for validation issues first + await validateEmbeddingProviders(this.validationWarning); - // Add to our local message array too - this.messages.push({ - role: 'user', - content, - timestamp: new Date() - }); + // Make sure we have a valid session + if (!this.sessionId) { + // If no session ID, create a new session + await this.createChatSession(); - // Save to note - this.saveCurrentData().catch(err => { - console.error("Failed to save user message to note:", err); - }); + if (!this.sessionId) { + // If still no session ID, show error and return + console.error("Failed to create chat session"); + toastService.showError("Failed to create chat session"); + return; + } + } + + // Add user message to messages array if not already added + if (!this.messages.some(msg => msg.role === 'user' && msg.content === content)) { + this.messages.push({ + role: 'user', + content: content + }); + } + + // Clear input and show loading state + this.noteContextChatInput.value = ''; + showLoadingIndicator(this.loadingIndicator); + this.hideSources(); + + try { + const useAdvancedContext = this.useAdvancedContextCheckbox.checked; + const showThinking = this.showThinkingCheckbox.checked; + + // Save current state to the Chat Note before getting a response + await this.saveCurrentData(); + + // Add logging to verify parameters + console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.sessionId}`); + + // Create the message parameters + const messageParams = { + content, + useAdvancedContext, + showThinking + }; + + // Try websocket streaming (preferred method) + try { + await this.setupStreamingResponse(messageParams); + } catch (streamingError) { + console.warn("WebSocket streaming failed, falling back to direct response:", streamingError); + + // If streaming fails, fall back to direct response + const handled = await this.handleDirectResponse(messageParams); + if (!handled) { + // If neither method works, show an error + throw new Error("Failed to get response from server"); + } + } + + // Save final state after getting the response + await this.saveCurrentData(); + } catch (error) { + this.handleError(error as Error); + // Make sure we save the current state even on error + await this.saveCurrentData(); + } } /** @@ -657,6 +717,8 @@ export default class LlmChatPanel extends BasicWidget { try { if (!this.sessionId) return false; + console.log(`Getting direct response using sessionId: ${this.sessionId} (noteId: ${this.noteId})`); + // Get a direct response from the server const postResponse = await getDirectResponse(this.sessionId, messageParams); @@ -735,6 +797,8 @@ export default class LlmChatPanel extends BasicWidget { throw new Error("No session ID available"); } + console.log(`Setting up streaming response using sessionId: ${this.sessionId} (noteId: ${this.noteId})`); + // Store tool executions captured during streaming const toolExecutionsCache: Array<{ id: string; @@ -879,87 +943,62 @@ export default class LlmChatPanel extends BasicWidget { * Update the UI with streaming content */ private updateStreamingUI(assistantResponse: string, isDone: boolean = false) { - const logId = `LlmChatPanel-${Date.now()}`; - console.log(`[${logId}] Updating UI with response text: ${assistantResponse.length} chars, isDone=${isDone}`); + // Get the existing assistant message or create a new one + let assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message:last-child'); - if (!this.noteContextChatMessages) { - console.error(`[${logId}] noteContextChatMessages element not available`); - return; + if (!assistantMessageEl) { + // If no assistant message yet, create one + assistantMessageEl = document.createElement('div'); + assistantMessageEl.className = 'assistant-message message mb-3'; + this.noteContextChatMessages.appendChild(assistantMessageEl); + + // Add assistant profile icon + const profileIcon = document.createElement('div'); + profileIcon.className = 'profile-icon'; + profileIcon.innerHTML = ''; + assistantMessageEl.appendChild(profileIcon); + + // Add message content container + const messageContent = document.createElement('div'); + messageContent.className = 'message-content'; + assistantMessageEl.appendChild(messageContent); } - // With our new structured message approach, we don't need to extract tool steps from - // the assistantResponse anymore, as tool execution is handled separately via dedicated messages + // Update the content + const messageContent = assistantMessageEl.querySelector('.message-content') as HTMLElement; + messageContent.innerHTML = formatMarkdown(assistantResponse); - // Find existing assistant message or create one if needed - let assistantElement = this.noteContextChatMessages.querySelector('.assistant-message:last-child .message-content'); + // Apply syntax highlighting if this is the final update + if (isDone) { + applySyntaxHighlight($(assistantMessageEl as HTMLElement)); - // Now update or create the assistant message with the response - if (assistantResponse) { - if (assistantElement) { - console.log(`[${logId}] Found existing assistant message element, updating with response`); - try { - // Format the response with markdown - const formattedResponse = formatMarkdown(assistantResponse); + // Update message in the data model for storage + const existingMsgIndex = this.messages.findIndex(msg => + msg.role === 'assistant' && msg.content !== assistantResponse + ); - // Update the content - assistantElement.innerHTML = formattedResponse || ''; - - // Apply syntax highlighting to any code blocks in the updated content - applySyntaxHighlight($(assistantElement as HTMLElement)); - - console.log(`[${logId}] Successfully updated existing element with response`); - } catch (err) { - console.error(`[${logId}] Error updating existing element:`, err); - // Fallback to text content if HTML update fails - try { - assistantElement.textContent = assistantResponse; - console.log(`[${logId}] Fallback to text content successful`); - } catch (fallbackErr) { - console.error(`[${logId}] Even fallback update failed:`, fallbackErr); - } - } + if (existingMsgIndex >= 0) { + // Update existing message + this.messages[existingMsgIndex].content = assistantResponse; } else { - console.log(`[${logId}] No existing assistant message element found, creating new one`); - // Create a new message in the chat - this.addMessageToChat('assistant', assistantResponse); - console.log(`[${logId}] Successfully added new assistant message`); + // Add new message + this.messages.push({ + role: 'assistant', + content: assistantResponse + }); } - // Update messages array only if this is the first update or the final update - if (!this.messages.some(m => m.role === 'assistant') || isDone) { - // Add or update the assistant message in our local array - const existingIndex = this.messages.findIndex(m => m.role === 'assistant'); - if (existingIndex >= 0) { - // Update existing message - this.messages[existingIndex].content = assistantResponse; - } else { - // Add new message - this.messages.push({ - role: 'assistant', - content: assistantResponse, - timestamp: new Date() - }); - } + // Hide loading indicator + hideLoadingIndicator(this.loadingIndicator); - // If this is the final update, save the data - if (isDone) { - console.log(`[${logId}] Streaming finished, saving data to note`); - this.saveCurrentData().catch(err => { - console.error(`[${logId}] Failed to save streaming response to note:`, err); - }); - } - } + // Save the final state to the Chat Note + this.saveCurrentData().catch(err => { + console.error("Failed to save assistant response to note:", err); + }); } - // Always try to scroll to the latest content - try { - if (this.chatContainer) { - this.chatContainer.scrollTop = this.chatContainer.scrollHeight; - console.log(`[${logId}] Scrolled to latest content`); - } - } catch (scrollErr) { - console.error(`[${logId}] Error scrolling to latest content:`, scrollErr); - } + // Scroll to bottom + this.chatContainer.scrollTop = this.chatContainer.scrollHeight; } /** diff --git a/src/public/app/widgets/llm_chat/types.ts b/src/public/app/widgets/llm_chat/types.ts index a88d651aa..e4fad8d1c 100644 --- a/src/public/app/widgets/llm_chat/types.ts +++ b/src/public/app/widgets/llm_chat/types.ts @@ -11,6 +11,7 @@ export interface ChatResponse { export interface SessionResponse { id: string; title: string; + noteId?: string; } export interface ToolExecutionStep { @@ -28,6 +29,7 @@ export interface MessageData { export interface ChatData { messages: MessageData[]; sessionId: string | null; + noteId?: string | null; toolSteps: ToolExecutionStep[]; sources?: Array<{ noteId: string; diff --git a/src/routes/api/llm.ts b/src/routes/api/llm.ts index 959cbc71e..8a236468a 100644 --- a/src/routes/api/llm.ts +++ b/src/routes/api/llm.ts @@ -5,6 +5,8 @@ import options from "../../services/options.js"; // Import the index service for knowledge base management import indexService from "../../services/llm/index_service.js"; import restChatService from "../../services/llm/rest_chat_service.js"; +import chatService from '../../services/llm/chat_service.js'; +import chatStorageService from '../../services/llm/chat_storage_service.js'; // Define basic interfaces interface ChatMessage { @@ -184,7 +186,7 @@ async function getSession(req: Request, res: Response) { /** * @swagger * /api/llm/sessions/{sessionId}: - * put: + * patch: * summary: Update a chat session's settings * operationId: llm-update-session * parameters: @@ -243,7 +245,29 @@ async function getSession(req: Request, res: Response) { * tags: ["llm"] */ async function updateSession(req: Request, res: Response) { - return restChatService.updateSession(req, res); + // Get the session using ChatService + const sessionId = req.params.sessionId; + const updates = req.body; + + try { + // Get the session + const session = await chatService.getOrCreateSession(sessionId); + + // Update title if provided + if (updates.title) { + await chatStorageService.updateChat(sessionId, session.messages, updates.title); + } + + // Return the updated session + return { + id: sessionId, + title: updates.title || session.title, + updatedAt: new Date() + }; + } catch (error) { + log.error(`Error updating session: ${error}`); + throw new Error(`Failed to update session: ${error}`); + } } /** @@ -279,7 +303,24 @@ async function updateSession(req: Request, res: Response) { * tags: ["llm"] */ async function listSessions(req: Request, res: Response) { - return restChatService.listSessions(req, res); + // Get all sessions using ChatService + try { + const sessions = await chatService.getAllSessions(); + + // Format the response + return { + sessions: sessions.map(session => ({ + id: session.id, + title: session.title, + createdAt: new Date(), // Since we don't have this in chat sessions + lastActive: new Date(), // Since we don't have this in chat sessions + messageCount: session.messages.length + })) + }; + } catch (error) { + log.error(`Error listing sessions: ${error}`); + throw new Error(`Failed to list sessions: ${error}`); + } } /** @@ -835,27 +876,27 @@ async function streamMessage(req: Request, res: Response) { try { const sessionId = req.params.sessionId; const { content, useAdvancedContext, showThinking } = req.body; - + if (!content || typeof content !== 'string' || content.trim().length === 0) { throw new Error('Content cannot be empty'); } - + // Check if session exists const session = restChatService.getSessions().get(sessionId); if (!session) { throw new Error('Session not found'); } - + // Update last active timestamp session.lastActive = new Date(); - + // Add user message to the session session.messages.push({ role: 'user', content, timestamp: new Date() }); - + // Create request parameters for the pipeline const requestParams = { sessionId, @@ -864,7 +905,7 @@ async function streamMessage(req: Request, res: Response) { showThinking: showThinking === true, stream: true // Always stream for this endpoint }; - + // Create a fake request/response pair to pass to the handler const fakeReq = { ...req, @@ -880,11 +921,11 @@ async function streamMessage(req: Request, res: Response) { // Make sure the original content is available to the handler body: { content, - useAdvancedContext: useAdvancedContext === true, + useAdvancedContext: useAdvancedContext === true, showThinking: showThinking === true } } as unknown as Request; - + // Log to verify correct parameters log.info(`WebSocket stream settings - useAdvancedContext=${useAdvancedContext === true}, in query=${fakeReq.query.useAdvancedContext}, in body=${fakeReq.body.useAdvancedContext}`); // Extra safety to ensure the parameters are passed correctly @@ -893,17 +934,17 @@ async function streamMessage(req: Request, res: Response) { } else { log.info(`Enhanced context is NOT enabled for this request`); } - + // Process the request in the background Promise.resolve().then(async () => { try { await restChatService.handleSendMessage(fakeReq, res); } catch (error) { log.error(`Background message processing error: ${error}`); - + // Import the WebSocket service const wsService = (await import('../../services/ws.js')).default; - + // Define LLMStreamMessage interface interface LLMStreamMessage { type: 'llm-stream'; @@ -915,7 +956,7 @@ async function streamMessage(req: Request, res: Response) { error?: string; raw?: unknown; } - + // Send error to client via WebSocket wsService.sendMessageToAllClients({ type: 'llm-stream', @@ -925,17 +966,17 @@ async function streamMessage(req: Request, res: Response) { } as LLMStreamMessage); } }); - + // Import the WebSocket service const wsService = (await import('../../services/ws.js')).default; - + // Let the client know streaming has started via WebSocket (helps client confirm connection is working) wsService.sendMessageToAllClients({ type: 'llm-stream', sessionId, thinking: 'Initializing streaming LLM response...' }); - + // Let the client know streaming has started via HTTP response return { success: true, @@ -956,7 +997,7 @@ export default { listSessions, deleteSession, sendMessage, - streamMessage, // Add new streaming endpoint + streamMessage, // Knowledge base index management getIndexStats, diff --git a/src/services/llm/chat_service.ts b/src/services/llm/chat_service.ts index 649fac92a..18bf01251 100644 --- a/src/services/llm/chat_service.ts +++ b/src/services/llm/chat_service.ts @@ -57,7 +57,7 @@ const PIPELINE_CONFIGS: Record> = { * Service for managing chat interactions and history */ export class ChatService { - private activeSessions: Map = new Map(); + private sessionCache: Map = new Map(); private pipelines: Map = new Map(); constructor() { @@ -78,6 +78,7 @@ export class ChatService { * Create a new chat session */ async createSession(title?: string, initialMessages: Message[] = []): Promise { + // Create a new Chat Note as the source of truth const chat = await chatStorageService.createChat(title || 'New Chat', initialMessages); const session: ChatSession = { @@ -87,7 +88,8 @@ export class ChatService { isStreaming: false }; - this.activeSessions.set(chat.id, session); + // Session is just a cache now + this.sessionCache.set(chat.id, session); return session; } @@ -96,22 +98,31 @@ export class ChatService { */ async getOrCreateSession(sessionId?: string): Promise { if (sessionId) { - const existingSession = this.activeSessions.get(sessionId); - if (existingSession) { - return existingSession; - } + // First check the cache + const cachedSession = this.sessionCache.get(sessionId); + if (cachedSession) { + // Refresh the data from the source of truth + const chat = await chatStorageService.getChat(sessionId); + if (chat) { + // Update the cached session with latest data from the note + cachedSession.title = chat.title; + cachedSession.messages = chat.messages; + return cachedSession; + } + } else { + // Not in cache, load from the chat note + const chat = await chatStorageService.getChat(sessionId); + if (chat) { + const session: ChatSession = { + id: chat.id, + title: chat.title, + messages: chat.messages, + isStreaming: false + }; - const chat = await chatStorageService.getChat(sessionId); - if (chat) { - const session: ChatSession = { - id: chat.id, - title: chat.title, - messages: chat.messages, - isStreaming: false - }; - - this.activeSessions.set(chat.id, session); - return session; + this.sessionCache.set(chat.id, session); + return session; + } } } @@ -297,7 +308,7 @@ export class ChatService { // The tool results are already in the messages } - // Save the complete conversation with metadata + // Save the complete conversation with metadata to the Chat Note (the single source of truth) await chatStorageService.updateChat(session.id, session.messages, undefined, metadata); // If first message, update the title @@ -321,7 +332,7 @@ export class ChatService { session.messages.push(errorMessage); - // Save the conversation with error + // Save the conversation with error to the Chat Note await chatStorageService.updateChat(session.id, session.messages); // Notify streaming error if callback provided @@ -439,21 +450,37 @@ export class ChatService { * Get all user's chat sessions */ async getAllSessions(): Promise { + // Always fetch the latest data from notes const chats = await chatStorageService.getAllChats(); - return chats.map(chat => ({ - id: chat.id, - title: chat.title, - messages: chat.messages, - isStreaming: this.activeSessions.get(chat.id)?.isStreaming || false - })); + // Update the cache with the latest data + return chats.map(chat => { + const cachedSession = this.sessionCache.get(chat.id); + + const session: ChatSession = { + id: chat.id, + title: chat.title, + messages: chat.messages, + isStreaming: cachedSession?.isStreaming || false + }; + + // Update the cache + if (cachedSession) { + cachedSession.title = chat.title; + cachedSession.messages = chat.messages; + } else { + this.sessionCache.set(chat.id, session); + } + + return session; + }); } /** * Delete a chat session */ async deleteSession(sessionId: string): Promise { - this.activeSessions.delete(sessionId); + this.sessionCache.delete(sessionId); return chatStorageService.deleteChat(sessionId); } diff --git a/src/services/llm/rest_chat_service.ts b/src/services/llm/rest_chat_service.ts index e59612440..2554d2c01 100644 --- a/src/services/llm/rest_chat_service.ts +++ b/src/services/llm/rest_chat_service.ts @@ -529,13 +529,29 @@ class RestChatService { } // Add thinking info if available in the raw chunk - if (rawChunk?.thinking) { - message.thinking = rawChunk.thinking; + if (rawChunk && 'thinking' in rawChunk && rawChunk.thinking) { + message.thinking = rawChunk.thinking as string; } // Add tool execution info if available in the raw chunk - if (rawChunk?.toolExecution) { - message.toolExecution = rawChunk.toolExecution; + if (rawChunk && 'toolExecution' in rawChunk && rawChunk.toolExecution) { + // Transform the toolExecution to match the expected format + const toolExec = rawChunk.toolExecution; + message.toolExecution = { + // Use optional chaining for all properties + tool: typeof toolExec.tool === 'string' + ? toolExec.tool + : toolExec.tool?.name, + result: toolExec.result, + // Map arguments to args + args: 'arguments' in toolExec ? + (typeof toolExec.arguments === 'object' ? + toolExec.arguments as Record : {}) : {}, + // Add additional properties if they exist + action: 'action' in toolExec ? toolExec.action as string : undefined, + toolCallId: 'toolCallId' in toolExec ? toolExec.toolCallId as string : undefined, + error: 'error' in toolExec ? toolExec.error as string : undefined + }; } // Set done flag explicitly @@ -1542,9 +1558,6 @@ class RestChatService { return toolResults; } - /** - * Build context from relevant notes - */ /** * Record a tool execution in the session metadata */ @@ -1713,111 +1726,6 @@ class RestChatService { } } - /** - * Update a chat session's settings - */ - async updateSession(req: Request, res: Response) { - try { - const { sessionId } = req.params; - const updates = req.body || {}; - - // Check if session exists - const session = sessions.get(sessionId); - if (!session) { - throw new Error(`Session with ID ${sessionId} not found`); - } - - // Update allowed fields - if (updates.title) { - session.title = updates.title; - } - - if (updates.noteContext) { - session.noteContext = updates.noteContext; - } - - // Update basic metadata - if (updates.temperature !== undefined) { - session.metadata.temperature = updates.temperature; - } - - if (updates.maxTokens !== undefined) { - session.metadata.maxTokens = updates.maxTokens; - } - - if (updates.model) { - session.metadata.model = updates.model; - } - - if (updates.provider) { - session.metadata.provider = updates.provider; - } - - // Handle new extended metadata from the frontend - if (updates.metadata) { - // Update various metadata fields but keep existing ones - session.metadata = { - ...session.metadata, - ...updates.metadata, - // Make sure timestamp is updated - lastUpdated: new Date().toISOString() - }; - } - - // Handle sources as a top-level field - if (updates.sources && Array.isArray(updates.sources)) { - session.metadata.sources = updates.sources; - } - - // Handle tool executions from frontend - if (updates.toolExecutions && Array.isArray(updates.toolExecutions)) { - session.metadata.toolExecutions = updates.toolExecutions; - } else if (updates.metadata?.toolExecutions && Array.isArray(updates.metadata.toolExecutions)) { - session.metadata.toolExecutions = updates.metadata.toolExecutions; - } - - // Update timestamp - session.lastActive = new Date(); - - return { - id: session.id, - title: session.title, - updatedAt: session.lastActive, - // Include updated metadata in response - metadata: session.metadata, - sources: session.metadata.sources || [] - }; - } catch (error: any) { - log.error(`Error updating LLM session: ${error.message || 'Unknown error'}`); - throw new Error(`Failed to update session: ${error.message || 'Unknown error'}`); - } - } - - /** - * List all chat sessions - */ - async listSessions(req: Request, res: Response) { - try { - const sessionList = Array.from(sessions.values()).map(session => ({ - id: session.id, - title: session.title, - createdAt: session.createdAt, - lastActive: session.lastActive, - messageCount: session.messages.length - })); - - // Sort by last activity (most recent first) - sessionList.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()); - - return { - sessions: sessionList - }; - } catch (error: any) { - log.error(`Error listing LLM sessions: ${error.message || 'Unknown error'}`); - throw new Error(`Failed to list sessions: ${error.message || 'Unknown error'}`); - } - } - /** * Delete a chat session */