2025-03-02 19:39:10 -08:00
import type { Message , ChatCompletionOptions } from './ai_interface.js' ;
import aiServiceManager from './ai_service_manager.js' ;
import chatStorageService from './chat_storage_service.js' ;
export interface ChatSession {
id : string ;
title : string ;
messages : Message [ ] ;
isStreaming? : boolean ;
options? : ChatCompletionOptions ;
}
/ * *
* Service for managing chat interactions and history
* /
export class ChatService {
private activeSessions : Map < string , ChatSession > = new Map ( ) ;
private streamingCallbacks : Map < string , ( content : string , isDone : boolean ) = > void > = new Map ( ) ;
/ * *
* Create a new chat session
* /
async createSession ( title? : string , initialMessages : Message [ ] = [ ] ) : Promise < ChatSession > {
const chat = await chatStorageService . createChat ( title || 'New Chat' , initialMessages ) ;
const session : ChatSession = {
id : chat.id ,
title : chat.title ,
messages : chat.messages ,
isStreaming : false
} ;
this . activeSessions . set ( chat . id , session ) ;
return session ;
}
/ * *
* Get an existing session or create a new one
* /
async getOrCreateSession ( sessionId? : string ) : Promise < ChatSession > {
if ( sessionId ) {
const existingSession = this . activeSessions . get ( sessionId ) ;
if ( existingSession ) {
return existingSession ;
}
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 ;
}
}
return this . createSession ( ) ;
}
/ * *
* Send a message in a chat session and get the AI response
* /
async sendMessage (
sessionId : string ,
content : string ,
options? : ChatCompletionOptions ,
streamCallback ? : ( content : string , isDone : boolean ) = > void
) : Promise < ChatSession > {
const session = await this . getOrCreateSession ( sessionId ) ;
// Add user message
const userMessage : Message = {
role : 'user' ,
content
} ;
session . messages . push ( userMessage ) ;
session . isStreaming = true ;
// Set up streaming if callback provided
if ( streamCallback ) {
this . streamingCallbacks . set ( session . id , streamCallback ) ;
}
try {
// Immediately save the user message
await chatStorageService . updateChat ( session . id , session . messages ) ;
// Generate AI response
const response = await aiServiceManager . generateChatCompletion (
session . messages ,
options || session . options
) ;
// Add assistant message
const assistantMessage : Message = {
role : 'assistant' ,
content : response.text
} ;
session . messages . push ( assistantMessage ) ;
session . isStreaming = false ;
// Save the complete conversation
await chatStorageService . updateChat ( session . id , session . messages ) ;
// If first message, update the title based on content
if ( session . messages . length <= 2 && ! session . title ) {
// Extract a title from the conversation
const title = this . generateTitleFromMessages ( session . messages ) ;
session . title = title ;
await chatStorageService . updateChat ( session . id , session . messages , title ) ;
}
// Notify streaming is complete
if ( streamCallback ) {
streamCallback ( response . text , true ) ;
this . streamingCallbacks . delete ( session . id ) ;
}
return session ;
} catch ( error : any ) {
session . isStreaming = false ;
console . error ( 'Error in AI chat:' , error ) ;
// Add error message so user knows something went wrong
const errorMessage : Message = {
role : 'assistant' ,
content : ` Error: Failed to generate response. ${ error . message || 'Please check AI settings and try again.' } `
} ;
session . messages . push ( errorMessage ) ;
// Save the conversation with error
await chatStorageService . updateChat ( session . id , session . messages ) ;
// Notify streaming is complete with error
if ( streamCallback ) {
streamCallback ( errorMessage . content , true ) ;
this . streamingCallbacks . delete ( session . id ) ;
}
return session ;
}
}
/ * *
* Add context from the current note to the chat
2025-03-11 18:07:28 +00:00
*
* @param sessionId - The ID of the chat session
* @param noteId - The ID of the note to add context from
* @param useSmartContext - Whether to use smart context extraction ( default : true )
* @returns The updated chat session
2025-03-02 19:39:10 -08:00
* /
2025-03-11 18:07:28 +00:00
async addNoteContext ( sessionId : string , noteId : string , useSmartContext = true ) : Promise < ChatSession > {
2025-03-02 19:39:10 -08:00
const session = await this . getOrCreateSession ( sessionId ) ;
2025-03-11 18:07:28 +00:00
// Get the last user message to use as context for semantic search
const lastUserMessage = [ . . . session . messages ] . reverse ( )
. find ( msg = > msg . role === 'user' && msg . content . length > 10 ) ? . content || '' ;
let context ;
if ( useSmartContext && lastUserMessage ) {
// Use smart context that considers the query for better relevance
context = await contextExtractor . getSmartContext ( noteId , lastUserMessage ) ;
} else {
// Fall back to full context if smart context is disabled or no query available
context = await contextExtractor . getFullContext ( noteId ) ;
}
2025-03-02 19:39:10 -08:00
const contextMessage : Message = {
role : 'user' ,
content : ` Here is the content of the note I want to discuss: \ n \ n ${ context } \ n \ nPlease help me with this information. `
} ;
session . messages . push ( contextMessage ) ;
await chatStorageService . updateChat ( session . id , session . messages ) ;
return session ;
}
2025-03-11 18:07:28 +00:00
/ * *
* Add semantically relevant context from a note based on a specific query
*
* @param sessionId - The ID of the chat session
* @param noteId - The ID of the note to add context from
* @param query - The specific query to find relevant information for
* @returns The updated chat session
* /
async addSemanticNoteContext ( sessionId : string , noteId : string , query : string ) : Promise < ChatSession > {
const session = await this . getOrCreateSession ( sessionId ) ;
// Use semantic context that considers the query for better relevance
2025-03-19 19:28:02 +00:00
const contextService = aiServiceManager . getContextService ( ) ;
const context = await contextService . getSemanticContext ( noteId , query ) ;
2025-03-11 18:07:28 +00:00
const contextMessage : Message = {
role : 'user' ,
content : ` Here is the relevant information from my notes based on my query " ${ query } ": \ n \ n ${ context } \ n \ nPlease help me understand this information in relation to my query. `
} ;
session . messages . push ( contextMessage ) ;
await chatStorageService . updateChat ( session . id , session . messages ) ;
return session ;
}
/ * *
2025-03-19 18:49:14 +00:00
* Send a message with enhanced semantic note context
2025-03-11 18:07:28 +00:00
* /
async sendContextAwareMessage (
sessionId : string ,
content : string ,
noteId : string ,
options? : ChatCompletionOptions ,
streamCallback ? : ( content : string , isDone : boolean ) = > void
) : Promise < ChatSession > {
const session = await this . getOrCreateSession ( sessionId ) ;
2025-03-19 18:49:14 +00:00
// Add user message
const userMessage : Message = {
role : 'user' ,
content
} ;
session . messages . push ( userMessage ) ;
session . isStreaming = true ;
// Set up streaming if callback provided
if ( streamCallback ) {
this . streamingCallbacks . set ( session . id , streamCallback ) ;
}
try {
// Immediately save the user message
await chatStorageService . updateChat ( session . id , session . messages ) ;
// Get the Trilium Context Service for enhanced context
2025-03-19 19:28:02 +00:00
const contextService = aiServiceManager . getContextService ( ) ;
2025-03-19 18:49:14 +00:00
// Get showThinking option if it exists
const showThinking = options ? . showThinking === true ;
// Get enhanced context for this note and query
const enhancedContext = await contextService . getAgentToolsContext (
noteId ,
content ,
showThinking
) ;
// Prepend a system message with context
const systemMessage : Message = {
role : 'system' ,
content : ` You are an AI assistant helping with Trilium Notes. Use this context to answer the user's question: \ n \ n ${ enhancedContext } `
} ;
2025-03-11 18:07:28 +00:00
2025-03-19 18:49:14 +00:00
// Create messages array with system message
const messagesWithContext = [ systemMessage , . . . session . messages ] ;
2025-03-11 18:07:28 +00:00
2025-03-19 18:49:14 +00:00
// Generate AI response
const response = await aiServiceManager . generateChatCompletion (
messagesWithContext ,
options
) ;
// Add assistant message
const assistantMessage : Message = {
role : 'assistant' ,
content : response.text
} ;
session . messages . push ( assistantMessage ) ;
session . isStreaming = false ;
// Save the complete conversation (without system message)
await chatStorageService . updateChat ( session . id , session . messages ) ;
// If first message, update the title
if ( session . messages . length <= 2 && ( ! session . title || session . title === 'New Chat' ) ) {
const title = this . generateTitleFromMessages ( session . messages ) ;
session . title = title ;
await chatStorageService . updateChat ( session . id , session . messages , title ) ;
}
// Notify streaming is complete
if ( streamCallback ) {
streamCallback ( response . text , true ) ;
this . streamingCallbacks . delete ( session . id ) ;
}
return session ;
} catch ( error : any ) {
session . isStreaming = false ;
console . error ( 'Error in context-aware chat:' , error ) ;
// Add error message
const errorMessage : Message = {
role : 'assistant' ,
content : ` Error: Failed to generate response with note context. ${ error . message || 'Please try again.' } `
} ;
session . messages . push ( errorMessage ) ;
// Save the conversation with error
await chatStorageService . updateChat ( session . id , session . messages ) ;
// Notify streaming is complete with error
if ( streamCallback ) {
streamCallback ( errorMessage . content , true ) ;
this . streamingCallbacks . delete ( session . id ) ;
}
return session ;
}
2025-03-11 18:07:28 +00:00
}
2025-03-02 19:39:10 -08:00
/ * *
* Get all user ' s chat sessions
* /
async getAllSessions ( ) : Promise < ChatSession [ ] > {
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
} ) ) ;
}
/ * *
* Delete a chat session
* /
async deleteSession ( sessionId : string ) : Promise < boolean > {
this . activeSessions . delete ( sessionId ) ;
this . streamingCallbacks . delete ( sessionId ) ;
return chatStorageService . deleteChat ( sessionId ) ;
}
/ * *
* Generate a title from the first messages in a conversation
* /
private generateTitleFromMessages ( messages : Message [ ] ) : string {
if ( messages . length < 2 ) {
return 'New Chat' ;
}
// Get the first user message
const firstUserMessage = messages . find ( m = > m . role === 'user' ) ;
if ( ! firstUserMessage ) {
return 'New Chat' ;
}
// Extract first line or first few words
const firstLine = firstUserMessage . content . split ( '\n' ) [ 0 ] . trim ( ) ;
if ( firstLine . length <= 30 ) {
return firstLine ;
}
// Take first 30 chars if too long
return firstLine . substring ( 0 , 27 ) + '...' ;
}
}
// Singleton instance
const chatService = new ChatService ( ) ;
export default chatService ;