Spaces:
Paused
Paused
| import { neo4jAdapter } from '../../adapters/Neo4jAdapter.js'; | |
| import { ingestRepository } from '../../services/GraphIngestor.js'; | |
| import { sentinelEngine } from '../../services/SentinelEngine.js'; | |
| // Handlers | |
| async function handleQueryKnowledgeGraph(args: any) { | |
| const { query, type = 'search', limit = 20 } = args; | |
| try { | |
| let results: any[]; | |
| let cypherUsed: string; | |
| switch (type) { | |
| case 'cypher': | |
| if (query.toLowerCase().includes('delete') || | |
| query.toLowerCase().includes('drop') || | |
| query.toLowerCase().includes('remove')) { | |
| throw new Error('Destructive queries not allowed via this interface'); | |
| } | |
| cypherUsed = query; | |
| results = await neo4jAdapter.executeQuery(query); | |
| break; | |
| case 'labels': | |
| cypherUsed = 'CALL db.labels() YIELD label RETURN label'; | |
| results = await neo4jAdapter.readQuery(cypherUsed); | |
| break; | |
| case 'relationships': | |
| cypherUsed = 'CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType'; | |
| results = await neo4jAdapter.readQuery(cypherUsed); | |
| break; | |
| case 'search': | |
| default: | |
| cypherUsed = ` | |
| MATCH (n) | |
| WHERE n.name CONTAINS $query | |
| OR n.title CONTAINS $query | |
| OR n.content CONTAINS $query | |
| OR n.description CONTAINS $query | |
| RETURN n, labels(n) as labels | |
| LIMIT $limit | |
| `; | |
| results = await neo4jAdapter.readQuery(cypherUsed, { | |
| query, | |
| limit: parseInt(String(limit)) | |
| }); | |
| break; | |
| } | |
| let gapDetection: any = null; | |
| if (type === 'search' && results.length < 3) { | |
| const confidence = results.length === 0 ? 0.0 : results.length / 10; | |
| gapDetection = await sentinelEngine.detectAndRegisterGap({ | |
| query, | |
| source: 'graph_query', | |
| resultCount: results.length, | |
| confidence, | |
| metadata: { queryType: type, limit } | |
| }); | |
| } | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| queryType: type, | |
| query: query, | |
| cypherExecuted: cypherUsed, | |
| resultCount: results.length, | |
| results: results, | |
| ...(gapDetection?.detected && { | |
| autoGapDetected: { | |
| gapId: gapDetection.gapId, | |
| reason: gapDetection.reason, | |
| message: '🔍 Knowledge gap auto-registered by Sentinel' | |
| } | |
| }) | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| error: error.message, | |
| queryType: type, | |
| query: query, | |
| hint: 'Check Neo4j connection or query syntax' | |
| }, null, 2) | |
| }], | |
| isError: true | |
| }; | |
| } | |
| } | |
| async function handleCreateGraphNode(args: any) { | |
| const { label, properties } = args; | |
| if (!label || !properties) { | |
| throw new Error('Label and properties are required'); | |
| } | |
| if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label)) { | |
| throw new Error('Invalid label format'); | |
| } | |
| try { | |
| const result = await neo4jAdapter.createNode(label, { | |
| ...properties, | |
| createdAt: new Date().toISOString(), | |
| source: 'neural-bridge' | |
| }); | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| success: true, | |
| action: 'node_created', | |
| label: label, | |
| node: result | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| throw new Error(`Failed to create node: ${error.message}`); | |
| } | |
| } | |
| async function handleCreateGraphRelationship(args: any) { | |
| const { fromNodeId, toNodeId, relationshipType, properties = {} } = args; | |
| if (!fromNodeId || !toNodeId || !relationshipType) { | |
| throw new Error('fromNodeId, toNodeId, and relationshipType are required'); | |
| } | |
| if (!/^[A-Z_][A-Z0-9_]*$/.test(relationshipType)) { | |
| throw new Error('Invalid relationship type format (use UPPERCASE_WITH_UNDERSCORES)'); | |
| } | |
| try { | |
| const result = await neo4jAdapter.createRelationship( | |
| fromNodeId, | |
| toNodeId, | |
| relationshipType, | |
| { | |
| ...properties, | |
| createdAt: new Date().toISOString(), | |
| source: 'neural-bridge' | |
| } | |
| ); | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| success: true, | |
| action: 'relationship_created', | |
| type: relationshipType, | |
| result: result | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| throw new Error(`Failed to create relationship: ${error.message}`); | |
| } | |
| } | |
| async function handleGetNodeConnections(args: any) { | |
| const { nodeId, direction = 'both', limit = 50 } = args; | |
| if (!nodeId) { | |
| throw new Error('nodeId is required'); | |
| } | |
| try { | |
| const connections = await neo4jAdapter.getNodeRelationships( | |
| nodeId, | |
| direction, | |
| limit | |
| ); | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| nodeId: nodeId, | |
| direction: direction, | |
| connectionCount: connections.length, | |
| connections: connections | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| throw new Error(`Failed to get connections: ${error.message}`); | |
| } | |
| } | |
| async function handleGetHarvestStats(args: any) { | |
| const timeRange = args?.timeRange || '24h'; | |
| try { | |
| const stats = await neo4jAdapter.readQuery(` | |
| MATCH (n) | |
| WITH labels(n) as nodeLabels, count(*) as cnt | |
| UNWIND nodeLabels as label | |
| RETURN label, sum(cnt) as count | |
| ORDER BY count DESC | |
| `); | |
| const relStats = await neo4jAdapter.readQuery(` | |
| MATCH ()-[r]->() | |
| RETURN type(r) as type, count(r) as count | |
| ORDER BY count DESC | |
| `); | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| timeRange, | |
| nodesByLabel: stats, | |
| relationshipsByType: relStats, | |
| lastUpdated: new Date().toISOString() | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| timeRange, | |
| filesScanned: 288, | |
| linesOfCode: 58317, | |
| nodesCreated: 1247, | |
| relationshipsCreated: 3891, | |
| note: 'Simulated stats - Neo4j connection issue', | |
| error: error.message | |
| }, null, 2) | |
| }] | |
| }; | |
| } | |
| } | |
| async function handleGetGraphStats(args: any) { | |
| try { | |
| const health = await neo4jAdapter.healthCheck(); | |
| const labelCounts = await neo4jAdapter.readQuery(` | |
| CALL db.labels() YIELD label | |
| CALL { | |
| WITH label | |
| MATCH (n) | |
| WHERE label IN labels(n) | |
| RETURN count(n) as count | |
| } | |
| RETURN label, count | |
| ORDER BY count DESC | |
| `).catch(() => []); | |
| const relCounts = await neo4jAdapter.readQuery(` | |
| CALL db.relationshipTypes() YIELD relationshipType | |
| CALL { | |
| WITH relationshipType | |
| MATCH ()-[r]->() | |
| WHERE type(r) = relationshipType | |
| RETURN count(r) as count | |
| } | |
| RETURN relationshipType, count | |
| ORDER BY count DESC | |
| `).catch(() => []); | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| health: health, | |
| labels: labelCounts, | |
| relationshipTypes: relCounts, | |
| timestamp: new Date().toISOString() | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| error: error.message, | |
| hint: 'Neo4j may not be running. Start with: docker-compose up neo4j' | |
| }, null, 2) | |
| }], | |
| isError: true | |
| }; | |
| } | |
| } | |
| async function handleIngestKnowledgeGraph(args: any) { | |
| const { path: targetPath, name, maxDepth = 10 } = args; | |
| if (!targetPath) { | |
| throw new Error('Path is required'); | |
| } | |
| try { | |
| console.error(`[Neural Bridge] 🚀 Starting ingestion of: ${targetPath}`); | |
| const result = await ingestRepository({ | |
| rootPath: targetPath, | |
| repositoryName: name, | |
| maxDepth: maxDepth | |
| }); | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| success: result.success, | |
| repositoryId: result.repositoryId, | |
| stats: result.stats, | |
| errors: result.errors, | |
| message: result.success | |
| ? `Successfully ingested ${result.stats.totalNodes} nodes with ${result.stats.relationshipsCreated} relationships` | |
| : 'Ingestion failed - check errors' | |
| }, null, 2) | |
| }] | |
| }; | |
| } catch (error: any) { | |
| return { | |
| content: [{ | |
| type: 'text', | |
| text: JSON.stringify({ | |
| error: error.message, | |
| hint: 'Ensure Neo4j is running and path exists' | |
| }, null, 2) | |
| }], | |
| isError: true | |
| }; | |
| } | |
| } | |
| export const GRAPH_TOOLS = [ | |
| { | |
| name: 'query_knowledge_graph', | |
| description: 'Query the Neo4j knowledge graph with Cypher or natural language search', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| query: { | |
| type: 'string', | |
| description: 'Cypher query or search term' | |
| }, | |
| type: { | |
| type: 'string', | |
| enum: ['search', 'cypher', 'labels', 'relationships'], | |
| description: 'Query type: search (text), cypher (raw), labels (list all), relationships (list types)' | |
| }, | |
| limit: { | |
| type: 'number', | |
| description: 'Maximum results to return (default: 20)' | |
| } | |
| }, | |
| required: ['query'] | |
| }, | |
| handler: handleQueryKnowledgeGraph | |
| }, | |
| { | |
| name: 'create_graph_node', | |
| description: 'Create or merge a node in the knowledge graph', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| label: { | |
| type: 'string', | |
| description: 'Node label (e.g., Component, Document, Concept)' | |
| }, | |
| properties: { | |
| type: 'object', | |
| description: 'Node properties (name, description, etc.)' | |
| } | |
| }, | |
| required: ['label', 'properties'] | |
| }, | |
| handler: handleCreateGraphNode | |
| }, | |
| { | |
| name: 'create_graph_relationship', | |
| description: 'Create a relationship between two nodes in the graph', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| fromNodeId: { | |
| type: 'string', | |
| description: 'Source node ID' | |
| }, | |
| toNodeId: { | |
| type: 'string', | |
| description: 'Target node ID' | |
| }, | |
| relationshipType: { | |
| type: 'string', | |
| description: 'Relationship type (e.g., DEPENDS_ON, CONTAINS, RELATED_TO)' | |
| }, | |
| properties: { | |
| type: 'object', | |
| description: 'Optional relationship properties' | |
| } | |
| }, | |
| required: ['fromNodeId', 'toNodeId', 'relationshipType'] | |
| }, | |
| handler: handleCreateGraphRelationship | |
| }, | |
| { | |
| name: 'get_node_connections', | |
| description: 'Get all connections (relationships) for a specific node', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| nodeId: { | |
| type: 'string', | |
| description: 'Node ID to get connections for' | |
| }, | |
| direction: { | |
| type: 'string', | |
| enum: ['in', 'out', 'both'], | |
| description: 'Direction of relationships' | |
| }, | |
| limit: { | |
| type: 'number', | |
| description: 'Maximum connections to return' | |
| } | |
| }, | |
| required: ['nodeId'] | |
| }, | |
| handler: handleGetNodeConnections | |
| }, | |
| { | |
| name: 'get_harvest_stats', | |
| description: 'Get OmniHarvester statistics and recent activity', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| timeRange: { | |
| type: 'string', | |
| enum: ['1h', '24h', '7d', '30d'], | |
| description: 'Time range for statistics' | |
| } | |
| } | |
| }, | |
| handler: handleGetHarvestStats | |
| }, | |
| { | |
| name: 'get_graph_stats', | |
| description: 'Get knowledge graph statistics (node counts, relationship counts by type)', | |
| inputSchema: { | |
| type: 'object', | |
| properties: {} | |
| }, | |
| handler: handleGetGraphStats | |
| }, | |
| { | |
| name: 'ingest_knowledge_graph', | |
| description: 'Ingest a repository or directory into the knowledge graph. Creates Repository, Directory, and File nodes with CONTAINS relationships.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| path: { | |
| type: 'string', | |
| description: 'Path to repository or directory to ingest' | |
| }, | |
| name: { | |
| type: 'string', | |
| description: 'Optional name for the repository' | |
| }, | |
| maxDepth: { | |
| type: 'number', | |
| description: 'Maximum directory depth to traverse (default: 10)' | |
| } | |
| }, | |
| required: ['path'] | |
| }, | |
| handler: handleIngestKnowledgeGraph | |
| } | |
| ]; | |