Skip to content

Commit 71a4ef7

Browse files
committed
feat(traversal): implement weighted graph traversal algorithm
- Add performTraversalByDepth for level-by-level scored exploration - Track visited nodes and relationship chains across depths - Support toggle between weighted and standard traversal modes
1 parent 03e3a9b commit 71a4ef7

File tree

1 file changed

+114
-4
lines changed

1 file changed

+114
-4
lines changed

src/mcp/handlers/traversal.handler.ts

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface TraversalOptions {
2525
summaryOnly?: boolean;
2626
title?: string;
2727
snippetLength?: number;
28+
useWeightedTraversal?: boolean;
2829
}
2930

3031
interface Connection {
@@ -38,7 +39,11 @@ export class TraversalHandler {
3839

3940
constructor(private neo4jService: Neo4jService) {}
4041

41-
async traverseFromNode(nodeId: string, options: TraversalOptions = {}): Promise<TraversalResult> {
42+
async traverseFromNode(
43+
nodeId: string,
44+
embedding: number[],
45+
options: TraversalOptions = {},
46+
): Promise<TraversalResult> {
4247
const {
4348
maxDepth = DEFAULTS.traversalDepth,
4449
skip = DEFAULTS.skipOffset,
@@ -50,6 +55,7 @@ export class TraversalHandler {
5055
summaryOnly = false,
5156
title = `Node Traversal from: ${nodeId}`,
5257
snippetLength = DEFAULTS.codeSnippetLength,
58+
useWeightedTraversal = false,
5359
} = options;
5460

5561
try {
@@ -60,7 +66,18 @@ export class TraversalHandler {
6066
return createErrorResponse(`Node with ID "${nodeId}" not found.`);
6167
}
6268

63-
const traversalData = await this.performTraversal(nodeId, maxDepth, skip, direction, relationshipTypes);
69+
const maxNodesPerDepth = Math.ceil(maxNodesPerChain * 1.5);
70+
const traversalData = useWeightedTraversal
71+
? await this.performTraversalByDepth(
72+
nodeId,
73+
embedding,
74+
maxDepth,
75+
maxNodesPerDepth,
76+
direction,
77+
relationshipTypes,
78+
)
79+
: await this.performTraversal(nodeId, embedding, maxDepth, skip, direction, relationshipTypes);
80+
6481
if (!traversalData) {
6582
return createSuccessResponse(`No connections found for node "${nodeId}".`);
6683
}
@@ -100,6 +117,7 @@ export class TraversalHandler {
100117

101118
private async performTraversal(
102119
nodeId: string,
120+
embedding: number[],
103121
maxDepth: number,
104122
skip: number,
105123
direction: 'OUTGOING' | 'INCOMING' | 'BOTH' = 'BOTH',
@@ -124,6 +142,98 @@ export class TraversalHandler {
124142
};
125143
}
126144

145+
private async performTraversalByDepth(
146+
nodeId: string,
147+
embedding: number[],
148+
maxDepth: number,
149+
maxNodesPerDepth: number,
150+
direction: 'OUTGOING' | 'INCOMING' | 'BOTH' = 'BOTH',
151+
relationshipTypes?: string[],
152+
) {
153+
// Track visited nodes to avoid cycles
154+
const visitedNodeIds = new Set<string>([nodeId]);
155+
156+
// Track the path (chain of relationships) to reach each node
157+
// Key: nodeId, Value: array of relationships from start node to this node
158+
const pathsToNode = new Map<string, any[]>();
159+
pathsToNode.set(nodeId, []); // Start node has empty path
160+
161+
// Track which nodes to explore at each depth
162+
let currentSourceIds = [nodeId];
163+
164+
// Result accumulators
165+
const allConnections: Connection[] = [];
166+
const nodeMap = new Map<string, Neo4jNode>(); // Dedupe nodes
167+
168+
for (let depth = 1; depth <= maxDepth; depth++) {
169+
if (currentSourceIds.length === 0) {
170+
console.log(`No source nodes to explore at depth ${depth}`);
171+
break;
172+
}
173+
174+
const traversalResults = await this.neo4jService.run(QUERIES.EXPLORE_DEPTH_LEVEL(direction, maxNodesPerDepth), {
175+
sourceNodeIds: currentSourceIds,
176+
visitedNodeIds: Array.from(visitedNodeIds),
177+
currentDepth: parseInt(depth.toString()),
178+
queryEmbedding: embedding,
179+
depthDecay: 0.85,
180+
});
181+
182+
if (traversalResults.length === 0) {
183+
console.log(`No connections found at depth ${depth}`);
184+
break;
185+
}
186+
187+
// Collect node IDs for next depth exploration
188+
const nextSourceIds: string[] = [];
189+
190+
for (const row of traversalResults) {
191+
const { node, relationship, sourceNodeId, scoring } = row.result;
192+
const neighborId = node.id;
193+
194+
// Skip if already visited (safety check)
195+
if (visitedNodeIds.has(neighborId)) continue;
196+
197+
// Mark as visited
198+
visitedNodeIds.add(neighborId);
199+
nextSourceIds.push(neighborId);
200+
201+
// Build the relationship chain:
202+
// This node's chain = parent's chain + this relationship
203+
const parentPath = pathsToNode.get(sourceNodeId) ?? [];
204+
const thisPath = [
205+
...parentPath,
206+
{
207+
type: relationship.type,
208+
start: relationship.startNodeId,
209+
end: relationship.endNodeId,
210+
properties: relationship.properties,
211+
score: scoring.combinedScore,
212+
},
213+
];
214+
pathsToNode.set(neighborId, thisPath);
215+
216+
// Create connection with full relationship chain
217+
const connection: Connection = {
218+
depth,
219+
node: node as Neo4jNode,
220+
relationshipChain: thisPath,
221+
};
222+
allConnections.push(connection);
223+
224+
// Accumulate unique nodes
225+
nodeMap.set(neighborId, node as Neo4jNode);
226+
}
227+
228+
// Move to next depth with the newly discovered nodes
229+
currentSourceIds = nextSourceIds;
230+
}
231+
232+
return {
233+
connections: allConnections,
234+
};
235+
}
236+
127237
private groupConnectionsByDepth(connections: Connection[]): Record<number, Connection[]> {
128238
return connections.reduce(
129239
(acc, conn) => {
@@ -160,7 +270,7 @@ export class TraversalHandler {
160270

161271
private formatTraversalJSON(
162272
startNode: Neo4jNode,
163-
traversalData: { connections: Connection[]; graph: any },
273+
traversalData: { connections: Connection[]; graph?: any },
164274
title: string,
165275
includeStartNodeDetails: boolean,
166276
includeCode: boolean,
@@ -203,7 +313,7 @@ export class TraversalHandler {
203313

204314
private formatSummaryOnlyJSON(
205315
startNode: Neo4jNode,
206-
traversalData: { connections: Connection[]; graph: any },
316+
traversalData: { connections: Connection[]; graph?: any },
207317
title: string,
208318
): any {
209319
const byDepth = this.groupConnectionsByDepth(traversalData.connections);

0 commit comments

Comments
 (0)