haystack/test/tools/test_tool.py
lif d4e623d069
fix: Raise error when async function is passed to Tool (#10264)
* fix: Raise error when async function is passed to Tool

- Added validation in Tool.__post_init__ to check if function is async
- Added validation in create_tool_from_function for early error detection
- Updated docstring to clarify that functions must be synchronous
- Added tests for both Tool init and @tool decorator with async functions

Closes #9580

* fix: Remove redundant async check from create_tool_from_function

Per reviewer feedback, the async validation is only needed in the
Tool class itself, since create_tool_from_function creates a Tool.

- Remove async check from create_tool_from_function
- Update docstrings to remove async-related notes
- Remove redundant tests for async functions in from_function tests
- Add release note

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add docstring note and tests for async function validation

- Updated Tool class docstring to indicate function must be synchronous
- Added test_from_function_async_raises_error test
- Added test_tool_decorator_async_raises_error test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 12:40:38 +01:00

157 lines
6.1 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import re
import pytest
from haystack.tools import Tool, _check_duplicate_tool_names
from haystack.tools.errors import ToolInvocationError
def get_weather_report(city: str) -> str:
return f"Weather report for {city}: 20°C, sunny"
def format_string(text: str) -> str:
return f"Formatted: {text}"
parameters = {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
async def async_get_weather(city: str) -> str:
return f"Weather report for {city}: 20°C, sunny"
class TestTool:
def test_init(self):
tool = Tool(
name="weather", description="Get weather report", parameters=parameters, function=get_weather_report
)
assert tool.name == "weather"
assert tool.description == "Get weather report"
assert tool.parameters == parameters
assert tool.function == get_weather_report
assert tool.inputs_from_state is None
assert tool.outputs_to_state is None
def test_init_invalid_parameters(self):
params = {"type": "invalid", "properties": {"city": {"type": "string"}}}
with pytest.raises(ValueError):
Tool(name="irrelevant", description="irrelevant", parameters=params, function=get_weather_report)
def test_init_async_function_raises_error(self):
with pytest.raises(ValueError, match="Async functions are not supported as tools"):
Tool(name="weather", description="Get weather report", parameters=parameters, function=async_get_weather)
@pytest.mark.parametrize(
"outputs_to_state",
[
pytest.param({"documents": ["some_value"]}, id="config-not-a-dict"),
pytest.param({"documents": {"source": get_weather_report}}, id="source-not-a-string"),
pytest.param({"documents": {"handler": "some_string", "source": "docs"}}, id="handler-not-callable"),
],
)
def test_init_invalid_output_structure(self, outputs_to_state):
with pytest.raises(ValueError):
Tool(
name="irrelevant",
description="irrelevant",
parameters={"type": "object", "properties": {"city": {"type": "string"}}},
function=get_weather_report,
outputs_to_state=outputs_to_state,
)
def test_tool_spec(self):
tool = Tool(
name="weather", description="Get weather report", parameters=parameters, function=get_weather_report
)
assert tool.tool_spec == {"name": "weather", "description": "Get weather report", "parameters": parameters}
def test_invoke(self):
tool = Tool(
name="weather", description="Get weather report", parameters=parameters, function=get_weather_report
)
assert tool.invoke(city="Berlin") == "Weather report for Berlin: 20°C, sunny"
def test_invoke_fail(self):
tool = Tool(
name="weather", description="Get weather report", parameters=parameters, function=get_weather_report
)
with pytest.raises(
ToolInvocationError,
match=re.escape(
"Failed to invoke Tool `weather` with parameters {}. Error: get_weather_report() missing 1 required "
"positional argument: 'city'"
),
):
tool.invoke()
def test_to_dict(self):
tool = Tool(
name="weather",
description="Get weather report",
parameters=parameters,
function=get_weather_report,
outputs_to_string={"handler": format_string},
inputs_from_state={"state_key": "tool_input_key"},
outputs_to_state={"documents": {"handler": get_weather_report, "source": "docs"}},
)
assert tool.to_dict() == {
"type": "haystack.tools.tool.Tool",
"data": {
"name": "weather",
"description": "Get weather report",
"parameters": parameters,
"function": "test_tool.get_weather_report",
"outputs_to_string": {"handler": "test_tool.format_string"},
"inputs_from_state": {"state_key": "tool_input_key"},
"outputs_to_state": {"documents": {"source": "docs", "handler": "test_tool.get_weather_report"}},
},
}
def test_from_dict(self):
tool_dict = {
"type": "haystack.tools.tool.Tool",
"data": {
"name": "weather",
"description": "Get weather report",
"parameters": parameters,
"function": "test_tool.get_weather_report",
"outputs_to_string": {"handler": "test_tool.format_string"},
"inputs_from_state": {"state_key": "tool_input_key"},
"outputs_to_state": {"documents": {"source": "docs", "handler": "test_tool.get_weather_report"}},
},
}
tool = Tool.from_dict(tool_dict)
assert tool.name == "weather"
assert tool.description == "Get weather report"
assert tool.parameters == parameters
assert tool.function == get_weather_report
assert tool.outputs_to_string == {"handler": format_string}
assert tool.inputs_from_state == {"state_key": "tool_input_key"}
assert tool.outputs_to_state == {"documents": {"source": "docs", "handler": get_weather_report}}
def test_check_duplicate_tool_names():
tools = [
Tool(name="weather", description="Get weather report", parameters=parameters, function=get_weather_report),
Tool(name="weather", description="A different description", parameters=parameters, function=get_weather_report),
]
with pytest.raises(ValueError):
_check_duplicate_tool_names(tools)
tools = [
Tool(name="weather", description="Get weather report", parameters=parameters, function=get_weather_report),
Tool(name="weather2", description="Get weather report", parameters=parameters, function=get_weather_report),
]
_check_duplicate_tool_names(tools)