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