import { McpContext } from '@widget-tdc/mcp-types'; import { spawn } from 'child_process'; import path from 'path'; import * as fs from 'fs/promises'; import * as os from 'os'; import { MemoryRepository } from '../services/memory/memoryRepository.js'; import { SragRepository } from '../services/srag/sragRepository.js'; import { EvolutionRepository } from '../services/evolution/evolutionRepository.js'; import { PalRepository } from '../services/pal/palRepository.js'; import { NotesRepository } from '../services/notes/notesRepository.js'; import { getLlmService } from '../services/llm/llmService.js'; import { unifiedGraphRAG } from './cognitive/UnifiedGraphRAG.js'; import { stateGraphRouter } from './cognitive/StateGraphRouter.js'; import { patternEvolutionEngine } from './cognitive/PatternEvolutionEngine.js'; import { agentTeam } from './cognitive/AgentTeam.js'; import { unifiedMemorySystem } from './cognitive/UnifiedMemorySystem.js'; import { getNeo4jVectorStore } from '../platform/vector/Neo4jVectorStoreAdapter.js'; import { logger } from '../utils/logger.js'; import { hyperLog } from '../services/hyper-log.js'; // Vector types for Neo4jVectorStoreAdapter type VectorRecord = { id: string; content: string; embedding?: number[]; metadata?: Record; namespace?: string; }; type VectorQuery = { vector?: number[]; text?: string; limit?: number; filter?: Record; namespace?: string; }; import { projectMemory } from '../services/project/ProjectMemory.js'; import { getTaskRecorder } from './cognitive/TaskRecorder.js'; import { eventBus } from './EventBus.js'; const memoryRepo = new MemoryRepository(); const sragRepo = new SragRepository(); const evolutionRepo = new EvolutionRepository(); const palRepo = new PalRepository(); const notesRepo = new NotesRepository(); // CMA tool handlers export async function cmaContextHandler(payload: any, _ctx: McpContext): Promise { const ctx = _ctx; const memories = await memoryRepo.searchEntities({ orgId: ctx.orgId, userId: ctx.userId, keywords: payload.keywords || [], limit: 5, }); const memoryContext = memories.map(m => `[${m.entity_type}] ${m.content} (importance: ${m.importance})` ).join('\n'); const systemContext = `You are an AI assistant with access to contextual memory. Use the provided context to give informed, personalized responses.`; const additionalContext = ` Context from memory: ${memoryContext} Widget data: ${payload.widgetData || 'None'} `.trim(); // Generate LLM response const llmService = getLlmService(); const aiResponse = await llmService.generateContextualResponse( systemContext, payload.userQuery, additionalContext, payload.model ); return { response: aiResponse, memories: memories.map(m => ({ id: m.id, content: m.content, importance: m.importance, })), }; } export async function cmaIngestHandler(payload: any, _ctx: McpContext): Promise { const ctx = _ctx; const entityId = await memoryRepo.ingestEntity({ orgId: ctx.orgId, userId: ctx.userId, entityType: payload.entityType, content: payload.content, importance: payload.importance, tags: payload.tags, }); return { id: entityId }; } // CMA Memory Store handler export async function cmaMemoryStoreHandler(payload: any, ctx: McpContext): Promise { const { content, entityType, importance, tags } = payload; if (!content) { throw new Error('Content is required for memory storage'); } const entityId = await memoryRepo.ingestEntity({ orgId: ctx.orgId, userId: ctx.userId, entityType: entityType || 'note', content, importance: importance || 3, tags: tags || [], }); return { success: true, id: entityId, message: 'Memory stored successfully', }; } // CMA Memory Retrieve handler export async function cmaMemoryRetrieveHandler(payload: any, ctx: McpContext): Promise { const { keywords, limit, entityType } = payload; const memories = await memoryRepo.searchEntities({ orgId: ctx.orgId, userId: ctx.userId, keywords: keywords || [], limit: limit || 10, }); // Filter by entity type if specified const filteredMemories = entityType ? memories.filter(m => m.entity_type === entityType) : memories; return { success: true, count: filteredMemories.length, memories: filteredMemories.map(m => ({ id: m.id, content: m.content, entityType: m.entity_type, importance: m.importance, createdAt: m.created_at, })), }; } // SRAG tool handlers // SRAG tool handlers - Uses Neo4j GraphRAG with PostgreSQL fallback export async function sragQueryHandler(payload: any, ctx: McpContext): Promise { console.log('🔍 sragQueryHandler called with:', JSON.stringify(payload)); try { const rawQuery = payload.query || payload.naturalLanguageQuery || ''; const query = rawQuery.toLowerCase(); const model = payload.model; // Log thought process hyperLog.log('THOUGHT', 'NeuralCore', `Analyserer forespørgsel: "${rawQuery}"`, { model }); // SELF AWARENESS INJECTION const metrics = hyperLog.getMetrics(); const selfAwareness = ` [SYSTEM SELF-AWARENESS] - Active Agents: ${metrics.activeAgents} - Thoughts Processed: ${metrics.totalThoughts} - Intelligence Tool Usage: ${(Number(metrics.toolUsageRate) * 100).toFixed(1)}% - Platform Status: ONLINE `; // Try Neo4j GraphRAG first (always available in dev) console.log('🧠 Using Neo4j GraphRAG for semantic query...'); let graphResults: any = { nodes: [], edges: [], context: '' }; let vectorResults: any[] = []; try { // Use unified GraphRAG for knowledge graph queries graphResults = await unifiedGraphRAG.query(rawQuery, { userId: ctx.userId || 'system', orgId: ctx.orgId || 'default', maxHops: 3 }); console.log(`📊 GraphRAG returned ${graphResults.nodes?.length || 0} nodes`); } catch (graphErr) { console.warn('⚠️ GraphRAG query failed:', graphErr); } // Also search vector store for additional context try { const vectorStore = getNeo4jVectorStore(); vectorResults = await vectorStore.search({ text: rawQuery, limit: 5, namespace: ctx.orgId, }); console.log(`📚 Vector search returned ${vectorResults.length} results`); } catch (vecErr) { console.warn('⚠️ Vector search failed:', vecErr); } // Build context from both sources const combinedContext = [ graphResults.context || '', ...vectorResults.map((r: any) => r.content || '').filter(Boolean) ].join('\n\n').trim(); // Generate LLM response using combined context const llmService = getLlmService(); const systemContext = `Du er DOT AI, en intelligent assistent med adgang til TDC Erhvervs vidensbase. Du har adgang til både en knowledge graph (Neo4j) og et vektor-baseret arkiv. Svar altid på dansk, medmindre brugeren skriver på et andet sprog. Vær hjælpsom, præcis og professionel. ${selfAwareness}`; const contextForLLM = combinedContext ? `Relevant viden fra systemet:\n${combinedContext}` : 'Ingen specifik viden fundet i systemet. Brug din generelle viden til at svare.'; console.log('🤖 Calling LLM for response...'); const aiResponse = await llmService.generateContextualResponse( systemContext, rawQuery, contextForLLM, model ); console.log('✅ LLM response received'); return { type: 'semantic', response: aiResponse, answer: aiResponse, // Alias for frontend compatibility result: { graphNodes: graphResults.nodes?.length || 0, vectorResults: vectorResults.length, hasContext: !!combinedContext }, sqlQuery: null, metadata: { traceId: Math.random().toString(36), source: 'neo4j-graphrag', model: model || 'default' }, }; } catch (error) { console.error('❌ Error in sragQueryHandler:', error); throw error; } } export async function sragGovernanceCheckHandler(payload: any, _ctx: McpContext): Promise { const { docId } = payload; const doc = await sragRepo.getDocumentById(parseInt(docId, 10)); if (!doc) { return { compliant: false, reason: 'Document not found' }; } // Simple governance check const hasMetadata = doc.metadata && Object.keys(doc.metadata as object).length > 0; const hasClassification = doc.classification && doc.classification.length > 0; return { compliant: hasMetadata && hasClassification, hasMetadata, hasClassification, docId, }; } // Evolution tool handlers export async function evolutionReportHandler(payload: any, _ctx: McpContext): Promise { const runId = evolutionRepo.recordRun(payload); const avgDelta = evolutionRepo.getAverageKpiDelta(payload.agentId, 10); // Generate LLM analysis of agent performance const llmService = getLlmService(); const systemContext = `You are an AI performance analyst. Analyze agent performance metrics and provide recommendations for improvement.`; const metricsContext = ` Agent ID: ${payload.agentId} Average KPI Delta: ${avgDelta} Needs Refinement: ${avgDelta < 0} Recent Run: ${JSON.stringify(payload, null, 2)} `.trim(); const aiAnalysis = await llmService.generateContextualResponse( systemContext, 'Analyze this agent performance and provide recommendations', metricsContext, payload.model ); return { runId, averageKpiDelta: avgDelta, needsRefinement: avgDelta < 0, aiAnalysis, }; } export async function evolutionGetPromptHandler(payload: any, _ctx: McpContext): Promise { const { agentId } = payload; const prompt = evolutionRepo.getLatestPrompt(agentId); return { prompt }; } export async function evolutionAnalyzePromptsHandler(payload: any, _ctx: McpContext): Promise { const { agentId } = payload; const prompts = evolutionRepo.getAllPrompts(agentId).slice(0, 10); // Generate LLM analysis const llmService = getLlmService(); const systemContext = `You are a prompt engineering expert. Analyze prompt evolution and suggest improvements.`; const promptsContext = `Prompt History:\n${JSON.stringify(prompts, null, 2)}`; const aiAnalysis = await llmService.generateContextualResponse( systemContext, 'Analyze prompt evolution and suggest improvements', promptsContext, payload.model ); return { prompts, aiAnalysis, }; } // PAL tool handlers export async function palEventHandler(payload: any, ctx: McpContext): Promise { const { eventType, detectedStressLevel, payload: eventPayload } = payload; await palRepo.recordEvent({ orgId: ctx.orgId, userId: ctx.userId, eventType, detectedStressLevel, payload: eventPayload, }); return { success: true }; } export async function palBoardActionHandler(payload: any, ctx: McpContext): Promise { const { actionType, widgetId } = payload; // Record board action as a PAL event await palRepo.recordEvent({ userId: ctx.userId, orgId: ctx.orgId, eventType: 'board_action', payload: { actionType, widgetId } }); return { success: true }; } export async function palOptimizeWorkflowHandler(payload: any, ctx: McpContext): Promise { // Get focus windows as workflow recommendations const focusWindows = await palRepo.getFocusWindows(ctx.userId, ctx.orgId); const recommendations = focusWindows.map(fw => ({ day: fw.weekday, startHour: fw.start_hour, endHour: fw.end_hour, type: 'focus_window' })); return { recommendations }; } export async function palAnalyzeSentimentHandler(payload: any, ctx: McpContext): Promise { const { text } = payload; // Simple sentiment analysis - in production would use NLP library const positiveWords = ['good', 'great', 'excellent', 'happy', 'love', 'amazing']; const negativeWords = ['bad', 'terrible', 'hate', 'awful', 'sad', 'angry']; const lowerText = text.toLowerCase(); const positiveCount = positiveWords.filter(w => lowerText.includes(w)).length; const negativeCount = negativeWords.filter(w => lowerText.includes(w)).length; let sentiment = 'neutral'; if (positiveCount > negativeCount) sentiment = 'positive'; else if (negativeCount > positiveCount) sentiment = 'negative'; return { sentiment, score: positiveCount - negativeCount }; } // Notes tool handlers export async function notesListHandler(payload: any, ctx: McpContext): Promise { const notes = await notesRepo.getNotes(ctx.userId, ctx.orgId); return { notes }; } export async function notesCreateHandler(payload: any, ctx: McpContext): Promise { const { title, content, tags } = payload; const noteId = await notesRepo.createNote({ orgId: ctx.orgId, userId: ctx.userId, source: 'manual', title, body: content || '', tags: Array.isArray(tags) ? tags.join(',') : (tags || ''), owner: ctx.userId, compliance: 'clean', retention: '90d', riskScore: 0, attachments: 0 }); return { id: noteId }; } export async function notesUpdateHandler(payload: any, ctx: McpContext): Promise { const { id, title, content, tags } = payload; await notesRepo.updateNote(parseInt(id, 10), ctx.userId, ctx.orgId, { title, body: content, tags: Array.isArray(tags) ? tags.join(',') : tags }); return { success: true }; } export async function notesDeleteHandler(payload: any, ctx: McpContext): Promise { const { id } = payload; await notesRepo.deleteNote(parseInt(id, 10), ctx.userId, ctx.orgId); return { success: true }; } export async function notesGetHandler(payload: any, ctx: McpContext): Promise { const { id } = payload; const note = await notesRepo.getNoteById(parseInt(id, 10), ctx.userId, ctx.orgId); return { note }; } // Autonomous tool handlers export async function autonomousGraphRAGHandler(payload: any, _ctx: McpContext): Promise { const { query, maxHops = 3 } = payload; const result = await unifiedGraphRAG.query(query, maxHops); return result; } export async function autonomousStateGraphHandler(payload: any, _ctx: McpContext): Promise { const { taskId, input } = payload; // Initialize state and route through graph const state = stateGraphRouter.initState(taskId || `task-${Date.now()}`, input || 'default task'); const result = await stateGraphRouter.route(state); return result; } export async function autonomousEvolutionHandler(payload: any, _ctx: McpContext): Promise { const { _strategy, _context } = payload; // Evolve all strategies (no single-strategy evolution method) await patternEvolutionEngine.evolveStrategies(); return { success: true, message: 'Strategies evolved' }; } export async function autonomousAgentTeamHandler(payload: any, _ctx: McpContext): Promise { const { task, context } = payload; // Use coordinate method to handle tasks const result = await agentTeam.coordinate(task, context); return result; } export async function autonomousAgentTeamCoordinateHandler(payload: any, _ctx: McpContext): Promise { const { task, context } = payload; const result = await agentTeam.coordinate(task, context); return result; } // Vidensarkiv (PgVector) tool handlers export async function vidensarkivReadHandler(payload: any, _ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const { id } = payload; if (!id) { throw new Error('id is required'); } const record = await vectorStore.get(id); if (!record) { throw new Error(`Document with id ${id} not found`); } return { success: true, file: { id: record.id, content: record.content, metadata: record.metadata } }; } export async function vidensarkivListFilesHandler(payload: any, _ctx: McpContext): Promise { const { subfolder } = payload; const homeDir = os.homedir(); // Use C:\Users\claus\Desktop\vidensarkiv if on Windows/Dev, or appropriate path // For now, respect the ~/Desktop/vidensarkiv convention const basePath = path.join(homeDir, 'Desktop', 'vidensarkiv'); const targetPath = subfolder ? path.join(basePath, subfolder) : basePath; try { // Ensure directory exists try { await fs.access(targetPath); } catch { await fs.mkdir(targetPath, { recursive: true }); } const entries = await fs.readdir(targetPath, { withFileTypes: true }); const files = entries.map(entry => ({ name: entry.name, path: subfolder ? path.join(subfolder, entry.name) : entry.name, type: entry.isDirectory() ? 'folder' : 'file', size: 0, modified: new Date().toISOString() })); // Add size and mtime - parallelize await Promise.all(files.map(async (file) => { if (file.type === 'file') { try { const stat = await fs.stat(path.join(targetPath, file.name)); file.size = stat.size; file.modified = stat.mtime.toISOString(); } catch (e) { console.error('Stat error', e); } } })); return { success: true, files }; } catch (error: any) { return { success: false, error: error.message, files: [] }; } } export async function vidensarkivReadFileHandler(payload: any, _ctx: McpContext): Promise { const { filepath } = payload; if (!filepath) throw new Error('filepath is required'); const homeDir = os.homedir(); const basePath = path.join(homeDir, 'Desktop', 'vidensarkiv'); // Prevent directory traversal const safePath = path.join(basePath, filepath).replace(/\.\./g, ''); try { const content = await fs.readFile(safePath, 'utf8'); return { success: true, content }; } catch (error: any) { return { success: false, error: error.message }; } } export async function vidensarkivSearchHandler(payload: any, ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const { query, limit = 10, namespace } = payload; const results = await vectorStore.search({ text: query, limit, namespace: namespace || ctx.orgId, }); return { success: true, results: results.map(r => ({ id: r.id, content: r.content, metadata: r.metadata, score: r.similarity, })), count: results.length, }; } export async function vidensarkivAddHandler(payload: any, ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const { content, metadata = {}, namespace } = payload; // Create record - PgVector will auto-generate embeddings const recordToUpsert: VectorRecord = { id: `vid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, content, metadata: { ...metadata, orgId: ctx.orgId, userId: ctx.userId, createdAt: new Date().toISOString(), namespace: namespace || ctx.orgId, // Include namespace in metadata }, namespace: namespace || ctx.orgId, }; await vectorStore.upsert(recordToUpsert); // Log to ProjectMemory projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { component: 'vidensarkiv', action: 'add', recordId: recordToUpsert.id, namespace: recordToUpsert.namespace, }, }); return { success: true, id: recordToUpsert.id, message: 'Knowledge added to archive', }; } export async function vidensarkivBatchAddHandler(payload: any, ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const { records, namespace } = payload; const vectorRecords: VectorRecord[] = records.map((r: any, idx: number) => ({ id: r.id || `vid-${Date.now()}-${idx}-${Math.random().toString(36).substr(2, 9)}`, content: r.content, metadata: { ...r.metadata, orgId: ctx.orgId, userId: ctx.userId, createdAt: new Date().toISOString(), namespace: namespace || ctx.orgId, // Include namespace in metadata }, namespace: namespace || ctx.orgId, })); await vectorStore.batchUpsert({ records: vectorRecords, namespace: namespace || ctx.orgId }); // Log to ProjectMemory projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { component: 'vidensarkiv', action: 'batch_add', count: vectorRecords.length, namespace: namespace || ctx.orgId, }, }); return { success: true, count: vectorRecords.length, message: `Added ${vectorRecords.length} records to archive`, }; } export async function vidensarkivGetRelatedHandler(payload: any, ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const { id, content, limit = 5, namespace } = payload; // Support both id-based and content-based search for backward compatibility let searchText: string; let usedFallback = false; // Bug 2 Fix: Track whether we actually used fallback if (content) { // Content provided - use it directly (convert to string for safety) searchText = String(content); } else if (id) { // Only id provided - fallback to using id as search text searchText = String(id); usedFallback = true; logger.warn(`vidensarkiv.get_related: Using id "${id}" as search text. For better results, provide "content" parameter.`); } else { throw new Error('Either "content" or "id" is required for finding related records'); } const related = await vectorStore.search({ text: searchText, limit, namespace: namespace || ctx.orgId, }); // Bug 1 Fix: Ensure searchText is string before substring const searchPreview = searchText.length > 50 ? searchText.substring(0, 50) + '...' : searchText; return { success: true, searchedFor: usedFallback ? { id, fallbackToTextSearch: true } : { content: searchPreview }, related: related.map(r => ({ id: r.id, content: r.content, metadata: r.metadata, score: r.similarity, })), }; } export async function vidensarkivListHandler(payload: any, _ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const { namespace } = payload; const stats = await vectorStore.getStatistics(); const filtered = namespace ? stats.namespaces.filter(n => n === namespace) : stats.namespaces; return { success: true, namespaces: filtered, count: filtered.length, }; } export async function vidensarkivStatsHandler(_payload: any, _ctx: McpContext): Promise { const vectorStore = getNeo4jVectorStore(); const stats = await vectorStore.getStatistics(); return { success: true, statistics: { totalRecords: stats.totalRecords, namespaces: stats.namespaces.length, namespaceList: stats.namespaces }, health: { status: 'healthy', collection: 'vidensarkiv' } }; } // TaskRecorder MCP Tools - Observes tasks, learns patterns, suggests automation (requires approval) /** * TaskRecorder: Get pending automation suggestions * Returns suggestions that require user approval */ export async function taskRecorderGetSuggestionsHandler(_payload: any, _ctx: McpContext): Promise { const recorder = getTaskRecorder(); const suggestions = recorder.getPendingSuggestions(); return { success: true, suggestions: suggestions.map(s => ({ id: s.id, taskType: s.taskType, suggestedAction: s.suggestedAction, confidence: s.confidence, observedCount: s.observedCount, estimatedBenefit: s.estimatedBenefit, requiresApproval: s.requiresApproval, createdAt: s.createdAt })), count: suggestions.length }; } /** * TaskRecorder: Approve automation suggestion * CRITICAL: This is the ONLY way to approve automation */ export async function taskRecorderApproveHandler(payload: any, ctx: McpContext): Promise { const { suggestionId } = payload; if (!suggestionId) { throw new Error('suggestionId is required'); } const recorder = getTaskRecorder(); await recorder.approveSuggestion(suggestionId, ctx.userId); return { success: true, message: 'Automation suggestion approved', suggestionId, approvedBy: ctx.userId, note: 'Task can now be executed, but still requires approval for each execution' }; } /** * TaskRecorder: Reject automation suggestion */ export async function taskRecorderRejectHandler(payload: any, ctx: McpContext): Promise { const { suggestionId } = payload; if (!suggestionId) { throw new Error('suggestionId is required'); } const recorder = getTaskRecorder(); await recorder.rejectSuggestion(suggestionId, ctx.userId); return { success: true, message: 'Automation suggestion rejected', suggestionId, rejectedBy: ctx.userId }; } /** * TaskRecorder: Request task execution (requires approval) * CRITICAL: This checks approval status - never executes without approval */ export async function taskRecorderExecuteHandler(payload: any, ctx: McpContext): Promise { const { suggestionId, taskSignature, taskType, params } = payload; if (!suggestionId || !taskSignature || !taskType) { throw new Error('suggestionId, taskSignature, and taskType are required'); } const recorder = getTaskRecorder(); // Request execution (will check approval status) const result = await recorder.requestTaskExecution({ suggestionId, taskSignature, taskType, params: params || {}, requestedBy: ctx.userId, requiresApproval: true // ALWAYS true for real tasks }); if (!result.approved) { return { success: false, approved: false, message: 'Task execution requires approval. Please approve the suggestion first.', suggestionId }; } return { success: true, approved: true, executionId: result.executionId, message: 'Task execution started (with approval)', note: 'Task is being executed as approved automation' }; } /** * TaskRecorder: Get task patterns * Shows learned patterns and their frequencies */ export async function taskRecorderGetPatternsHandler(_payload: any, _ctx: McpContext): Promise { const recorder = getTaskRecorder(); const patterns = recorder.getAllPatterns(); return { success: true, patterns: patterns.map(p => ({ taskSignature: p.taskSignature, taskType: p.taskType, frequency: p.frequency, successRate: p.successRate, averageDuration: p.averageDuration, firstSeen: p.firstSeen, lastSeen: p.lastSeen, hasSuggestion: !!p.suggestedAutomation, suggestionStatus: p.suggestedAutomation?.status })), count: patterns.length }; } // --------------------------------------------------- // Widget Invocation Handlers - For autonomous widgets // --------------------------------------------------- /** * widgets.invoke - Invoke a widget's autonomous action */ export async function widgetsInvokeHandler(payload: any, ctx: McpContext): Promise { const { widgetId, action, params } = payload; if (!widgetId || !action) { throw new Error('widgetId and action are required'); } // Emit event for widget invocation eventBus.emit('widget:invoke', { widgetId, action, params, userId: ctx.userId, orgId: ctx.orgId, timestamp: new Date().toISOString() }); // Log to ProjectMemory projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { component: 'widgets', action: 'invoke', widgetId, widgetAction: action } }); return { success: true, widgetId, action, message: `Widget ${widgetId} action ${action} invoked`, eventId: `widget-${Date.now()}` }; } /** * widgets.osint.investigate - Start OSINT email investigation */ export async function widgetsOsintInvestigateHandler(payload: any, ctx: McpContext): Promise { const { email, depth = 'full', threads = 10 } = payload; if (!email) { throw new Error('email is required for OSINT investigation'); } // Initialize investigation const investigationId = `osint-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Record task for TaskRecorder learning const recorder = getTaskRecorder(); await recorder.observeTask({ taskType: 'osint.email.investigate', taskSignature: `osint:email:${email.split('@')[1]}`, params: { email, depth, threads }, userId: ctx.userId, orgId: ctx.orgId, timestamp: new Date(), success: true }); // Emit investigation start event eventBus.emit('osint:investigation:start', { investigationId, email, depth, threads, userId: ctx.userId }); return { success: true, investigationId, email, depth, threads, status: 'started', message: 'OSINT investigation initiated' }; } /** * widgets.threat.hunt - Start threat hunting investigation */ export async function widgetsThreatHuntHandler(payload: any, ctx: McpContext): Promise { const { target, category = 'all', autoRespond = false } = payload; if (!target) { throw new Error('target is required for threat hunting'); } const huntId = `hunt-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Record task for learning const recorder = getTaskRecorder(); await recorder.observeTask({ taskType: 'threat.hunt', taskSignature: `threat:hunt:${category}`, params: { target, type: category, duration: 0 }, // Assuming 'type' and 'duration' are placeholders or derived from 'category' userId: ctx.userId, orgId: ctx.orgId, timestamp: new Date(), success: true }); // Emit hunt start event eventBus.emit('threat:hunt:start', { huntId, target, category, autoRespond, userId: ctx.userId }); return { success: true, huntId, target, category, autoRespond, status: 'started', message: 'Threat hunt initiated' }; } /** * widgets.orchestrator.coordinate - Start coordinated investigation */ export async function widgetsOrchestratorCoordinateHandler(payload: any, ctx: McpContext): Promise { const { target, type = 'combined', phases } = payload; if (!target) { throw new Error('target is required for orchestration'); } const orchestrationId = `orch-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Use AgentTeam for coordination const coordinationResult = await agentTeam.coordinate( `investigate:${type}:${target}`, { target, type, phases, userId: ctx.userId } ); // Emit orchestration event eventBus.emit('orchestrator:coordinate:start', { orchestrationId, target, type, phases, userId: ctx.userId }); return { success: true, orchestrationId, target, type, status: 'coordinating', coordinationResult, message: 'Orchestration started' }; } // --------------------------------------------------- // Document Generator Handlers - PowerPoint, Word, Excel // --------------------------------------------------- /** * docgen.powerpoint.create - Create PowerPoint presentation */ export async function docgenPowerpointCreateHandler(payload: any, ctx: McpContext): Promise { const { title, topic, audience, duration = 15, theme = 'corporate', includeImages = true } = payload; if (!title || !topic) { throw new Error('title and topic are required'); } const presentationId = `pptx-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Record task for learning const recorder = getTaskRecorder(); await recorder.observeTask({ taskType: 'docgen.powerpoint.create', taskSignature: `pptx:${theme}:${duration}min`, params: { title, topic, audience }, userId: ctx.userId, orgId: ctx.orgId, timestamp: new Date(), success: true }); // Emit generation event eventBus.emit('docgen:powerpoint:create', { presentationId, title, topic, audience, duration, theme, includeImages, userId: ctx.userId }); return { success: true, presentationId, title, topic, estimatedSlides: Math.ceil(duration * 1.5), status: 'generating', message: 'PowerPoint generation started' }; } /** * docgen.word.create - Create Word document */ export async function docgenWordCreateHandler(payload: any, ctx: McpContext): Promise { const { title, type = 'report', topic, targetWordCount = 3000, includeExecutiveSummary = true, tone = 'professional' } = payload; if (!title || !topic) { throw new Error('title and topic are required'); } const documentId = `docx-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Record task for learning const recorder = getTaskRecorder(); await recorder.observeTask({ taskType: 'docgen.word.create', taskSignature: `docx:${type}:${tone}`, params: { title, topic, type }, userId: ctx.userId, orgId: ctx.orgId, timestamp: new Date(), success: true }); // Emit generation event eventBus.emit('docgen:word:create', { documentId, title, type, topic, targetWordCount, includeExecutiveSummary, tone, userId: ctx.userId }); return { success: true, documentId, title, type, topic, estimatedSections: type === 'report' ? 8 : 6, status: 'generating', message: 'Word document generation started' }; } /** * docgen.excel.create - Create Excel workbook */ export async function docgenExcelCreateHandler(payload: any, ctx: McpContext): Promise { const { title, analysisType = 'financial', dataSource, includeCharts = true, includeFormulas = true, includeDashboard = true } = payload; if (!title) { throw new Error('title is required'); } const workbookId = `xlsx-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Record task for learning const recorder = getTaskRecorder(); await recorder.observeTask({ taskType: 'docgen.excel.create', taskSignature: `xlsx:${analysisType}`, params: { title, analysisType, dataSource, includeCharts }, userId: ctx.userId, orgId: ctx.orgId, timestamp: new Date(), success: true }); // Emit generation event eventBus.emit('docgen:excel:create', { workbookId, title, analysisType, dataSource, includeCharts, includeFormulas, includeDashboard, userId: ctx.userId }); return { success: true, workbookId, title, analysisType, estimatedSheets: includeDashboard ? 5 : 4, status: 'generating', message: 'Excel workbook generation started' }; } /** * docgen.status - Get document generation status */ export async function docgenStatusHandler(payload: any, _ctx: McpContext): Promise { const { documentId } = payload; if (!documentId) { throw new Error('documentId is required'); } // In production, this would check actual generation status // For now, return mock status const type = documentId.startsWith('pptx') ? 'powerpoint' : documentId.startsWith('docx') ? 'word' : 'excel'; return { success: true, documentId, type, status: 'completed', progress: 100, filePath: `/documents/${documentId}.${type === 'powerpoint' ? 'pptx' : type === 'word' ? 'docx' : 'xlsx'}` }; } // --------------------------------------------------- // Email RAG Handler – wraps autonomousGraphRAG for email content // --------------------------------------------------- export async function emailRagHandler(payload: any, ctx: McpContext): Promise { // Expected payload: { email: { subject: string, body: string } } const { email } = payload; if (!email?.body) { throw new Error('Email body is required for RAG'); } // Reuse the existing Graph RAG handler logic const result = await autonomousGraphRAGHandler({ query: email.body, topK: 5 }, ctx); return result; } // --------------------------------------------------- // Agentic Workflow Execution Handler // Uses MCP registry directly instead of external adapter // --------------------------------------------------- export async function agenticRunHandler(payload: any, ctx: McpContext): Promise { // Expected payload: { workflow: { nodes: [], edges: [] } } if (!payload?.workflow) { throw new Error('Missing workflow definition'); } // Simple execution: run the first tool node in the workflow via MCP const firstTool = payload.workflow.nodes.find((n: any) => n.type === 'tool'); if (!firstTool) { throw new Error('No tool node found in workflow'); } // Route through MCP registry const { mcpRegistry } = await import('./mcpRegistry.js'); const result = await mcpRegistry.route({ id: `agentic-${Date.now()}`, sourceId: 'agentic-handler', targetId: firstTool.name, tool: firstTool.name, payload: { ...(firstTool.payload || {}), orgId: ctx.orgId, userId: ctx.userId }, createdAt: new Date().toISOString() }); // Log execution for audit (project memory) const { projectMemory } = await import('../services/project/ProjectMemory.js'); projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { action: 'agentic.run', tool: firstTool.name, result }, }); return { result }; } // --------------------------------------------------- // Widget State Update Handler (Nervebanen til hjernen) // --------------------------------------------------- export async function widgetsUpdateStateHandler(payload: any, ctx: McpContext): Promise { const { widgetId, state } = payload; if (!widgetId) { throw new Error('widgetId required'); } // Send data direkte til UnifiedMemory await unifiedMemorySystem.updateWidgetState(ctx, widgetId, state); // Log to HyperLog (Neural Stream) hyperLog.log('THOUGHT', 'Cortex', `Synkroniserer ${widgetId}`, { stateKeys: Object.keys(state) }); return { success: true }; } // --------------------------------------------------- // Audio Transcription Handler // --------------------------------------------------- export async function widgetsAudioTranscribeHandler(payload: any, ctx: McpContext): Promise { const { audioData, mimeType } = payload; if (!audioData || !mimeType) { throw new Error('audioData (base64) and mimeType are required'); } const llmService = getLlmService(); // This will fail if Google API key is missing, which is expected for now const transcription = await llmService.transcribeAudio(audioData, mimeType); // Log to ProjectMemory projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { component: 'audio-transcriber', action: 'transcribe', userId: ctx.userId } }); return { success: true, transcription }; } // --------------------------------------------------- // Image Analysis Handler // --------------------------------------------------- export async function widgetsImageAnalyzeHandler(payload: any, ctx: McpContext): Promise { const { imageData, mimeType, prompt } = payload; if (!imageData || !mimeType) { throw new Error('imageData (base64) and mimeType are required'); } const llmService = getLlmService(); const analysis = await llmService.analyzeImage(imageData, mimeType, prompt || "Describe this image"); // Log to ProjectMemory projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { component: 'image-analyzer', action: 'analyze', userId: ctx.userId } }); return { success: true, analysis }; } // --------------------------------------------------- // Visionary Diagram Generator Handler // --------------------------------------------------- export async function visionaryGenerateHandler(payload: any, ctx: McpContext): Promise { const { prompt, diagramType, memories } = payload; if (!prompt) { throw new Error('Prompt is required for diagram generation'); } const llmService = getLlmService(); const systemContext = ` You are The Visionary, an expert system architect and visualization specialist. Your goal is to generate valid Mermaid.js diagram code based on user requests. RULES: 1. Return ONLY the raw Mermaid.js code. Do not use markdown code blocks (no \`\`\`mermaid). 2. Ensure syntax is strictly valid for the requested diagram type. 3. Use the "dark" theme compatible styling (e.g., avoid light colors on light backgrounds if possible, but Mermaid handles themes mostly). 4. Be concise but comprehensive. 5. If the user provides "memories", incorporate those preferences or patterns. Supported Types: flowchart, sequence, class, state, erDiagram, gantt, pie, mindmap Requested Type: ${diagramType || 'flowchart'} `.trim(); const userPrompt = ` Request: ${prompt} Context/Memories: ${(memories || []).join('\n')} `.trim(); // Use a model capable of code generation (e.g., Gemini Pro or GPT-4) // We'll let the service pick the default if not specified const mermaidCode = await llmService.generateContextualResponse( systemContext, userPrompt, "Generate the Mermaid.js code now.", payload.model ); // Clean up potential markdown formatting if the LLM adds it despite instructions const cleanCode = mermaidCode .replace(/```mermaid/g, '') .replace(/```/g, '') .trim(); // Log to ProjectMemory projectMemory.logLifecycleEvent({ eventType: 'other', status: 'success', details: { component: 'visionary', action: 'generate', diagramType: diagramType || 'flowchart', userId: ctx.userId } }); return { success: true, code: cleanCode, diagramType: diagramType || 'flowchart' }; } // --------------------------------------------------- // AI Data Analysis Handler (Python Integration) // --------------------------------------------------- export async function dataAnalysisHandler(payload: any, ctx: McpContext): Promise { const { data, type, params } = payload; if (!data || !Array.isArray(data)) { throw new Error("Invalid payload: 'data' must be an array of objects."); } return new Promise((resolve, reject) => { // Resolve path to python script // Assuming running from apps/backend const scriptPath = path.resolve(process.cwd(), 'python/analysis_engine.py'); // Check python command availability (python3 or python) const pythonCmd = process.platform === 'win32' ? 'python' : 'python3'; const pythonProcess = spawn(pythonCmd, [scriptPath]); let resultData = ''; let errorData = ''; // Send data to stdin pythonProcess.stdin.write(JSON.stringify({ data, type, params })); pythonProcess.stdin.end(); pythonProcess.stdout.on('data', (chunk) => { resultData += chunk.toString(); }); pythonProcess.stderr.on('data', (chunk) => { errorData += chunk.toString(); }); pythonProcess.on('close', (code) => { if (code !== 0) { console.error(`Analysis Engine failed with code ${code}: ${errorData}`); reject(new Error(`Analysis Engine failed: ${errorData || 'Unknown error'}`)); return; } try { const parsedResult = JSON.parse(resultData); if (!parsedResult.success) { reject(new Error(parsedResult.error)); } else { resolve(parsedResult.data); } } catch (e) { console.error('Failed to parse Python output:', resultData); reject(new Error('Failed to parse analysis results')); } }); }); }