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
6 changes: 3 additions & 3 deletions haystack/tools/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ def add(self, tool: "Tool | Toolset") -> None:
if not isinstance(tool, (Tool, Toolset)):
raise TypeError(f"Expected Tool or Toolset, got {type(tool).__name__}")

# Warm up the source before flattening so that lazily-loaded toolsets (e.g. MCPToolset)
# expose their tools, and so newly added tools are ready to use right away.
if self._is_warmed_up and hasattr(tool, "warm_up"):
# Toolsets must be warmed before flattening so lazily loaded tools are not lost.
# Plain tools are still only warmed immediately when this Toolset is already warm.
if isinstance(tool, Toolset) or (self._is_warmed_up and hasattr(tool, "warm_up")):
tool.warm_up()

new_tools = [tool] if isinstance(tool, Tool) else list(tool)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
Ensure ``Toolset.add()`` warms lazy child toolsets before flattening them so dynamically loaded tools are
preserved.
41 changes: 41 additions & 0 deletions test/tools/test_toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,22 @@ def warm_up(self) -> None:
super().warm_up()


class LazyWarmUpToolset(Toolset):
"""A Toolset that only exposes its tools during warm_up()."""

def __init__(self, tools):
super().__init__([])
self._tools_to_load = tools
self.warm_up_count = 0

def warm_up(self) -> None:
if self._is_warmed_up:
return
self.warm_up_count += 1
self.tools.extend(self._tools_to_load)
super().warm_up()


class TestToolset:
def test_toolset_with_multiple_tools(self, add_tool, multiply_tool):
"""Test that a Toolset with multiple tools works properly."""
Expand Down Expand Up @@ -419,6 +435,31 @@ def test_add_toolset_after_warm_up_warms_added_toolset(self):
assert added.warm_up_count == 1
assert all(tool.warm_up_count == 1 for tool in added_tools)

def test_add_lazy_toolset_before_warm_up_preserves_loaded_tools(self):
toolset = Toolset()
added_tools = [WarmUpCountingTool("a"), WarmUpCountingTool("b")]
added = LazyWarmUpToolset(added_tools)

toolset.add(added)

assert added.warm_up_count == 1
assert [tool.name for tool in toolset] == ["a", "b"]
assert all(tool.warm_up_count == 1 for tool in added_tools)
assert toolset._is_warmed_up is False

added.warm_up()
assert added.warm_up_count == 1

def test_add_lazy_toolset_checks_loaded_tool_names_for_duplicates(self):
existing = WarmUpCountingTool("duplicate")
toolset = Toolset([existing])
added = LazyWarmUpToolset([WarmUpCountingTool("duplicate")])

with pytest.raises(ValueError, match="duplicate"):
toolset.add(added)

assert toolset.tools == [existing]

def test_plus_returns_new_unwarmed_toolset(self):
ts1 = Toolset([WarmUpCountingTool("a")])
ts1.warm_up()
Expand Down