Skip to content

Commit c4e0ae5

Browse files
committed
fix: warm up individual tools inside Toolsets in warm_up_tools()
Related Issues: * Follows up on PR deepset-ai#9942 (feat: Add warm_up() method to ChatGenerators) * Addresses bug discovered during implementation of PR deepset-ai#9942 for issue deepset-ai#9907 Proposed Changes: The warm_up_tools() utility function was only calling warm_up() on Toolset objects themselves, but not on the individual Tool instances contained within them. This meant tools inside a Toolset were not properly initialized before use. This PR modifies warm_up_tools() to iterate through Toolsets and call warm_up() on each individual tool, in addition to calling warm_up() on the Toolset itself. Changes: - Modified warm_up_tools() in haystack/tools/utils.py to iterate through Toolsets when encountered (both as single argument and within lists) - Added iteration to call warm_up() on each individual Tool inside Toolsets - Added comprehensive test class TestWarmUpTools with 7 test cases How did you test it: - Added 7 comprehensive unit tests in test/tools/test_tools_utils.py: * test_warm_up_tools_with_none - handles None input * test_warm_up_tools_with_single_tool - single tool in list * test_warm_up_tools_with_single_toolset - KEY TEST: verifies both Toolset and individual tools are warmed * test_warm_up_tools_with_list_containing_toolset - toolset within list * test_warm_up_tools_with_multiple_toolsets - multiple toolsets * test_warm_up_tools_with_mixed_tools_and_toolsets - mixed scenarios * test_warm_up_tools_idempotency - safe to call multiple times Notes for the reviewer: I discovered this bug while implementing PR deepset-ai#9942 (for issue deepset-ai#9907). When a Toolset object is passed to a component's tools parameter, the warm_up_tools() function only calls Toolset.warm_up(), which is a no-op. It doesn't iterate through the individual tools inside the Toolset to warm them up. acknowledged by @vblagoje and @sjrl This implementation: - Modified warm_up_tools() to iterate through Toolsets and call warm_up() on each individual tool - Added comprehensive tests for Toolset warming behavior - Verified both the Toolset and its contained tools are warmed up Checklist: I have read the contributors guidelines and the code of conduct I have updated the related issue with new insights and changes I added unit tests and updated the docstrings I've used one of the conventional commit types for my PR title: fix: I documented my code I ran pre-commit hooks and fixed any issue
1 parent c27c8e9 commit c4e0ae5

File tree

2 files changed

+233
-2
lines changed

2 files changed

+233
-2
lines changed

haystack/tools/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,24 @@ def warm_up_tools(tools: "Optional[ToolsType]" = None) -> None:
2424
if isinstance(tools, Toolset):
2525
if hasattr(tools, "warm_up"):
2626
tools.warm_up()
27+
# Also warm up individual tools inside the toolset
28+
for tool in tools:
29+
if hasattr(tool, "warm_up"):
30+
tool.warm_up()
2731
return
2832

2933
# If tools is a list, warm up each item (Tool or Toolset)
3034
if isinstance(tools, list):
3135
for item in tools:
32-
if isinstance(item, (Toolset, Tool)) and hasattr(item, "warm_up"):
36+
if isinstance(item, Toolset):
37+
# Warm up the toolset itself
38+
if hasattr(item, "warm_up"):
39+
item.warm_up()
40+
# Also warm up individual tools inside the toolset
41+
for tool in item:
42+
if hasattr(tool, "warm_up"):
43+
tool.warm_up()
44+
elif isinstance(item, Tool) and hasattr(item, "warm_up"):
3345
item.warm_up()
3446

3547

test/tools/test_tools_utils.py

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from haystack.tools import Tool, Toolset, flatten_tools_or_toolsets
7+
from haystack.tools import Tool, Toolset, flatten_tools_or_toolsets, warm_up_tools
88

99

1010
def add_numbers(a: int, b: int) -> int:
@@ -171,3 +171,222 @@ def test_flatten_multiple_toolsets(self, add_tool, multiply_tool, subtract_tool)
171171
assert result[0].name == "add"
172172
assert result[1].name == "multiply"
173173
assert result[2].name == "subtract"
174+
175+
176+
class WarmupTrackingTool(Tool):
177+
"""A tool that tracks whether warm_up was called."""
178+
179+
def __init__(self, *args, **kwargs):
180+
super().__init__(*args, **kwargs)
181+
self.was_warmed_up = False
182+
183+
def warm_up(self):
184+
self.was_warmed_up = True
185+
186+
187+
class WarmupTrackingToolset(Toolset):
188+
"""A toolset that tracks whether warm_up was called."""
189+
190+
def __init__(self, tools):
191+
super().__init__(tools)
192+
self.was_warmed_up = False
193+
194+
def warm_up(self):
195+
self.was_warmed_up = True
196+
197+
198+
class TestWarmUpTools:
199+
"""Tests for the warm_up_tools() function"""
200+
201+
def test_warm_up_tools_with_none(self):
202+
"""Test that warm_up_tools with None does nothing."""
203+
# Should not raise any errors
204+
warm_up_tools(None)
205+
206+
def test_warm_up_tools_with_single_tool(self):
207+
"""Test that warm_up_tools works with a single tool in a list."""
208+
tool = WarmupTrackingTool(
209+
name="test_tool",
210+
description="A test tool",
211+
parameters={"type": "object", "properties": {}},
212+
function=lambda: "test",
213+
)
214+
215+
assert not tool.was_warmed_up
216+
warm_up_tools([tool])
217+
assert tool.was_warmed_up
218+
219+
def test_warm_up_tools_with_single_toolset(self):
220+
"""
221+
Test that when passing a single Toolset, both the Toolset.warm_up()
222+
and each individual tool's warm_up() are called.
223+
"""
224+
tool1 = WarmupTrackingTool(
225+
name="tool1",
226+
description="First tool",
227+
parameters={"type": "object", "properties": {}},
228+
function=lambda: "tool1",
229+
)
230+
tool2 = WarmupTrackingTool(
231+
name="tool2",
232+
description="Second tool",
233+
parameters={"type": "object", "properties": {}},
234+
function=lambda: "tool2",
235+
)
236+
237+
toolset = WarmupTrackingToolset([tool1, tool2])
238+
239+
assert not toolset.was_warmed_up
240+
assert not tool1.was_warmed_up
241+
assert not tool2.was_warmed_up
242+
243+
warm_up_tools(toolset)
244+
245+
# Both the toolset itself and individual tools should be warmed up
246+
assert toolset.was_warmed_up
247+
assert tool1.was_warmed_up
248+
assert tool2.was_warmed_up
249+
250+
def test_warm_up_tools_with_list_containing_toolset(self):
251+
"""Test that when a Toolset is in a list, individual tools inside get warmed up."""
252+
tool1 = WarmupTrackingTool(
253+
name="tool1",
254+
description="First tool",
255+
parameters={"type": "object", "properties": {}},
256+
function=lambda: "tool1",
257+
)
258+
tool2 = WarmupTrackingTool(
259+
name="tool2",
260+
description="Second tool",
261+
parameters={"type": "object", "properties": {}},
262+
function=lambda: "tool2",
263+
)
264+
265+
toolset = WarmupTrackingToolset([tool1, tool2])
266+
267+
assert not toolset.was_warmed_up
268+
assert not tool1.was_warmed_up
269+
assert not tool2.was_warmed_up
270+
271+
warm_up_tools([toolset])
272+
273+
# Both the toolset itself and individual tools should be warmed up
274+
assert toolset.was_warmed_up
275+
assert tool1.was_warmed_up
276+
assert tool2.was_warmed_up
277+
278+
def test_warm_up_tools_with_multiple_toolsets(self):
279+
"""Test multiple Toolsets in a list."""
280+
tool1 = WarmupTrackingTool(
281+
name="tool1",
282+
description="First tool",
283+
parameters={"type": "object", "properties": {}},
284+
function=lambda: "tool1",
285+
)
286+
tool2 = WarmupTrackingTool(
287+
name="tool2",
288+
description="Second tool",
289+
parameters={"type": "object", "properties": {}},
290+
function=lambda: "tool2",
291+
)
292+
tool3 = WarmupTrackingTool(
293+
name="tool3",
294+
description="Third tool",
295+
parameters={"type": "object", "properties": {}},
296+
function=lambda: "tool3",
297+
)
298+
299+
toolset1 = WarmupTrackingToolset([tool1])
300+
toolset2 = WarmupTrackingToolset([tool2, tool3])
301+
302+
assert not toolset1.was_warmed_up
303+
assert not toolset2.was_warmed_up
304+
assert not tool1.was_warmed_up
305+
assert not tool2.was_warmed_up
306+
assert not tool3.was_warmed_up
307+
308+
warm_up_tools([toolset1, toolset2])
309+
310+
# Both toolsets and all individual tools should be warmed up
311+
assert toolset1.was_warmed_up
312+
assert toolset2.was_warmed_up
313+
assert tool1.was_warmed_up
314+
assert tool2.was_warmed_up
315+
assert tool3.was_warmed_up
316+
317+
def test_warm_up_tools_with_mixed_tools_and_toolsets(self):
318+
"""Test list with both Tool objects and Toolsets."""
319+
standalone_tool = WarmupTrackingTool(
320+
name="standalone",
321+
description="Standalone tool",
322+
parameters={"type": "object", "properties": {}},
323+
function=lambda: "standalone",
324+
)
325+
toolset_tool1 = WarmupTrackingTool(
326+
name="toolset_tool1",
327+
description="Tool in toolset",
328+
parameters={"type": "object", "properties": {}},
329+
function=lambda: "toolset_tool1",
330+
)
331+
toolset_tool2 = WarmupTrackingTool(
332+
name="toolset_tool2",
333+
description="Another tool in toolset",
334+
parameters={"type": "object", "properties": {}},
335+
function=lambda: "toolset_tool2",
336+
)
337+
338+
toolset = WarmupTrackingToolset([toolset_tool1, toolset_tool2])
339+
340+
assert not standalone_tool.was_warmed_up
341+
assert not toolset.was_warmed_up
342+
assert not toolset_tool1.was_warmed_up
343+
assert not toolset_tool2.was_warmed_up
344+
345+
warm_up_tools([standalone_tool, toolset])
346+
347+
# All tools and the toolset should be warmed up
348+
assert standalone_tool.was_warmed_up
349+
assert toolset.was_warmed_up
350+
assert toolset_tool1.was_warmed_up
351+
assert toolset_tool2.was_warmed_up
352+
353+
def test_warm_up_tools_idempotency(self):
354+
"""Test that calling warm_up_tools() multiple times is safe."""
355+
356+
class WarmupCountingTool(Tool):
357+
"""A tool that counts how many times warm_up was called."""
358+
359+
def __init__(self, *args, **kwargs):
360+
super().__init__(*args, **kwargs)
361+
self.warm_up_count = 0
362+
363+
def warm_up(self):
364+
self.warm_up_count += 1
365+
366+
class WarmupCountingToolset(Toolset):
367+
"""A toolset that counts how many times warm_up was called."""
368+
369+
def __init__(self, tools):
370+
super().__init__(tools)
371+
self.warm_up_count = 0
372+
373+
def warm_up(self):
374+
self.warm_up_count += 1
375+
376+
tool = WarmupCountingTool(
377+
name="counting_tool",
378+
description="A counting tool",
379+
parameters={"type": "object", "properties": {}},
380+
function=lambda: "test",
381+
)
382+
toolset = WarmupCountingToolset([tool])
383+
384+
# Call warm_up_tools multiple times
385+
warm_up_tools(toolset)
386+
warm_up_tools(toolset)
387+
warm_up_tools(toolset)
388+
389+
# warm_up_tools itself doesn't prevent multiple calls,
390+
# but verify the calls actually happen multiple times
391+
assert toolset.warm_up_count == 3
392+
assert tool.warm_up_count == 3

0 commit comments

Comments
 (0)