import BaseAPI from '../common/BaseAPI.js';
import { v4 as uuidv4 } from 'uuid';
import { EventEmitter } from 'events';
/**
* Chat API handler for generating chat responses with memory context
*/
export default class ChatAPI extends BaseAPI {
constructor(config = {}) {
super(config);
this.memoryManager = null;
this.llmHandler = null;
this.conversationCache = new Map();
this.similarityThreshold = config.similarityThreshold || 0.7;
this.contextWindow = config.contextWindow || 5;
}
async initialize() {
await super.initialize();
// Get dependencies from registry
const registry = this.config.registry;
if (!registry) {
throw new Error('Registry is required for ChatAPI');
}
try {
this.memoryManager = registry.get('memory');
this.llmHandler = registry.get('llm');
this.logger.info('ChatAPI initialized successfully');
} catch (error) {
this.logger.error('Failed to initialize ChatAPI:', error);
throw error;
}
}
/**
* Execute a chat operation
*/
async executeOperation(operation, params) {
this._validateParams(params);
const start = Date.now();
try {
let result;
switch (operation) {
case 'chat':
result = await this.generateChatResponse(params);
break;
case 'stream':
result = await this.streamChatResponse(params);
break;
case 'completion':
result = await this.generateCompletion(params);
break;
default:
throw new Error(`Unknown operation: ${operation}`);
}
const duration = Date.now() - start;
this._emitMetric(`chat.${operation}.duration`, duration);
this._emitMetric(`chat.${operation}.count`, 1);
return result;
} catch (error) {
this._emitMetric(`chat.${operation}.errors`, 1);
throw error;
}
}
/**
* Generate a chat response with memory context
*/
async generateChatResponse({ prompt, conversationId, useMemory = true, temperature = 0.7, model }) {
if (!prompt) {
throw new Error('Prompt is required');
}
try {
// Get or create conversation
const conversation = this._getConversation(conversationId);
// Get relevant memories if needed
let relevantMemories = [];
if (useMemory) {
relevantMemories = await this.memoryManager.retrieveRelevantInteractions(
prompt,
this.similarityThreshold
);
}
// Create context from conversation history and relevant memories
const context = this._buildContext(conversation, relevantMemories);
// Generate response
// Generate response with the specified model or use default
const response = await this.llmHandler.generateResponse(
prompt,
context,
{
temperature,
model: model // Will fall back to LLMHandler's default if not provided
}
);
this.logger.log(`Generated response using model: ${model || 'default'}`);
// Update conversation history
conversation.history.push({ role: 'user', content: prompt });
conversation.history.push({ role: 'assistant', content: response });
// Trim conversation if needed
if (conversation.history.length > this.contextWindow * 2) {
conversation.history = conversation.history.slice(-this.contextWindow * 2);
}
// Store in memory if enabled
if (useMemory) {
const embedding = await this.memoryManager.generateEmbedding(
`${prompt} ${response}`
);
const concepts = await this.memoryManager.extractConcepts(
`${prompt} ${response}`
);
await this.memoryManager.addInteraction(
prompt,
response,
embedding,
concepts,
{
conversationId: conversation.id,
timestamp: Date.now()
}
);
}
this._emitMetric('chat.generate.count', 1);
return {
response,
conversationId: conversation.id,
memoryIds: relevantMemories.map(m => m.interaction.id || '')
.filter(id => id !== '')
};
} catch (error) {
this._emitMetric('chat.generate.errors', 1);
throw error;
}
}
/**
* Stream a chat response with memory context
*/
async streamChatResponse({ prompt, conversationId, useMemory = true, temperature = 0.7 }) {
if (!prompt) {
throw new Error('Prompt is required');
}
try {
// Create a stream
const stream = new EventEmitter();
// Process in background
(async () => {
try {
// Get or create conversation
const conversation = this._getConversation(conversationId);
// Get relevant memories if needed
let relevantMemories = [];
if (useMemory) {
relevantMemories = await this.memoryManager.retrieveRelevantInteractions(
prompt,
this.similarityThreshold
);
}
// Create context from conversation history and relevant memories
const context = this._buildContext(conversation, relevantMemories);
// Generate streaming response
let responseText = '';
const responseStream = await this.llmHandler.generateStreamingResponse(
prompt,
context,
{ temperature }
);
responseStream.on('data', (chunk) => {
responseText += chunk;
stream.emit('data', { chunk });
});
responseStream.on('end', async () => {
// Update conversation history
conversation.history.push({ role: 'user', content: prompt });
conversation.history.push({ role: 'assistant', content: responseText });
// Trim conversation if needed
if (conversation.history.length > this.contextWindow * 2) {
conversation.history = conversation.history.slice(-this.contextWindow * 2);
}
// Store in memory if enabled
if (useMemory) {
const embedding = await this.memoryManager.generateEmbedding(
`${prompt} ${responseText}`
);
const concepts = await this.memoryManager.extractConcepts(
`${prompt} ${responseText}`
);
await this.memoryManager.addInteraction(
prompt,
responseText,
embedding,
concepts,
{
conversationId: conversation.id,
timestamp: Date.now()
}
);
}
// End stream
stream.emit('end');
});
responseStream.on('error', (error) => {
stream.emit('error', error);
});
} catch (error) {
stream.emit('error', error);
}
})();
this._emitMetric('chat.stream.count', 1);
return stream;
} catch (error) {
this._emitMetric('chat.stream.errors', 1);
throw error;
}
}
/**
* Generate a text completion with memory context
*/
async generateCompletion({ prompt, max_tokens = 100, temperature = 0.7 }) {
if (!prompt) {
throw new Error('Prompt is required');
}
try {
// Get relevant memories
const relevantMemories = await this.memoryManager.retrieveRelevantInteractions(
prompt,
this.similarityThreshold
);
// Create context from relevant memories
const context = relevantMemories.map(m =>
`${m.interaction.prompt} ${m.interaction.output}`
).join('\n\n');
// Generate completion
const completion = await this.llmHandler.generateCompletion(
prompt,
context,
{ max_tokens, temperature }
);
this._emitMetric('chat.completion.count', 1);
return {
completion,
memoryIds: relevantMemories.map(m => m.interaction.id || '')
.filter(id => id !== '')
};
} catch (error) {
this._emitMetric('chat.completion.errors', 1);
throw error;
}
}
/**
* Get or create a conversation
*/
_getConversation(conversationId) {
if (!conversationId) {
return this._createConversation();
}
const conversation = this.conversationCache.get(conversationId);
if (!conversation) {
// Create a new conversation with the provided ID
return this._createConversation(conversationId);
}
return conversation;
}
/**
* Create a new conversation
*/
_createConversation(id = null) {
const conversationId = id || uuidv4();
const conversation = {
id: conversationId,
created: Date.now(),
lastAccessed: Date.now(),
history: []
};
this.conversationCache.set(conversationId, conversation);
return conversation;
}
/**
* Build context for the LLM from conversation history and relevant memories
*/
_buildContext(conversation, relevantMemories) {
const context = {
conversation: conversation.history,
relevantMemories: relevantMemories.map(memory => ({
prompt: memory.interaction.prompt,
response: memory.interaction.output,
similarity: memory.similarity
}))
};
return context;
}
/**
* Get chat API metrics
*/
async getMetrics() {
const baseMetrics = await super.getMetrics();
return {
...baseMetrics,
conversations: {
count: this.conversationCache.size,
active: this._getActiveConversationCount()
},
operations: {
chat: {
count: await this._getMetricValue('chat.chat.count'),
errors: await this._getMetricValue('chat.chat.errors'),
duration: await this._getMetricValue('chat.chat.duration')
},
stream: {
count: await this._getMetricValue('chat.stream.count'),
errors: await this._getMetricValue('chat.stream.errors')
},
completion: {
count: await this._getMetricValue('chat.completion.count'),
errors: await this._getMetricValue('chat.completion.errors')
}
}
};
}
_getActiveConversationCount() {
const now = Date.now();
const activeThreshold = 30 * 60 * 1000; // 30 minutes
let count = 0;
for (const conversation of this.conversationCache.values()) {
if (now - conversation.lastAccessed < activeThreshold) {
count++;
}
}
return count;
}
async _getMetricValue(metricName) {
// In a real implementation, this would fetch from a metrics store
return 0;
}
}