Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions quantmind/knowledge/_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,24 @@ def walk_dfs(self) -> Iterator[TreeNode]:
stack.extend(reversed(node.children_ids))

def find_path(self, node_id: UUID) -> list[TreeNode]:
"""Root-to-node path. Empty if `node_id` is not in the tree."""
"""Root-to-node path.

Returns an empty list if ``node_id`` is not in the tree. If the
ancestor chain is malformed (a ``parent_id`` points outside the
node map, or the parents form a cycle), the walk stops early and
returns the best-effort partial path ending at ``node_id`` instead
of raising or looping forever. Node data may come from an LLM, so
``parent_id`` carries no referential guarantee.
"""
if node_id not in self.nodes:
return []
path: list[TreeNode] = []
cursor: UUID | None = node_id
while cursor is not None:
visited: set[UUID] = set()
while cursor is not None and cursor in self.nodes:
if cursor in visited:
break
visited.add(cursor)
node = self.nodes[cursor]
path.append(node)
cursor = node.parent_id
Expand Down
34 changes: 34 additions & 0 deletions tests/knowledge/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ def test_find_path_unknown(self):
tree = _make_tree()
self.assertEqual(tree.find_path(uuid4()), [])

def test_find_path_dangling_parent(self):
# A parent_id pointing outside the node map must not raise.
missing = uuid4()
child_id = uuid4()
child = TreeNode(
node_id=child_id,
parent_id=missing,
title="Child",
summary="orphan",
)
tree = _SampleTree(
as_of=_now(),
source=_src(),
root_node_id=child_id,
nodes={child_id: child},
)
path = tree.find_path(child_id)
self.assertEqual([n.title for n in path], ["Child"])

def test_find_path_cyclic_parent(self):
# A parent_id cycle must terminate, not loop forever.
a_id, b_id = uuid4(), uuid4()
a = TreeNode(node_id=a_id, parent_id=b_id, title="A", summary="a")
b = TreeNode(node_id=b_id, parent_id=a_id, title="B", summary="b")
tree = _SampleTree(
as_of=_now(),
source=_src(),
root_node_id=a_id,
nodes={a_id: a, b_id: b},
)
path = tree.find_path(a_id)
self.assertEqual({n.title for n in path}, {"A", "B"})
self.assertEqual(len(path), 2)

def test_embedding_text_uses_root(self):
tree = _make_tree()
self.assertEqual(tree.embedding_text(), "Root\nroot summary")
Expand Down