twining_neighbors
Explore connections in a knowledge graph by traversing from an entity to related nodes, with configurable depth and relation filtering to understand entity relationships.
Instructions
Traverse the knowledge graph from an entity, returning neighbors up to a given depth (max 3). Supports filtering by relation type. Useful for understanding how entities connect.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| entity | Yes | Entity ID or name to start traversal from | |
| depth | No | Traversal depth (1-3, default: 1) | |
| relation_types | No | Filter to only these relation types |
Implementation Reference
- src/tools/graph-tools.ts:106-144 (handler)Registration and handler definition for twining_neighbors tool. It calls the engine.neighbors method.
// twining_neighbors — Traverse neighbors from an entity server.registerTool( "twining_neighbors", { description: "Traverse the knowledge graph from an entity, returning neighbors up to a given depth (max 3). Supports filtering by relation type. Useful for understanding how entities connect.", inputSchema: { entity: z .string() .describe("Entity ID or name to start traversal from"), depth: z .number() .optional() .describe("Traversal depth (1-3, default: 1)"), relation_types: z .array(z.string()) .optional() .describe("Filter to only these relation types"), }, }, async (args) => { try { const result = await engine.neighbors( args.entity, args.depth, args.relation_types, ); return toolResult(result); } catch (e) { if (e instanceof TwiningError) { return toolError(e.message, e.code); } return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, ); - src/engine/graph.ts:62-144 (handler)Implementation of the neighbor traversal logic (GraphEngine.neighbors).
async neighbors( entityIdOrName: string, depth?: number, relationTypes?: string[], ): Promise<NeighborsResult> { // Resolve center entity const center = await this.resolveEntity(entityIdOrName); if (!center) { throw new TwiningError( `Entity not found: "${entityIdOrName}"`, "NOT_FOUND", ); } const maxDepth = Math.min(Math.max(depth ?? 1, 1), 3); const relations = await this.graphStore.getRelations(); const entities = await this.graphStore.getEntities(); // Build entity lookup map const entityMap = new Map<string, Entity>(); for (const e of entities) { entityMap.set(e.id, e); } // Filter relations by type if specified const filteredRelations = relationTypes ? relations.filter((r) => relationTypes.includes(r.type)) : relations; // Build adjacency list: entityId -> [{neighborId, relation, direction}] const adjacency = new Map< string, { neighborId: string; relation: Relation; direction: RelationDirection }[] >(); for (const rel of filteredRelations) { // Outgoing: source -> target if (!adjacency.has(rel.source)) adjacency.set(rel.source, []); adjacency.get(rel.source)!.push({ neighborId: rel.target, relation: rel, direction: "outgoing", }); // Incoming: target -> source if (!adjacency.has(rel.target)) adjacency.set(rel.target, []); adjacency.get(rel.target)!.push({ neighborId: rel.source, relation: rel, direction: "incoming", }); } // BFS const visited = new Set<string>(); visited.add(center.id); const result: NeighborEntry[] = []; let frontier = [center.id]; for (let d = 0; d < maxDepth; d++) { const nextFrontier: string[] = []; for (const nodeId of frontier) { const neighbors = adjacency.get(nodeId) ?? []; for (const { neighborId, relation, direction } of neighbors) { if (!visited.has(neighborId)) { visited.add(neighborId); const entity = entityMap.get(neighborId); if (entity) { result.push({ entity, relation, direction }); nextFrontier.push(neighborId); } } } } frontier = nextFrontier; if (frontier.length === 0) break; } return { center, neighbors: result }; }