55import  os 
66import  sys 
77import  warnings 
8+ from  importlib .machinery  import  ModuleSpec 
89from  pathlib  import  Path 
10+ from  posixpath  import  expanduser 
11+ from  types  import  ModuleType 
912from  typing  import  List , cast 
1013
1114from  ..types .tools  import  AgentTool 
1518logger  =  logging .getLogger (__name__ )
1619
1720
21+ def  load_tool_from_string (tool_string : str ) ->  List [AgentTool ]:
22+     """Load tools follows strands supported input string formats. 
23+ 
24+     This function can load a tool based on a string in the following ways: 
25+     1. Local file path to a module based tool: `./path/to/module/tool.py` 
26+     2. Module import path 
27+       2.1. Path to a module based tool: `strands_tools.file_read` 
28+       2.2. Path to a module with multiple AgentTool instances (@tool decorated): `tests.fixtures.say_tool` 
29+       2.3. Path to a module and a specific function: `tests.fixtures.say_tool:say` 
30+     """ 
31+     # Case 1: Local file path to a tool 
32+     # Ex: ./path/to/my_cool_tool.py 
33+     tool_path  =  expanduser (tool_string )
34+     if  os .path .exists (tool_path ):
35+         return  load_tools_from_file_path (tool_path )
36+ 
37+     # Case 2: Module import path 
38+     # Ex: test.fixtures.say_tool:say (Load specific @tool decorated function) 
39+     # Ex: strands_tools.file_read (Load all @tool decorated functions, or module tool) 
40+     return  load_tools_from_module_path (tool_string )
41+ 
42+ 
43+ def  load_tools_from_file_path (tool_path : str ) ->  List [AgentTool ]:
44+     """Load module from specified path, and then load tools from that module. 
45+ 
46+     This function attempts to load the passed in path as a python module, and if it succeeds, 
47+     then it tries to import strands tool(s) from that module. 
48+     """ 
49+     abs_path  =  str (Path (tool_path ).resolve ())
50+     logger .debug ("tool_path=<%s> | loading python tool from path" , abs_path )
51+ 
52+     # Load the module by spec 
53+ 
54+     # Using this to determine the module name 
55+     # ./path/to/my_cool_tool.py -> my_cool_tool 
56+     module_name  =  os .path .basename (tool_path ).split ("." )[0 ]
57+ 
58+     # This function import a module based on its path, and gives it the provided name 
59+ 
60+     spec : ModuleSpec  =  cast (ModuleSpec , importlib .util .spec_from_file_location (module_name , abs_path ))
61+     if  not  spec :
62+         raise  ImportError (f"Could not create spec for { module_name }  )
63+     if  not  spec .loader :
64+         raise  ImportError (f"No loader available for { module_name }  )
65+ 
66+     module  =  importlib .util .module_from_spec (spec )
67+     # Load, or re-load, the module 
68+     sys .modules [module_name ] =  module 
69+     # Execute the module to run any top level code 
70+     spec .loader .exec_module (module )
71+ 
72+     return  load_tools_from_module (module , module_name )
73+ 
74+ 
75+ def  load_tools_from_module_path (module_path : str ) ->  list [AgentTool ]:
76+     """Load strands tool from a module path. 
77+ 
78+     Example module paths: 
79+     my.module.path 
80+     my.module.path:tool_name 
81+     """ 
82+     if  ":"  in  module_path :
83+         module_name , tool_func_name  =  module_path .split (":" )
84+     else :
85+         module_name , tool_func_name  =  (module_path , None )
86+ 
87+     try :
88+         module  =  importlib .import_module (module_name )
89+     except  ModuleNotFoundError  as  e :
90+         raise  AttributeError (f'Tool string: "{ module_path }  ) from  e 
91+ 
92+     # If a ':' is present in the string, then its a targeted function in a module 
93+     if  tool_func_name :
94+         if  tool_func_name  in  dir (module ):
95+             target_tool  =  getattr (module , tool_func_name )
96+             if  isinstance (target_tool , DecoratedFunctionTool ):
97+                 return  [target_tool ]
98+ 
99+             raise  AttributeError (f"Tool { tool_func_name } { module_name }  )
100+ 
101+     # Else, try to import all of the @tool decorated tools, or the module based tool 
102+     module_name  =  module_path .split ("." )[- 1 ]
103+     return  load_tools_from_module (module , module_name )
104+ 
105+ 
106+ def  load_tools_from_module (module : ModuleType , module_name : str ) ->  list [AgentTool ]:
107+     """Load tools from a module. 
108+ 
109+     First checks if the passed in module has instances of DecoratedToolFunction classes as atributes to the module. 
110+     If so, then it returns them as a list of tools. If not, then it attempts to load the module as a module based too. 
111+     """ 
112+     logger .debug ("tool_name=<%s>, module=<%s> | loading tools from module" , module_name , module_name )
113+ 
114+     # Try and see if any of the attributes in the module are function-based tools decorated with @tool 
115+     # This means that there may be more than one tool available in this module, so we load them all 
116+ 
117+     function_tools : List [AgentTool ] =  []
118+     # Function tools will appear as attributes in the module 
119+     for  attr_name  in  dir (module ):
120+         attr  =  getattr (module , attr_name )
121+         # Check if the module attribute is a DecoratedFunctiontool 
122+         if  isinstance (attr , DecoratedFunctionTool ):
123+             logger .debug ("tool_name=<%s>, module=<%s> | found function-based tool in module" , attr_name , module_name )
124+             function_tools .append (cast (AgentTool , attr ))
125+ 
126+     if  function_tools :
127+         return  function_tools 
128+ 
129+     # Finally, if no DecoratedFunctionTools are found in the module, fall back 
130+     # to module based tools, and search for TOOL_SPEC + function 
131+     module_tool_name  =  module_name 
132+     tool_spec  =  getattr (module , "TOOL_SPEC" , None )
133+     if  not  tool_spec :
134+         raise  AttributeError (
135+             f"The module { module_tool_name }  
136+             "This module must contain @tool decorated function(s), or must be a module based tool." 
137+         )
138+ 
139+     # If this is a module based tool, the module should have a function with the same name as the module itself 
140+     if  not  hasattr (module , module_tool_name ):
141+         raise  AttributeError (f"Module-based tool { module_tool_name } { module_tool_name }  )
142+ 
143+     tool_func  =  getattr (module , module_tool_name )
144+     if  not  callable (tool_func ):
145+         raise  TypeError (f"Tool { module_tool_name }  )
146+ 
147+     return  [PythonAgentTool (module_tool_name , tool_spec , tool_func )]
148+ 
149+ 
18150class  ToolLoader :
19151    """Handles loading of tools from different sources.""" 
20152
21153    @staticmethod  
22154    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. 
155+         """DEPRECATED:  Load a Python tool module and return all discovered function-based tools as a list. 
24156
25157        This method always returns a list of AgentTool (possibly length 1). It is the 
26158        canonical API for retrieving multiple tools from a single Python file. 
27159        """ 
160+         warnings .warn (
161+             "ToolLoader.load_python_tool is deprecated and will be removed in Strands SDK 2.0. " 
162+             "Use the `load_tools_from_string` or `load_tools_from_module` methods instead." ,
163+             DeprecationWarning ,
164+             stacklevel = 2 ,
165+         )
28166        try :
29167            # Support module:function style (e.g. package.module:function) 
30168            if  not  os .path .exists (tool_path ) and  ":"  in  tool_path :
@@ -108,7 +246,7 @@ def load_python_tool(tool_path: str, tool_name: str) -> AgentTool:
108246        """ 
109247        warnings .warn (
110248            "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 ." ,
249+             "Use the `load_tools_from_string` or `load_tools_from_module` methods instead ." ,
112250            DeprecationWarning ,
113251            stacklevel = 2 ,
114252        )
@@ -127,7 +265,7 @@ def load_tool(cls, tool_path: str, tool_name: str) -> AgentTool:
127265        """ 
128266        warnings .warn (
129267            "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 ." ,
268+             "Use the `load_tools_from_string` or `load_tools_from_module` methods instead ." ,
131269            DeprecationWarning ,
132270            stacklevel = 2 ,
133271        )
@@ -140,7 +278,7 @@ def load_tool(cls, tool_path: str, tool_name: str) -> AgentTool:
140278
141279    @classmethod  
142280    def  load_tools (cls , tool_path : str , tool_name : str ) ->  list [AgentTool ]:
143-         """Load tools from a file based on its file extension. 
281+         """DEPRECATED:  Load tools from a file based on its file extension. 
144282
145283        Args: 
146284            tool_path: Path to the tool file. 
@@ -154,6 +292,12 @@ def load_tools(cls, tool_path: str, tool_name: str) -> list[AgentTool]:
154292            ValueError: If the tool file has an unsupported extension. 
155293            Exception: For other errors during tool loading. 
156294        """ 
295+         warnings .warn (
296+             "ToolLoader.load_tools is deprecated and will be removed in Strands SDK 2.0. " 
297+             "Use the `load_tools_from_string` or `load_tools_from_module` methods instead." ,
298+             DeprecationWarning ,
299+             stacklevel = 2 ,
300+         )
157301        ext  =  Path (tool_path ).suffix .lower ()
158302        abs_path  =  str (Path (tool_path ).resolve ())
159303
0 commit comments