• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

deepset-ai / haystack / 18647136217

20 Oct 2025 08:52AM UTC coverage: 92.179% (-0.04%) from 92.22%
18647136217

Pull #9856

github

web-flow
Merge dc9eda57a into 1de94413c
Pull Request #9856: Add Tools warm_up

13425 of 14564 relevant lines covered (92.18%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.34
haystack/tools/toolset.py
1
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2
#
3
# SPDX-License-Identifier: Apache-2.0
4

5
from dataclasses import dataclass, field
1✔
6
from typing import Any, Iterator, Union
1✔
7

8
from haystack.core.serialization import generate_qualified_class_name, import_class_by_name
1✔
9
from haystack.tools.tool import Tool, _check_duplicate_tool_names
1✔
10

11

12
@dataclass
1✔
13
class Toolset:
1✔
14
    """
15
    A collection of related Tools that can be used and managed as a cohesive unit.
16

17
    Toolset serves two main purposes:
18

19
    1. Group related tools together:
20
       Toolset allows you to organize related tools into a single collection, making it easier
21
       to manage and use them as a unit in Haystack pipelines.
22

23
       Example:
24
       ```python
25
       from haystack.tools import Tool, Toolset
26
       from haystack.components.tools import ToolInvoker
27

28
       # Define math functions
29
       def add_numbers(a: int, b: int) -> int:
30
           return a + b
31

32
       def subtract_numbers(a: int, b: int) -> int:
33
           return a - b
34

35
       # Create tools with proper schemas
36
       add_tool = Tool(
37
           name="add",
38
           description="Add two numbers",
39
           parameters={
40
               "type": "object",
41
               "properties": {
42
                   "a": {"type": "integer"},
43
                   "b": {"type": "integer"}
44
               },
45
               "required": ["a", "b"]
46
           },
47
           function=add_numbers
48
       )
49

50
       subtract_tool = Tool(
51
           name="subtract",
52
           description="Subtract b from a",
53
           parameters={
54
               "type": "object",
55
               "properties": {
56
                   "a": {"type": "integer"},
57
                   "b": {"type": "integer"}
58
               },
59
               "required": ["a", "b"]
60
           },
61
           function=subtract_numbers
62
       )
63

64
       # Create a toolset with the math tools
65
       math_toolset = Toolset([add_tool, subtract_tool])
66

67
       # Use the toolset with a ToolInvoker or ChatGenerator component
68
       invoker = ToolInvoker(tools=math_toolset)
69
       ```
70

71
    2. Base class for dynamic tool loading:
72
       By subclassing Toolset, you can create implementations that dynamically load tools
73
       from external sources like OpenAPI URLs, MCP servers, or other resources.
74

75
       Example:
76
       ```python
77
       from haystack.core.serialization import generate_qualified_class_name
78
       from haystack.tools import Tool, Toolset
79
       from haystack.components.tools import ToolInvoker
80

81
       class CalculatorToolset(Toolset):
82
           '''A toolset for calculator operations.'''
83

84
           def __init__(self):
85
               tools = self._create_tools()
86
               super().__init__(tools)
87

88
           def _create_tools(self):
89
               # These Tool instances are obviously defined statically and for illustration purposes only.
90
               # In a real-world scenario, you would dynamically load tools from an external source here.
91
               tools = []
92
               add_tool = Tool(
93
                   name="add",
94
                   description="Add two numbers",
95
                   parameters={
96
                       "type": "object",
97
                       "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
98
                       "required": ["a", "b"],
99
                   },
100
                   function=lambda a, b: a + b,
101
               )
102

103
               multiply_tool = Tool(
104
                   name="multiply",
105
                   description="Multiply two numbers",
106
                   parameters={
107
                       "type": "object",
108
                       "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
109
                       "required": ["a", "b"],
110
                   },
111
                   function=lambda a, b: a * b,
112
               )
113

114
               tools.append(add_tool)
115
               tools.append(multiply_tool)
116

117
               return tools
118

119
           def to_dict(self):
120
               return {
121
                   "type": generate_qualified_class_name(type(self)),
122
                   "data": {},  # no data to serialize as we define the tools dynamically
123
               }
124

125
           @classmethod
126
           def from_dict(cls, data):
127
               return cls()  # Recreate the tools dynamically during deserialization
128

129
       # Create the dynamic toolset and use it with ToolInvoker
130
       calculator_toolset = CalculatorToolset()
131
       invoker = ToolInvoker(tools=calculator_toolset)
132
       ```
133

134
    Toolset implements the collection interface (__iter__, __contains__, __len__, __getitem__),
135
    making it behave like a list of Tools. This makes it compatible with components that expect
136
    iterable tools, such as ToolInvoker or Haystack chat generators.
137

138
    When implementing a custom Toolset subclass for dynamic tool loading:
139
    - Perform the dynamic loading in the __init__ method
140
    - Override to_dict() and from_dict() methods if your tools are defined dynamically
141
    - Serialize endpoint descriptors rather than tool instances if your tools
142
      are loaded from external sources
143
    """
144

145
    # Use field() with default_factory to initialize the list
146
    tools: list[Tool] = field(default_factory=list)
1✔
147

148
    def __post_init__(self):
1✔
149
        """
150
        Validate and set up the toolset after initialization.
151

152
        This handles the case when tools are provided during initialization.
153
        """
154
        # If initialization was done a single Tool, raise an error
155
        if isinstance(self.tools, Tool):
1✔
156
            raise TypeError("A single Tool cannot be directly passed to Toolset. Please use a list: Toolset([tool])")
×
157

158
        # Check for duplicate tool names in the initial set
159
        _check_duplicate_tool_names(self.tools)
1✔
160

161
    def __iter__(self) -> Iterator[Tool]:
1✔
162
        """
163
        Return an iterator over the Tools in this Toolset.
164

165
        This allows the Toolset to be used wherever a list of Tools is expected.
166

167
        :returns: An iterator yielding Tool instances
168
        """
169
        return iter(self.tools)
1✔
170

171
    def __contains__(self, item: Any) -> bool:
1✔
172
        """
173
        Check if a tool is in this Toolset.
174

175
        Supports checking by:
176
        - Tool instance: tool in toolset
177
        - Tool name: "tool_name" in toolset
178

179
        :param item: Tool instance or tool name string
180
        :returns: True if contained, False otherwise
181
        """
182
        if isinstance(item, str):
1✔
183
            return any(tool.name == item for tool in self.tools)
1✔
184
        if isinstance(item, Tool):
1✔
185
            return item in self.tools
1✔
186
        return False
×
187

188
    def warm_up(self) -> None:
1✔
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
1✔
196

197
    def add(self, tool: Union[Tool, "Toolset"]) -> None:
1✔
198
        """
199
        Add a new Tool or merge another Toolset.
200

201
        :param tool: A Tool instance or another Toolset to add
202
        :raises ValueError: If adding the tool would result in duplicate tool names
203
        :raises TypeError: If the provided object is not a Tool or Toolset
204
        """
205
        new_tools = []
1✔
206

207
        if isinstance(tool, Tool):
1✔
208
            new_tools = [tool]
1✔
209
        elif isinstance(tool, Toolset):
×
210
            new_tools = list(tool)
×
211
        else:
212
            raise TypeError(f"Expected Tool or Toolset, got {type(tool).__name__}")
×
213

214
        # Check for duplicates before adding
215
        combined_tools = self.tools + new_tools
1✔
216
        _check_duplicate_tool_names(combined_tools)
1✔
217

218
        self.tools.extend(new_tools)
1✔
219

220
    def to_dict(self) -> dict[str, Any]:
1✔
221
        """
222
        Serialize the Toolset to a dictionary.
223

224
        :returns: A dictionary representation of the Toolset
225

226
        Note for subclass implementers:
227
        The default implementation is ideal for scenarios where Tool resolution is static. However, if your subclass
228
        of Toolset dynamically resolves Tool instances from external sources—such as an MCP server, OpenAPI URL, or
229
        a local OpenAPI specification—you should consider serializing the endpoint descriptor instead of the Tool
230
        instances themselves. This strategy preserves the dynamic nature of your Toolset and minimizes the overhead
231
        associated with serializing potentially large collections of Tool objects. Moreover, by serializing the
232
        descriptor, you ensure that the deserialization process can accurately reconstruct the Tool instances, even
233
        if they have been modified or removed since the last serialization. Failing to serialize the descriptor may
234
        lead to issues where outdated or incorrect Tool configurations are loaded, potentially causing errors or
235
        unexpected behavior.
236
        """
237
        return {
1✔
238
            "type": generate_qualified_class_name(type(self)),
239
            "data": {"tools": [tool.to_dict() for tool in self.tools]},
240
        }
241

242
    @classmethod
1✔
243
    def from_dict(cls, data: dict[str, Any]) -> "Toolset":
1✔
244
        """
245
        Deserialize a Toolset from a dictionary.
246

247
        :param data: Dictionary representation of the Toolset
248
        :returns: A new Toolset instance
249
        """
250
        inner_data = data["data"]
1✔
251
        tools_data = inner_data.get("tools", [])
1✔
252

253
        tools = []
1✔
254
        for tool_data in tools_data:
1✔
255
            tool_class = import_class_by_name(tool_data["type"])
1✔
256
            if not issubclass(tool_class, Tool):
1✔
257
                raise TypeError(f"Class '{tool_class}' is not a subclass of Tool")
×
258
            tools.append(tool_class.from_dict(tool_data))
1✔
259

260
        return cls(tools=tools)
1✔
261

262
    def __add__(self, other: Union[Tool, "Toolset", list[Tool]]) -> "Toolset":
1✔
263
        """
264
        Concatenate this Toolset with another Tool, Toolset, or list of Tools.
265

266
        :param other: Another Tool, Toolset, or list of Tools to concatenate
267
        :returns: A new Toolset containing all tools
268
        :raises TypeError: If the other parameter is not a Tool, Toolset, or list of Tools
269
        :raises ValueError: If the combination would result in duplicate tool names
270
        """
271
        if isinstance(other, Tool):
1✔
272
            return Toolset(tools=self.tools + [other])
1✔
273
        if isinstance(other, Toolset):
1✔
274
            return _ToolsetWrapper([self, other])
1✔
275
        if isinstance(other, list) and all(isinstance(item, Tool) for item in other):
1✔
276
            return Toolset(tools=self.tools + other)
1✔
277
        raise TypeError(f"Cannot add {type(other).__name__} to Toolset")
1✔
278

279
    def __len__(self) -> int:
1✔
280
        """
281
        Return the number of Tools in this Toolset.
282

283
        :returns: Number of Tools
284
        """
285
        return len(self.tools)
1✔
286

287
    def __getitem__(self, index):
1✔
288
        """
289
        Get a Tool by index.
290

291
        :param index: Index of the Tool to get
292
        :returns: The Tool at the specified index
293
        """
294
        return self.tools[index]
1✔
295

296

297
class _ToolsetWrapper(Toolset):
1✔
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]):
1✔
306
        super().__init__([tool for toolset in toolsets for tool in toolset])
1✔
307
        self.toolsets = toolsets
1✔
308

309
    def __iter__(self):
1✔
310
        """Iterate over all tools from all toolsets."""
311
        for toolset in self.toolsets:
1✔
312
            yield from toolset
1✔
313

314
    def __contains__(self, item):
1✔
315
        """Check if a tool is in any of the toolsets."""
316
        return any(item in toolset for toolset in self.toolsets)
1✔
317

318
    def warm_up(self):
1✔
319
        """Warm up all toolsets."""
320
        for toolset in self.toolsets:
1✔
321
            toolset.warm_up()
1✔
322

323
    def __len__(self):
1✔
324
        """Return total number of tools across all toolsets."""
325
        return sum(len(toolset) for toolset in self.toolsets)
1✔
326

327
    def __getitem__(self, index):
1✔
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):
1✔
336
        """Add another toolset or tool to this wrapper."""
337
        if isinstance(other, Toolset):
1✔
338
            return _ToolsetWrapper(self.toolsets + [other])
1✔
339
        if isinstance(other, Tool):
1✔
340
            return _ToolsetWrapper(self.toolsets + [Toolset([other])])
1✔
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")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc