From 65a2e6e5b4e2bfcc1039433aa06e40d360296f26 Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Tue, 21 Oct 2025 16:07:29 +0200 Subject: [PATCH 1/5] Add integration test for mixing Tool/Toolset --- .../tests/test_nvidia_chat_generator.py | 220 +++++++++++++++++- 1 file changed, 219 insertions(+), 1 deletion(-) diff --git a/integrations/nvidia/tests/test_nvidia_chat_generator.py b/integrations/nvidia/tests/test_nvidia_chat_generator.py index 516fc2936f..9e2567390b 100644 --- a/integrations/nvidia/tests/test_nvidia_chat_generator.py +++ b/integrations/nvidia/tests/test_nvidia_chat_generator.py @@ -11,7 +11,7 @@ import pytz from haystack.components.generators.utils import print_streaming_chunk from haystack.dataclasses import ChatMessage, StreamingChunk -from haystack.tools import Tool +from haystack.tools import Tool, Toolset from haystack.utils.auth import Secret from openai import AsyncOpenAI, OpenAIError from openai.types.chat import ChatCompletion, ChatCompletionMessage @@ -329,6 +329,114 @@ def test_live_run_with_json_object(self): assert isinstance(output["rating"], int) assert "Inception" in output["title"] + @pytest.mark.skipif( + not os.environ.get("NVIDIA_API_KEY", None), + reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", + ) + @pytest.mark.integration + def test_integration_mixing_init_and_runtime_tools(self): + """Test mixing tools from init and runtime by passing tools to both __init__ and run().""" + + def weather_function(city: str) -> str: + """Get weather information for a city.""" + return f"Weather in {city}: 22°C, sunny" + + def time_function(city: str) -> str: + """Get current time in a city.""" + return f"Current time in {city}: 14:30" + + # Create tools + weather_tool = Tool( + name="weather", + description="Get weather information for a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=weather_function, + ) + + time_tool = Tool( + name="time", + description="Get current time in a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=time_function, + ) + + # Initialize with weather_tool + component = NvidiaChatGenerator(tools=[weather_tool]) + + # Pass time_tool at runtime - runtime tools should take precedence + messages = [ChatMessage.from_user("What's the time in Tokyo?")] + results = component.run(messages, tools=[time_tool]) + + assert len(results["replies"]) == 1 + message = results["replies"][0] + + # Should use time_tool since it was passed at runtime + assert message.tool_calls is not None + tool_call = message.tool_calls[0] + assert tool_call.tool_name == "time" + assert tool_call.arguments == {"city": "Tokyo"} + + @pytest.mark.skipif( + not os.environ.get("NVIDIA_API_KEY", None), + reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", + ) + @pytest.mark.integration + def test_integration_mixing_tools_and_toolset(self): + """Test mixing Tool list and Toolset at runtime.""" + + def weather_function(city: str) -> str: + """Get weather information for a city.""" + return f"Weather in {city}: 22°C, sunny" + + def time_function(city: str) -> str: + """Get current time in a city.""" + return f"Current time in {city}: 14:30" + + def echo_function(text: str) -> str: + """Echo a text.""" + return text + + # Create tools + weather_tool = Tool( + name="weather", + description="Get weather information for a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=weather_function, + ) + + time_tool = Tool( + name="time", + description="Get current time in a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=time_function, + ) + + echo_tool = Tool( + name="echo", + description="Echo a text", + parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}, + function=echo_function, + ) + + # Create Toolset with weather and time tools + toolset = Toolset([weather_tool, time_tool]) + + # Initialize with toolset + component = NvidiaChatGenerator(tools=toolset) + + # Pass echo_tool as a list at runtime - runtime tools should take precedence + messages = [ChatMessage.from_user("Echo this: Hello World")] + results = component.run(messages, tools=[echo_tool]) + + assert len(results["replies"]) == 1 + message = results["replies"][0] + + # Should use echo_tool since it was passed at runtime + assert message.tool_calls is not None + tool_call = message.tool_calls[0] + assert tool_call.tool_name == "echo" + assert tool_call.arguments == {"text": "Hello World"} + class TestNvidiaChatGeneratorAsync: def test_init_default_async(self, monkeypatch): @@ -439,3 +547,113 @@ async def callback(chunk: StreamingChunk): assert counter > 1 assert "Paris" in responses + + @pytest.mark.skipif( + not os.environ.get("NVIDIA_API_KEY", None), + reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", + ) + @pytest.mark.integration + @pytest.mark.asyncio + async def test_integration_mixing_init_and_runtime_tools_async(self): + """Test mixing tools from init and runtime in async mode.""" + + def weather_function(city: str) -> str: + """Get weather information for a city.""" + return f"Weather in {city}: 22°C, sunny" + + def time_function(city: str) -> str: + """Get current time in a city.""" + return f"Current time in {city}: 14:30" + + # Create tools + weather_tool = Tool( + name="weather", + description="Get weather information for a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=weather_function, + ) + + time_tool = Tool( + name="time", + description="Get current time in a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=time_function, + ) + + # Initialize with weather_tool + component = NvidiaChatGenerator(tools=[weather_tool]) + + # Pass time_tool at runtime - runtime tools should take precedence + messages = [ChatMessage.from_user("What's the time in Tokyo?")] + results = await component.run_async(messages, tools=[time_tool]) + + assert len(results["replies"]) == 1 + message = results["replies"][0] + + # Should use time_tool since it was passed at runtime + assert message.tool_calls is not None + tool_call = message.tool_calls[0] + assert tool_call.tool_name == "time" + assert tool_call.arguments == {"city": "Tokyo"} + + @pytest.mark.skipif( + not os.environ.get("NVIDIA_API_KEY", None), + reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", + ) + @pytest.mark.integration + @pytest.mark.asyncio + async def test_integration_mixing_tools_and_toolset_async(self): + """Test mixing Tool list and Toolset at runtime in async mode.""" + + def weather_function(city: str) -> str: + """Get weather information for a city.""" + return f"Weather in {city}: 22°C, sunny" + + def time_function(city: str) -> str: + """Get current time in a city.""" + return f"Current time in {city}: 14:30" + + def echo_function(text: str) -> str: + """Echo a text.""" + return text + + # Create tools + weather_tool = Tool( + name="weather", + description="Get weather information for a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=weather_function, + ) + + time_tool = Tool( + name="time", + description="Get current time in a city", + parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + function=time_function, + ) + + echo_tool = Tool( + name="echo", + description="Echo a text", + parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}, + function=echo_function, + ) + + # Create Toolset with weather and time tools + toolset = Toolset([weather_tool, time_tool]) + + # Initialize with toolset + component = NvidiaChatGenerator(tools=toolset) + + # Pass echo_tool as a list at runtime - runtime tools should take precedence + messages = [ChatMessage.from_user("Echo this: Hello World")] + results = await component.run_async(messages, tools=[echo_tool]) + + assert len(results["replies"]) == 1 + message = results["replies"][0] + + # Should use echo_tool since it was passed at runtime + assert message.tool_calls is not None + tool_call = message.tool_calls[0] + assert tool_call.tool_name == "echo" + assert tool_call.arguments == {"text": "Hello World"} From fcbf85b7e9a9e6ce8f02db3fa9a0debd9139b04e Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Wed, 22 Oct 2025 10:22:44 +0200 Subject: [PATCH 2/5] PR feedback --- .../tests/test_nvidia_chat_generator.py | 110 ++++++++++-------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/integrations/nvidia/tests/test_nvidia_chat_generator.py b/integrations/nvidia/tests/test_nvidia_chat_generator.py index 9e2567390b..c030d83860 100644 --- a/integrations/nvidia/tests/test_nvidia_chat_generator.py +++ b/integrations/nvidia/tests/test_nvidia_chat_generator.py @@ -34,6 +34,11 @@ def weather(city: str): return f"The weather in {city} is sunny and 32°C" +def echo_function(text: str) -> str: + """Echo a text.""" + return text + + @pytest.fixture def tools(): tool_parameters = { @@ -437,6 +442,63 @@ def echo_function(text: str) -> str: assert tool_call.tool_name == "echo" assert tool_call.arguments == {"text": "Hello World"} + def test_to_dict_with_mixed_tools_and_toolset(self, tools, monkeypatch): + """Test serialization with a mixed list containing both Tool and Toolset objects.""" + monkeypatch.setenv("NVIDIA_API_KEY", "test-api-key") + + # Create additional tools for the toolset using module-level function + echo_tool = Tool( + name="echo", + description="Echo a text", + parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}, + function=echo_function, + ) + + # Create a mixed list: some individual tools + a toolset + toolset = Toolset([echo_tool]) + mixed_tools = tools + [toolset] # List containing both Tool objects and a Toolset + + component = NvidiaChatGenerator(model="meta/llama-3.1-8b-instruct", tools=mixed_tools) + data = component.to_dict() + + assert data["init_parameters"]["tools"] is not None + assert isinstance(data["init_parameters"]["tools"], list) + assert len(data["init_parameters"]["tools"]) == len(mixed_tools) + + # Check that we have both Tool and Toolset in the serialized data + tool_types = [tool["type"] for tool in data["init_parameters"]["tools"]] + assert "haystack.tools.tool.Tool" in tool_types + assert "haystack.tools.toolset.Toolset" in tool_types + + def test_from_dict_with_mixed_tools_and_toolset(self, tools, monkeypatch): + """Test deserialization with a mixed list containing both Tool and Toolset objects.""" + monkeypatch.setenv("NVIDIA_API_KEY", "test-api-key") + + # Create additional tools for the toolset using module-level function + echo_tool = Tool( + name="echo", + description="Echo a text", + parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}, + function=echo_function, + ) + + # Create a mixed list: some individual tools + a toolset + toolset = Toolset([echo_tool]) + mixed_tools = tools + [toolset] # List containing both Tool objects and a Toolset + + component = NvidiaChatGenerator(model="meta/llama-3.1-8b-instruct", tools=mixed_tools) + data = component.to_dict() + + deserialized_component = NvidiaChatGenerator.from_dict(data) + + assert isinstance(deserialized_component.tools, list) + assert len(deserialized_component.tools) == len(mixed_tools) + + # Check that we have both Tool and Toolset objects in the deserialized list + tool_types = [type(tool).__name__ for tool in deserialized_component.tools] + assert "Tool" in tool_types + assert "Toolset" in tool_types + class TestNvidiaChatGeneratorAsync: def test_init_default_async(self, monkeypatch): @@ -548,54 +610,6 @@ async def callback(chunk: StreamingChunk): assert counter > 1 assert "Paris" in responses - @pytest.mark.skipif( - not os.environ.get("NVIDIA_API_KEY", None), - reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", - ) - @pytest.mark.integration - @pytest.mark.asyncio - async def test_integration_mixing_init_and_runtime_tools_async(self): - """Test mixing tools from init and runtime in async mode.""" - - def weather_function(city: str) -> str: - """Get weather information for a city.""" - return f"Weather in {city}: 22°C, sunny" - - def time_function(city: str) -> str: - """Get current time in a city.""" - return f"Current time in {city}: 14:30" - - # Create tools - weather_tool = Tool( - name="weather", - description="Get weather information for a city", - parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, - function=weather_function, - ) - - time_tool = Tool( - name="time", - description="Get current time in a city", - parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, - function=time_function, - ) - - # Initialize with weather_tool - component = NvidiaChatGenerator(tools=[weather_tool]) - - # Pass time_tool at runtime - runtime tools should take precedence - messages = [ChatMessage.from_user("What's the time in Tokyo?")] - results = await component.run_async(messages, tools=[time_tool]) - - assert len(results["replies"]) == 1 - message = results["replies"][0] - - # Should use time_tool since it was passed at runtime - assert message.tool_calls is not None - tool_call = message.tool_calls[0] - assert tool_call.tool_name == "time" - assert tool_call.arguments == {"city": "Tokyo"} - @pytest.mark.skipif( not os.environ.get("NVIDIA_API_KEY", None), reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", From 779d09a95aaf013f15d118442a25c9081b0c170a Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Wed, 22 Oct 2025 10:27:35 +0200 Subject: [PATCH 3/5] Lint --- integrations/nvidia/tests/test_nvidia_chat_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/nvidia/tests/test_nvidia_chat_generator.py b/integrations/nvidia/tests/test_nvidia_chat_generator.py index c030d83860..416dbc8b3d 100644 --- a/integrations/nvidia/tests/test_nvidia_chat_generator.py +++ b/integrations/nvidia/tests/test_nvidia_chat_generator.py @@ -456,7 +456,7 @@ def test_to_dict_with_mixed_tools_and_toolset(self, tools, monkeypatch): # Create a mixed list: some individual tools + a toolset toolset = Toolset([echo_tool]) - mixed_tools = tools + [toolset] # List containing both Tool objects and a Toolset + mixed_tools = [*tools, toolset] # List containing both Tool objects and a Toolset component = NvidiaChatGenerator(model="meta/llama-3.1-8b-instruct", tools=mixed_tools) data = component.to_dict() @@ -484,7 +484,7 @@ def test_from_dict_with_mixed_tools_and_toolset(self, tools, monkeypatch): # Create a mixed list: some individual tools + a toolset toolset = Toolset([echo_tool]) - mixed_tools = tools + [toolset] # List containing both Tool objects and a Toolset + mixed_tools = [*tools, toolset] # List containing both Tool objects and a Toolset component = NvidiaChatGenerator(model="meta/llama-3.1-8b-instruct", tools=mixed_tools) data = component.to_dict() From 2b52e26ea7b3a41788c8eab38796ecf4d678b8e0 Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Wed, 22 Oct 2025 11:11:41 +0200 Subject: [PATCH 4/5] PR feedback --- .../tests/test_nvidia_chat_generator.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/integrations/nvidia/tests/test_nvidia_chat_generator.py b/integrations/nvidia/tests/test_nvidia_chat_generator.py index 416dbc8b3d..2e138c9fdb 100644 --- a/integrations/nvidia/tests/test_nvidia_chat_generator.py +++ b/integrations/nvidia/tests/test_nvidia_chat_generator.py @@ -426,21 +426,23 @@ def echo_function(text: str) -> str: # Create Toolset with weather and time tools toolset = Toolset([weather_tool, time_tool]) - # Initialize with toolset - component = NvidiaChatGenerator(tools=toolset) + # Initialize without tools + component = NvidiaChatGenerator() - # Pass echo_tool as a list at runtime - runtime tools should take precedence - messages = [ChatMessage.from_user("Echo this: Hello World")] - results = component.run(messages, tools=[echo_tool]) + # Mix tools and toolset at runtime + messages = [ChatMessage.from_user("What's the weather in Tokyo and echo 'test'")] + results = component.run(messages, tools=[echo_tool, toolset]) assert len(results["replies"]) == 1 message = results["replies"][0] - # Should use echo_tool since it was passed at runtime + # Should have access to both echo tool and tools from toolset assert message.tool_calls is not None - tool_call = message.tool_calls[0] - assert tool_call.tool_name == "echo" - assert tool_call.arguments == {"text": "Hello World"} + assert len(message.tool_calls) >= 1 + + # Check that we can use tools from both the list and toolset + tool_names = [call.tool_name for call in message.tool_calls] + assert "echo" in tool_names or "weather" in tool_names def test_to_dict_with_mixed_tools_and_toolset(self, tools, monkeypatch): """Test serialization with a mixed list containing both Tool and Toolset objects.""" @@ -656,18 +658,20 @@ def echo_function(text: str) -> str: # Create Toolset with weather and time tools toolset = Toolset([weather_tool, time_tool]) - # Initialize with toolset - component = NvidiaChatGenerator(tools=toolset) + # Initialize without tools + component = NvidiaChatGenerator() - # Pass echo_tool as a list at runtime - runtime tools should take precedence - messages = [ChatMessage.from_user("Echo this: Hello World")] - results = await component.run_async(messages, tools=[echo_tool]) + # Mix tools and toolset at runtime + messages = [ChatMessage.from_user("What's the weather in Tokyo and echo 'test'")] + results = await component.run_async(messages, tools=[echo_tool, toolset]) assert len(results["replies"]) == 1 message = results["replies"][0] - # Should use echo_tool since it was passed at runtime + # Should have access to both echo tool and tools from toolset assert message.tool_calls is not None - tool_call = message.tool_calls[0] - assert tool_call.tool_name == "echo" - assert tool_call.arguments == {"text": "Hello World"} + assert len(message.tool_calls) >= 1 + + # Check that we can use tools from both the list and toolset + tool_names = [call.tool_name for call in message.tool_calls] + assert "echo" in tool_names or "weather" in tool_names From 5a3c68d3e520480dda9ae78d75723b0f818c9154 Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Wed, 22 Oct 2025 14:30:28 +0200 Subject: [PATCH 5/5] Remove test --- .../tests/test_nvidia_chat_generator.py | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/integrations/nvidia/tests/test_nvidia_chat_generator.py b/integrations/nvidia/tests/test_nvidia_chat_generator.py index 2e138c9fdb..e5ea5ce340 100644 --- a/integrations/nvidia/tests/test_nvidia_chat_generator.py +++ b/integrations/nvidia/tests/test_nvidia_chat_generator.py @@ -334,53 +334,6 @@ def test_live_run_with_json_object(self): assert isinstance(output["rating"], int) assert "Inception" in output["title"] - @pytest.mark.skipif( - not os.environ.get("NVIDIA_API_KEY", None), - reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.", - ) - @pytest.mark.integration - def test_integration_mixing_init_and_runtime_tools(self): - """Test mixing tools from init and runtime by passing tools to both __init__ and run().""" - - def weather_function(city: str) -> str: - """Get weather information for a city.""" - return f"Weather in {city}: 22°C, sunny" - - def time_function(city: str) -> str: - """Get current time in a city.""" - return f"Current time in {city}: 14:30" - - # Create tools - weather_tool = Tool( - name="weather", - description="Get weather information for a city", - parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, - function=weather_function, - ) - - time_tool = Tool( - name="time", - description="Get current time in a city", - parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, - function=time_function, - ) - - # Initialize with weather_tool - component = NvidiaChatGenerator(tools=[weather_tool]) - - # Pass time_tool at runtime - runtime tools should take precedence - messages = [ChatMessage.from_user("What's the time in Tokyo?")] - results = component.run(messages, tools=[time_tool]) - - assert len(results["replies"]) == 1 - message = results["replies"][0] - - # Should use time_tool since it was passed at runtime - assert message.tool_calls is not None - tool_call = message.tool_calls[0] - assert tool_call.tool_name == "time" - assert tool_call.arguments == {"city": "Tokyo"} - @pytest.mark.skipif( not os.environ.get("NVIDIA_API_KEY", None), reason="Export an env var called NVIDIA_API_KEY containing the NVIDIA API key to run this test.",