/ test / tools / test_toolset.py
test_toolset.py
  1  # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
  2  #
  3  # SPDX-License-Identifier: Apache-2.0
  4  
  5  import pytest
  6  
  7  from haystack import Pipeline
  8  from haystack.components.tools import ToolInvoker
  9  from haystack.core.serialization import generate_qualified_class_name
 10  from haystack.dataclasses import ChatMessage
 11  from haystack.dataclasses.chat_message import ToolCall
 12  from haystack.tools import Tool, Toolset
 13  from haystack.tools.errors import ToolInvocationError
 14  
 15  
 16  # Common functions for tests
 17  def add_numbers(a: int, b: int) -> int:
 18      """Add two numbers."""
 19      return a + b
 20  
 21  
 22  def multiply_numbers(a: int, b: int) -> int:
 23      """Multiply two numbers."""
 24      return a * b
 25  
 26  
 27  def subtract_numbers(a: int, b: int) -> int:
 28      """Subtract b from a."""
 29      return a - b
 30  
 31  
 32  class CustomToolset(Toolset):
 33      def __init__(self, tools, custom_attr):
 34          super().__init__(tools)
 35          self.custom_attr = custom_attr
 36  
 37      def to_dict(self):
 38          data = super().to_dict()
 39          data["custom_attr"] = self.custom_attr
 40          return data
 41  
 42      @classmethod
 43      def from_dict(cls, data):
 44          tools = [Tool.from_dict(tool_data) for tool_data in data["data"]["tools"]]
 45          custom_attr = data["custom_attr"]
 46          return cls(tools=tools, custom_attr=custom_attr)
 47  
 48  
 49  class CalculatorToolset(Toolset):
 50      """A toolset for calculator operations."""
 51  
 52      def __init__(self):
 53          super().__init__([])
 54          self._create_tools()
 55  
 56      def _create_tools(self):
 57          add_tool = Tool(
 58              name="add",
 59              description="Add two numbers",
 60              parameters={
 61                  "type": "object",
 62                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
 63                  "required": ["a", "b"],
 64              },
 65              function=add_numbers,
 66          )
 67  
 68          multiply_tool = Tool(
 69              name="multiply",
 70              description="Multiply two numbers",
 71              parameters={
 72                  "type": "object",
 73                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
 74                  "required": ["a", "b"],
 75              },
 76              function=multiply_numbers,
 77          )
 78  
 79          self.add(add_tool)
 80          self.add(multiply_tool)
 81  
 82      def to_dict(self):
 83          return {
 84              "type": generate_qualified_class_name(type(self)),
 85              "data": {},  # no data to serialize as we define the tools dynamically
 86          }
 87  
 88      @classmethod
 89      def from_dict(cls, data):
 90          return cls()
 91  
 92  
 93  def weather_function(location):
 94      weather_info = {
 95          "Berlin": {"weather": "mostly sunny", "temperature": 7, "unit": "celsius"},
 96          "Paris": {"weather": "mostly cloudy", "temperature": 8, "unit": "celsius"},
 97          "Rome": {"weather": "sunny", "temperature": 14, "unit": "celsius"},
 98      }
 99      return weather_info.get(location, {"weather": "unknown", "temperature": 0, "unit": "celsius"})
100  
101  
102  weather_parameters = {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}
103  
104  
105  @pytest.fixture
106  def weather_tool():
107      return Tool(
108          name="weather_tool",
109          description="Provides weather information for a given location.",
110          parameters=weather_parameters,
111          function=weather_function,
112      )
113  
114  
115  @pytest.fixture
116  def faulty_tool():
117      def faulty_tool_func(location):
118          raise Exception("This tool always fails.")
119  
120      faulty_tool_parameters = {
121          "type": "object",
122          "properties": {"location": {"type": "string"}},
123          "required": ["location"],
124      }
125  
126      return Tool(
127          name="faulty_tool",
128          description="A tool that always fails when invoked.",
129          parameters=faulty_tool_parameters,
130          function=faulty_tool_func,
131      )
132  
133  
134  class TestToolset:
135      def test_toolset_with_multiple_tools(self):
136          """Test that a Toolset with multiple tools works properly."""
137          add_tool = Tool(
138              name="add",
139              description="Add two numbers",
140              parameters={
141                  "type": "object",
142                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
143                  "required": ["a", "b"],
144              },
145              function=add_numbers,
146          )
147  
148          multiply_tool = Tool(
149              name="multiply",
150              description="Multiply two numbers",
151              parameters={
152                  "type": "object",
153                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
154                  "required": ["a", "b"],
155              },
156              function=multiply_numbers,
157          )
158  
159          toolset = Toolset([add_tool, multiply_tool])
160  
161          assert len(toolset) == 2
162          assert toolset[0].name == "add"
163          assert toolset[1].name == "multiply"
164  
165          invoker = ToolInvoker(tools=toolset)
166  
167          add_call = ToolCall(tool_name="add", arguments={"a": 2, "b": 3})
168          add_message = ChatMessage.from_assistant(tool_calls=[add_call])
169  
170          multiply_call = ToolCall(tool_name="multiply", arguments={"a": 4, "b": 5})
171          multiply_message = ChatMessage.from_assistant(tool_calls=[multiply_call])
172  
173          result = invoker.run(messages=[add_message, multiply_message])
174  
175          assert len(result["tool_messages"]) == 2
176          tool_results = [message.tool_call_result.result for message in result["tool_messages"]]
177          assert "5" in tool_results
178          assert "20" in tool_results
179  
180      def test_toolset_adding(self):
181          """Test that tools can be added to a Toolset."""
182          toolset = Toolset()
183          assert len(toolset) == 0
184  
185          add_tool = Tool(
186              name="add",
187              description="Add two numbers",
188              parameters={
189                  "type": "object",
190                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
191                  "required": ["a", "b"],
192              },
193              function=add_numbers,
194          )
195  
196          toolset.add(add_tool)
197          assert len(toolset) == 1
198          assert toolset[0].name == "add"
199  
200          invoker = ToolInvoker(tools=toolset)
201          tool_call = ToolCall(tool_name="add", arguments={"a": 2, "b": 3})
202          message = ChatMessage.from_assistant(tool_calls=[tool_call])
203          result = invoker.run(messages=[message])
204  
205          assert len(result["tool_messages"]) == 1
206          assert result["tool_messages"][0].tool_call_result.result == "5"
207  
208      def test_toolset_addition(self):
209          """Test that toolsets can be combined."""
210          add_tool = Tool(
211              name="add",
212              description="Add two numbers",
213              parameters={
214                  "type": "object",
215                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
216                  "required": ["a", "b"],
217              },
218              function=add_numbers,
219          )
220  
221          multiply_tool = Tool(
222              name="multiply",
223              description="Multiply two numbers",
224              parameters={
225                  "type": "object",
226                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
227                  "required": ["a", "b"],
228              },
229              function=multiply_numbers,
230          )
231  
232          subtract_tool = Tool(
233              name="subtract",
234              description="Subtract two numbers",
235              parameters={
236                  "type": "object",
237                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
238                  "required": ["a", "b"],
239              },
240              function=subtract_numbers,
241          )
242  
243          toolset1 = Toolset([add_tool])
244          toolset2 = Toolset([multiply_tool])
245  
246          combined_toolset = toolset1 + toolset2
247          assert len(combined_toolset) == 2
248  
249          combined_toolset = combined_toolset + subtract_tool
250          assert len(combined_toolset) == 3
251  
252          tool_names = [tool.name for tool in combined_toolset]
253          assert "add" in tool_names
254          assert "multiply" in tool_names
255          assert "subtract" in tool_names
256  
257          invoker = ToolInvoker(tools=combined_toolset)
258  
259          add_call = ToolCall(tool_name="add", arguments={"a": 10, "b": 5})
260          multiply_call = ToolCall(tool_name="multiply", arguments={"a": 10, "b": 5})
261          subtract_call = ToolCall(tool_name="subtract", arguments={"a": 10, "b": 5})
262  
263          message = ChatMessage.from_assistant(tool_calls=[add_call, multiply_call, subtract_call])
264  
265          result = invoker.run(messages=[message])
266  
267          assert len(result["tool_messages"]) == 3
268          tool_results = [message.tool_call_result.result for message in result["tool_messages"]]
269          assert "15" in tool_results
270          assert "50" in tool_results
271          assert "5" in tool_results
272  
273      def test_toolset_contains(self):
274          """Test that the __contains__ method works correctly."""
275          add_tool = Tool(
276              name="add",
277              description="Add two numbers",
278              parameters={
279                  "type": "object",
280                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
281                  "required": ["a", "b"],
282              },
283              function=add_numbers,
284          )
285  
286          multiply_tool = Tool(
287              name="multiply",
288              description="Multiply two numbers",
289              parameters={
290                  "type": "object",
291                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
292                  "required": ["a", "b"],
293              },
294              function=multiply_numbers,
295          )
296  
297          toolset = Toolset([add_tool])
298  
299          # Test with a tool instance
300          assert add_tool in toolset
301          assert multiply_tool not in toolset
302  
303          # Test with a tool name
304          assert "add" in toolset
305          assert "multiply" not in toolset
306          assert "non_existent_tool" not in toolset
307  
308      def test_toolset_add_various_types(self):
309          """Test that the __add__ method works with various object types."""
310          add_tool = Tool(
311              name="add",
312              description="Add two numbers",
313              parameters={
314                  "type": "object",
315                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
316                  "required": ["a", "b"],
317              },
318              function=add_numbers,
319          )
320  
321          multiply_tool = Tool(
322              name="multiply",
323              description="Multiply two numbers",
324              parameters={
325                  "type": "object",
326                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
327                  "required": ["a", "b"],
328              },
329              function=multiply_numbers,
330          )
331  
332          subtract_tool = Tool(
333              name="subtract",
334              description="Subtract two numbers",
335              parameters={
336                  "type": "object",
337                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
338                  "required": ["a", "b"],
339              },
340              function=subtract_numbers,
341          )
342  
343          # Test adding a single tool
344          toolset1 = Toolset([add_tool])
345          result1 = toolset1 + multiply_tool
346          assert len(result1) == 2
347          assert add_tool in result1
348          assert multiply_tool in result1
349  
350          # Test adding another toolset
351          toolset2 = Toolset([subtract_tool])
352          result2 = toolset1 + toolset2
353          assert len(result2) == 2
354          assert add_tool in result2
355          assert subtract_tool in result2
356  
357          # Test adding a list of tools
358          result3 = toolset1 + [multiply_tool, subtract_tool]
359          assert len(result3) == 3
360          assert add_tool in result3
361          assert multiply_tool in result3
362          assert subtract_tool in result3
363  
364          # Test adding types that aren't supported
365          with pytest.raises(TypeError):
366              toolset1 + "not_a_tool"
367  
368          with pytest.raises(TypeError):
369              toolset1 + 123
370  
371      def test_toolset_serialization(self):
372          """Test that a Toolset can be serialized and deserialized."""
373          add_tool = Tool(
374              name="add",
375              description="Add two numbers",
376              parameters={
377                  "type": "object",
378                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
379                  "required": ["a", "b"],
380              },
381              function=add_numbers,
382          )
383  
384          toolset = Toolset([add_tool])
385  
386          serialized = toolset.to_dict()
387  
388          deserialized = Toolset.from_dict(serialized)
389  
390          assert len(deserialized) == 1
391          assert deserialized[0].name == "add"
392          assert deserialized[0].description == "Add two numbers"
393  
394          invoker = ToolInvoker(tools=deserialized)
395          tool_call = ToolCall(tool_name="add", arguments={"a": 2, "b": 3})
396          message = ChatMessage.from_assistant(tool_calls=[tool_call])
397          result = invoker.run(messages=[message])
398  
399          assert len(result["tool_messages"]) == 1
400          assert result["tool_messages"][0].tool_call_result.result == "5"
401  
402      def test_custom_toolset_serialization(self):
403          """Test serialization and deserialization of a custom Toolset subclass."""
404          add_tool = Tool(
405              name="add",
406              description="Add two numbers",
407              parameters={
408                  "type": "object",
409                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
410                  "required": ["a", "b"],
411              },
412              function=add_numbers,
413          )
414  
415          custom_attr_value = "custom_value"
416          custom_toolset = CustomToolset(tools=[add_tool], custom_attr=custom_attr_value)
417  
418          serialized = custom_toolset.to_dict()
419          assert serialized["type"].endswith("CustomToolset")
420          assert serialized["custom_attr"] == custom_attr_value
421          assert len(serialized["data"]["tools"]) == 1
422          assert serialized["data"]["tools"][0]["data"]["name"] == "add"
423  
424          deserialized = CustomToolset.from_dict(serialized)
425          assert isinstance(deserialized, CustomToolset)
426          assert deserialized.custom_attr == custom_attr_value
427          assert len(deserialized) == 1
428          assert deserialized[0].name == "add"
429  
430          invoker = ToolInvoker(tools=deserialized)
431          tool_call = ToolCall(tool_name="add", arguments={"a": 2, "b": 3})
432          message = ChatMessage.from_assistant(tool_calls=[tool_call])
433          result = invoker.run(messages=[message])
434  
435          assert len(result["tool_messages"]) == 1
436          assert result["tool_messages"][0].tool_call_result.result == "5"
437  
438      def test_toolset_duplicate_tool_names(self):
439          """Test that a Toolset raises an error for duplicate tool names."""
440          add_tool1 = Tool(
441              name="add",
442              description="Add two numbers (first)",
443              parameters={
444                  "type": "object",
445                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
446                  "required": ["a", "b"],
447              },
448              function=add_numbers,
449          )
450  
451          add_tool2 = Tool(
452              name="add",
453              description="Add two numbers (second)",
454              parameters={
455                  "type": "object",
456                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
457                  "required": ["a", "b"],
458              },
459              function=add_numbers,
460          )
461  
462          with pytest.raises(ValueError, match="Duplicate tool names found"):
463              Toolset([add_tool1, add_tool2])
464  
465          toolset = Toolset([add_tool1])
466  
467          with pytest.raises(ValueError, match="Duplicate tool names found"):
468              toolset.add(add_tool2)
469  
470          toolset2 = Toolset([add_tool2])
471          with pytest.raises(ValueError, match="Duplicate tool names found"):
472              _ = toolset + toolset2
473  
474  
475  class TestToolsetIntegration:
476      """Integration tests for Toolset in complete pipelines."""
477  
478      def test_custom_toolset_serde_in_pipeline(self):
479          """Test serialization and deserialization of a custom toolset within a pipeline."""
480  
481          pipeline = Pipeline()
482          pipeline.add_component("tool_invoker", ToolInvoker(tools=CalculatorToolset()))
483  
484          pipeline_dict = pipeline.to_dict()
485  
486          tool_invoker_dict = pipeline_dict["components"]["tool_invoker"]
487          assert tool_invoker_dict["type"] == "haystack.components.tools.tool_invoker.ToolInvoker"
488          assert len(tool_invoker_dict["init_parameters"]["tools"]["data"]) == 0
489  
490          new_pipeline = Pipeline.from_dict(pipeline_dict)
491          assert new_pipeline == pipeline
492  
493      def test_regular_toolset_serde_in_pipeline(self):
494          """Test serialization and deserialization of regular Toolsets within a pipeline."""
495  
496          add_tool = Tool(
497              name="add",
498              description="Add two numbers",
499              parameters={
500                  "type": "object",
501                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
502                  "required": ["a", "b"],
503              },
504              function=add_numbers,
505          )
506  
507          multiply_tool = Tool(
508              name="multiply",
509              description="Multiply two numbers",
510              parameters={
511                  "type": "object",
512                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
513                  "required": ["a", "b"],
514              },
515              function=multiply_numbers,
516          )
517  
518          toolset = Toolset([add_tool, multiply_tool])
519  
520          pipeline = Pipeline()
521          pipeline.add_component("tool_invoker", ToolInvoker(tools=toolset))
522  
523          pipeline_dict = pipeline.to_dict()
524  
525          tool_invoker_dict = pipeline_dict["components"]["tool_invoker"]
526          assert tool_invoker_dict["type"] == "haystack.components.tools.tool_invoker.ToolInvoker"
527  
528          # Verify the serialized toolset
529          tools_dict = tool_invoker_dict["init_parameters"]["tools"]
530          assert tools_dict["type"] == "haystack.tools.toolset.Toolset"
531          assert len(tools_dict["data"]["tools"]) == 2
532          tool_names = [tool["data"]["name"] for tool in tools_dict["data"]["tools"]]
533          assert "add" in tool_names
534          assert "multiply" in tool_names
535  
536          # Deserialize and verify
537          new_pipeline = Pipeline.from_dict(pipeline_dict)
538          assert new_pipeline == pipeline
539  
540  
541  class TestToolsetWithToolInvoker:
542      def test_init_with_toolset(self, weather_tool):
543          """Test initializing ToolInvoker with a Toolset."""
544          toolset = Toolset(tools=[weather_tool])
545          invoker = ToolInvoker(tools=toolset)
546          assert invoker.tools == toolset
547          assert invoker._tools_with_names == {tool.name: tool for tool in toolset}
548  
549      def test_serde_with_toolset(self, weather_tool):
550          """Test serialization and deserialization of ToolInvoker with a Toolset."""
551          toolset = Toolset(tools=[weather_tool])
552          invoker = ToolInvoker(tools=toolset)
553          data = invoker.to_dict()
554          deserialized_invoker = ToolInvoker.from_dict(data)
555          assert deserialized_invoker.tools == invoker.tools
556          assert deserialized_invoker._tools_with_names == invoker._tools_with_names
557          assert deserialized_invoker.raise_on_failure == invoker.raise_on_failure
558          assert deserialized_invoker.convert_result_to_json_string == invoker.convert_result_to_json_string
559  
560      def test_tool_invocation_error_with_toolset(self, faulty_tool):
561          """Test tool invocation errors with a Toolset."""
562          toolset = Toolset(tools=[faulty_tool])
563          invoker = ToolInvoker(tools=toolset)
564          tool_call = ToolCall(tool_name="faulty_tool", arguments={"location": "Berlin"})
565          tool_call_message = ChatMessage.from_assistant(tool_calls=[tool_call])
566          with pytest.raises(ToolInvocationError):
567              invoker.run(messages=[tool_call_message])
568  
569      def test_toolinvoker_deserialization_with_custom_toolset(self, weather_tool):
570          """Test deserialization of ToolInvoker with a custom Toolset."""
571          custom_toolset = CustomToolset(tools=[weather_tool], custom_attr="custom_value")
572          invoker = ToolInvoker(tools=custom_toolset)
573          data = invoker.to_dict()
574  
575          assert isinstance(data, dict)
576          assert "type" in data and "init_parameters" in data
577          tools_data = data["init_parameters"]["tools"]
578          assert isinstance(tools_data, dict)
579          assert len(tools_data["data"]["tools"]) == 1
580          assert tools_data["data"]["tools"][0]["type"] == "haystack.tools.tool.Tool"
581          assert tools_data.get("custom_attr") == "custom_value"
582  
583          deserialized_invoker = ToolInvoker.from_dict(data)
584          assert deserialized_invoker.tools == invoker.tools
585          assert deserialized_invoker._tools_with_names == invoker._tools_with_names
586          assert deserialized_invoker.raise_on_failure == invoker.raise_on_failure
587          assert deserialized_invoker.convert_result_to_json_string == invoker.convert_result_to_json_string
588  
589  
590  class TestToolsetList:
591      """Tests for list[Toolset] functionality."""
592  
593      def test_tool_invoker_with_list_of_toolsets(self, weather_tool):
594          """Test that ToolInvoker can be initialized with a mixed list of Tools and Toolsets."""
595          add_tool = Tool(
596              name="add",
597              description="Add two numbers",
598              parameters={
599                  "type": "object",
600                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
601                  "required": ["a", "b"],
602              },
603              function=add_numbers,
604          )
605          multiply_tool = Tool(
606              name="multiply",
607              description="Multiply two numbers",
608              parameters={
609                  "type": "object",
610                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
611                  "required": ["a", "b"],
612              },
613              function=multiply_numbers,
614          )
615  
616          toolset1 = Toolset([weather_tool])
617          # Mix: Toolset, standalone Tool, another Toolset
618          toolset2 = Toolset([add_tool])
619  
620          invoker = ToolInvoker(tools=[toolset1, multiply_tool, toolset2])
621  
622          # Verify tools are flattened internally
623          assert "weather_tool" in invoker._tools_with_names
624          assert "multiply" in invoker._tools_with_names
625          assert "add" in invoker._tools_with_names
626          assert len(invoker._tools_with_names) == 3
627  
628      def test_tool_invoker_run_with_list_of_toolsets(self, weather_tool):
629          """Test running ToolInvoker with a list of Toolsets."""
630          add_tool = Tool(
631              name="add",
632              description="Add two numbers",
633              parameters={
634                  "type": "object",
635                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
636                  "required": ["a", "b"],
637              },
638              function=add_numbers,
639          )
640  
641          toolset1 = Toolset([weather_tool])
642          toolset2 = Toolset([add_tool])
643  
644          invoker = ToolInvoker(tools=[toolset1, toolset2])
645  
646          # Call both tools
647          weather_call = ToolCall(tool_name="weather_tool", arguments={"location": "Berlin"})
648          add_call = ToolCall(tool_name="add", arguments={"a": 5, "b": 3})
649          message = ChatMessage.from_assistant(tool_calls=[weather_call, add_call])
650  
651          result = invoker.run(messages=[message])
652  
653          assert len(result["tool_messages"]) == 2
654          assert "mostly sunny" in result["tool_messages"][0].tool_call_result.result
655          assert "8" in result["tool_messages"][1].tool_call_result.result
656  
657      def test_tool_invoker_serde_with_list_of_toolsets(self, weather_tool):
658          """Test serialization and deserialization of ToolInvoker with a list of Toolsets."""
659          add_tool = Tool(
660              name="add",
661              description="Add two numbers",
662              parameters={
663                  "type": "object",
664                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
665                  "required": ["a", "b"],
666              },
667              function=add_numbers,
668          )
669  
670          toolset1 = Toolset([weather_tool])
671          toolset2 = Toolset([add_tool])
672  
673          invoker = ToolInvoker(tools=[toolset1, toolset2])
674          data = invoker.to_dict()
675  
676          # Verify serialization preserves list[Toolset] structure
677          tools_data = data["init_parameters"]["tools"]
678          assert isinstance(tools_data, list)
679          assert len(tools_data) == 2
680          assert all(isinstance(ts, dict) for ts in tools_data)
681          assert tools_data[0]["type"] == "haystack.tools.toolset.Toolset"
682          assert tools_data[1]["type"] == "haystack.tools.toolset.Toolset"
683  
684          # Deserialize and verify
685          deserialized_invoker = ToolInvoker.from_dict(data)
686          assert isinstance(deserialized_invoker.tools, list)
687          assert len(deserialized_invoker.tools) == 2
688          assert all(isinstance(ts, Toolset) for ts in deserialized_invoker.tools)
689          assert deserialized_invoker._tools_with_names == invoker._tools_with_names
690  
691      def test_pipeline_with_list_of_toolsets(self):
692          """Test that a Pipeline can serialize/deserialize with a list of Toolsets."""
693          add_tool = Tool(
694              name="add",
695              description="Add two numbers",
696              parameters={
697                  "type": "object",
698                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
699                  "required": ["a", "b"],
700              },
701              function=add_numbers,
702          )
703  
704          multiply_tool = Tool(
705              name="multiply",
706              description="Multiply two numbers",
707              parameters={
708                  "type": "object",
709                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
710                  "required": ["a", "b"],
711              },
712              function=multiply_numbers,
713          )
714  
715          toolset1 = Toolset([add_tool])
716          toolset2 = Toolset([multiply_tool])
717  
718          pipeline = Pipeline()
719          pipeline.add_component("tool_invoker", ToolInvoker(tools=[toolset1, toolset2]))
720  
721          pipeline_dict = pipeline.to_dict()
722  
723          # Verify the serialized structure
724          tool_invoker_dict = pipeline_dict["components"]["tool_invoker"]
725          tools_data = tool_invoker_dict["init_parameters"]["tools"]
726          assert isinstance(tools_data, list)
727          assert len(tools_data) == 2
728          assert all(ts["type"] == "haystack.tools.toolset.Toolset" for ts in tools_data)
729  
730          # Deserialize and verify functionality
731          new_pipeline = Pipeline.from_dict(pipeline_dict)
732          assert new_pipeline == pipeline
733  
734          # Run the pipeline to verify it works
735          tool_call = ToolCall(tool_name="add", arguments={"a": 10, "b": 5})
736          message = ChatMessage.from_assistant(tool_calls=[tool_call])
737          result = new_pipeline.run(data={"tool_invoker": {"messages": [message]}})
738  
739          assert "tool_invoker" in result
740          assert len(result["tool_invoker"]["tool_messages"]) == 1
741          assert "15" in result["tool_invoker"]["tool_messages"][0].tool_call_result.result
742  
743      def test_list_of_toolsets_runtime_override(self, weather_tool):
744          """Test that list of Toolsets can be passed as runtime override to ToolInvoker.run()."""
745          add_tool = Tool(
746              name="add",
747              description="Add two numbers",
748              parameters={
749                  "type": "object",
750                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
751                  "required": ["a", "b"],
752              },
753              function=add_numbers,
754          )
755  
756          multiply_tool = Tool(
757              name="multiply",
758              description="Multiply two numbers",
759              parameters={
760                  "type": "object",
761                  "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
762                  "required": ["a", "b"],
763              },
764              function=multiply_numbers,
765          )
766  
767          # Initialize with one toolset
768          toolset1 = Toolset([weather_tool])
769          invoker = ToolInvoker(tools=toolset1)
770  
771          # Override with a different list of toolsets at runtime
772          toolset2 = Toolset([add_tool])
773          toolset3 = Toolset([multiply_tool])
774  
775          add_call = ToolCall(tool_name="add", arguments={"a": 3, "b": 7})
776          message = ChatMessage.from_assistant(tool_calls=[add_call])
777  
778          result = invoker.run(messages=[message], tools=[toolset2, toolset3])
779  
780          assert len(result["tool_messages"]) == 1
781          assert "10" in result["tool_messages"][0].tool_call_result.result