4
4
import logging
5
5
import os
6
6
import sys
7
+ import warnings
7
8
from pathlib import Path
8
- from typing import List , Union , cast
9
+ from typing import List , cast
9
10
10
11
from ..types .tools import AgentTool
11
12
from .decorator import DecoratedFunctionTool
@@ -18,62 +19,42 @@ class ToolLoader:
18
19
"""Handles loading of tools from different sources."""
19
20
20
21
@staticmethod
21
- def load_python_tool (tool_path : str , tool_name : str ) -> Union [ AgentTool , List [AgentTool ] ]:
22
- """Load a Python tool module.
22
+ def load_python_tools (tool_path : str , tool_name : str ) -> List [AgentTool ]:
23
+ """Load a Python tool module and return all discovered function-based tools as a list .
23
24
24
- Args:
25
- tool_path: Path to the Python tool file.
26
- tool_name: Name of the tool.
27
-
28
- Returns:
29
- A single AgentTool or a list of AgentTool instances when multiple function-based tools
30
- are defined in the module.
31
-
32
-
33
- Raises:
34
- AttributeError: If required attributes are missing from the tool module.
35
- ImportError: If there are issues importing the tool module.
36
- TypeError: If the tool function is not callable.
37
- ValueError: If function in module is not a valid tool.
38
- Exception: For other errors during tool loading.
25
+ This method always returns a list of AgentTool (possibly length 1). It is the
26
+ canonical API for retrieving multiple tools from a single Python file.
39
27
"""
40
28
try :
41
- # Check if tool_path is in the format "package.module:function"; but keep in mind windows whose file path
42
- # could have a ':' so also ensure that it's not a file
29
+ # Support module:function style (e.g. package.module:function)
43
30
if not os .path .exists (tool_path ) and ":" in tool_path :
44
31
module_path , function_name = tool_path .rsplit (":" , 1 )
45
32
logger .debug ("tool_name=<%s>, module_path=<%s> | importing tool from path" , function_name , module_path )
46
33
47
34
try :
48
- # Import the module
49
35
module = __import__ (module_path , fromlist = ["*" ])
50
-
51
- # Get the function
52
- if not hasattr (module , function_name ):
53
- raise AttributeError (f"Module { module_path } has no function named { function_name } " )
54
-
55
- func = getattr (module , function_name )
56
-
57
- if isinstance (func , DecoratedFunctionTool ):
58
- logger .debug (
59
- "tool_name=<%s>, module_path=<%s> | found function-based tool" , function_name , module_path
60
- )
61
- # mypy has problems converting between DecoratedFunctionTool <-> AgentTool
62
- return cast (AgentTool , func )
63
- else :
64
- raise ValueError (
65
- f"Function { function_name } in { module_path } is not a valid tool (missing @tool decorator)"
66
- )
67
-
68
36
except ImportError as e :
69
37
raise ImportError (f"Failed to import module { module_path } : { str (e )} " ) from e
70
38
39
+ if not hasattr (module , function_name ):
40
+ raise AttributeError (f"Module { module_path } has no function named { function_name } " )
41
+
42
+ func = getattr (module , function_name )
43
+ if isinstance (func , DecoratedFunctionTool ):
44
+ logger .debug (
45
+ "tool_name=<%s>, module_path=<%s> | found function-based tool" , function_name , module_path
46
+ )
47
+ return [cast (AgentTool , func )]
48
+ else :
49
+ raise ValueError (
50
+ f"Function { function_name } in { module_path } is not a valid tool (missing @tool decorator)"
51
+ )
52
+
71
53
# Normal file-based tool loading
72
54
abs_path = str (Path (tool_path ).resolve ())
73
-
74
55
logger .debug ("tool_path=<%s> | loading python tool from path" , abs_path )
75
56
76
- # First load the module to get TOOL_SPEC and check for Lambda deployment
57
+ # Load the module by spec
77
58
spec = importlib .util .spec_from_file_location (tool_name , abs_path )
78
59
if not spec :
79
60
raise ImportError (f"Could not create spec for { tool_name } " )
@@ -84,32 +65,26 @@ def load_python_tool(tool_path: str, tool_name: str) -> Union[AgentTool, List[Ag
84
65
sys .modules [tool_name ] = module
85
66
spec .loader .exec_module (module )
86
67
87
- # First, check for function-based tools with @tool decorator
68
+ # Collect function-based tools decorated with @tool
88
69
function_tools : List [AgentTool ] = []
89
70
for attr_name in dir (module ):
90
71
attr = getattr (module , attr_name )
91
72
if isinstance (attr , DecoratedFunctionTool ):
92
73
logger .debug (
93
74
"tool_name=<%s>, tool_path=<%s> | found function-based tool in path" , attr_name , tool_path
94
75
)
95
- # Cast as AgentTool for mypy
96
76
function_tools .append (cast (AgentTool , attr ))
97
77
98
- # If any function-based tools found, return them.
99
78
if function_tools :
100
- # Backwards compatibility: return single tool if only one found
101
- if len (function_tools ) == 1 :
102
- return function_tools [0 ]
103
79
return function_tools
104
80
105
- # If no function-based tools found, fall back to traditional module-level tool
81
+ # Fall back to module-level TOOL_SPEC + function
106
82
tool_spec = getattr (module , "TOOL_SPEC" , None )
107
83
if not tool_spec :
108
84
raise AttributeError (
109
85
f"Tool { tool_name } missing TOOL_SPEC (neither at module level nor as a decorated function)"
110
86
)
111
87
112
- # Standard local tool loading
113
88
tool_func_name = tool_name
114
89
if not hasattr (module , tool_func_name ):
115
90
raise AttributeError (f"Tool { tool_name } missing function { tool_func_name } " )
@@ -118,22 +93,41 @@ def load_python_tool(tool_path: str, tool_name: str) -> Union[AgentTool, List[Ag
118
93
if not callable (tool_func ):
119
94
raise TypeError (f"Tool { tool_name } function is not callable" )
120
95
121
- return PythonAgentTool (tool_name , tool_spec , tool_func )
96
+ return [ PythonAgentTool (tool_name , tool_spec , tool_func )]
122
97
123
98
except Exception :
124
- logger .exception ("tool_name=<%s>, sys_path=<%s> | failed to load python tool" , tool_name , sys .path )
99
+ logger .exception ("tool_name=<%s>, sys_path=<%s> | failed to load python tool(s) " , tool_name , sys .path )
125
100
raise
126
101
102
+ @staticmethod
103
+ def load_python_tool (tool_path : str , tool_name : str ) -> AgentTool :
104
+ """DEPRECATED: Load a Python tool module and return a single AgentTool for backwards compatibility.
105
+
106
+ Use `load_python_tools` to retrieve all tools defined in a .py file (returns a list).
107
+ This function will emit a `DeprecationWarning` and return the first discovered tool.
108
+ """
109
+ warnings .warn (
110
+ "ToolLoader.load_python_tool is deprecated and will be removed in Strands SDK 2.0. "
111
+ "Use ToolLoader.load_python_tools(...) which always returns a list of AgentTool." ,
112
+ DeprecationWarning ,
113
+ stacklevel = 2 ,
114
+ )
115
+
116
+ tools = ToolLoader .load_python_tools (tool_path , tool_name )
117
+ if not tools :
118
+ raise RuntimeError (f"No tools found in { tool_path } for { tool_name } " )
119
+ return tools [0 ]
120
+
127
121
@classmethod
128
- def load_tool (cls , tool_path : str , tool_name : str ) -> Union [ AgentTool , List [ AgentTool ]] :
122
+ def load_tool (cls , tool_path : str , tool_name : str ) -> AgentTool :
129
123
"""Load a tool based on its file extension.
130
124
131
125
Args:
132
126
tool_path: Path to the tool file.
133
127
tool_name: Name of the tool.
134
128
135
129
Returns:
136
- A single Tool instance or a list of Tool instances .
130
+ A single Tool instance.
137
131
138
132
Raises:
139
133
FileNotFoundError: If the tool file does not exist.
0 commit comments