UNPKG

anon-identity

Version:

Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure

675 lines 25.3 kB
"use strict"; /** * Conversation Manager for MCP * * Orchestrates conversation flow between agents and LLMs with context management */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ConversationManager = void 0; const events_1 = require("events"); const types_1 = require("../types"); /** * Conversation Manager */ class ConversationManager extends events_1.EventEmitter { constructor(messageRouter, contextManager, agentLLMManager, config = { autoSummarize: true, maxIdleTime: 1800000, // 30 minutes contextSwitchThreshold: 0.7, multiTurnEnabled: true, persistHistory: true, enableStreaming: true }) { super(); this.messageRouter = messageRouter; this.contextManager = contextManager; this.agentLLMManager = agentLLMManager; this.config = config; this.sessions = new Map(); this.agentSessions = new Map(); // agentDID -> sessionIds this.multiTurnStates = new Map(); this.startIdleCheck(); this.setupEventHandlers(); } /** * Start conversation session */ async startConversation(agentDID, options = {}) { const sessionId = `conv-${agentDID}-${Date.now()}`; // Create context metadata const contextMetadata = { agentName: agentDID.split(':').pop() || 'Unknown Agent', purpose: options.purpose || 'General conversation', domain: options.domain || 'general', priority: options.priority || types_1.ContextPriority.MEDIUM, retention: { duration: 24 * 60 * 60 * 1000, // 24 hours default autoCompress: true, autoDelete: false, archiveAfter: 30 * 24 * 60 * 60 * 1000, // 30 days ...options.retention }, sharedWith: [] }; // Create conversation context const context = await this.contextManager.createContext(agentDID, sessionId, contextMetadata); // Add system message if provided if (options.systemPrompt) { await this.contextManager.addMessage(context.conversationId, { role: types_1.MessageRole.SYSTEM, content: options.systemPrompt, metadata: { source: 'conversation-manager', type: 'system-prompt' } }); } // Create session const session = { id: sessionId, agentDID, contextId: context.conversationId, metadata: { domain: context.metadata.domain, purpose: context.metadata.purpose, startedAt: new Date(), lastActivity: new Date(), messageCount: 0, totalTokens: 0 }, status: 'active', participants: [agentDID] }; this.sessions.set(sessionId, session); // Update agent sessions index const agentSessions = this.agentSessions.get(agentDID) || new Set(); agentSessions.add(sessionId); this.agentSessions.set(agentDID, agentSessions); this.emit('conversation_started', session); return session; } /** * Send message in conversation */ async sendMessage(sessionId, content, options = {}) { const session = this.sessions.get(sessionId); if (!session || session.status !== 'active') { throw new types_1.MCPError({ code: types_1.MCPErrorCode.INVALID_REQUEST, message: `Invalid or inactive session: ${sessionId}`, timestamp: new Date(), retryable: false }); } const turnStartTime = Date.now(); const context = this.contextManager.getContext(session.contextId); if (!context) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.INVALID_REQUEST, message: `Context not found: ${session.contextId}`, timestamp: new Date(), retryable: false }); } try { // Add user message to context const userMessage = await this.contextManager.addMessage(context.conversationId, { role: options.role || types_1.MessageRole.USER, content, metadata: { ...options.metadata, source: 'user', sessionId } }); // Create LLM request const request = { id: `req-${sessionId}-${Date.now()}`, type: options.enableFunctions ? types_1.LLMRequestType.FUNCTION_CALL : types_1.LLMRequestType.COMPLETION, prompt: await this.buildPrompt(context), agentDID: session.agentDID, sessionId, parameters: { temperature: options.temperature, maxTokens: options.maxTokens, model: undefined // Let provider selection handle this }, metadata: { agentDID: session.agentDID, sessionId, requestId: `req-${sessionId}-${Date.now()}`, timestamp: new Date(), source: 'conversation-manager', priority: options.priority || types_1.RequestPriority.MEDIUM, conversationId: context.conversationId, providerId: options.providerId } }; // Handle function definitions if enabled if (options.enableFunctions) { request.functions = await this.getFunctionDefinitions(session.agentDID); } let response; let functionCalls = []; let functionResults = []; // Route request if (options.streamResponse && this.config.enableStreaming) { response = await this.handleStreamingRequest(request, session, context); } else { response = await this.messageRouter.routeMessage(request); } // Handle function calls if (response.functionCall) { functionCalls = [response.functionCall]; // Execute function if handler available const result = await this.agentLLMManager.executeFunctionCall(response.functionCall); functionResults = [{ functionCallId: response.functionCall.id || `func-${Date.now()}`, result, executionTime: 0, // Will be calculated by the function handler timestamp: new Date() }]; // Add function call message await this.contextManager.addMessage(context.conversationId, { role: types_1.MessageRole.ASSISTANT, content: response.content || '', functionCall: response.functionCall, metadata: { source: 'llm', provider: response.provider, model: response.model } }); // Add function result message await this.contextManager.addMessage(context.conversationId, { role: types_1.MessageRole.FUNCTION, content: JSON.stringify(result), functionResult: functionResults[0], metadata: { source: 'function', functionName: response.functionCall.name } }); // Handle multi-turn if enabled if (this.config.multiTurnEnabled) { const followUpResponse = await this.handleMultiTurn(session, context, response, functionResults[0]); if (followUpResponse) { response = followUpResponse; } } } else { // Add assistant response to context await this.contextManager.addMessage(context.conversationId, { role: types_1.MessageRole.ASSISTANT, content: response.content || '', metadata: { source: 'llm', provider: response.provider, model: response.model, usage: response.usage } }); } // Update session metadata session.metadata.lastActivity = new Date(); session.metadata.messageCount++; if (response.usage) { session.metadata.totalTokens += response.usage.totalTokens || 0; } // Check if context switch needed if (this.shouldSwitchContext(context)) { await this.handleContextSwitch(session); } const turnResult = { request, response, context, session, functionCalls, functionResults, metrics: { turnDuration: Date.now() - turnStartTime, tokensUsed: response.usage?.totalTokens || 0, providerUsed: response.provider || 'unknown' } }; this.emit('turn_completed', turnResult); return turnResult; } catch (error) { this.emit('turn_error', { sessionId, error, context }); throw error; } } /** * Stream message in conversation */ async *streamMessage(sessionId, content, options = {}) { const session = this.sessions.get(sessionId); if (!session || session.status !== 'active') { throw new types_1.MCPError({ code: types_1.MCPErrorCode.INVALID_REQUEST, message: `Invalid or inactive session: ${sessionId}`, timestamp: new Date(), retryable: false }); } const context = this.contextManager.getContext(session.contextId); if (!context) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.INVALID_REQUEST, message: `Context not found: ${session.contextId}`, timestamp: new Date(), retryable: false }); } // Add user message await this.contextManager.addMessage(context.conversationId, { role: options.role || types_1.MessageRole.USER, content, metadata: { ...options.metadata, source: 'user', sessionId } }); // Create request const request = { id: `stream-${sessionId}-${Date.now()}`, type: types_1.LLMRequestType.COMPLETION, prompt: await this.buildPrompt(context), agentDID: session.agentDID, sessionId, parameters: { temperature: options.temperature, maxTokens: options.maxTokens, stream: true }, metadata: { agentDID: session.agentDID, sessionId, requestId: `stream-${sessionId}-${Date.now()}`, timestamp: new Date(), source: 'conversation-manager', priority: options.priority || types_1.RequestPriority.MEDIUM, conversationId: context.conversationId } }; let fullContent = ''; let totalTokens = 0; try { for await (const chunk of this.messageRouter.streamMessage(request)) { fullContent += chunk.content || ''; totalTokens += chunk.tokens || 0; yield chunk; } // Add complete response to context await this.contextManager.addMessage(context.conversationId, { role: types_1.MessageRole.ASSISTANT, content: fullContent, metadata: { source: 'llm', streaming: true, totalTokens } }); // Update session session.metadata.lastActivity = new Date(); session.metadata.messageCount++; session.metadata.totalTokens += totalTokens; } catch (error) { this.emit('stream_error', { sessionId, error, partialContent: fullContent }); throw error; } } /** * Handle streaming request */ async handleStreamingRequest(request, session, context) { let fullContent = ''; let totalTokens = 0; let provider = ''; let model = ''; for await (const chunk of this.messageRouter.streamMessage(request)) { fullContent += chunk.content || ''; totalTokens += chunk.tokens || 0; provider = chunk.provider || provider; model = chunk.model || model; // Emit streaming chunk this.emit('streaming_chunk', { sessionId: session.id, chunk }); } return { id: request.id, status: 'success', content: fullContent, provider, model, timestamp: new Date(), usage: { totalTokens, promptTokens: 0, completionTokens: totalTokens, model, provider } }; } /** * Handle multi-turn conversation */ async handleMultiTurn(session, context, lastResponse, functionResult) { const state = this.multiTurnStates.get(session.id) || { sessionId: session.id, pendingFunctions: [], awaitingInput: false, turnCount: 0 }; state.turnCount++; state.lastResponse = lastResponse; // Check if we need to continue the conversation const needsContinuation = this.shouldContinueMultiTurn(lastResponse, functionResult); if (needsContinuation) { // Build follow-up request const followUpRequest = { id: `followup-${session.id}-${Date.now()}`, type: types_1.LLMRequestType.COMPLETION, prompt: await this.buildPrompt(context), agentDID: session.agentDID, sessionId: session.id, parameters: { temperature: 0.7, maxTokens: 500 }, metadata: { agentDID: session.agentDID, sessionId: session.id, requestId: `followup-${session.id}-${Date.now()}`, timestamp: new Date(), source: 'multi-turn', priority: types_1.RequestPriority.MEDIUM, conversationId: context.conversationId, multiTurn: true } }; try { const response = await this.messageRouter.routeMessage(followUpRequest); // Add follow-up response to context await this.contextManager.addMessage(context.conversationId, { role: types_1.MessageRole.ASSISTANT, content: response.content || '', metadata: { source: 'llm-followup', provider: response.provider, model: response.model, multiTurn: true } }); this.multiTurnStates.set(session.id, state); return response; } catch (error) { // Clean up state on error this.multiTurnStates.delete(session.id); throw error; } } else { // Clean up multi-turn state this.multiTurnStates.delete(session.id); return null; } } /** * Check if multi-turn should continue */ shouldContinueMultiTurn(lastResponse, functionResult) { // Continue if function result indicates more work needed if (functionResult.result?.continue === true) { return true; } // Continue if response asks for more information const content = lastResponse.content?.toLowerCase() || ''; if (content.includes('need more') || content.includes('additional information') || content.includes('clarification')) { return true; } // Continue if response is incomplete if (content.endsWith('...') || content.includes('let me continue') || content.includes('furthermore')) { return true; } return false; } /** * Build prompt from context */ async buildPrompt(context) { let prompt = ''; // Add context summary if available if (context.summary) { prompt += `Previous conversation summary:\n${context.summary}\n\n`; } // Add recent messages const recentMessages = context.history.slice(-10); // Last 10 messages for (const message of recentMessages) { let roleLabel = message.role; if (message.role === types_1.MessageRole.ASSISTANT) { roleLabel = 'Assistant'; } else if (message.role === types_1.MessageRole.USER) { roleLabel = 'User'; } else if (message.role === types_1.MessageRole.SYSTEM) { roleLabel = 'System'; } else if (message.role === types_1.MessageRole.FUNCTION) { roleLabel = `Function (${message.functionCall?.name || 'unknown'})`; } prompt += `${roleLabel}: ${message.content}\n`; if (message.functionCall) { prompt += `Function Call: ${message.functionCall.name}(${JSON.stringify(message.functionCall.arguments)})\n`; } } return prompt.trim(); } /** * Get function definitions for agent */ async getFunctionDefinitions(agentDID) { // This would integrate with the agent's available functions // For now, return empty array return []; } /** * Check if context should be switched */ shouldSwitchContext(context) { const tokenRatio = context.tokens / context.maxTokens; return tokenRatio > this.config.contextSwitchThreshold; } /** * Handle context switch */ async handleContextSwitch(session) { const oldContext = this.contextManager.getContext(session.contextId); if (!oldContext) return; // Compress old context await this.contextManager.compressContext(oldContext); // Create new context const newContext = await this.contextManager.createContext(session.agentDID, `${session.id}-continued`, { ...oldContext.metadata, purpose: `Continued from ${session.contextId}` }); // Add summary message if (oldContext.summary) { await this.contextManager.addMessage(newContext.conversationId, { role: types_1.MessageRole.SYSTEM, content: `Previous conversation summary: ${oldContext.summary}`, metadata: { source: 'context-switch', previousContextId: oldContext.conversationId } }); } // Update session session.contextId = newContext.conversationId; this.emit('context_switched', { session, oldContextId: oldContext.conversationId, newContextId: newContext.conversationId }); } /** * Pause conversation */ pauseConversation(sessionId) { const session = this.sessions.get(sessionId); if (session) { session.status = 'paused'; this.emit('conversation_paused', session); } } /** * Resume conversation */ resumeConversation(sessionId) { const session = this.sessions.get(sessionId); if (session && session.status === 'paused') { session.status = 'active'; session.metadata.lastActivity = new Date(); this.emit('conversation_resumed', session); } } /** * End conversation */ async endConversation(sessionId) { const session = this.sessions.get(sessionId); if (!session) return; session.status = 'ended'; session.metadata.lastActivity = new Date(); // Clean up multi-turn state this.multiTurnStates.delete(sessionId); // Remove from agent sessions const agentSessions = this.agentSessions.get(session.agentDID); if (agentSessions) { agentSessions.delete(sessionId); if (agentSessions.size === 0) { this.agentSessions.delete(session.agentDID); } } // Auto-summarize if enabled if (this.config.autoSummarize) { const context = this.contextManager.getContext(session.contextId); if (context && context.history.length > 5) { await this.contextManager.compressContext(context); } } this.emit('conversation_ended', session); } /** * Get agent conversations */ getAgentConversations(agentDID) { const sessionIds = this.agentSessions.get(agentDID) || new Set(); const sessions = Array.from(sessionIds) .map(id => this.sessions.get(id)) .filter(Boolean); return sessions.sort((a, b) => b.metadata.lastActivity.getTime() - a.metadata.lastActivity.getTime()); } /** * Get conversation statistics */ getStatistics() { const sessions = Array.from(this.sessions.values()); return { totalSessions: sessions.length, activeSessions: sessions.filter(s => s.status === 'active').length, pausedSessions: sessions.filter(s => s.status === 'paused').length, endedSessions: sessions.filter(s => s.status === 'ended').length, averageMessagesPerSession: sessions.reduce((sum, s) => sum + s.metadata.messageCount, 0) / (sessions.length || 1), averageTokensPerSession: sessions.reduce((sum, s) => sum + s.metadata.totalTokens, 0) / (sessions.length || 1), multiTurnSessions: this.multiTurnStates.size }; } /** * Setup event handlers */ setupEventHandlers() { this.messageRouter.on('provider_health_updated', (health) => { this.emit('provider_health_updated', health); }); this.contextManager.on('context_compressed', (event) => { this.emit('context_compressed', event); }); this.agentLLMManager.on('function_executed', (event) => { this.emit('function_executed', event); }); } /** * Start idle session check */ startIdleCheck() { this.idleCheckTimer = setInterval(() => { this.checkIdleSessions(); }, 60000); // Check every minute } /** * Check for idle sessions */ checkIdleSessions() { const now = Date.now(); const idleSessions = []; for (const [id, session] of this.sessions) { const idleTime = now - session.metadata.lastActivity.getTime(); if (idleTime > this.config.maxIdleTime && session.status === 'active') { this.pauseConversation(id); idleSessions.push(id); } } if (idleSessions.length > 0) { this.emit('sessions_idle', idleSessions); } } /** * Shutdown */ shutdown() { if (this.idleCheckTimer) { clearInterval(this.idleCheckTimer); } // End all active sessions for (const [sessionId, session] of this.sessions) { if (session.status === 'active') { this.endConversation(sessionId); } } this.sessions.clear(); this.agentSessions.clear(); this.multiTurnStates.clear(); this.removeAllListeners(); } } exports.ConversationManager = ConversationManager; exports.default = ConversationManager; //# sourceMappingURL=conversation-manager.js.map