UNPKG

zrald

Version:

Advanced Graph RAG MCP Server with sophisticated graph structures, operators, and agentic capabilities for AI agents

380 lines 17.3 kB
import { BaseOperator } from './base-operator.js'; import { KHopPathOperatorConfigSchema, SteinerOperatorConfigSchema } from '../types/graph.js'; /** * KHopPath Operator - Multi-step path finding * Identifies paths of specified length between entities, establishing how concepts connect through intermediate steps */ export class KHopPathOperator extends BaseOperator { constructor(graphDb, vectorStore) { super('KHopPathOperator', 'subgraph', graphDb, vectorStore); } async execute(config) { const validatedConfig = await this.validateConfig(config, KHopPathOperatorConfigSchema); const allPaths = []; const pathNodes = []; const pathRelationships = []; const scores = {}; const seenNodeIds = new Set(); const seenRelIds = new Set(); // Find paths between all source-target pairs for (const sourceId of validatedConfig.source_nodes) { for (const targetId of validatedConfig.target_nodes) { if (sourceId === targetId) continue; const paths = await this.findKHopPaths(sourceId, targetId, validatedConfig.max_hops, validatedConfig.relationship_types, validatedConfig.path_limit); allPaths.push(...paths); } } // Process and score all paths for (const path of allPaths) { const pathScore = this.calculatePathScore(path); // Add nodes from path for (const node of path.nodes) { if (!seenNodeIds.has(node.id)) { pathNodes.push(node); seenNodeIds.add(node.id); scores[node.id] = Math.max(scores[node.id] || 0, pathScore); } } // Add relationships from path for (const rel of path.relationships) { if (!seenRelIds.has(rel.id)) { pathRelationships.push(rel); seenRelIds.add(rel.id); scores[rel.id] = Math.max(scores[rel.id] || 0, pathScore); } } } // Sort paths by score and limit results const scoredPaths = allPaths.map(path => ({ path, score: this.calculatePathScore(path) })).sort((a, b) => b.score - a.score); return this.createResult(pathNodes, pathRelationships, [], scores, { path_finding_method: 'k_hop_paths', max_hops: validatedConfig.max_hops, total_paths_found: allPaths.length, source_target_pairs: validatedConfig.source_nodes.length * validatedConfig.target_nodes.length, relationship_types_filter: validatedConfig.relationship_types, top_paths: scoredPaths.slice(0, 10).map(sp => ({ score: sp.score, length: sp.path.nodes.length, node_ids: sp.path.nodes.map(n => n.id), relationship_types: sp.path.relationships.map(r => r.type) })) }); } async findKHopPaths(sourceId, targetId, maxHops, relationshipTypes, pathLimit = 10) { const paths = []; const visited = new Set(); // Use breadth-first search to find paths const queue = [{ currentNode: sourceId, path: [sourceId], relationships: [], depth: 0 }]; while (queue.length > 0 && paths.length < pathLimit) { const current = queue.shift(); if (current.depth >= maxHops) continue; if (current.currentNode === targetId && current.depth > 0) { // Found a path to target const pathNodes = []; for (const nodeId of current.path) { const node = await this.graphDb.getNode(nodeId); if (node) pathNodes.push(node); } paths.push({ nodes: pathNodes, relationships: current.relationships }); continue; } // Explore neighbors const relationships = await this.graphDb.getRelationships(current.currentNode, 'outgoing'); for (const rel of relationships) { // Filter by relationship type if specified if (relationshipTypes && !relationshipTypes.includes(rel.type)) { continue; } const nextNodeId = rel.target_id; // Avoid cycles (except for reaching the target) if (current.path.includes(nextNodeId) && nextNodeId !== targetId) { continue; } queue.push({ currentNode: nextNodeId, path: [...current.path, nextNodeId], relationships: [...current.relationships, rel], depth: current.depth + 1 }); } } return paths; } calculatePathScore(path) { if (path.relationships.length === 0) return 0; // Score based on relationship weights and path length const avgRelationshipWeight = path.relationships.reduce((sum, rel) => sum + rel.weight * rel.confidence, 0) / path.relationships.length; // Penalize longer paths const lengthPenalty = Math.pow(0.8, path.relationships.length - 1); // Boost score for diverse relationship types const uniqueRelTypes = new Set(path.relationships.map(r => r.type)).size; const diversityBoost = 1 + (uniqueRelTypes - 1) * 0.1; return avgRelationshipWeight * lengthPenalty * diversityBoost; } } /** * Steiner Operator - Minimal connecting networks * Constructs minimal connecting networks between multiple entities, identifying the most efficient way to bridge disparate concepts */ export class SteinerOperator extends BaseOperator { constructor(graphDb, vectorStore) { super('SteinerOperator', 'subgraph', graphDb, vectorStore); } async execute(config) { const validatedConfig = await this.validateConfig(config, SteinerOperatorConfigSchema); if (validatedConfig.terminal_nodes.length < 2) { throw new Error('Steiner tree requires at least 2 terminal nodes'); } // Build the Steiner tree/forest const steinerResult = await this.buildSteinerTree(validatedConfig.terminal_nodes, validatedConfig.edge_weights, validatedConfig.algorithm, validatedConfig.max_tree_size); const scores = {}; // Score nodes based on their role in the Steiner tree for (const node of steinerResult.nodes) { scores[node.id] = this.calculateSteinerNodeScore(node, validatedConfig.terminal_nodes, steinerResult); } // Score relationships based on their importance in connectivity for (const rel of steinerResult.relationships) { scores[rel.id] = this.calculateSteinerEdgeScore(rel, steinerResult); } return this.createResult(steinerResult.nodes, steinerResult.relationships, [], scores, { algorithm_used: validatedConfig.algorithm, terminal_nodes: validatedConfig.terminal_nodes, total_tree_cost: steinerResult.totalCost, tree_efficiency: steinerResult.efficiency, steiner_nodes: steinerResult.steinerNodes, connectivity_analysis: steinerResult.connectivityAnalysis }); } async buildSteinerTree(terminalNodes, edgeWeights, algorithm = 'approximation', maxTreeSize = 100) { if (algorithm === 'exact' && terminalNodes.length > 10) { console.warn('Exact Steiner tree algorithm is computationally expensive for large inputs, falling back to approximation'); algorithm = 'approximation'; } // Get all terminal nodes const terminals = []; for (const nodeId of terminalNodes) { const node = await this.graphDb.getNode(nodeId); if (node) terminals.push(node); } if (algorithm === 'approximation') { return await this.approximateSteinerTree(terminals, edgeWeights, maxTreeSize); } else { return await this.exactSteinerTree(terminals, edgeWeights, maxTreeSize); } } async approximateSteinerTree(terminals, edgeWeights, maxTreeSize = 100) { // Use a minimum spanning tree approximation const allNodes = [...terminals]; const allRelationships = []; const steinerNodes = []; const seenNodeIds = new Set(terminals.map(t => t.id)); // Build a graph that includes paths between all terminal pairs for (let i = 0; i < terminals.length; i++) { for (let j = i + 1; j < terminals.length; j++) { const paths = await this.graphDb.findPaths(terminals[i].id, terminals[j].id, 3); // Add the shortest path if (paths.length > 0) { const shortestPath = paths.reduce((shortest, current) => current.relationships.length < shortest.relationships.length ? current : shortest); // Add intermediate nodes (Steiner nodes) for (const node of shortestPath.nodes) { if (!seenNodeIds.has(node.id)) { allNodes.push(node); seenNodeIds.add(node.id); if (!terminals.find(t => t.id === node.id)) { steinerNodes.push(node.id); } } } allRelationships.push(...shortestPath.relationships); } } } // Build minimum spanning tree from the candidate edges const mstResult = this.buildMinimumSpanningTree(allNodes, allRelationships, edgeWeights); const totalCost = mstResult.relationships.reduce((sum, rel) => { const weight = edgeWeights?.[rel.id] || rel.weight; return sum + weight; }, 0); const efficiency = this.calculateTreeEfficiency(mstResult.nodes, mstResult.relationships, terminals); return { nodes: mstResult.nodes, relationships: mstResult.relationships, totalCost, efficiency, steinerNodes, connectivityAnalysis: { terminal_connectivity: this.analyzeTerminalConnectivity(terminals, mstResult.relationships), tree_diameter: this.calculateTreeDiameter(mstResult.nodes, mstResult.relationships), branching_factor: this.calculateBranchingFactor(mstResult.nodes, mstResult.relationships) } }; } async exactSteinerTree(terminals, edgeWeights, maxTreeSize = 100) { // For exact Steiner tree, we would implement the Dreyfus-Wagner algorithm // This is computationally expensive, so we'll use a simplified heuristic approach console.warn('Exact Steiner tree algorithm not fully implemented, using enhanced approximation'); return await this.approximateSteinerTree(terminals, edgeWeights, maxTreeSize); } buildMinimumSpanningTree(nodes, relationships, edgeWeights) { // Kruskal's algorithm for MST const sortedEdges = relationships .map(rel => ({ relationship: rel, weight: edgeWeights?.[rel.id] || rel.weight })) .sort((a, b) => a.weight - b.weight); const mstRelationships = []; const mstNodes = []; const nodeComponents = new Map(); // Initialize each node as its own component for (const node of nodes) { nodeComponents.set(node.id, node.id); } // Find root of component (with path compression) const findRoot = (nodeId) => { const parent = nodeComponents.get(nodeId); if (parent !== nodeId) { const root = findRoot(parent); nodeComponents.set(nodeId, root); return root; } return nodeId; }; // Union two components const union = (nodeId1, nodeId2) => { const root1 = findRoot(nodeId1); const root2 = findRoot(nodeId2); if (root1 !== root2) { nodeComponents.set(root1, root2); return true; } return false; }; // Build MST for (const edge of sortedEdges) { const rel = edge.relationship; if (union(rel.source_id, rel.target_id)) { mstRelationships.push(rel); // Add nodes if not already included const sourceNode = nodes.find(n => n.id === rel.source_id); const targetNode = nodes.find(n => n.id === rel.target_id); if (sourceNode && !mstNodes.find(n => n.id === sourceNode.id)) { mstNodes.push(sourceNode); } if (targetNode && !mstNodes.find(n => n.id === targetNode.id)) { mstNodes.push(targetNode); } } } return { nodes: mstNodes, relationships: mstRelationships }; } calculateSteinerNodeScore(node, terminalNodes, steinerResult) { let score = 0.5; // Base score // Terminal nodes get higher scores if (terminalNodes.includes(node.id)) { score += 0.3; } // Steiner nodes (intermediate) get moderate scores based on connectivity if (steinerResult.steinerNodes.includes(node.id)) { const connectivity = steinerResult.relationships.filter((rel) => rel.source_id === node.id || rel.target_id === node.id).length; score += Math.min(0.2, connectivity * 0.05); } return Math.min(1.0, score); } calculateSteinerEdgeScore(rel, steinerResult) { // Edges in the Steiner tree are important for connectivity const baseScore = rel.weight * rel.confidence; // Boost score for edges connecting terminal nodes const connectsTerminals = steinerResult.connectivityAnalysis.terminal_connectivity .some((conn) => conn.edge_id === rel.id); return connectsTerminals ? baseScore * 1.2 : baseScore; } calculateTreeEfficiency(nodes, relationships, terminals) { // Efficiency = (number of terminals) / (total nodes in tree) return terminals.length / nodes.length; } analyzeTerminalConnectivity(terminals, relationships) { // Analyze how terminals are connected through the tree const connectivity = []; for (let i = 0; i < terminals.length; i++) { for (let j = i + 1; j < terminals.length; j++) { // Find path between terminals[i] and terminals[j] in the tree const pathExists = this.findPathInTree(terminals[i].id, terminals[j].id, relationships); connectivity.push({ terminal1: terminals[i].id, terminal2: terminals[j].id, connected: pathExists.length > 0, path_length: pathExists.length }); } } return connectivity; } findPathInTree(sourceId, targetId, relationships) { // Simple DFS to find path in tree const visited = new Set(); const path = []; const dfs = (currentId) => { if (currentId === targetId) { path.push(currentId); return true; } visited.add(currentId); path.push(currentId); for (const rel of relationships) { let nextId = null; if (rel.source_id === currentId && !visited.has(rel.target_id)) { nextId = rel.target_id; } else if (rel.target_id === currentId && !visited.has(rel.source_id)) { nextId = rel.source_id; } if (nextId && dfs(nextId)) { return true; } } path.pop(); return false; }; dfs(sourceId); return path; } calculateTreeDiameter(nodes, relationships) { // Calculate the longest path in the tree let maxDistance = 0; for (const node1 of nodes) { for (const node2 of nodes) { if (node1.id !== node2.id) { const path = this.findPathInTree(node1.id, node2.id, relationships); maxDistance = Math.max(maxDistance, path.length - 1); } } } return maxDistance; } calculateBranchingFactor(nodes, relationships) { // Calculate average branching factor const degrees = nodes.map(node => { return relationships.filter(rel => rel.source_id === node.id || rel.target_id === node.id).length; }); return degrees.reduce((sum, degree) => sum + degree, 0) / degrees.length; } } //# sourceMappingURL=subgraph-operators.js.map