zrald
Version:
Advanced Graph RAG MCP Server with sophisticated graph structures, operators, and agentic capabilities for AI agents
380 lines • 17.3 kB
JavaScript
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