From b71b6919d37a47536cae495aad83e03b980fc72f Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Tue, 21 Oct 2025 16:00:46 +0200 Subject: [PATCH 1/5] Add integration test for mixing Tool/Toolset --- .../tests/test_openrouter_chat_generator.py | 110 ++++++++++++++++- .../test_openrouter_chat_generator_async.py | 112 +++++++++++++++++- 2 files changed, 220 insertions(+), 2 deletions(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index af07dffa99..df0a2cf866 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -9,7 +9,7 @@ from haystack.components.generators.utils import print_streaming_chunk from haystack.components.tools import ToolInvoker from haystack.dataclasses import ChatMessage, ChatRole, StreamingChunk, ToolCall -from haystack.tools import Tool +from haystack.tools import Tool, Toolset from haystack.utils.auth import Secret from openai import OpenAIError from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage @@ -605,6 +605,114 @@ def test_live_run_with_response_format_pydantic_model(self, calendar_event_model assert isinstance(msg["event_date"], str) assert isinstance(msg["event_location"], str) + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter 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 = OpenRouterChatGenerator(tools=[weather_tool]) + + # Pass both tools 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("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter 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 = OpenRouterChatGenerator(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 TestChatCompletionChunkConversion: def test_handle_stream_response(self): diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator_async.py b/integrations/openrouter/tests/test_openrouter_chat_generator_async.py index e1a5320170..0803555982 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator_async.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator_async.py @@ -9,7 +9,7 @@ ChatRole, StreamingChunk, ) -from haystack.tools import Tool +from haystack.tools import Tool, Toolset from openai import AsyncOpenAI from openai.types.chat import ChatCompletion, ChatCompletionMessage from openai.types.chat.chat_completion import Choice @@ -262,3 +262,113 @@ async def callback(chunk: StreamingChunk): assert tool_call.tool_name == "weather" assert tool_call.arguments == {"city": "Paris"} assert tool_message.meta["finish_reason"] == "tool_calls" + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter 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 = OpenRouterChatGenerator(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("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter 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 = OpenRouterChatGenerator(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 2449b5f3cc30575f51f10075d499ed7816997e84 Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Wed, 22 Oct 2025 10:06:38 +0200 Subject: [PATCH 2/5] Update comment for clarity in test case --- integrations/openrouter/tests/test_openrouter_chat_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index df0a2cf866..b1b2d289f3 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -639,7 +639,7 @@ def time_function(city: str) -> str: # Initialize with weather_tool component = OpenRouterChatGenerator(tools=[weather_tool]) - # Pass both tools at runtime - runtime tools should take precedence + # Pass tools - runtime tools should take precedence messages = [ChatMessage.from_user("What's the time in Tokyo?")] results = component.run(messages, tools=[time_tool]) From 8c7ae410bf555e9ad64cadbc14b537fbfd7e9841 Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Wed, 22 Oct 2025 14:34:47 +0200 Subject: [PATCH 3/5] Remove tests --- .../tests/test_openrouter_chat_generator.py | 47 ------------------ .../test_openrouter_chat_generator_async.py | 48 ------------------- 2 files changed, 95 deletions(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index b1b2d289f3..8570e79c62 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -605,53 +605,6 @@ def test_live_run_with_response_format_pydantic_model(self, calendar_event_model assert isinstance(msg["event_date"], str) assert isinstance(msg["event_location"], str) - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter 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 = OpenRouterChatGenerator(tools=[weather_tool]) - - # Pass tools - 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("OPENROUTER_API_KEY", None), reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.", diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator_async.py b/integrations/openrouter/tests/test_openrouter_chat_generator_async.py index 0803555982..b08f869848 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator_async.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator_async.py @@ -263,54 +263,6 @@ async def callback(chunk: StreamingChunk): assert tool_call.arguments == {"city": "Paris"} assert tool_message.meta["finish_reason"] == "tool_calls" - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter 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 = OpenRouterChatGenerator(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("OPENROUTER_API_KEY", None), reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.", From de1e0503b14fdb5759b7cab3f81523ab18537ffe Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Thu, 23 Oct 2025 11:17:14 +0200 Subject: [PATCH 4/5] Update test --- .../tests/test_openrouter_chat_generator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index 8570e79c62..f6a2b20e04 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -650,17 +650,18 @@ def echo_function(text: str) -> str: # Create Toolset with weather and time tools toolset = Toolset([weather_tool, time_tool]) - # Initialize with toolset - component = OpenRouterChatGenerator(tools=toolset) + # Initialize with no tools, we'll pass them at runtime + component = OpenRouterChatGenerator() - # Pass echo_tool as a list at runtime - runtime tools should take precedence + # Pass mixed list: echo_tool (individual) and toolset (weather + time) at runtime + # This tests that both individual tools and toolsets can be combined messages = [ChatMessage.from_user("Echo this: Hello World")] - results = component.run(messages, tools=[echo_tool]) + 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 be able to use echo_tool from the runtime mixed list assert message.tool_calls is not None tool_call = message.tool_calls[0] assert tool_call.tool_name == "echo" From e491ee5b3fc700b0511add227b6b2c7b27c4df4d Mon Sep 17 00:00:00 2001 From: Vladimir Blagojevic Date: Thu, 23 Oct 2025 11:31:13 +0200 Subject: [PATCH 5/5] Update async test --- .../tests/test_openrouter_chat_generator_async.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator_async.py b/integrations/openrouter/tests/test_openrouter_chat_generator_async.py index b08f869848..1b20e068f0 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator_async.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator_async.py @@ -309,17 +309,18 @@ def echo_function(text: str) -> str: # Create Toolset with weather and time tools toolset = Toolset([weather_tool, time_tool]) - # Initialize with toolset - component = OpenRouterChatGenerator(tools=toolset) + # Initialize with no tools, we'll pass them at runtime + component = OpenRouterChatGenerator() - # Pass echo_tool as a list at runtime - runtime tools should take precedence + # Pass mixed list: echo_tool (individual) and toolset (weather + time) at runtime + # This tests that both individual tools and toolsets can be combined messages = [ChatMessage.from_user("Echo this: Hello World")] - results = await component.run_async(messages, tools=[echo_tool]) + 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 be able to use echo_tool from the runtime mixed list assert message.tool_calls is not None tool_call = message.tool_calls[0] assert tool_call.tool_name == "echo"