Skip to content

Commit 9bc59c3

Browse files
vblagojesjrl
andauthored
Add Tools warm_up (#9856)
* Tools warmup initial * Fix lint * Improve pydocs for warm_up * Further improve pydocs for warm_up * No need to warm_up tools in Agent as they are warmed up by ToolInvoker * Simplify Toolset __add__ logic * Simplify _ToolsetWrapper * Add unit tests * ToolInvoker warm_up * Improve Tool pydoc * Resurrect serde_utils.py * Update tests * Call ToolInvoker warm_up in agent warm_up * Lint * Move warm_up tests to ToolInvoker * Update tests * Remove tests * Pydoc nit * PR feedback * ToolInvoker's warm_up is idempotent * Add reno note * Update releasenotes/notes/tools-warm-up-support-e16cc043fed3653f.yaml Co-authored-by: Sebastian Husch Lee <[email protected]> * Make ComponentTool warm_up idempotent * Update warm_up_tools to use ToolsType * Linting * Add warm up test for mixed list of Tool/Toolset instances --------- Co-authored-by: Sebastian Husch Lee <[email protected]>
1 parent 4cb4854 commit 9bc59c3

File tree

11 files changed

+467
-14
lines changed

11 files changed

+467
-14
lines changed

haystack/components/agents/agent.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ def warm_up(self) -> None:
203203
if not self._is_warmed_up:
204204
if hasattr(self.chat_generator, "warm_up"):
205205
self.chat_generator.warm_up()
206+
if hasattr(self._tool_invoker, "warm_up") and self._tool_invoker is not None:
207+
self._tool_invoker.warm_up()
206208
self._is_warmed_up = True
207209

208210
def to_dict(self) -> dict[str, Any]:

haystack/components/tools/tool_invoker.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
deserialize_tools_or_toolset_inplace,
2525
flatten_tools_or_toolsets,
2626
serialize_tools_or_toolset,
27+
warm_up_tools,
2728
)
2829
from haystack.tools.errors import ToolInvocationError
2930
from haystack.tracing.utils import _serializable_value
@@ -216,6 +217,7 @@ def __init__(
216217
self.convert_result_to_json_string = convert_result_to_json_string
217218

218219
self._tools_with_names = self._validate_and_prepare_tools(tools)
220+
self._is_warmed_up = False
219221

220222
@staticmethod
221223
def _make_context_bound_invoke(tool_to_invoke: Tool, final_args: dict[str, Any]) -> Callable[[], Any]:
@@ -485,6 +487,17 @@ def _prepare_tool_call_params(
485487

486488
return tool_calls, tool_call_params, error_messages
487489

490+
def warm_up(self):
491+
"""
492+
Warm up the tool invoker.
493+
494+
This will warm up the tools registered in the tool invoker.
495+
This method is idempotent and will only warm up the tools once.
496+
"""
497+
if not self._is_warmed_up:
498+
warm_up_tools(self.tools)
499+
self._is_warmed_up = True
500+
488501
@component.output_types(tool_messages=list[ChatMessage], state=State)
489502
def run(
490503
self,

haystack/tools/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from haystack.tools.component_tool import ComponentTool
1414
from haystack.tools.pipeline_tool import PipelineTool
1515
from haystack.tools.serde_utils import deserialize_tools_or_toolset_inplace, serialize_tools_or_toolset
16-
from haystack.tools.utils import flatten_tools_or_toolsets
16+
from haystack.tools.utils import flatten_tools_or_toolsets, warm_up_tools
1717

1818
# Type alias for tools parameter - allows mixing Tools and Toolsets in a list
1919
ToolsType = Union[list[Union[Tool, Toolset]], Toolset]
@@ -27,7 +27,8 @@
2727
"PipelineTool",
2828
"serialize_tools_or_toolset",
2929
"Tool",
30+
"ToolsType",
3031
"Toolset",
3132
"tool",
32-
"ToolsType",
33+
"warm_up_tools",
3334
]

haystack/tools/component_tool.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,16 @@ def component_invoker(**kwargs):
211211
outputs_to_string=outputs_to_string,
212212
)
213213
self._component = component
214+
self._is_warmed_up = False
215+
216+
def warm_up(self):
217+
"""
218+
Prepare the ComponentTool for use.
219+
"""
220+
if not self._is_warmed_up:
221+
if hasattr(self._component, "warm_up"):
222+
self._component.warm_up()
223+
self._is_warmed_up = True
214224

215225
def to_dict(self) -> dict[str, Any]:
216226
"""

haystack/tools/tool.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class Tool:
2121
Accurate definitions of the textual attributes such as `name` and `description`
2222
are important for the Language Model to correctly prepare the call.
2323
24+
For resource-intensive operations like establishing connections to remote services or
25+
loading models, override the `warm_up()` method. This method is called before the Tool
26+
is used and should be idempotent, as it may be called multiple times during
27+
pipeline/agent setup.
28+
2429
:param name:
2530
Name of the Tool.
2631
:param description:
@@ -98,6 +103,16 @@ def tool_spec(self) -> dict[str, Any]:
98103
"""
99104
return {"name": self.name, "description": self.description, "parameters": self.parameters}
100105

106+
def warm_up(self) -> None:
107+
"""
108+
Prepare the Tool for use.
109+
110+
Override this method to establish connections to remote services, load models,
111+
or perform other resource-intensive initialization. This method should be idempotent,
112+
as it may be called multiple times.
113+
"""
114+
pass
115+
101116
def invoke(self, **kwargs: Any) -> Any:
102117
"""
103118
Invoke the Tool with the provided keyword arguments.

haystack/tools/toolset.py

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ def __contains__(self, item: Any) -> bool:
185185
return item in self.tools
186186
return False
187187

188+
def warm_up(self) -> None:
189+
"""
190+
Prepare the Toolset for use.
191+
192+
Override this method to set up shared resources like database connections or HTTP sessions.
193+
This method should be idempotent, as it may be called multiple times.
194+
"""
195+
pass
196+
188197
def add(self, tool: Union[Tool, "Toolset"]) -> None:
189198
"""
190199
Add a new Tool or merge another Toolset.
@@ -260,18 +269,12 @@ def __add__(self, other: Union[Tool, "Toolset", list[Tool]]) -> "Toolset":
260269
:raises ValueError: If the combination would result in duplicate tool names
261270
"""
262271
if isinstance(other, Tool):
263-
combined_tools = self.tools + [other]
264-
elif isinstance(other, Toolset):
265-
combined_tools = self.tools + list(other)
266-
elif isinstance(other, list) and all(isinstance(item, Tool) for item in other):
267-
combined_tools = self.tools + other
268-
else:
269-
raise TypeError(f"Cannot add {type(other).__name__} to Toolset")
270-
271-
# Check for duplicates
272-
_check_duplicate_tool_names(combined_tools)
273-
274-
return Toolset(tools=combined_tools)
272+
return Toolset(tools=self.tools + [other])
273+
if isinstance(other, Toolset):
274+
return _ToolsetWrapper([self, other])
275+
if isinstance(other, list) and all(isinstance(item, Tool) for item in other):
276+
return Toolset(tools=self.tools + other)
277+
raise TypeError(f"Cannot add {type(other).__name__} to Toolset")
275278

276279
def __len__(self) -> int:
277280
"""
@@ -289,3 +292,52 @@ def __getitem__(self, index):
289292
:returns: The Tool at the specified index
290293
"""
291294
return self.tools[index]
295+
296+
297+
class _ToolsetWrapper(Toolset):
298+
"""
299+
A wrapper that holds multiple toolsets and provides a unified interface.
300+
301+
This is used internally when combining different types of toolsets to preserve
302+
their individual configurations while still being usable with ToolInvoker.
303+
"""
304+
305+
def __init__(self, toolsets: list[Toolset]):
306+
super().__init__([tool for toolset in toolsets for tool in toolset])
307+
self.toolsets = toolsets
308+
309+
def __iter__(self):
310+
"""Iterate over all tools from all toolsets."""
311+
for toolset in self.toolsets:
312+
yield from toolset
313+
314+
def __contains__(self, item):
315+
"""Check if a tool is in any of the toolsets."""
316+
return any(item in toolset for toolset in self.toolsets)
317+
318+
def warm_up(self):
319+
"""Warm up all toolsets."""
320+
for toolset in self.toolsets:
321+
toolset.warm_up()
322+
323+
def __len__(self):
324+
"""Return total number of tools across all toolsets."""
325+
return sum(len(toolset) for toolset in self.toolsets)
326+
327+
def __getitem__(self, index):
328+
"""Get a tool by index across all toolsets."""
329+
# Leverage iteration instead of manual index tracking
330+
for i, tool in enumerate(self):
331+
if i == index:
332+
return tool
333+
raise IndexError("ToolsetWrapper index out of range")
334+
335+
def __add__(self, other):
336+
"""Add another toolset or tool to this wrapper."""
337+
if isinstance(other, Toolset):
338+
return _ToolsetWrapper(self.toolsets + [other])
339+
if isinstance(other, Tool):
340+
return _ToolsetWrapper(self.toolsets + [Toolset([other])])
341+
if isinstance(other, list) and all(isinstance(item, Tool) for item in other):
342+
return _ToolsetWrapper(self.toolsets + [Toolset(other)])
343+
raise TypeError(f"Cannot add {type(other).__name__} to ToolsetWrapper")

haystack/tools/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@
1111
from haystack.tools import ToolsType
1212

1313

14+
def warm_up_tools(tools: "Optional[ToolsType]" = None) -> None:
15+
"""
16+
Warm up tools from various formats (Tools, Toolsets, or mixed lists).
17+
18+
:param tools: A list of Tool and/or Toolset objects, a single Toolset, or None.
19+
"""
20+
if tools is None:
21+
return
22+
23+
# If tools is a single Toolset, warm up the toolset itself
24+
if isinstance(tools, Toolset):
25+
if hasattr(tools, "warm_up"):
26+
tools.warm_up()
27+
return
28+
29+
# If tools is a list, warm up each item (Tool or Toolset)
30+
if isinstance(tools, list):
31+
for item in tools:
32+
if isinstance(item, (Toolset, Tool)) and hasattr(item, "warm_up"):
33+
item.warm_up()
34+
35+
1436
def flatten_tools_or_toolsets(tools: "Optional[ToolsType]") -> list[Tool]:
1537
"""
1638
Flatten tools from various formats into a list of Tool instances.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
features:
3+
- |
4+
Added a `warm_up()` function to the `Tool` dataclass, allowing tools to perform resource-intensive initialization
5+
before execution. Tools and Toolsets can now override the `warm_up()` method to establish connections to
6+
remote services, load models, or perform other preparatory operations. The `ToolInvoker` and `Agent`
7+
automatically call `warm_up()` on their tools during their own warm-up phase, ensuring tools are ready
8+
before use.

0 commit comments

Comments
 (0)