zrald
Version:
Advanced Graph RAG MCP Server with sophisticated graph structures, operators, and agentic capabilities for AI agents
207 lines • 10.2 kB
JavaScript
import { BaseOperator } from './base-operator.js';
import { OneHopOperatorConfigSchema, AggregatorOperatorConfigSchema } from '../types/graph.js';
/**
* OneHop Operator - Direct neighborhood exploration
* Performs direct neighborhood exploration, retrieving all relationships connected to specified nodes
*/
export class OneHopOperator extends BaseOperator {
constructor(graphDb, vectorStore) {
super('OneHopOperator', 'relationship', graphDb, vectorStore);
}
async execute(config) {
const validatedConfig = await this.validateConfig(config, OneHopOperatorConfigSchema);
const allRelationships = [];
const connectedNodes = [];
const seenNodeIds = new Set();
const seenRelIds = new Set();
const scores = {};
// Process each source node
for (const sourceNodeId of validatedConfig.source_nodes) {
const sourceNode = await this.graphDb.getNode(sourceNodeId);
if (!sourceNode)
continue;
if (!seenNodeIds.has(sourceNodeId)) {
connectedNodes.push(sourceNode);
seenNodeIds.add(sourceNodeId);
scores[sourceNodeId] = 1.0; // Source nodes get max score
}
// Get relationships for this node
const relationships = await this.graphDb.getRelationships(sourceNodeId, validatedConfig.direction);
for (const relationship of relationships) {
// Filter by relationship type if specified
if (validatedConfig.relationship_types &&
!validatedConfig.relationship_types.includes(relationship.type)) {
continue;
}
if (!seenRelIds.has(relationship.id)) {
allRelationships.push(relationship);
seenRelIds.add(relationship.id);
}
// Add connected nodes
const targetNodeId = relationship.source_id === sourceNodeId
? relationship.target_id
: relationship.source_id;
if (!seenNodeIds.has(targetNodeId)) {
const targetNode = await this.graphDb.getNode(targetNodeId);
if (targetNode) {
connectedNodes.push(targetNode);
seenNodeIds.add(targetNodeId);
// Score based on relationship weight and confidence
scores[targetNodeId] = relationship.weight * relationship.confidence * 0.8;
}
}
}
}
// Apply depth filtering if max_depth > 1
if (validatedConfig.max_depth > 1) {
await this.expandToMaxDepth(validatedConfig, connectedNodes, allRelationships, seenNodeIds, seenRelIds, scores, 1);
}
return this.createResult(connectedNodes, allRelationships, [], this.normalizeScores(scores), {
exploration_type: 'one_hop',
direction: validatedConfig.direction,
max_depth: validatedConfig.max_depth,
relationship_types_filter: validatedConfig.relationship_types,
total_hops_explored: Math.min(validatedConfig.max_depth, 1)
});
}
async expandToMaxDepth(config, nodes, relationships, seenNodeIds, seenRelIds, scores, currentDepth) {
if (currentDepth >= config.max_depth)
return;
const currentLevelNodes = nodes.filter(node => !config.source_nodes.includes(node.id) &&
scores[node.id] > 0.5 / (currentDepth + 1));
for (const node of currentLevelNodes) {
const nodeRelationships = await this.graphDb.getRelationships(node.id, config.direction);
for (const relationship of nodeRelationships) {
if (config.relationship_types &&
!config.relationship_types.includes(relationship.type)) {
continue;
}
if (!seenRelIds.has(relationship.id)) {
relationships.push(relationship);
seenRelIds.add(relationship.id);
}
const targetNodeId = relationship.source_id === node.id
? relationship.target_id
: relationship.source_id;
if (!seenNodeIds.has(targetNodeId)) {
const targetNode = await this.graphDb.getNode(targetNodeId);
if (targetNode) {
nodes.push(targetNode);
seenNodeIds.add(targetNodeId);
// Decay score with depth
const depthDecay = Math.pow(0.7, currentDepth);
scores[targetNodeId] = relationship.weight * relationship.confidence * depthDecay;
}
}
}
}
// Recurse to next depth
await this.expandToMaxDepth(config, nodes, relationships, seenNodeIds, seenRelIds, scores, currentDepth + 1);
}
}
/**
* Aggregator Operator - Multi-relationship synthesis
* Synthesizes information across multiple relationships, combining evidence from various connections
*/
export class AggregatorOperator extends BaseOperator {
constructor(graphDb, vectorStore) {
super('AggregatorOperator', 'relationship', graphDb, vectorStore);
}
async execute(config) {
const validatedConfig = await this.validateConfig(config, AggregatorOperatorConfigSchema);
const aggregatedNodes = [];
const aggregatedRelationships = [];
const aggregatedScores = {};
const relationshipGroups = new Map();
// Collect all relationships for each source node
for (const sourceNodeId of validatedConfig.source_nodes) {
const sourceNode = await this.graphDb.getNode(sourceNodeId);
if (!sourceNode)
continue;
if (!aggregatedNodes.find(n => n.id === sourceNodeId)) {
aggregatedNodes.push(sourceNode);
}
const relationships = await this.graphDb.getRelationships(sourceNodeId, 'both');
// Group relationships by target node
for (const relationship of relationships) {
const targetNodeId = relationship.source_id === sourceNodeId
? relationship.target_id
: relationship.source_id;
if (!relationshipGroups.has(targetNodeId)) {
relationshipGroups.set(targetNodeId, []);
}
relationshipGroups.get(targetNodeId).push(relationship);
}
}
// Aggregate scores for each target node
for (const [targetNodeId, relationships] of relationshipGroups) {
const targetNode = await this.graphDb.getNode(targetNodeId);
if (!targetNode)
continue;
if (!aggregatedNodes.find(n => n.id === targetNodeId)) {
aggregatedNodes.push(targetNode);
}
// Add unique relationships
for (const rel of relationships) {
if (!aggregatedRelationships.find(r => r.id === rel.id)) {
aggregatedRelationships.push(rel);
}
}
// Calculate aggregated score
const aggregatedScore = this.calculateAggregatedScore(relationships, validatedConfig.aggregation_method, validatedConfig.relationship_weights, validatedConfig.normalize_scores);
aggregatedScores[targetNodeId] = aggregatedScore;
}
// Apply relationship-specific scoring
const relationshipScores = {};
for (const relationship of aggregatedRelationships) {
const weight = validatedConfig.relationship_weights?.[relationship.type] || 1.0;
relationshipScores[relationship.id] = relationship.weight * relationship.confidence * weight;
}
return this.createResult(aggregatedNodes, aggregatedRelationships, [], {
...aggregatedScores,
...relationshipScores
}, {
aggregation_method: validatedConfig.aggregation_method,
total_relationship_groups: relationshipGroups.size,
normalization_applied: validatedConfig.normalize_scores,
relationship_types_processed: [...new Set(aggregatedRelationships.map(r => r.type))]
});
}
calculateAggregatedScore(relationships, method, weights, normalize = true) {
if (relationships.length === 0)
return 0;
const scores = relationships.map(rel => {
const baseScore = rel.weight * rel.confidence;
const typeWeight = weights?.[rel.type] || 1.0;
return baseScore * typeWeight;
});
let aggregatedScore;
switch (method) {
case 'sum':
aggregatedScore = scores.reduce((sum, score) => sum + score, 0);
break;
case 'average':
aggregatedScore = scores.reduce((sum, score) => sum + score, 0) / scores.length;
break;
case 'max':
aggregatedScore = Math.max(...scores);
break;
case 'weighted':
// Weighted average with relationship confidence as weights
const totalWeight = relationships.reduce((sum, rel) => sum + rel.confidence, 0);
aggregatedScore = totalWeight > 0
? scores.reduce((sum, score, i) => sum + score * relationships[i].confidence, 0) / totalWeight
: 0;
break;
default:
aggregatedScore = scores.reduce((sum, score) => sum + score, 0) / scores.length;
}
// Apply normalization if requested
if (normalize && method === 'sum') {
// Normalize sum by number of relationships to prevent bias toward highly connected nodes
aggregatedScore = aggregatedScore / Math.sqrt(relationships.length);
}
return Math.min(1.0, Math.max(0.0, aggregatedScore));
}
}
//# sourceMappingURL=relationship-operators.js.map