UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

624 lines 24.2 kB
import { z } from 'zod'; import { createLogger } from '../utils/logger.js'; /** * Cross-Tool Memory Service * * Preserves context across tool invocations, learns from interactions, * and provides intelligent context for subsequent queries. */ // Memory schemas export const QueryContextSchema = z.object({ id: z.string(), timestamp: z.string().datetime(), tool: z.string(), input: z.any(), output: z.any(), insights: z.array(z.string()).optional(), metadata: z.object({ execution_time_ms: z.number(), confidence_score: z.number().optional(), data_quality: z.string().optional() }).optional() }); export const ConversationThreadSchema = z.object({ thread_id: z.string(), created_at: z.string().datetime(), last_active: z.string().datetime(), queries: z.array(QueryContextSchema), context: z.object({ domain: z.string().optional(), organization_id: z.string().optional(), project_ids: z.array(z.string()).optional(), key_metrics: z.record(z.any()).optional(), learned_preferences: z.record(z.any()).optional() }), summary: z.string().optional() }); export const MemoryStoreSchema = z.object({ threads: z.map(z.string(), ConversationThreadSchema), global_insights: z.array(z.object({ insight: z.string(), confidence: z.number(), source_queries: z.array(z.string()), timestamp: z.string().datetime() })), domain_knowledge: z.record(z.object({ facts: z.array(z.string()), patterns: z.array(z.string()), benchmarks: z.record(z.any()) })) }); export class CrossToolMemory { logger = createLogger({ component: 'CrossToolMemory' }); memoryStore; activeThreadId = null; // Memory configuration CONFIG = { MAX_QUERIES_PER_THREAD: 50, THREAD_TIMEOUT_HOURS: 24, INSIGHT_CONFIDENCE_THRESHOLD: 0.7, PATTERN_DETECTION_MIN_SAMPLES: 3 }; constructor() { this.memoryStore = { threads: new Map(), global_insights: [], domain_knowledge: {} }; } /** * Start or continue a conversation thread */ async startThread(threadId) { const id = threadId || this.generateThreadId(); if (!this.memoryStore.threads.has(id)) { const newThread = { thread_id: id, created_at: new Date().toISOString(), last_active: new Date().toISOString(), queries: [], context: {} }; this.memoryStore.threads.set(id, newThread); this.logger.info('Started new conversation thread', { thread_id: id }); } this.activeThreadId = id; return id; } /** * Record a query and its results */ async recordQuery(tool, input, output, metadata) { if (!this.activeThreadId) { await this.startThread(); } const thread = this.memoryStore.threads.get(this.activeThreadId); if (!thread) return; const query = { id: this.generateQueryId(), timestamp: new Date().toISOString(), tool, input, output, metadata }; // Extract insights from output query.insights = this.extractInsights(output, tool); // Add to thread thread.queries.push(query); thread.last_active = new Date().toISOString(); // Update context this.updateThreadContext(thread, query); // Learn from query await this.learnFromQuery(query, thread); // Cleanup old queries if needed if (thread.queries.length > this.CONFIG.MAX_QUERIES_PER_THREAD) { thread.queries = thread.queries.slice(-this.CONFIG.MAX_QUERIES_PER_THREAD); } this.logger.debug('Recorded query', { thread_id: this.activeThreadId, tool, insights_count: query.insights?.length || 0 }); } /** * Get relevant context for a new query */ async getRelevantContext(tool, input) { const context = { previous_results: [], related_insights: [], domain_facts: [], suggestions: [] }; if (!this.activeThreadId) return context; const thread = this.memoryStore.threads.get(this.activeThreadId); if (!thread) return context; // Get previous results from same tool const sameTool = thread.queries .filter(q => q.tool === tool) .slice(-3); context.previous_results = sameTool.map(q => ({ input: q.input, key_outputs: this.extractKeyOutputs(q.output), timestamp: q.timestamp })); // Get related insights if (input.project_id || input.organization_id) { context.related_insights = this.findRelatedInsights(input.project_id || input.organization_id, thread); } // Get domain facts const domain = thread.context.domain || this.inferDomain(input); if (domain && this.memoryStore.domain_knowledge[domain]) { context.domain_facts = this.memoryStore.domain_knowledge[domain].facts.slice(0, 5); } // Generate suggestions context.suggestions = this.generateSuggestions(tool, input, thread); return context; } /** * Get cross-tool insights */ async getCrossToolInsights() { const insights = { patterns: [], recommendations: [], optimization_opportunities: [] }; if (!this.activeThreadId) return insights; const thread = this.memoryStore.threads.get(this.activeThreadId); if (!thread || thread.queries.length < this.CONFIG.PATTERN_DETECTION_MIN_SAMPLES) { return insights; } // Detect patterns insights.patterns = this.detectQueryPatterns(thread); // Generate recommendations based on patterns insights.recommendations = this.generateCrossToolRecommendations(insights.patterns, thread); // Identify optimization opportunities insights.optimization_opportunities = this.identifyOptimizations(thread); return insights; } /** * Summarize conversation thread */ async summarizeThread(threadId) { const id = threadId || this.activeThreadId; if (!id) return this.getEmptySummary(); const thread = this.memoryStore.threads.get(id); if (!thread) return this.getEmptySummary(); const summary = { summary: this.generateThreadSummary(thread), key_findings: this.extractKeyFindings(thread), decisions_made: this.extractDecisions(thread), next_steps: this.suggestNextSteps(thread) }; // Store summary in thread thread.summary = summary.summary; return summary; } /** * Export memory for persistence */ async exportMemory() { const exportData = { threads: Array.from(this.memoryStore.threads.entries()).map(([id, thread]) => ({ id, thread })), global_insights: this.memoryStore.global_insights, domain_knowledge: this.memoryStore.domain_knowledge, export_timestamp: new Date().toISOString() }; return JSON.stringify(exportData, null, 2); } /** * Import memory from persistence */ async importMemory(data) { try { const importData = JSON.parse(data); // Restore threads this.memoryStore.threads = new Map(importData.threads.map((item) => [item.id, item.thread])); // Restore insights and knowledge this.memoryStore.global_insights = importData.global_insights || []; this.memoryStore.domain_knowledge = importData.domain_knowledge || {}; this.logger.info('Memory imported successfully', { thread_count: this.memoryStore.threads.size, insight_count: this.memoryStore.global_insights.length }); } catch (error) { this.logger.error('Failed to import memory', error); throw error; } } // Private helper methods generateThreadId() { return `thread_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } generateQueryId() { return `query_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } extractInsights(output, tool) { const insights = []; // Tool-specific insight extraction switch (tool) { case 'predict_roi': if (output.summary?.expected_roi > 150) { insights.push(`High ROI project: ${output.summary.expected_roi}%`); } if (output.summary?.payback_period_months < 12) { insights.push(`Quick payback: ${output.summary.payback_period_months} months`); } break; case 'compare_projects': if (output.insights?.best_overall) { insights.push(`Best project: ${output.insights.best_overall}`); } if (output.insights?.synergies?.length > 0) { insights.push('Synergy opportunities identified between projects'); } break; } // Extract insights from AI-optimized responses if (output.insights?.primary) { insights.push(...output.insights.primary.slice(0, 2)); } return insights; } updateThreadContext(thread, query) { // Update organization/project context if (query.input.organization_id) { thread.context.organization_id = query.input.organization_id; } if (query.input.project?.project_name) { if (!thread.context.project_ids) { thread.context.project_ids = []; } if (query.output.project_id) { thread.context.project_ids.push(query.output.project_id); } } // Update domain if (query.input.project?.industry) { thread.context.domain = query.input.project.industry; } // Update key metrics if (!thread.context.key_metrics) { thread.context.key_metrics = {}; } if (query.output.summary) { Object.assign(thread.context.key_metrics, { last_roi: query.output.summary.expected_roi, last_payback: query.output.summary.payback_period_months, last_npv: query.output.summary.net_present_value }); } } async learnFromQuery(query, thread) { // Learn domain patterns if (thread.context.domain) { if (!this.memoryStore.domain_knowledge[thread.context.domain]) { this.memoryStore.domain_knowledge[thread.context.domain] = { facts: [], patterns: [], benchmarks: {} }; } const domain = this.memoryStore.domain_knowledge[thread.context.domain]; // Extract facts if (query.insights && query.insights.length > 0) { domain.facts.push(...query.insights); // Keep unique facts domain.facts = Array.from(new Set(domain.facts)).slice(-50); } // Update benchmarks if (query.output.summary) { domain.benchmarks.avg_roi = this.updateAverage(domain.benchmarks.avg_roi, query.output.summary.expected_roi); domain.benchmarks.avg_payback = this.updateAverage(domain.benchmarks.avg_payback, query.output.summary.payback_period_months); } } // Learn global insights if (query.metadata?.confidence_score && query.metadata.confidence_score > this.CONFIG.INSIGHT_CONFIDENCE_THRESHOLD) { query.insights?.forEach(insight => { this.memoryStore.global_insights.push({ insight, confidence: query.metadata.confidence_score, source_queries: [query.id], timestamp: query.timestamp }); }); // Keep recent insights this.memoryStore.global_insights = this.memoryStore.global_insights .slice(-100); } } extractKeyOutputs(output) { // Extract most important fields based on response structure if (output.executive_summary) { return { headline: output.executive_summary.headline, key_insight: output.executive_summary.key_insight, primary_metric: output.executive_summary.primary_metric }; } if (output.summary) { return { roi: output.summary.expected_roi, payback: output.summary.payback_period_months, npv: output.summary.net_present_value }; } return output; } findRelatedInsights(identifier, thread) { const insights = []; // Find insights from queries with same identifier thread.queries.forEach(query => { if (query.input.project_id === identifier || query.input.organization_id === identifier) { insights.push(...(query.insights || [])); } }); // Add global insights that might be relevant const relevantGlobal = this.memoryStore.global_insights .filter(gi => gi.confidence > this.CONFIG.INSIGHT_CONFIDENCE_THRESHOLD) .map(gi => gi.insight) .slice(0, 3); insights.push(...relevantGlobal); return Array.from(new Set(insights)).slice(0, 5); } inferDomain(input) { if (input.project?.industry) return input.project.industry; if (input.industry) return input.industry; // Infer from project type if (input.project_type?.includes('customer_service')) return 'service'; if (input.project_type?.includes('data_analytics')) return 'technology'; return null; } generateSuggestions(tool, input, thread) { const suggestions = []; // Suggest based on previous queries const previousTools = Array.from(new Set(thread.queries.map(q => q.tool))); if (tool === 'compare_projects' && thread.context.project_ids?.length === 1) { suggestions.push('Add more projects for meaningful comparison'); } // Suggest based on patterns if (thread.queries.length > 5) { const lastQueries = thread.queries.slice(-3); if (lastQueries.every(q => q.tool === tool)) { suggestions.push('Try a different tool for complementary insights'); } } // Suggest based on results if (input.enable_benchmarks === false) { suggestions.push('Enable benchmarks for industry comparison'); } return suggestions; } detectQueryPatterns(thread) { const patterns = []; const toolSequences = new Map(); // Analyze tool usage sequences for (let i = 0; i < thread.queries.length - 1; i++) { const sequence = `${thread.queries[i].tool} → ${thread.queries[i + 1].tool}`; toolSequences.set(sequence, (toolSequences.get(sequence) || 0) + 1); } // Convert to patterns toolSequences.forEach((count, sequence) => { if (count >= 2) { const tools = sequence.split(' → '); patterns.push({ pattern: `Sequential use of ${sequence}`, tools_involved: tools, frequency: count, confidence: Math.min(0.9, count / thread.queries.length) }); } }); // Detect iterative refinement pattern const sameToolRuns = this.detectConsecutiveTools(thread.queries); sameToolRuns.forEach(run => { if (run.count >= 3) { patterns.push({ pattern: `Iterative refinement with ${run.tool}`, tools_involved: [run.tool], frequency: run.count, confidence: 0.85 }); } }); return patterns; } generateCrossToolRecommendations(patterns, thread) { const recommendations = []; patterns.forEach(pattern => { if (pattern.pattern.includes('Sequential use')) { recommendations.push(`Consider creating a workflow template for ${pattern.tools_involved.join(' → ')}`); } if (pattern.pattern.includes('Iterative refinement')) { recommendations.push(`Multiple ${pattern.tools_involved[0]} runs detected - consider batch processing`); } }); // Add recommendations based on missing tool combinations const usedTools = new Set(thread.queries.map(q => q.tool)); if (usedTools.has('predict_roi') && !usedTools.has('compare_projects')) { recommendations.push('Consider comparing this project with alternatives'); } if (usedTools.size === 1 && thread.queries.length > 3) { recommendations.push('Leverage other tools for comprehensive analysis'); } return recommendations; } identifyOptimizations(thread) { const optimizations = []; // Check for redundant queries const similarQueries = this.findSimilarQueries(thread.queries); if (similarQueries.length > 0) { optimizations.push('Detected similar queries - consider caching or parameter optimization'); } // Check for slow queries const slowQueries = thread.queries.filter(q => q.metadata?.execution_time_ms && q.metadata.execution_time_ms > 5000); if (slowQueries.length > 0) { optimizations.push('Some queries are slow - consider optimizing input parameters'); } // Check for low confidence results const lowConfidence = thread.queries.filter(q => q.metadata?.confidence_score && q.metadata.confidence_score < 0.7); if (lowConfidence.length > 0) { optimizations.push('Low confidence results detected - enable benchmarks for better accuracy'); } return optimizations; } getEmptySummary() { return { summary: 'No active thread', key_findings: [], decisions_made: [], next_steps: [] }; } generateThreadSummary(thread) { const toolUsage = this.getToolUsageStats(thread); const duration = this.getThreadDuration(thread); const projectCount = thread.context.project_ids?.length || 0; return `Analysis session with ${thread.queries.length} queries over ${duration}. ` + `Tools used: ${toolUsage}. ` + `${projectCount > 0 ? `Analyzed ${projectCount} project(s).` : ''} ` + `${thread.context.domain ? `Focus area: ${thread.context.domain}.` : ''}`; } extractKeyFindings(thread) { const findings = new Set(); // Extract from query insights thread.queries.forEach(query => { query.insights?.forEach(insight => findings.add(insight)); }); // Extract from high-value metrics if (thread.context.key_metrics?.last_roi && thread.context.key_metrics.last_roi > 150) { findings.add(`High ROI opportunity: ${thread.context.key_metrics.last_roi}%`); } return Array.from(findings).slice(0, 5); } extractDecisions(thread) { const decisions = []; // Look for recommendation acceptances thread.queries.forEach(query => { if (query.output.recommendations?.next_action) { decisions.push(query.output.recommendations.next_action); } }); return Array.from(new Set(decisions)); } suggestNextSteps(thread) { const steps = []; const lastQuery = thread.queries[thread.queries.length - 1]; if (lastQuery) { // Suggest based on last tool used switch (lastQuery.tool) { case 'predict_roi': steps.push('Compare with alternative projects'); steps.push('Create implementation roadmap'); break; case 'compare_projects': steps.push('Deep dive into winning project'); steps.push('Analyze risk mitigation strategies'); break; } } // Suggest based on patterns if (thread.queries.length > 10) { steps.push('Export analysis results for presentation'); } return steps; } updateAverage(current, newValue) { if (!current) return newValue; return (current + newValue) / 2; // Simplified - in reality would track count } detectConsecutiveTools(queries) { const runs = []; let currentTool = null; let count = 0; queries.forEach(query => { if (query.tool === currentTool) { count++; } else { if (currentTool && count > 1) { runs.push({ tool: currentTool, count }); } currentTool = query.tool; count = 1; } }); if (currentTool && count > 1) { runs.push({ tool: currentTool, count }); } return runs; } findSimilarQueries(queries) { const similar = []; for (let i = 0; i < queries.length - 1; i++) { for (let j = i + 1; j < queries.length; j++) { if (queries[i].tool === queries[j].tool) { const similarity = this.calculateInputSimilarity(queries[i].input, queries[j].input); if (similarity > 0.8) { similar.push([queries[i].id, queries[j].id]); } } } } return similar; } calculateInputSimilarity(input1, input2) { // Simplified similarity - in reality would use more sophisticated comparison const keys1 = Object.keys(input1).sort(); const keys2 = Object.keys(input2).sort(); if (keys1.join(',') !== keys2.join(',')) return 0; let matchCount = 0; keys1.forEach(key => { if (JSON.stringify(input1[key]) === JSON.stringify(input2[key])) { matchCount++; } }); return matchCount / keys1.length; } getToolUsageStats(thread) { const toolCounts = new Map(); thread.queries.forEach(query => { toolCounts.set(query.tool, (toolCounts.get(query.tool) || 0) + 1); }); return Array.from(toolCounts.entries()) .map(([tool, count]) => `${tool} (${count}x)`) .join(', '); } getThreadDuration(thread) { const start = new Date(thread.created_at).getTime(); const end = new Date(thread.last_active).getTime(); const durationMs = end - start; if (durationMs < 60000) { return `${Math.round(durationMs / 1000)} seconds`; } else if (durationMs < 3600000) { return `${Math.round(durationMs / 60000)} minutes`; } else { return `${Math.round(durationMs / 3600000)} hours`; } } } // Export singleton instance export const crossToolMemory = new CrossToolMemory(); //# sourceMappingURL=cross-tool-memory.js.map