44import logging
55import os
66import sys
7+ import warnings
78from pathlib import Path
8- from typing import List , Union , cast
9+ from typing import List , cast
910
1011from ..types .tools import AgentTool
1112from .decorator import DecoratedFunctionTool
@@ -18,62 +19,42 @@ class ToolLoader:
1819 """Handles loading of tools from different sources."""
1920
2021 @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 .
2324
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.
3927 """
4028 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)
4330 if not os .path .exists (tool_path ) and ":" in tool_path :
4431 module_path , function_name = tool_path .rsplit (":" , 1 )
4532 logger .debug ("tool_name=<%s>, module_path=<%s> | importing tool from path" , function_name , module_path )
4633
4734 try :
48- # Import the module
4935 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-
6836 except ImportError as e :
6937 raise ImportError (f"Failed to import module { module_path } : { str (e )} " ) from e
7038
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+
7153 # Normal file-based tool loading
7254 abs_path = str (Path (tool_path ).resolve ())
73-
7455 logger .debug ("tool_path=<%s> | loading python tool from path" , abs_path )
7556
76- # First load the module to get TOOL_SPEC and check for Lambda deployment
57+ # Load the module by spec
7758 spec = importlib .util .spec_from_file_location (tool_name , abs_path )
7859 if not spec :
7960 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
8465 sys .modules [tool_name ] = module
8566 spec .loader .exec_module (module )
8667
87- # First, check for function-based tools with @tool decorator
68+ # Collect function-based tools decorated with @tool
8869 function_tools : List [AgentTool ] = []
8970 for attr_name in dir (module ):
9071 attr = getattr (module , attr_name )
9172 if isinstance (attr , DecoratedFunctionTool ):
9273 logger .debug (
9374 "tool_name=<%s>, tool_path=<%s> | found function-based tool in path" , attr_name , tool_path
9475 )
95- # Cast as AgentTool for mypy
9676 function_tools .append (cast (AgentTool , attr ))
9777
98- # If any function-based tools found, return them.
9978 if function_tools :
100- # Backwards compatibility: return single tool if only one found
101- if len (function_tools ) == 1 :
102- return function_tools [0 ]
10379 return function_tools
10480
105- # If no function-based tools found, fall back to traditional module-level tool
81+ # Fall back to module-level TOOL_SPEC + function
10682 tool_spec = getattr (module , "TOOL_SPEC" , None )
10783 if not tool_spec :
10884 raise AttributeError (
10985 f"Tool { tool_name } missing TOOL_SPEC (neither at module level nor as a decorated function)"
11086 )
11187
112- # Standard local tool loading
11388 tool_func_name = tool_name
11489 if not hasattr (module , tool_func_name ):
11590 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
11893 if not callable (tool_func ):
11994 raise TypeError (f"Tool { tool_name } function is not callable" )
12095
121- return PythonAgentTool (tool_name , tool_spec , tool_func )
96+ return [ PythonAgentTool (tool_name , tool_spec , tool_func )]
12297
12398 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 )
125100 raise
126101
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+
127121 @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 :
129123 """Load a tool based on its file extension.
130124
131125 Args:
132126 tool_path: Path to the tool file.
133127 tool_name: Name of the tool.
134128
135129 Returns:
136- A single Tool instance or a list of Tool instances .
130+ A single Tool instance.
137131
138132 Raises:
139133 FileNotFoundError: If the tool file does not exist.
0 commit comments