44import logging
55import os
66import sys
7+ import warnings
78from pathlib import Path
8- from typing import cast
9+ from typing import List , cast
910
1011from ..types .tools import AgentTool
1112from .decorator import DecoratedFunctionTool
@@ -18,60 +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 ) -> AgentTool :
22- """Load a Python tool module.
23-
24- Args:
25- tool_path: Path to the Python tool file.
26- tool_name: Name of the tool.
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.
2724
28- Returns:
29- Tool instance.
30-
31- Raises:
32- AttributeError: If required attributes are missing from the tool module.
33- ImportError: If there are issues importing the tool module.
34- TypeError: If the tool function is not callable.
35- ValueError: If function in module is not a valid tool.
36- 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.
3727 """
3828 try :
39- # Check if tool_path is in the format "package.module:function"; but keep in mind windows whose file path
40- # could have a colon so also ensure that it's not a file
29+ # Support module:function style (e.g. package.module:function)
4130 if not os .path .exists (tool_path ) and ":" in tool_path :
4231 module_path , function_name = tool_path .rsplit (":" , 1 )
4332 logger .debug ("tool_name=<%s>, module_path=<%s> | importing tool from path" , function_name , module_path )
4433
4534 try :
46- # Import the module
4735 module = __import__ (module_path , fromlist = ["*" ])
48-
49- # Get the function
50- if not hasattr (module , function_name ):
51- raise AttributeError (f"Module { module_path } has no function named { function_name } " )
52-
53- func = getattr (module , function_name )
54-
55- if isinstance (func , DecoratedFunctionTool ):
56- logger .debug (
57- "tool_name=<%s>, module_path=<%s> | found function-based tool" , function_name , module_path
58- )
59- # mypy has problems converting between DecoratedFunctionTool <-> AgentTool
60- return cast (AgentTool , func )
61- else :
62- raise ValueError (
63- f"Function { function_name } in { module_path } is not a valid tool (missing @tool decorator)"
64- )
65-
6636 except ImportError as e :
6737 raise ImportError (f"Failed to import module { module_path } : { str (e )} " ) from e
6838
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+
6953 # Normal file-based tool loading
7054 abs_path = str (Path (tool_path ).resolve ())
71-
7255 logger .debug ("tool_path=<%s> | loading python tool from path" , abs_path )
7356
74- # First load the module to get TOOL_SPEC and check for Lambda deployment
57+ # Load the module by spec
7558 spec = importlib .util .spec_from_file_location (tool_name , abs_path )
7659 if not spec :
7760 raise ImportError (f"Could not create spec for { tool_name } " )
@@ -82,24 +65,26 @@ def load_python_tool(tool_path: str, tool_name: str) -> AgentTool:
8265 sys .modules [tool_name ] = module
8366 spec .loader .exec_module (module )
8467
85- # First, check for function-based tools with @tool decorator
68+ # Collect function-based tools decorated with @tool
69+ function_tools : List [AgentTool ] = []
8670 for attr_name in dir (module ):
8771 attr = getattr (module , attr_name )
8872 if isinstance (attr , DecoratedFunctionTool ):
8973 logger .debug (
9074 "tool_name=<%s>, tool_path=<%s> | found function-based tool in path" , attr_name , tool_path
9175 )
92- # mypy has problems converting between DecoratedFunctionTool <-> AgentTool
93- return cast (AgentTool , attr )
76+ function_tools .append (cast (AgentTool , attr ))
77+
78+ if function_tools :
79+ return function_tools
9480
95- # If no function-based tools found, fall back to traditional module-level tool
81+ # Fall back to module-level TOOL_SPEC + function
9682 tool_spec = getattr (module , "TOOL_SPEC" , None )
9783 if not tool_spec :
9884 raise AttributeError (
9985 f"Tool { tool_name } missing TOOL_SPEC (neither at module level nor as a decorated function)"
10086 )
10187
102- # Standard local tool loading
10388 tool_func_name = tool_name
10489 if not hasattr (module , tool_func_name ):
10590 raise AttributeError (f"Tool { tool_name } missing function { tool_func_name } " )
@@ -108,22 +93,61 @@ def load_python_tool(tool_path: str, tool_name: str) -> AgentTool:
10893 if not callable (tool_func ):
10994 raise TypeError (f"Tool { tool_name } function is not callable" )
11095
111- return PythonAgentTool (tool_name , tool_spec , tool_func )
96+ return [ PythonAgentTool (tool_name , tool_spec , tool_func )]
11297
11398 except Exception :
114- 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 )
115100 raise
116101
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+
117121 @classmethod
118122 def load_tool (cls , tool_path : str , tool_name : str ) -> AgentTool :
119- """Load a tool based on its file extension.
123+ """DEPRECATED: Load a single tool based on its file extension for backwards compatibility.
124+
125+ Use `load_tools` to retrieve all tools defined in a file (returns a list).
126+ This function will emit a `DeprecationWarning` and return the first discovered tool.
127+ """
128+ warnings .warn (
129+ "ToolLoader.load_tool is deprecated and will be removed in Strands SDK 2.0. "
130+ "Use ToolLoader.load_tools(...) which always returns a list of AgentTool." ,
131+ DeprecationWarning ,
132+ stacklevel = 2 ,
133+ )
134+
135+ tools = ToolLoader .load_tools (tool_path , tool_name )
136+ if not tools :
137+ raise RuntimeError (f"No tools found in { tool_path } for { tool_name } " )
138+
139+ return tools [0 ]
140+
141+ @classmethod
142+ def load_tools (cls , tool_path : str , tool_name : str ) -> list [AgentTool ]:
143+ """Load tools from a file based on its file extension.
120144
121145 Args:
122146 tool_path: Path to the tool file.
123147 tool_name: Name of the tool.
124148
125149 Returns:
126- Tool instance.
150+ A single Tool instance.
127151
128152 Raises:
129153 FileNotFoundError: If the tool file does not exist.
@@ -138,7 +162,7 @@ def load_tool(cls, tool_path: str, tool_name: str) -> AgentTool:
138162
139163 try :
140164 if ext == ".py" :
141- return cls .load_python_tool (abs_path , tool_name )
165+ return cls .load_python_tools (abs_path , tool_name )
142166 else :
143167 raise ValueError (f"Unsupported tool file type: { ext } " )
144168 except Exception :
0 commit comments