UNPKG

@alanhelmick/memorable

Version:

An AI memory system enabling personalized, context-aware interactions through advanced memory management and emotional intelligence

1 lines 95.4 kB
{"version":3,"file":"index.cjs","sources":["../src/utils/logger.js","../src/constants/emotions.js","../src/services/humeService.js","../src/services/videoStreamService.js","../src/config/redis.js","../src/services/emotionalContextService.js","../src/config/database.js","../src/services/customModelService.js","../src/services/modelSelectionService.js","../src/utils/healthCheck.js","../src/config/weaviate.js","../src/index.js"],"sourcesContent":["import winston from 'winston';\n\nconst logLevels = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\nconst logColors = {\n error: 'red',\n warn: 'yellow',\n info: 'green',\n debug: 'blue',\n};\n\nlet logger = null;\n\nexport async function setupLogger() {\n if (logger) {\n return logger;\n }\n \n await createLogsDirectory();\n\n // Add colors to Winston\n winston.addColors(logColors);\n\n const format = winston.format.combine(\n winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n winston.format.errors({ stack: true }),\n winston.format.colorize({ all: true }),\n winston.format.printf(({ level, message, timestamp, stack }) => {\n if (stack) {\n return `${timestamp} ${level}: ${message}\\n${stack}`;\n }\n return `${timestamp} ${level}: ${message}`;\n })\n );\n\n logger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n levels: logLevels,\n format,\n transports: [\n // Console transport\n new winston.transports.Console(),\n \n // File transport for errors\n new winston.transports.File({\n filename: 'logs/error.log',\n level: 'error',\n format: winston.format.uncolorize(),\n }),\n \n // File transport for all logs\n new winston.transports.File({\n filename: 'logs/combined.log',\n format: winston.format.uncolorize(),\n }),\n ],\n exceptionHandlers: [\n new winston.transports.File({\n filename: 'logs/exceptions.log',\n format: winston.format.uncolorize(),\n }),\n ],\n rejectionHandlers: [\n new winston.transports.File({\n filename: 'logs/rejections.log',\n format: winston.format.uncolorize(),\n }),\n ],\n });\n\n // Create a stream for Morgan HTTP logging\n logger.stream = {\n write: (message) => {\n logger.info(message.trim());\n },\n };\n\n return logger;\n}\n\nexport async function createLogsDirectory() {\n const { mkdir } = await import('fs/promises');\n try {\n await mkdir('logs', { recursive: true });\n } catch (error) {\n if (error.code !== 'EEXIST') {\n console.error('Failed to create logs directory:', error);\n }\n }\n}\n\nexport { logger };","export const expressionColors = {\n admiration: \"#ffc58f\",\n adoration: \"#ffc6cc\",\n aestheticAppreciation: \"#e2cbff\",\n amusement: \"#febf52\",\n anger: \"#b21816\",\n annoyance: \"#ffffff\",\n anxiety: \"#6e42cc\",\n awe: \"#7dabd3\",\n awkwardness: \"#d7d99d\",\n boredom: \"#a4a4a4\",\n calmness: \"#a9cce1\",\n concentration: \"#336cff\",\n contemplation: \"#b0aeef\",\n confusion: \"#c66a26\",\n contempt: \"#76842d\",\n contentment: \"#e5c6b4\",\n craving: \"#54591c\",\n determination: \"#ff5c00\",\n disappointment: \"#006c7c\",\n disapproval: \"#ffffff\",\n disgust: \"#1a7a41\",\n distress: \"#c5f264\",\n doubt: \"#998644\",\n ecstasy: \"#ff48a4\",\n embarrassment: \"#63c653\",\n empathicPain: \"#ca5555\",\n enthusiasm: \"#ffffff\",\n entrancement: \"#7554d6\",\n envy: \"#1d4921\",\n excitement: \"#fff974\",\n fear: \"#d1c9ef\",\n gratitude: \"#ffffff\",\n guilt: \"#879aa1\",\n horror: \"#772e7a\",\n interest: \"#a9cce1\",\n joy: \"#ffd600\",\n love: \"#f44f4c\",\n neutral: \"#879aa1\",\n nostalgia: \"#b087a1\",\n pain: \"#8c1d1d\",\n pride: \"#9a4cb6\",\n realization: \"#217aa8\",\n relief: \"#fe927a\",\n romance: \"#f0cc86\",\n sadness: \"#305575\",\n sarcasm: \"#ffffff\",\n satisfaction: \"#a6ddaf\",\n sexualDesire: \"#aa0d59\",\n shame: \"#8a6262\",\n surprise: \"#70e63a\",\n surpriseNegative: \"#70e63a\",\n surprisePositive: \"#7affff\",\n sympathy: \"#7f88e0\",\n tiredness: \"#757575\",\n triumph: \"#ec8132\",\n};\n\nexport const isExpressionColor = (color) => {\n return color in expressionColors;\n};\n\n// Get the number of emotional dimensions from the colors\nexport const EMOTION_DIMENSIONS = Object.keys(expressionColors).length;\n\n// Convert emotion to RGB vector for neural processing\nexport const emotionToVector = (emotion) => {\n const color = expressionColors[emotion];\n if (!color) return null;\n\n // Convert hex to RGB\n const r = parseInt(color.slice(1, 3), 16) / 255;\n const g = parseInt(color.slice(3, 5), 16) / 255;\n const b = parseInt(color.slice(5, 7), 16) / 255;\n\n return [r, g, b];\n};\n\n// Convert RGB vector back to closest emotion\nexport const vectorToEmotion = (vector) => {\n if (!vector || vector.length !== 3) return 'neutral';\n\n // Convert vector back to hex color\n const toHex = (n) => {\n const hex = Math.round(n * 255).toString(16);\n return hex.length === 1 ? '0' + hex : hex;\n };\n const color = `#${toHex(vector[0])}${toHex(vector[1])}${toHex(vector[2])}`;\n\n // Find closest matching emotion\n let closestEmotion = 'neutral';\n let minDistance = Infinity;\n\n for (const [emotion, emotionColor] of Object.entries(expressionColors)) {\n const distance = colorDistance(color, emotionColor);\n if (distance < minDistance) {\n minDistance = distance;\n closestEmotion = emotion;\n }\n }\n\n return closestEmotion;\n};\n\n// Calculate Euclidean distance between two hex colors\nconst colorDistance = (color1, color2) => {\n const r1 = parseInt(color1.slice(1, 3), 16);\n const g1 = parseInt(color1.slice(3, 5), 16);\n const b1 = parseInt(color1.slice(5, 7), 16);\n\n const r2 = parseInt(color2.slice(1, 3), 16);\n const g2 = parseInt(color2.slice(3, 5), 16);\n const b2 = parseInt(color2.slice(5, 7), 16);\n\n return Math.sqrt(\n Math.pow(r1 - r2, 2) +\n Math.pow(g1 - g2, 2) +\n Math.pow(b1 - b2, 2)\n );\n};","import WebSocket from 'ws';\nimport { logger } from '../utils/logger.js';\nimport { expressionColors, emotionToVector } from '../constants/emotions.js';\n\nexport class HumeService {\n constructor() {\n this.ws = null;\n this.apiKey = process.env.HUME_API_KEY;\n this.endpoint = process.env.HUME_ENDPOINT;\n this.isConnected = false;\n this.activeStreams = new Map();\n this.messageQueue = [];\n this.processingQueue = false;\n this.lastActivityTime = Date.now();\n this.inactivityTimeout = 60000; // 1 minute inactivity timeout\n this.reconnectAttempts = 0;\n this.maxReconnectAttempts = 5;\n this.reconnectDelay = 1000; // Start with 1 second\n }\n\n async connect(config = {}) {\n if (!this.apiKey) {\n throw new Error('Hume API key not configured');\n }\n\n return new Promise((resolve, reject) => {\n try {\n const params = new URLSearchParams({\n apiKey: this.apiKey,\n ...config\n });\n\n const wsUrl = `${this.endpoint}?${params.toString()}`;\n this.ws = new WebSocket(wsUrl);\n\n this.ws.on('open', () => {\n logger.info('Connected to Hume.ai websocket');\n this.isConnected = true;\n this.reconnectAttempts = 0;\n this.reconnectDelay = 1000;\n this.setupInactivityCheck();\n resolve();\n });\n\n this.ws.on('message', (data) => {\n this.lastActivityTime = Date.now();\n this.handleMessage(data);\n });\n\n this.ws.on('error', (error) => {\n logger.error('Hume websocket error:', error);\n this.handleError(error);\n });\n\n this.ws.on('close', () => {\n logger.info('Hume websocket closed');\n this.isConnected = false;\n this.handleDisconnect();\n });\n } catch (error) {\n reject(error);\n }\n });\n }\n\n setupInactivityCheck() {\n setInterval(() => {\n const inactiveTime = Date.now() - this.lastActivityTime;\n if (inactiveTime >= this.inactivityTimeout) {\n logger.warn('WebSocket inactive, reconnecting...');\n this.reconnect();\n }\n }, 10000); // Check every 10 seconds\n }\n\n async reconnect() {\n if (this.reconnectAttempts < this.maxReconnectAttempts) {\n this.reconnectAttempts++;\n this.reconnectDelay *= 2; // Exponential backoff\n logger.info(`Attempting to reconnect in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);\n \n setTimeout(async () => {\n try {\n await this.connect();\n // Resubscribe active streams\n for (const [streamId, config] of this.activeStreams) {\n await this.startStream(streamId, config);\n }\n } catch (error) {\n logger.error('Reconnection attempt failed:', error);\n }\n }, this.reconnectDelay);\n } else {\n logger.error('Max reconnection attempts reached');\n }\n }\n\n async startStream(streamId, config) {\n if (!this.isConnected) {\n await this.connect();\n }\n\n const streamConfig = {\n models: config.models || { language: {}, face: {}, prosody: {} },\n raw_text: config.rawText || true,\n reset_stream: config.resetStream || false\n };\n\n this.activeStreams.set(streamId, {\n config: streamConfig,\n callbacks: new Map(),\n buffer: []\n });\n\n await this.sendMessage({\n type: 'stream_start',\n stream_id: streamId,\n config: streamConfig\n });\n\n logger.info(`Started stream ${streamId}`);\n }\n\n async stopStream(streamId) {\n const stream = this.activeStreams.get(streamId);\n if (!stream) return;\n\n await this.sendMessage({\n type: 'stream_end',\n stream_id: streamId\n });\n\n this.activeStreams.delete(streamId);\n logger.info(`Stopped stream ${streamId}`);\n }\n\n async processText(text, streamId = null) {\n const id = streamId || `text_${Date.now()}`;\n if (!streamId) {\n await this.startStream(id, { models: { language: {} } });\n }\n\n return this.sendData(id, {\n type: 'text',\n data: text\n });\n }\n\n async processVoice(audioData, streamId = null) {\n const id = streamId || `voice_${Date.now()}`;\n if (!streamId) {\n await this.startStream(id, { models: { prosody: {} } });\n }\n\n // Split audio into 5-second chunks\n const chunks = this.splitAudioIntoChunks(audioData);\n const results = [];\n\n for (const chunk of chunks) {\n const result = await this.sendData(id, {\n type: 'prosody',\n data: chunk.toString('base64')\n });\n results.push(result);\n }\n\n if (!streamId) {\n await this.stopStream(id);\n }\n\n return this.mergeResults(results);\n }\n\n async processFacial(imageData, streamId = null) {\n const id = streamId || `face_${Date.now()}`;\n if (!streamId) {\n await this.startStream(id, { models: { face: {} } });\n }\n\n const result = await this.sendData(id, {\n type: 'face',\n data: imageData.toString('base64')\n });\n\n if (!streamId) {\n await this.stopStream(id);\n }\n\n return result;\n }\n\n splitAudioIntoChunks(audioData, chunkSize = 5000) {\n // Split audio data into 5-second chunks\n const chunks = [];\n let offset = 0;\n while (offset < audioData.length) {\n chunks.push(audioData.slice(offset, offset + chunkSize));\n offset += chunkSize;\n }\n return chunks;\n }\n\n mergeResults(results) {\n // Merge multiple chunk results into a single result\n const merged = {\n emotions: new Map()\n };\n\n results.forEach(result => {\n result.emotions.forEach(emotion => {\n const existing = merged.emotions.get(emotion.name) || { score: 0, count: 0 };\n existing.score += emotion.score;\n existing.count += 1;\n merged.emotions.set(emotion.name, existing);\n });\n });\n\n // Average the scores\n return Array.from(merged.emotions.entries()).map(([name, data]) => ({\n name,\n score: data.score / data.count\n }));\n }\n\n async sendData(streamId, data) {\n return new Promise((resolve, reject) => {\n const messageId = Date.now().toString();\n const stream = this.activeStreams.get(streamId);\n \n if (!stream) {\n reject(new Error(`Stream ${streamId} not found`));\n return;\n }\n\n stream.callbacks.set(messageId, (response) => {\n if (response.error) {\n reject(new Error(response.error));\n } else {\n resolve(this.processEmotions(response.emotions));\n }\n });\n\n this.sendMessage({\n id: messageId,\n stream_id: streamId,\n ...data\n });\n });\n }\n\n async sendMessage(message) {\n if (!this.isConnected) {\n throw new Error('WebSocket not connected');\n }\n\n this.messageQueue.push(message);\n if (!this.processingQueue) {\n await this.processMessageQueue();\n }\n }\n\n async processMessageQueue() {\n this.processingQueue = true;\n while (this.messageQueue.length > 0) {\n const message = this.messageQueue.shift();\n try {\n this.ws.send(JSON.stringify(message));\n this.lastActivityTime = Date.now();\n // Small delay to respect rate limits\n await new Promise(resolve => setTimeout(resolve, 20));\n } catch (error) {\n logger.error('Failed to send message:', error);\n this.messageQueue.unshift(message);\n break;\n }\n }\n this.processingQueue = false;\n }\n\n handleMessage(data) {\n try {\n const message = JSON.parse(data);\n const stream = this.activeStreams.get(message.stream_id);\n \n if (stream && message.id && stream.callbacks.has(message.id)) {\n const callback = stream.callbacks.get(message.id);\n callback(message);\n stream.callbacks.delete(message.id);\n }\n } catch (error) {\n logger.error('Error handling Hume message:', error);\n }\n }\n\n processEmotions(emotions) {\n return emotions.map(emotion => ({\n name: emotion.name,\n score: emotion.score,\n vector: emotionToVector(emotion.name),\n color: expressionColors[emotion.name],\n confidence: emotion.confidence || emotion.score\n }))\n .filter(emotion => emotion.confidence >= 0.1)\n .sort((a, b) => b.score - a.score);\n }\n\n async close() {\n // Stop all active streams\n for (const streamId of this.activeStreams.keys()) {\n await this.stopStream(streamId);\n }\n\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n this.isConnected = false;\n logger.info('Hume websocket connection closed');\n }\n }\n}\n\n// Create singleton instance\nconst humeService = new HumeService();\n\nexport default humeService;","import { logger } from '../utils/logger.js';\nimport humeService from './humeService.js';\n\nexport class VideoStreamService {\n constructor() {\n this.activeStreams = new Map();\n this.chunkDuration = 5000; // 5 seconds in milliseconds\n this.maxResolution = { width: 3000, height: 3000 };\n this.processingInterval = 1000; // Process every second\n }\n\n async startStream(streamId, onEmotionUpdate, config = {}) {\n if (this.activeStreams.has(streamId)) {\n logger.warn(`Stream ${streamId} is already active`);\n return;\n }\n\n const streamContext = {\n id: streamId,\n buffer: [],\n lastProcessed: Date.now(),\n onUpdate: onEmotionUpdate,\n processingInterval: null,\n config: {\n resetStream: config.resetStream || false,\n models: {\n face: config.faceConfig || {}\n },\n ...config\n }\n };\n\n try {\n // Start Hume stream\n await humeService.startStream(streamId, streamContext.config);\n\n // Start processing interval\n streamContext.processingInterval = setInterval(\n () => this.processStreamBuffer(streamContext),\n this.processingInterval\n );\n\n this.activeStreams.set(streamId, streamContext);\n logger.info(`Started video stream ${streamId}`);\n } catch (error) {\n logger.error(`Failed to start video stream ${streamId}:`, error);\n throw error;\n }\n }\n\n async stopStream(streamId) {\n const streamContext = this.activeStreams.get(streamId);\n if (!streamContext) {\n logger.warn(`Stream ${streamId} not found`);\n return;\n }\n\n // Clear processing interval\n if (streamContext.processingInterval) {\n clearInterval(streamContext.processingInterval);\n }\n\n // Process any remaining frames\n await this.processStreamBuffer(streamContext);\n\n // Stop Hume stream\n await humeService.stopStream(streamId);\n\n this.activeStreams.delete(streamId);\n logger.info(`Stopped video stream ${streamId}`);\n }\n\n async addFrame(streamId, frameData, timestamp = Date.now()) {\n const streamContext = this.activeStreams.get(streamId);\n if (!streamContext) {\n logger.warn(`Stream ${streamId} not found, frame discarded`);\n return;\n }\n\n try {\n // Validate frame dimensions\n const dimensions = await this.getFrameDimensions(frameData);\n if (!this.validateFrameDimensions(dimensions)) {\n logger.warn(`Frame dimensions exceed maximum (${dimensions.width}x${dimensions.height})`);\n return;\n }\n\n streamContext.buffer.push({\n data: frameData,\n timestamp\n });\n\n // Trim buffer if it gets too large\n this.trimBuffer(streamContext);\n } catch (error) {\n logger.error(`Error adding frame to stream ${streamId}:`, error);\n }\n }\n\n async processStreamBuffer(streamContext) {\n if (streamContext.buffer.length === 0) return;\n\n try {\n const now = Date.now();\n const chunkStartTime = now - this.chunkDuration;\n\n // Get frames within the current chunk\n const chunkFrames = streamContext.buffer.filter(\n frame => frame.timestamp >= chunkStartTime\n );\n\n if (chunkFrames.length === 0) return;\n\n // Select the best frame from the chunk\n const selectedFrame = this.selectBestFrame(chunkFrames);\n\n // Process the frame with Hume\n const emotions = await humeService.processFacial(\n selectedFrame.data,\n streamContext.id\n );\n\n // Update emotional state\n if (streamContext.onUpdate && emotions.length > 0) {\n streamContext.onUpdate({\n streamId: streamContext.id,\n timestamp: now,\n emotions,\n frameCount: chunkFrames.length,\n selectedFrameTime: selectedFrame.timestamp\n });\n }\n\n // Remove processed frames\n streamContext.buffer = streamContext.buffer.filter(\n frame => frame.timestamp > chunkStartTime\n );\n\n streamContext.lastProcessed = now;\n } catch (error) {\n logger.error(`Error processing stream ${streamContext.id}:`, error);\n }\n }\n\n selectBestFrame(frames) {\n // For now, select the middle frame\n // Could be enhanced with frame quality detection\n return frames[Math.floor(frames.length / 2)];\n }\n\n trimBuffer(streamContext) {\n const now = Date.now();\n // Keep only frames from the last chunk duration\n streamContext.buffer = streamContext.buffer.filter(\n frame => now - frame.timestamp <= this.chunkDuration\n );\n }\n\n async getFrameDimensions(frameData) {\n // Implementation would depend on how frames are provided\n // This is a placeholder that should be implemented based on\n // the actual frame format (e.g., raw pixels, base64 image, etc.)\n return {\n width: 1280, // placeholder\n height: 720 // placeholder\n };\n }\n\n validateFrameDimensions(dimensions) {\n return dimensions.width <= this.maxResolution.width &&\n dimensions.height <= this.maxResolution.height;\n }\n\n getStreamStatus(streamId) {\n const streamContext = this.activeStreams.get(streamId);\n if (!streamContext) return null;\n\n return {\n id: streamContext.id,\n isActive: true,\n bufferSize: streamContext.buffer.length,\n lastProcessed: streamContext.lastProcessed,\n timeSinceLastProcess: Date.now() - streamContext.lastProcessed,\n config: streamContext.config\n };\n }\n\n getAllStreams() {\n return Array.from(this.activeStreams.keys()).map(id => this.getStreamStatus(id));\n }\n\n async cleanup() {\n // Stop all active streams\n for (const streamId of this.activeStreams.keys()) {\n await this.stopStream(streamId);\n }\n }\n}\n\n// Create singleton instance\nconst videoStreamService = new VideoStreamService();\n\nexport default videoStreamService;","import { createClient } from 'redis';\nimport { logger } from '../utils/logger.js';\n\nlet client = null;\n\nexport async function setupRedis() {\n try {\n const url = process.env.REDIS_URL;\n if (!url) {\n throw new Error('REDIS_URL environment variable is not set');\n }\n\n client = createClient({\n url,\n socket: {\n reconnectStrategy: (retries) => {\n const maxRetryTime = 3000; // 3 seconds\n const retryTime = Math.min(retries * 100, maxRetryTime);\n logger.info(`Retrying Redis connection in ${retryTime}ms (attempt ${retries + 1})`);\n return retryTime;\n },\n connectTimeout: 10000, // 10 seconds\n keepAlive: 5000, // 5 seconds\n },\n database: 0,\n commandsQueueMaxLength: 100000,\n readonly: false,\n legacyMode: false,\n isolationPoolOptions: {\n min: 5,\n max: 20,\n acquireTimeoutMillis: 5000,\n createTimeoutMillis: 5000,\n idleTimeoutMillis: 5000,\n createRetryIntervalMillis: 200,\n }\n });\n\n // Event handlers\n client.on('connect', () => {\n logger.info('Redis client connecting...');\n });\n\n client.on('ready', () => {\n logger.info('Redis client connected and ready');\n });\n\n client.on('error', (err) => {\n logger.error('Redis client error:', err);\n });\n\n client.on('reconnecting', () => {\n logger.info('Redis client reconnecting...');\n });\n\n client.on('end', () => {\n logger.info('Redis client connection closed');\n });\n\n // Connect to Redis\n await client.connect();\n\n // Test connection\n await testConnection();\n\n // Initialize data structures\n await initializeDataStructures();\n\n return client;\n } catch (error) {\n logger.error('Failed to setup Redis:', error);\n throw error;\n }\n}\n\nasync function testConnection() {\n try {\n await client.ping();\n logger.info('Redis connection test successful');\n } catch (error) {\n logger.error('Redis connection test failed:', error);\n throw error;\n }\n}\n\nasync function initializeDataStructures() {\n try {\n // Initialize emotional state hash if it doesn't exist\n const emotionalStateExists = await client.exists('emotional_state');\n if (!emotionalStateExists) {\n await client.hSet('emotional_state', {\n emotion: 'neutral',\n vector: '[]',\n confidence: '1.0',\n timestamp: Date.now().toString(),\n type: 'system'\n });\n }\n\n // Initialize memory windows\n const windows = ['short_term', 'medium_term', 'long_term'];\n for (const window of windows) {\n const key = `memory:window:${window}`;\n const exists = await client.exists(key);\n if (!exists) {\n await client.zAdd(key, {\n score: Date.now(),\n value: JSON.stringify({\n type: 'system',\n content: 'initialized',\n timestamp: Date.now()\n })\n });\n }\n }\n\n // Initialize attention system structures\n const attentionExists = await client.exists('attention:4w');\n if (!attentionExists) {\n await client.hSet('attention:4w', {\n who: '[]',\n what: '[]',\n when: '[]',\n where: '[]'\n });\n }\n\n logger.info('Redis data structures initialized');\n } catch (error) {\n logger.error('Failed to initialize Redis data structures:', error);\n throw error;\n }\n}\n\nexport function getRedisClient() {\n if (!client) {\n throw new Error('Redis client not initialized. Call setupRedis first.');\n }\n return client;\n}\n\nexport async function closeRedis() {\n if (client) {\n try {\n await client.quit();\n client = null;\n logger.info('Redis connection closed gracefully');\n } catch (error) {\n logger.error('Error closing Redis connection:', error);\n // Force close if graceful shutdown fails\n client.disconnect();\n client = null;\n }\n }\n}\n\n// Cleanup handler for graceful shutdown\nprocess.on('SIGTERM', async () => {\n logger.info('SIGTERM received, closing Redis connection...');\n await closeRedis();\n});\n\nprocess.on('SIGINT', async () => {\n logger.info('SIGINT received, closing Redis connection...');\n await closeRedis();\n});","import { logger } from '../utils/logger.js';\nimport humeService from './humeService.js';\nimport videoStreamService from './videoStreamService.js';\nimport { getRedisClient } from '../config/redis.js';\nimport { emotionToVector, vectorToEmotion } from '../constants/emotions.js';\n\nexport class EmotionalContextService {\n constructor() {\n this.redis = null;\n this.activeContexts = new Map();\n this.emotionalBuffer = new Map();\n this.bufferTimeout = 5000; // 5 seconds\n this.customModelEnabled = false;\n this.weights = {\n evi: 0.5, // EVI's built-in emotional processing\n video: 0.3, // Video facial analysis\n voice: 0.2 // Voice prosody analysis\n };\n }\n\n async initialize() {\n this.redis = getRedisClient();\n await this.setupEmotionalBuffers();\n await this.loadCustomModel();\n }\n\n async loadCustomModel() {\n try {\n const hasCustomModel = await this.redis.exists('custom_model_config');\n if (hasCustomModel) {\n const config = JSON.parse(await this.redis.get('custom_model_config'));\n this.customModelEnabled = config.enabled;\n if (config.weights) {\n this.weights = config.weights;\n }\n logger.info('Custom model configuration loaded');\n }\n } catch (error) {\n logger.error('Failed to load custom model:', error);\n }\n }\n\n async setupEmotionalBuffers() {\n try {\n const bufferKey = 'emotional_context_buffers';\n const exists = await this.redis.exists(bufferKey);\n \n if (!exists) {\n await this.redis.hSet(bufferKey, {\n active_sessions: '{}',\n buffer_timeouts: '{}'\n });\n }\n } catch (error) {\n logger.error('Failed to setup emotional buffers:', error);\n throw error;\n }\n }\n\n async startContext(contextId, options = {}) {\n const context = {\n id: contextId,\n startTime: Date.now(),\n options: {\n useVideo: options.useVideo ?? false,\n useVoice: options.useVoice ?? true,\n useEVI: options.useEVI ?? false,\n customModel: options.customModel ?? this.customModelEnabled,\n bufferSize: options.bufferSize ?? 5,\n ...options\n },\n emotionalState: {\n current: 'neutral',\n confidence: 1.0,\n vector: emotionToVector('neutral'),\n history: [],\n sources: {}\n }\n };\n\n this.activeContexts.set(contextId, context);\n\n // Start video stream if enabled\n if (context.options.useVideo) {\n await videoStreamService.startStream(contextId, (emotionData) => {\n this.handleVideoEmotion(contextId, emotionData);\n }, {\n resetStream: true,\n faceConfig: {\n // Configure face detection settings\n minConfidence: 0.7,\n returnPoints: true\n }\n });\n }\n\n // Initialize Hume stream for voice if not using EVI\n if (context.options.useVoice && !context.options.useEVI) {\n await humeService.startStream(contextId, {\n models: { prosody: {} },\n resetStream: true\n });\n }\n\n logger.info(`Started emotional context ${contextId} with options:`, context.options);\n return context;\n }\n\n async handleEVIEmotion(contextId, eviEmotion) {\n const context = this.activeContexts.get(contextId);\n if (!context) {\n logger.warn(`Context ${contextId} not found for EVI emotion`);\n return;\n }\n\n // Store EVI emotion in context sources\n context.emotionalState.sources.evi = {\n emotion: eviEmotion.emotion,\n confidence: eviEmotion.confidence,\n vector: eviEmotion.vector,\n timestamp: Date.now()\n };\n\n await this.updateEmotionalState(contextId, {\n emotion: eviEmotion.emotion,\n confidence: eviEmotion.confidence,\n vector: eviEmotion.vector,\n source: 'evi',\n timestamp: Date.now()\n });\n }\n\n async handleVideoEmotion(contextId, videoEmotion) {\n const context = this.activeContexts.get(contextId);\n if (!context) {\n logger.warn(`Context ${contextId} not found for video emotion`);\n return;\n }\n\n if (videoEmotion.emotions.length > 0) {\n const dominant = videoEmotion.emotions[0];\n \n // Store video emotion in context sources\n context.emotionalState.sources.video = {\n emotion: dominant.name,\n confidence: dominant.confidence,\n vector: dominant.vector,\n timestamp: videoEmotion.timestamp\n };\n\n await this.updateEmotionalState(contextId, {\n emotion: dominant.name,\n confidence: dominant.confidence,\n vector: dominant.vector,\n source: 'video',\n timestamp: videoEmotion.timestamp\n });\n }\n }\n\n async handleVoiceEmotion(contextId, voiceData) {\n const context = this.activeContexts.get(contextId);\n if (!context) {\n logger.warn(`Context ${contextId} not found for voice emotion`);\n return;\n }\n\n try {\n let emotions;\n if (context.options.useEVI) {\n // EVI handles voice emotion processing\n return;\n } else {\n // Process voice through Hume\n emotions = await humeService.processVoice(voiceData, contextId);\n }\n\n if (emotions.length > 0) {\n const dominant = emotions[0];\n \n // Store voice emotion in context sources\n context.emotionalState.sources.voice = {\n emotion: dominant.name,\n confidence: dominant.confidence,\n vector: dominant.vector,\n timestamp: Date.now()\n };\n\n await this.updateEmotionalState(contextId, {\n emotion: dominant.name,\n confidence: dominant.confidence,\n vector: dominant.vector,\n source: 'voice',\n timestamp: Date.now()\n });\n }\n } catch (error) {\n logger.error(`Error processing voice emotion for context ${contextId}:`, error);\n }\n }\n\n async updateEmotionalState(contextId, emotionData) {\n const context = this.activeContexts.get(contextId);\n if (!context) return;\n\n // Add to emotional buffer\n if (!this.emotionalBuffer.has(contextId)) {\n this.emotionalBuffer.set(contextId, []);\n }\n this.emotionalBuffer.get(contextId).push(emotionData);\n\n // Process buffer if it reaches the size limit or after timeout\n if (this.emotionalBuffer.get(contextId).length >= context.options.bufferSize) {\n await this.processEmotionalBuffer(contextId);\n } else {\n // Set timeout to process buffer\n setTimeout(async () => {\n await this.processEmotionalBuffer(contextId);\n }, this.bufferTimeout);\n }\n }\n\n async processEmotionalBuffer(contextId) {\n const buffer = this.emotionalBuffer.get(contextId);\n if (!buffer || buffer.length === 0) return;\n\n const context = this.activeContexts.get(contextId);\n if (!context) return;\n\n const combinedVector = new Array(buffer[0].vector.length).fill(0);\n let totalWeight = 0;\n\n // Process each source type separately\n const sourceGroups = this.groupBySource(buffer);\n \n for (const [source, emotions] of Object.entries(sourceGroups)) {\n const weight = this.weights[source] * this.calculateSourceConfidence(emotions);\n const sourceVector = this.combineSourceEmotions(emotions);\n \n sourceVector.forEach((v, i) => {\n combinedVector[i] += v * weight;\n });\n totalWeight += weight;\n }\n\n // Normalize vector\n if (totalWeight > 0) {\n combinedVector.forEach((_, i) => {\n combinedVector[i] /= totalWeight;\n });\n }\n\n // Update context state\n context.emotionalState = {\n current: vectorToEmotion(combinedVector),\n confidence: totalWeight,\n vector: combinedVector,\n sources: context.emotionalState.sources,\n history: [\n ...context.emotionalState.history,\n {\n timestamp: Date.now(),\n emotions: buffer,\n sources: { ...context.emotionalState.sources }\n }\n ].slice(-100) // Keep last 100 emotional states\n };\n\n // Store in Redis\n await this.redis.hSet(`emotional_context:${contextId}`, {\n state: JSON.stringify(context.emotionalState),\n lastUpdate: Date.now().toString()\n });\n\n // Clear buffer\n this.emotionalBuffer.set(contextId, []);\n }\n\n groupBySource(buffer) {\n return buffer.reduce((groups, emotion) => {\n const source = emotion.source;\n if (!groups[source]) {\n groups[source] = [];\n }\n groups[source].push(emotion);\n return groups;\n }, {});\n }\n\n calculateSourceConfidence(emotions) {\n return emotions.reduce((sum, e) => sum + e.confidence, 0) / emotions.length;\n }\n\n combineSourceEmotions(emotions) {\n const vector = new Array(emotions[0].vector.length).fill(0);\n emotions.forEach(emotion => {\n emotion.vector.forEach((v, i) => {\n vector[i] += v * emotion.confidence;\n });\n });\n return vector;\n }\n\n async getEmotionalContext(contextId) {\n const context = this.activeContexts.get(contextId);\n if (!context) {\n const storedContext = await this.redis.hGetAll(`emotional_context:${contextId}`);\n if (storedContext.state) {\n return JSON.parse(storedContext.state);\n }\n return null;\n }\n return context.emotionalState;\n }\n\n async stopContext(contextId) {\n const context = this.activeContexts.get(contextId);\n if (!context) return;\n\n // Process any remaining emotions in buffer\n await this.processEmotionalBuffer(contextId);\n\n // Stop video stream if active\n if (context.options.useVideo) {\n await videoStreamService.stopStream(contextId);\n }\n\n // Stop Hume stream if using it for voice\n if (context.options.useVoice && !context.options.useEVI) {\n await humeService.stopStream(contextId);\n }\n\n // Clean up\n this.activeContexts.delete(contextId);\n this.emotionalBuffer.delete(contextId);\n\n logger.info(`Stopped emotional context ${contextId}`);\n }\n\n async cleanup() {\n // Stop all active contexts\n for (const contextId of this.activeContexts.keys()) {\n await this.stopContext(contextId);\n }\n }\n}\n\n// Create singleton instance\nconst emotionalContextService = new EmotionalContextService();\n\nexport default emotionalContextService;","import { MongoClient } from 'mongodb';\nimport { logger } from '../utils/logger.js';\n\nlet client = null;\n\nexport async function setupDatabase() {\n try {\n const uri = process.env.MONGODB_URI;\n if (!uri) {\n throw new Error('MONGODB_URI environment variable is not set');\n }\n\n client = new MongoClient(uri, {\n useNewUrlParser: true,\n useUnifiedTopology: true,\n });\n\n await client.connect();\n logger.info('Successfully connected to MongoDB');\n\n // Create time series collections\n const db = client.db('memorable');\n \n // Create collections with time series configuration\n await createTimeSeriesCollection(db, 'emotions', 'timestamp');\n await createTimeSeriesCollection(db, 'interactions', 'timestamp');\n await createTimeSeriesCollection(db, 'contextual_data', 'timestamp');\n \n return client;\n } catch (error) {\n logger.error('MongoDB connection error:', error);\n throw error;\n }\n}\n\nasync function createTimeSeriesCollection(db, collectionName, timeField) {\n try {\n await db.createCollection(collectionName, {\n timeseries: {\n timeField,\n metaField: \"metadata\",\n granularity: \"seconds\"\n }\n });\n logger.info(`Created time series collection: ${collectionName}`);\n } catch (error) {\n // Collection might already exist\n if (error.code !== 48) { // 48 is the error code for \"collection already exists\"\n throw error;\n }\n }\n}\n\nexport function getDatabase() {\n if (!client) {\n throw new Error('Database not initialized. Call setupDatabase first.');\n }\n return client.db('memorable');\n}\n\nexport async function closeDatabase() {\n if (client) {\n await client.close();\n client = null;\n logger.info('MongoDB connection closed');\n }\n}","import { logger } from '../utils/logger.js';\nimport { getRedisClient } from '../config/redis.js';\nimport { getDatabase } from '../config/database.js';\n\nexport class CustomModelService {\n constructor() {\n this.redis = null;\n this.db = null;\n this.apiKey = process.env.HUME_API_KEY;\n this.apiEndpoint = 'https://api.hume.ai/v0/custom/models';\n this.activeModels = new Map();\n this.trainingJobs = new Map();\n }\n\n async initialize() {\n this.redis = getRedisClient();\n this.db = getDatabase();\n await this.loadActiveModels();\n }\n\n async loadActiveModels() {\n try {\n const models = await this.db.collection('custom_models')\n .find({ status: 'active' })\n .toArray();\n\n models.forEach(model => {\n this.activeModels.set(model.modelId, model);\n });\n\n logger.info(`Loaded ${models.length} active custom models`);\n } catch (error) {\n logger.error('Failed to load active models:', error);\n }\n }\n\n async createTrainingJob(userId, config) {\n try {\n const jobId = `train_${Date.now()}_${userId}`;\n const job = {\n id: jobId,\n userId,\n status: 'preparing',\n config: {\n name: config.name,\n description: config.description,\n labelSet: config.labels || [],\n dataConfig: {\n includeExpressions: true,\n includeLanguage: true,\n includeProsody: true\n },\n ...config\n },\n created: new Date(),\n updated: new Date()\n };\n\n await this.db.collection('training_jobs').insertOne(job);\n this.trainingJobs.set(jobId, job);\n\n // Start collecting training data\n await this.collectTrainingData(jobId);\n\n return jobId;\n } catch (error) {\n logger.error('Failed to create training job:', error);\n throw error;\n }\n }\n\n async collectTrainingData(jobId) {\n const job = this.trainingJobs.get(jobId);\n if (!job) throw new Error(`Training job ${jobId} not found`);\n\n try {\n // Update job status\n job.status = 'collecting';\n await this.updateJobStatus(job);\n\n // Get user's emotional history\n const emotionalHistory = await this.getEmotionalHistory(job.userId);\n\n // Process and label the data\n const labeledData = await this.processTrainingData(emotionalHistory, job.config);\n\n // Store processed data\n await this.storeTrainingData(jobId, labeledData);\n\n // Start training if enough data\n if (labeledData.length >= 100) {\n await this.startModelTraining(jobId);\n } else {\n job.status = 'insufficient_data';\n await this.updateJobStatus(job);\n }\n } catch (error) {\n logger.error(`Failed to collect training data for job ${jobId}:`, error);\n job.status = 'failed';\n job.error = error.message;\n await this.updateJobStatus(job);\n }\n }\n\n async getEmotionalHistory(userId) {\n const history = await this.db.collection('emotional_history')\n .find({\n userId,\n timestamp: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } // Last 30 days\n })\n .sort({ timestamp: 1 })\n .toArray();\n\n return history;\n }\n\n async processTrainingData(history, config) {\n const labeledData = [];\n\n for (const entry of history) {\n if (!entry.emotionalState || !entry.context) continue;\n\n const processedEntry = {\n timestamp: entry.timestamp,\n labels: this.generateLabels(entry, config.labelSet),\n data: {\n expressions: entry.emotionalState.sources.video || null,\n language: entry.context.text || null,\n prosody: entry.emotionalState.sources.voice || null\n }\n };\n\n if (this.validateTrainingEntry(processedEntry)) {\n labeledData.push(processedEntry);\n }\n }\n\n return labeledData;\n }\n\n generateLabels(entry, labelSet) {\n const labels = new Set();\n\n // Generate labels based on emotional state and context\n labelSet.forEach(label => {\n if (this.matchesLabelCriteria(entry, label)) {\n labels.add(label);\n }\n });\n\n return Array.from(labels);\n }\n\n matchesLabelCriteria(entry, label) {\n // Implement label matching logic based on emotional state and context\n // This would be customized based on the specific requirements\n return false;\n }\n\n validateTrainingEntry(entry) {\n return entry.labels.length > 0 &&\n (entry.data.expressions || entry.data.language || entry.data.prosody);\n }\n\n async storeTrainingData(jobId, data) {\n await this.db.collection('training_data').insertOne({\n jobId,\n data,\n timestamp: new Date()\n });\n }\n\n async startModelTraining(jobId) {\n const job = this.trainingJobs.get(jobId);\n if (!job) throw new Error(`Training job ${jobId} not found`);\n\n try {\n // Update job status\n job.status = 'training';\n await this.updateJobStatus(job);\n\n // Get training data\n const trainingData = await this.db.collection('training_data')\n .findOne({ jobId });\n\n // Submit training job to Hume API\n const response = await fetch(this.apiEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Hume-Api-Key': this.apiKey\n },\n body: JSON.stringify({\n name: job.config.name,\n description: job.config.description,\n data: trainingData.data\n })\n });\n\n if (!response.ok) {\n throw new Error(`Hume API error: ${response.statusText}`);\n }\n\n const result = await response.json();\n job.modelId = result.model_id;\n job.status = 'training';\n await this.updateJobStatus(job);\n\n // Start monitoring training progress\n this.monitorTraining(jobId);\n } catch (error) {\n logger.error(`Failed to start training for job ${jobId}:`, error);\n job.status = 'failed';\n job.error = error.message;\n await this.updateJobStatus(job);\n }\n }\n\n async monitorTraining(jobId) {\n const job = this.trainingJobs.get(jobId);\n if (!job || !job.modelId) return;\n\n try {\n const response = await fetch(`${this.apiEndpoint}/${job.modelId}`, {\n headers: { 'X-Hume-Api-Key': this.apiKey }\n });\n\n if (!response.ok) {\n throw new Error(`Hume API error: ${response.statusText}`);\n }\n\n const status = await response.json();\n\n if (status.status === 'completed') {\n await this.handleTrainingComplete(job);\n } else if (status.status === 'failed') {\n await this.handleTrainingFailed(job, status.error);\n } else {\n // Check again in 5 minutes\n setTimeout(() => this.monitorTraining(jobId), 5 * 60 * 1000);\n }\n } catch (error) {\n logger.error(`Error monitoring training for job ${jobId}:`, error);\n }\n }\n\n async handleTrainingComplete(job) {\n try {\n // Update job status\n job.status = 'completed';\n await this.updateJobStatus(job);\n\n // Add model to active models\n const model = {\n modelId: job.modelId,\n name: job.config.name,\n description: job.config.description,\n userId: job.userId,\n status: 'active',\n created: new Date(),\n lastUsed: null\n };\n\n await this.db.collection('custom_models').insertOne(model);\n this.activeModels.set(job.modelId, model);\n\n logger.info(`Training completed for job ${job.id}`);\n } catch (error) {\n logger.error(`Error handling training completion for job ${job.id}:`, error);\n }\n }\n\n async handleTrainingFailed(job, error) {\n job.status = 'failed';\n job.error = error;\n await this.updateJobStatus(job);\n logger.error(`Training failed for job ${job.id}:`, error);\n }\n\n async updateJobStatus(job) {\n await this.db.collection('training_jobs').updateOne(\n { id: job.id },\n {\n $set: {\n status: job.status,\n error: job.error,\n updated: new Date(),\n modelId: job.modelId\n }\n }\n );\n }\n\n async getJobStatus(jobId) {\n const job = this.trainingJobs.get(jobId);\n if (!job) {\n const stored = await this.db.collection('training_jobs')\n .findOne({ id: jobId });\n return stored;\n }\n return job;\n }\n\n async getActiveModels(userId) {\n return Array.from(this.activeModels.values())\n .filter(model => model.userId === userId);\n }\n\n async deleteModel(modelId) {\n try {\n // Delete from Hume API\n await fetch(`${this.apiEndpoint}/${modelId}`, {\n method: 'DELETE',\n headers: { 'X-Hume-Api-Key': this.apiKey }\n });\n\n // Update local state\n this.activeModels.delete(modelId);\n\n // Update database\n await this.db.collection('custom_models').updateOne(\n { modelId },\n { $set: { status: 'deleted' } }\n );\n\n logger.info(`Deleted custom model ${modelId}`);\n } catch (error) {\n logger.error(`Failed to delete model ${modelId}:`, error);\n throw error;\n }\n }\n}\n\n// Create singleton instance\nconst customModelService = new CustomModelService();\n\nexport default customModelService;","import { logger } from '../utils/logger.js';\nimport os from 'os';\n\nexport class ModelSelectionService {\n constructor() {\n this.isServerEnvironment = process.env.NODE_ENV === 'production';\n this.hasGPU = process.env.ENABLE_CUDA === '1';\n this.modelConfigs = {\n local: {\n default: 'ollama/mistral:3.2-small',\n management: 'ollama/mistral:3.2-small',\n embedding: 'ollama/nomic-embed-text',\n fallback: 'ollama/tinyllama'\n },\n server: {\n default: 'ollama/mistral:7b-instruct',\n management: 'ollama/mixtral:8x7b-instruct',\n embedding: 'ollama/nomic-embed-text:latest',\n fallback: 'ollama/mistral:3.2-small'\n }\n };\n\n // Performance tracking\n this.metrics = {\n requestCount: 0,\n totalLatency: 0,\n errors: 0,\n lastSwitchTime: Date.now(),\n modelUsage: new Map()\n };\n\n // Memory thresholds (percentage)\n this.memoryThresholds = {\n warning: 80,\n critical: 90\n };\n\n // Memoization caches\n this.responseCache = new Map();\n this.modelStateCache = new Map();\n this.taskPatternCache = new Map();\n\n // Cache configuration\n this.cacheConfig = {\n maxSize: 1000,\n ttl: 3600000, // 1 hour\n criticalityThreshold: 0.8\n };\n\n // Initialize performance monitoring\n this.startPerformanceMonitoring();\n }\n\n getModelConfig(type = 'default') {\n const environment = this.isServerEnvironment ? 'server' : 'local';\n const config = this.modelConfigs[environment];\n\n // If GPU is available locally, we can use larger models\n if (!this.isServerEnvironment && this.hasGPU) {\n return this.modelConfigs.server[type];\n }\n\n return config[type];\n }\n\n async validateModel(modelName) {\n try {\n // Check if model is available in Ollama\n const response = await fetch('http://localhost:11434/api/tags');\n const { models } = await response.json();\n \n return models.some(model => model.name === modelName);\n } catch (error) {\n logger.error('Error validating model:', error);\n this.metrics.errors++;\n return false;\n }\n }\n\n async ensureModel(type = 'default') {\n const modelName = this.getModelConfig(type);\n const isAvailable = await this.validateModel(modelName);\n\n if (!isAvailable) {\n logger.info(`Model ${modelName} not found, falling back to smaller model`);\n return this.modelConfigs[this.isServerEnvironment ? 'server' : 'local'].fallback;\n }\n\n return modelName;\n }\n\n getResourceLimits() {\n if (this.isServerEnvironment) {\n return {\n maxMe