Skip to content

Commit 9e13ed9

Browse files
zastrowmjsamuel1
authored andcommitted
Remove FunctionTool as a breaking change (strands-agents#325)
!chore: Remove FunctionTool as a breaking change Previously, FunctionTool was deprecated in favor of DecoratedFunctionTool but it was kept in for backwards compatability. However, we'll soon be making a couple of breaking changes, so remove FunctionTool as part of the breaking wave
1 parent 141bc33 commit 9e13ed9

File tree

7 files changed

+13
-203
lines changed

7 files changed

+13
-203
lines changed

src/strands/tools/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
from .decorator import tool
77
from .structured_output import convert_pydantic_to_tool_spec
88
from .thread_pool_executor import ThreadPoolExecutorWrapper
9-
from .tools import FunctionTool, InvalidToolUseNameException, PythonAgentTool, normalize_schema, normalize_tool_spec
9+
from .tools import InvalidToolUseNameException, PythonAgentTool, normalize_schema, normalize_tool_spec
1010

1111
__all__ = [
1212
"tool",
13-
"FunctionTool",
1413
"PythonAgentTool",
1514
"InvalidToolUseNameException",
1615
"normalize_schema",

src/strands/tools/tools.py

Lines changed: 1 addition & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@
44
Python module-based tools, as well as utilities for validating tool uses and normalizing tool schemas.
55
"""
66

7-
import inspect
87
import logging
98
import re
10-
from typing import Any, Callable, Dict, Optional, cast
11-
12-
from typing_extensions import Unpack
9+
from typing import Any, Callable, Dict
1310

1411
from ..types.tools import AgentTool, ToolResult, ToolSpec, ToolUse
1512

@@ -144,132 +141,6 @@ def normalize_tool_spec(tool_spec: ToolSpec) -> ToolSpec:
144141
return normalized
145142

146143

147-
class FunctionTool(AgentTool):
148-
"""Tool implementation for function-based tools created with @tool.
149-
150-
This class adapts Python functions decorated with @tool to the AgentTool interface.
151-
"""
152-
153-
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
154-
"""Compatability shim to allow callers to continue working after the introduction of DecoratedFunctionTool."""
155-
if isinstance(args[0], AgentTool):
156-
return args[0]
157-
158-
return super().__new__(cls)
159-
160-
def __init__(self, func: Callable[[ToolUse, Unpack[Any]], ToolResult], tool_name: Optional[str] = None) -> None:
161-
"""Initialize a function-based tool.
162-
163-
Args:
164-
func: The decorated function.
165-
tool_name: Optional tool name (defaults to function name).
166-
167-
Raises:
168-
ValueError: If func is not decorated with @tool.
169-
"""
170-
super().__init__()
171-
172-
self._func = func
173-
174-
# Get TOOL_SPEC from the decorated function
175-
if hasattr(func, "TOOL_SPEC") and isinstance(func.TOOL_SPEC, dict):
176-
self._tool_spec = cast(ToolSpec, func.TOOL_SPEC)
177-
# Use name from tool spec if available, otherwise use function name or passed tool_name
178-
name = self._tool_spec.get("name", tool_name or func.__name__)
179-
if isinstance(name, str):
180-
self._name = name
181-
else:
182-
raise ValueError(f"Tool name must be a string, got {type(name)}")
183-
else:
184-
raise ValueError(f"Function {func.__name__} is not decorated with @tool")
185-
186-
@property
187-
def tool_name(self) -> str:
188-
"""Get the name of the tool.
189-
190-
Returns:
191-
The name of the tool.
192-
"""
193-
return self._name
194-
195-
@property
196-
def tool_spec(self) -> ToolSpec:
197-
"""Get the tool specification for this function-based tool.
198-
199-
Returns:
200-
The tool specification.
201-
"""
202-
return self._tool_spec
203-
204-
@property
205-
def tool_type(self) -> str:
206-
"""Get the type of the tool.
207-
208-
Returns:
209-
The string "function" indicating this is a function-based tool.
210-
"""
211-
return "function"
212-
213-
@property
214-
def supports_hot_reload(self) -> bool:
215-
"""Check if this tool supports automatic reloading when modified.
216-
217-
Returns:
218-
Always true for function-based tools.
219-
"""
220-
return True
221-
222-
def invoke(self, tool: ToolUse, *args: Any, **kwargs: Any) -> ToolResult:
223-
"""Execute the function with the given tool use request.
224-
225-
Args:
226-
tool: The tool use request containing the tool name, ID, and input parameters.
227-
*args: Additional positional arguments to pass to the function.
228-
**kwargs: Additional keyword arguments to pass to the function.
229-
230-
Returns:
231-
A ToolResult containing the status and content from the function execution.
232-
"""
233-
# Make sure to pass through all kwargs, including 'agent' if provided
234-
try:
235-
# Check if the function accepts agent as a keyword argument
236-
sig = inspect.signature(self._func)
237-
if "agent" in sig.parameters:
238-
# Pass agent if function accepts it
239-
return self._func(tool, **kwargs)
240-
else:
241-
# Skip passing agent if function doesn't accept it
242-
filtered_kwargs = {k: v for k, v in kwargs.items() if k != "agent"}
243-
return self._func(tool, **filtered_kwargs)
244-
except Exception as e:
245-
return {
246-
"toolUseId": tool.get("toolUseId", "unknown"),
247-
"status": "error",
248-
"content": [{"text": f"Error executing function: {str(e)}"}],
249-
}
250-
251-
@property
252-
def original_function(self) -> Callable:
253-
"""Get the original function (without wrapper).
254-
255-
Returns:
256-
Undecorated function.
257-
"""
258-
if hasattr(self._func, "original_function"):
259-
return cast(Callable, self._func.original_function)
260-
return self._func
261-
262-
def get_display_properties(self) -> dict[str, str]:
263-
"""Get properties to display in UI representations.
264-
265-
Returns:
266-
Function properties (e.g., function name).
267-
"""
268-
properties = super().get_display_properties()
269-
properties["Function"] = self.original_function.__name__
270-
return properties
271-
272-
273144
class PythonAgentTool(AgentTool):
274145
"""Tool implementation for Python-based tools.
275146

tests-integ/test_model_openai.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ class Weather(BaseModel):
6767
assert result.weather == "sunny"
6868

6969

70-
@pytest.skip(
71-
reason="OpenAI provider cannot use tools that return images - https://github.com/strands-agents/sdk-python/issues/320"
72-
)
7370
def test_tool_returning_images(model, test_image_path):
7471
@tool
7572
def tool_with_image_return():
@@ -88,7 +85,7 @@ def tool_with_image_return():
8885
],
8986
}
9087

91-
agent = Agent(model=model, tools=[tool_with_image_return])
88+
agent = Agent(model, tools=[tool_with_image_return])
9289
# NOTE - this currently fails with: "Invalid 'messages[3]'. Image URLs are only allowed for messages with role
9390
# 'user', but this message with role 'tool' contains an image URL."
9491
# See https://github.com/strands-agents/sdk-python/issues/320 for additional details

tests/strands/agent/test_agent.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,8 @@ def tool_imported():
122122

123123
@pytest.fixture
124124
def tool(tool_decorated, tool_registry):
125-
function_tool = strands.tools.tools.FunctionTool(tool_decorated, tool_name="tool_decorated")
126-
tool_registry.register_tool(function_tool)
127-
128-
return function_tool
125+
tool_registry.register_tool(tool_decorated)
126+
return tool_decorated
129127

130128

131129
@pytest.fixture
@@ -156,8 +154,7 @@ def agent(
156154
# Only register the tool directly if tools wasn't parameterized
157155
if not hasattr(request, "param") or request.param is None:
158156
# Create a new function tool directly from the decorated function
159-
function_tool = strands.tools.tools.FunctionTool(tool_decorated, tool_name="tool_decorated")
160-
agent.tool_registry.register_tool(function_tool)
157+
agent.tool_registry.register_tool(tool_decorated)
161158

162159
return agent
163160

@@ -809,8 +806,7 @@ def test_agent_tool_with_name_normalization(agent, tool_registry, mock_randint):
809806
def function(system_prompt: str) -> str:
810807
return system_prompt
811808

812-
tool = strands.tools.tools.FunctionTool(function)
813-
agent.tool_registry.register_tool(tool)
809+
agent.tool_registry.register_tool(function)
814810

815811
mock_randint.return_value = 1
816812

tests/strands/event_loop/test_event_loop.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,9 @@ def tool(tool_registry):
6060
def tool_for_testing(random_string: str) -> str:
6161
return random_string
6262

63-
function_tool = strands.tools.tools.FunctionTool(tool_for_testing)
64-
tool_registry.register_tool(function_tool)
63+
tool_registry.register_tool(tool_for_testing)
6564

66-
return function_tool
65+
return tool_for_testing
6766

6867

6968
@pytest.fixture

tests/strands/handlers/test_tool_handler.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,11 @@ def tool_use_identity(tool_registry):
2121
def identity(a: int) -> int:
2222
return a
2323

24-
identity_tool = strands.tools.tools.FunctionTool(identity)
25-
tool_registry.register_tool(identity_tool)
24+
tool_registry.register_tool(identity)
2625

2726
return {"toolUseId": "identity", "name": "identity", "input": {"a": 1}}
2827

2928

30-
@pytest.fixture
31-
def tool_use_error(tool_registry):
32-
def error():
33-
return
34-
35-
error.TOOL_SPEC = {"invalid": True}
36-
37-
error_tool = strands.tools.tools.FunctionTool(error)
38-
tool_registry.register_tool(error_tool)
39-
40-
return {"toolUseId": "error", "name": "error", "input": {}}
41-
42-
4329
def test_process(tool_handler, tool_use_identity):
4430
tru_result = tool_handler.process(
4531
tool_use_identity,

tests/strands/tools/test_tools.py

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import strands
44
from strands.tools.tools import (
5-
FunctionTool,
65
InvalidToolUseNameException,
76
PythonAgentTool,
87
normalize_schema,
@@ -408,15 +407,10 @@ def identity(a: int) -> int:
408407

409408

410409
@pytest.fixture
411-
def tool_function(function):
410+
def tool(function):
412411
return strands.tools.tool(function)
413412

414413

415-
@pytest.fixture
416-
def tool(tool_function):
417-
return FunctionTool(tool_function, tool_name="identity")
418-
419-
420414
def test__init__invalid_name():
421415
with pytest.raises(ValueError, match="Tool name must be a string"):
422416

@@ -476,9 +470,7 @@ def test_original_function_not_decorated():
476470
def identity(a: int):
477471
return a
478472

479-
identity.TOOL_SPEC = {}
480-
481-
tool = FunctionTool(identity, tool_name="identity")
473+
tool = strands.tool(func=identity, name="identity")
482474

483475
tru_name = tool.original_function.__name__
484476
exp_name = "identity"
@@ -509,39 +501,9 @@ def test_invoke_with_agent():
509501
def identity(a: int, agent: dict = None):
510502
return a, agent
511503

512-
tool = FunctionTool(identity, tool_name="identity")
513-
# FunctionTool is a pass through for AgentTool instances until we remove it in a future release (#258)
514-
assert tool == identity
515-
516504
exp_output = {"toolUseId": "unknown", "status": "success", "content": [{"text": "(2, {'state': 1})"}]}
517505

518-
tru_output = tool.invoke({"input": {"a": 2}}, agent={"state": 1})
519-
520-
assert tru_output == exp_output
521-
522-
523-
def test_invoke_exception():
524-
def identity(a: int):
525-
return a
526-
527-
identity.TOOL_SPEC = {}
528-
529-
tool = FunctionTool(identity, tool_name="identity")
530-
531-
tru_output = tool.invoke({}, invalid=1)
532-
exp_output = {
533-
"toolUseId": "unknown",
534-
"status": "error",
535-
"content": [
536-
{
537-
"text": (
538-
"Error executing function: "
539-
"test_invoke_exception.<locals>.identity() "
540-
"got an unexpected keyword argument 'invalid'"
541-
)
542-
}
543-
],
544-
}
506+
tru_output = identity.invoke({"input": {"a": 2}}, agent={"state": 1})
545507

546508
assert tru_output == exp_output
547509

0 commit comments

Comments
 (0)