From 250666d48c8845c132f67c537d4718049a2a099b Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Sun, 28 Dec 2025 20:58:50 +0800 Subject: [PATCH] fix(memory): return connected relations in openNodes The openNodes method now returns relations where either endpoint matches the requested entities, instead of requiring both endpoints to match. This allows traversing the graph from specific nodes. Fixes #3137 Signed-off-by: majiayu000 <1835304752@qq.com> --- src/memory/__tests__/knowledge-graph.test.ts | 35 ++++++++++++++++---- src/memory/index.ts | 17 +++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index a65d527b64..bd6706d270 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -336,16 +336,39 @@ describe('KnowledgeGraphManager', () => { expect(result.entities.map(e => e.name)).toContain('Bob'); }); - it('should include relations between opened nodes', async () => { + it('should include relations connected to opened nodes', async () => { const result = await manager.openNodes(['Alice', 'Bob']); - expect(result.relations).toHaveLength(1); - expect(result.relations[0].from).toBe('Alice'); - expect(result.relations[0].to).toBe('Bob'); + // Returns both Alice->Bob (between opened nodes) and Bob->Charlie (Bob is opened) + expect(result.relations).toHaveLength(2); + expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true); + expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Charlie')).toBe(true); }); - it('should exclude relations to unopened nodes', async () => { + it('should include both incoming and outgoing relations for single node', async () => { + // When opening Bob, should return both incoming (Alice->Bob) and outgoing (Bob->Charlie) relations const result = await manager.openNodes(['Bob']); - expect(result.relations).toHaveLength(0); + expect(result.relations).toHaveLength(2); + expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true); + expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Charlie')).toBe(true); + }); + + it('should return outgoing relations from requested node (issue #3137)', async () => { + // Reproduces issue #3137: open_nodes should return relations where the node is either source or target + await manager.createEntities([ + { name: '2025-12-17', entityType: 'work-day', observations: [] }, + { name: 'incident/example', entityType: 'incident', observations: ['Test incident'] }, + ]); + + await manager.createRelations([ + { from: '2025-12-17', to: 'incident/example', relationType: 'worked-on' }, + ]); + + const result = await manager.openNodes(['2025-12-17']); + expect(result.entities).toHaveLength(1); + expect(result.relations).toHaveLength(1); + expect(result.relations[0].from).toBe('2025-12-17'); + expect(result.relations[0].to).toBe('incident/example'); + expect(result.relations[0].relationType).toBe('worked-on'); }); it('should handle opening non-existent nodes', async () => { diff --git a/src/memory/index.ts b/src/memory/index.ts index c7d781d2c4..52ab71dc7f 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -200,23 +200,24 @@ export class KnowledgeGraphManager { async openNodes(names: string[]): Promise { const graph = await this.loadGraph(); - + // Filter entities const filteredEntities = graph.entities.filter(e => names.includes(e.name)); - + // Create a Set of filtered entity names for quick lookup const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + + // Filter relations to include those where either endpoint is in the filtered entities + // This returns both outgoing and incoming relations for the requested nodes + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to) ); - + const filteredGraph: KnowledgeGraph = { entities: filteredEntities, relations: filteredRelations, }; - + return filteredGraph; } }