/** * BrainyChat - Magical Chat Command Center * * A smart chat system that leverages Brainy's standard noun/verb types * to create intelligent, persistent conversations with automatic context loading. * * Key Features: * - Uses standard NounType.Message for all chat messages * - Employs VerbType.Communicates and VerbType.Precedes for conversation flow * - Auto-discovery of previous sessions using Brainy's search capabilities * - Full-featured chat with memory and context management */ import { BrainyData } from '../brainyData.js' import { NounType, VerbType, type Message, type GraphNoun, type GraphVerb } from '../types/graphTypes.js' export interface ChatMessage { id: string content: string speaker: 'user' | 'assistant' | string // Allow custom speaker names for multi-agent sessionId: string timestamp: Date metadata?: { model?: string usage?: { prompt_tokens?: number completion_tokens?: number } context?: Record } } export interface ChatSession { id: string title?: string createdAt: Date lastMessageAt: Date messageCount: number participants: string[] metadata?: { tags?: string[] summary?: string archived?: boolean } } /** * BrainyChat with automatic context loading and intelligent memory * * Full-featured chat functionality with conversation persistence */ export class BrainyChat { private brainy: BrainyData private currentSessionId: string | null = null private sessionCache = new Map() constructor(brainy: BrainyData) { this.brainy = brainy } /** * Initialize chat system and auto-discover last session * Uses Brainy's advanced search to find the most recent conversation */ async initialize(): Promise { try { // Search for the most recent chat message using Brainy's search const recentMessages = await this.brainy.search( 'recent chat conversation', { limit: 1, nounTypes: [NounType.Message], metadata: { messageType: 'chat' } } ) if (recentMessages.length > 0) { const lastMessage = recentMessages[0] const sessionId = lastMessage.metadata?.sessionId if (sessionId) { this.currentSessionId = sessionId return await this.loadSession(sessionId) } } } catch (error: any) { console.debug('No previous session found, starting fresh:', error?.message) } return null } /** * Start a new chat session * Automatically generates a session ID and stores session metadata */ async startNewSession(title?: string, participants: string[] = ['user', 'assistant']): Promise { const sessionId = `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` const session: ChatSession = { id: sessionId, title, createdAt: new Date(), lastMessageAt: new Date(), messageCount: 0, participants, metadata: { tags: ['active'] } } // Store session using BrainyData addNoun() method await this.brainy.addNoun( { sessionType: 'chat', title: title || `Chat Session ${new Date().toLocaleDateString()}`, createdAt: session.createdAt.toISOString(), lastMessageAt: session.lastMessageAt.toISOString(), messageCount: session.messageCount, participants: session.participants }, NounType.Concept, // Chat sessions are concepts { id: sessionId, sessionType: 'chat' } ) this.currentSessionId = sessionId this.sessionCache.set(sessionId, session) return session } /** * Add a message to the current session * Stores using standard NounType.Message and creates conversation flow relationships */ async addMessage( content: string, speaker: string = 'user', metadata?: ChatMessage['metadata'] ): Promise { if (!this.currentSessionId) { await this.startNewSession() } const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` const timestamp = new Date() const message: ChatMessage = { id: messageId, content, speaker, sessionId: this.currentSessionId!, timestamp, metadata } // Store message using BrainyData addNoun() method await this.brainy.addNoun( { messageType: 'chat', content, speaker, sessionId: this.currentSessionId!, timestamp: timestamp.toISOString(), ...metadata }, NounType.Message, // Chat messages are Message type { id: messageId, messageType: 'chat', sessionId: this.currentSessionId!, speaker } ) // Create relationships using standard verb types await this.createMessageRelationships(messageId) // Update session metadata await this.updateSessionMetadata() return message } /** * Ask a question and get a template-based response * This provides basic functionality without requiring an LLM */ async ask(question: string, options?: { includeSources?: boolean maxSources?: number sessionId?: string }): Promise { // Add the user's question to the chat await this.addMessage(question, 'user') // Search for relevant content using Brainy's search const searchResults = await this.brainy.search(question, { limit: options?.maxSources || 5 }) // Generate a template-based response let response = '' if (searchResults.length === 0) { response = "I don't have enough information to answer that question based on the current data." } else { // Check if this is a count question if (question.toLowerCase().includes('how many') || question.toLowerCase().includes('count')) { response = `Based on the search results, I found ${searchResults.length} relevant items.` } // Check if this is a list question else if (question.toLowerCase().includes('list') || question.toLowerCase().includes('show me')) { response = `Here are the relevant items I found:\n${searchResults.map((r, i) => `${i + 1}. ${r.metadata?.title || r.metadata?.content || r.id}`).join('\n')}` } // General question else { response = `Based on the available data, I found information related to your question. The most relevant content includes: ${searchResults[0].metadata?.title || searchResults[0].metadata?.content || searchResults[0].id}` } // Add sources if requested if (options?.includeSources && searchResults.length > 0) { response += '\n\nSources: ' + searchResults.map(r => r.id).join(', ') } } // Add the assistant's response to the chat await this.addMessage(response, 'assistant') return response } /** * Get conversation history for current session * Uses Brainy's graph traversal to get messages in chronological order */ async getHistory(limit: number = 50): Promise { if (!this.currentSessionId) return [] try { // Search for messages in this session using Brainy's search const messageNouns = await this.brainy.search( '', // Empty query to get all messages { limit: limit, nounTypes: [NounType.Message], metadata: { sessionId: this.currentSessionId, messageType: 'chat' } } ) return messageNouns.map((noun: any) => this.nounToChatMessage(noun)) } catch (error) { console.error('Error retrieving chat history:', error) return [] } } /** * Search across all chat sessions and messages * Leverages Brainy's powerful vector and semantic search */ async searchMessages( query: string, options?: { sessionId?: string speaker?: string limit?: number semanticSearch?: boolean } ): Promise { const metadata: Record = { messageType: 'chat' } if (options?.sessionId) { metadata.sessionId = options.sessionId } if (options?.speaker) { metadata.speaker = options.speaker } try { const results = await this.brainy.search( options?.semanticSearch !== false ? query : '', { limit: options?.limit || 20, nounTypes: [NounType.Message], metadata } ) return results.map((noun: any) => this.nounToChatMessage(noun)) } catch (error) { console.error('Error searching messages:', error) return [] } } /** * Get all chat sessions * Uses Brainy's search to find all conversation sessions */ async getSessions(limit: number = 20): Promise { try { const sessionNouns = await this.brainy.search( '', { limit: limit, nounTypes: [NounType.Concept], metadata: { sessionType: 'chat' } } ) return sessionNouns.map((noun: any) => this.nounToChatSession(noun)) } catch (error) { console.error('Error retrieving sessions:', error) return [] } } /** * Switch to a different session * Automatically loads context and history */ async switchToSession(sessionId: string): Promise { try { const session = await this.loadSession(sessionId) if (session) { this.currentSessionId = sessionId this.sessionCache.set(sessionId, session) } return session } catch (error) { console.error('Error switching to session:', error) return null } } /** * Archive a session * Maintains full searchability while organizing conversations */ async archiveSession(sessionId: string): Promise { try { // Since BrainyData doesn't have update, add an archive marker await this.brainy.addNoun( { archivedSessionId: sessionId, archivedAt: new Date().toISOString(), action: 'archive' }, NounType.State, // Archive markers are State { sessionId, archived: true } ) return true } catch (error) { console.error('Error archiving session:', error) } return false } /** * Generate session summary * Creates a simple summary of the conversation * For AI summaries, users can integrate their own LLM */ async generateSessionSummary(sessionId: string): Promise { try { const messages = await this.getHistoryForSession(sessionId, 100) const content = messages .map(msg => `${msg.speaker}: ${msg.content}`) .join('\n') // Use Brainy's AI to generate summary (placeholder - would need actual AI integration) const summaryResponse = `Summary of ${messages.length} messages discussing various topics in ${sessionId}` return summaryResponse || null } catch (error) { console.error('Error generating session summary:', error) return null } } // Private helper methods private async createMessageRelationships(messageId: string): Promise { // Link message to session using unified addVerb API await this.brainy.addVerb( messageId, this.currentSessionId!, VerbType.PartOf, { relationship: 'message-in-session' } ) // Find previous message to create conversation flow using VerbType.Precedes const previousMessages = await this.brainy.search( '', { limit: 1, nounTypes: [NounType.Message], metadata: { sessionId: this.currentSessionId, messageType: 'chat' } } ) if (previousMessages.length > 0 && previousMessages[0].id !== messageId) { await this.brainy.addVerb( previousMessages[0].id, messageId, VerbType.Precedes, { relationship: 'message-sequence' } ) } } private async loadSession(sessionId: string): Promise { try { const sessionNouns = await this.brainy.search( '', { limit: 1, nounTypes: [NounType.Concept], metadata: { sessionType: 'chat' } } ) // Filter by session ID manually since BrainyData search may not support ID filtering const matchingSession = sessionNouns.find(noun => noun.id === sessionId) if (matchingSession) { return this.nounToChatSession(matchingSession) } } catch (error) { console.error('Error loading session:', error) } return null } private async getHistoryForSession(sessionId: string, limit: number = 50): Promise { try { const messageNouns = await this.brainy.search( '', { limit: limit, nounTypes: [NounType.Message], metadata: { sessionId: sessionId, messageType: 'chat' } } ) return messageNouns.map((noun: any) => this.nounToChatMessage(noun)) } catch (error) { console.error('Error retrieving session history:', error) return [] } } private async updateSessionMetadata(): Promise { if (!this.currentSessionId) return // Since BrainyData doesn't have update functionality, we'll skip this // In a real implementation, you'd need update capabilities console.debug('Session metadata update skipped - BrainyData lacks update API') } private nounToChatMessage(noun: any): ChatMessage { return { id: noun.id, content: noun.metadata?.content || noun.data?.content || '', speaker: noun.metadata?.speaker || noun.data?.speaker || 'unknown', sessionId: noun.metadata?.sessionId || noun.data?.sessionId || '', timestamp: new Date(noun.metadata?.timestamp || noun.data?.timestamp || Date.now()), metadata: noun.metadata } } private nounToChatSession(noun: any): ChatSession { return { id: noun.id, title: noun.metadata?.title || noun.data?.title || 'Untitled Session', createdAt: new Date(noun.metadata?.createdAt || noun.data?.createdAt || Date.now()), lastMessageAt: new Date(noun.metadata?.lastMessageAt || noun.data?.lastMessageAt || Date.now()), messageCount: noun.metadata?.messageCount || noun.data?.messageCount || 0, participants: noun.metadata?.participants || noun.data?.participants || ['user', 'assistant'], metadata: noun.metadata } } private toTimestamp(date: Date): { seconds: number; nanoseconds: number } { const seconds = Math.floor(date.getTime() / 1000) const nanoseconds = (date.getTime() % 1000) * 1000000 return { seconds, nanoseconds } } // Public API methods for CLI integration getCurrentSessionId(): string | null { return this.currentSessionId } getCurrentSession(): ChatSession | null { return this.currentSessionId ? this.sessionCache.get(this.currentSessionId) || null : null } }