Spaces:
Paused
Paused
| 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<string, any>; | |
| namespace?: string; | |
| }; | |
| type VectorQuery = { | |
| vector?: number[]; | |
| text?: string; | |
| limit?: number; | |
| filter?: Record<string, any>; | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| const { agentId } = payload; | |
| const prompt = evolutionRepo.getLatestPrompt(agentId); | |
| return { prompt }; | |
| } | |
| export async function evolutionAnalyzePromptsHandler(payload: any, _ctx: McpContext): Promise<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| // 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<any> { | |
| 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<any> { | |
| const notes = await notesRepo.getNotes(ctx.userId, ctx.orgId); | |
| return { notes }; | |
| } | |
| export async function notesCreateHandler(payload: any, ctx: McpContext): Promise<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| const { query, maxHops = 3 } = payload; | |
| const result = await unifiedGraphRAG.query(query, maxHops); | |
| return result; | |
| } | |
| export async function autonomousStateGraphHandler(payload: any, _ctx: McpContext): Promise<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| // 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<any> { | |
| // 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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<any> { | |
| 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')); | |
| } | |
| }); | |
| }); | |
| } | |