Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
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
}
];