@@ -41,6 +41,122 @@ def _create_client(cls, llm_config: LLMConfig) -> AsyncOpenAI:
4141
4242 return AsyncOpenAI (** client_kwargs )
4343
44+ @classmethod
45+ def _resolve_tool (cls , tool_name : str | type , config : GlobalConfig ) -> type [BaseTool ]:
46+ """Resolve a single tool from its name or class.
47+
48+ Args:
49+ tool_name: Tool name (string) or tool class
50+ config: Global configuration containing tool definitions
51+
52+ Returns:
53+ Resolved tool class
54+
55+ Raises:
56+ TypeError: If tool class is not a subclass of BaseTool
57+ ValueError: If tool cannot be resolved
58+ """
59+ # If tool_name is already a class, use it directly
60+ if isinstance (tool_name , type ):
61+ if not issubclass (tool_name , BaseTool ):
62+ raise TypeError (f"Tool class '{ tool_name .__name__ } ' must be a subclass of BaseTool" )
63+ return tool_name
64+
65+ tool_class = None
66+
67+ # First, try to find tool in config.tools
68+ if tool_name in config .tools :
69+ tool_def = config .tools [tool_name ]
70+ base_class = tool_def .base_class
71+
72+ if base_class is None :
73+ # Generate default path: sgr_agent_core.tools.{ToolName}
74+ # Convert tool_name to ToolName (capitalize first letter, handle underscores)
75+ tool_class_name = "" .join (word .capitalize () for word in tool_name .split ("_" ))
76+ base_class = f"sgr_agent_core.tools.{ tool_class_name } "
77+
78+ # Resolve base_class (can be string, ImportString, or class)
79+ if isinstance (base_class , str ):
80+ # Try import string resolution
81+ if "." in base_class :
82+ # Relative import - resolve relative to config file location
83+ # This is handled by sys.path in agent_config.from_yaml
84+ try :
85+ module_parts = base_class .split ("." )
86+ if len (module_parts ) >= 2 :
87+ module_path = "." .join (module_parts [:- 1 ])
88+ class_name = module_parts [- 1 ]
89+ module = __import__ (module_path , fromlist = [class_name ])
90+ tool_class = getattr (module , class_name )
91+ except (ImportError , AttributeError ) as e :
92+ logger .warning (
93+ f"Failed to import tool '{ tool_name } ' from '{ base_class } ': { e } . " f"Trying registry..."
94+ )
95+ else :
96+ # Try registry
97+ tool_class = ToolRegistry .get (base_class )
98+ elif isinstance (base_class , type ):
99+ # Validate it's a BaseTool subclass
100+ if not issubclass (base_class , BaseTool ):
101+ raise TypeError (f"Tool '{ tool_name } ' base_class must be a subclass of BaseTool" )
102+ tool_class = base_class
103+ else :
104+ # ImportString - should be resolved by pydantic
105+ tool_class = base_class
106+ else :
107+ # Tool not in config.tools, try registry
108+ tool_class = ToolRegistry .get (tool_name )
109+
110+ # If still not found, try as import string
111+ if tool_class is None :
112+ if "." in tool_name :
113+ try :
114+ module_parts = tool_name .split ("." )
115+ if len (module_parts ) >= 2 :
116+ module_path = "." .join (module_parts [:- 1 ])
117+ class_name = module_parts [- 1 ]
118+ module = __import__ (module_path , fromlist = [class_name ])
119+ tool_class = getattr (module , class_name )
120+ except (ImportError , AttributeError ) as e :
121+ error_msg = (
122+ f"Tool '{ tool_name } ' not found in config.tools, registry, or failed to import.\n "
123+ f"Available tools in config: { ', ' .join (config .tools .keys ())} \n "
124+ f"Available tools in registry: "
125+ f"{ ', ' .join ([c .__name__ for c in ToolRegistry .list_items ()])} \n "
126+ f"Import error: { e } \n "
127+ f" - Check that the tool name is correct\n "
128+ f" - Define the tool in the 'tools' section of your config\n "
129+ f" - Or ensure the tool is registered in ToolRegistry"
130+ )
131+ logger .error (error_msg )
132+ raise ValueError (error_msg ) from e
133+
134+ if tool_class is None :
135+ error_msg = (
136+ f"Tool '{ tool_name } ' not found.\n "
137+ f"Available tools in config: { ', ' .join (config .tools .keys ())} \n "
138+ f"Available tools in registry: { ', ' .join ([c .__name__ for c in ToolRegistry .list_items ()])} \n "
139+ f" - Define the tool in the 'tools' section of your config\n "
140+ f" - Or ensure the tool is registered in ToolRegistry"
141+ )
142+ logger .error (error_msg )
143+ raise ValueError (error_msg )
144+
145+ return tool_class
146+
147+ @classmethod
148+ def _resolve_tools (cls , tool_names : list [str | type ], config : GlobalConfig ) -> list [type [BaseTool ]]:
149+ """Resolve multiple tools from their names or classes.
150+
151+ Args:
152+ tool_names: List of tool names (strings) or tool classes
153+ config: Global configuration containing tool definitions
154+
155+ Returns:
156+ List of resolved tool classes
157+ """
158+ return [cls ._resolve_tool (tool_name , config ) for tool_name in tool_names ]
159+
44160 @classmethod
45161 async def create (cls , agent_def : AgentDefinition , task_messages : list [ChatCompletionMessageParam ]) -> Agent :
46162 """Create an agent instance from a definition.
@@ -97,99 +213,9 @@ async def create(cls, agent_def: AgentDefinition, task_messages: list[ChatComple
97213 logger .error (error_msg )
98214 raise ValueError (error_msg )
99215 mcp_tools : list = await MCP2ToolConverter .build_tools_from_mcp (agent_def .mcp )
100-
101- tools = [* mcp_tools ]
102216 config = GlobalConfig ()
103-
104- for tool_name in agent_def .tools :
105- tool_class = None
106-
107- # If tool_name is already a class, use it directly
108- if isinstance (tool_name , type ):
109- if not issubclass (tool_name , BaseTool ):
110- raise TypeError (f"Tool class '{ tool_name .__name__ } ' must be a subclass of BaseTool" )
111- tools .append (tool_name )
112- continue
113-
114- # First, try to find tool in config.tools
115- if tool_name in config .tools :
116- tool_def = config .tools [tool_name ]
117- base_class = tool_def .base_class
118-
119- if base_class is None :
120- # Generate default path: sgr_agent_core.tools.{ToolName}
121- # Convert tool_name to ToolName (capitalize first letter, handle underscores)
122- tool_class_name = "" .join (word .capitalize () for word in tool_name .split ("_" ))
123- base_class = f"sgr_agent_core.tools.{ tool_class_name } "
124-
125- # Resolve base_class (can be string, ImportString, or class)
126- if isinstance (base_class , str ):
127- # Try import string resolution
128- if "." in base_class :
129- # Relative import - resolve relative to config file location
130- # This is handled by sys.path in agent_config.from_yaml
131- try :
132- module_parts = base_class .split ("." )
133- if len (module_parts ) >= 2 :
134- module_path = "." .join (module_parts [:- 1 ])
135- class_name = module_parts [- 1 ]
136- module = __import__ (module_path , fromlist = [class_name ])
137- tool_class = getattr (module , class_name )
138- except (ImportError , AttributeError ) as e :
139- logger .warning (
140- f"Failed to import tool '{ tool_name } ' from '{ base_class } ': { e } . " f"Trying registry..."
141- )
142- else :
143- # Try registry
144- tool_class = ToolRegistry .get (base_class )
145- elif isinstance (base_class , type ):
146- # Validate it's a BaseTool subclass
147- if not issubclass (base_class , BaseTool ):
148- raise TypeError (f"Tool '{ tool_name } ' base_class must be a subclass of BaseTool" )
149- tool_class = base_class
150- else :
151- # ImportString - should be resolved by pydantic
152- tool_class = base_class
153- else :
154- # Tool not in config.tools, try registry
155- tool_class = ToolRegistry .get (tool_name )
156-
157- # If still not found, try as import string
158- if tool_class is None :
159- if "." in tool_name :
160- try :
161- module_parts = tool_name .split ("." )
162- if len (module_parts ) >= 2 :
163- module_path = "." .join (module_parts [:- 1 ])
164- class_name = module_parts [- 1 ]
165- module = __import__ (module_path , fromlist = [class_name ])
166- tool_class = getattr (module , class_name )
167- except (ImportError , AttributeError ) as e :
168- error_msg = (
169- f"Tool '{ tool_name } ' not found in config.tools, registry, or failed to import.\n "
170- f"Available tools in config: { ', ' .join (config .tools .keys ())} \n "
171- f"Available tools in registry: "
172- f"{ ', ' .join ([c .__name__ for c in ToolRegistry .list_items ()])} \n "
173- f"Import error: { e } \n "
174- f" - Check that the tool name is correct\n "
175- f" - Define the tool in the 'tools' section of your config\n "
176- f" - Or ensure the tool is registered in ToolRegistry"
177- )
178- logger .error (error_msg )
179- raise ValueError (error_msg ) from e
180-
181- if tool_class is None :
182- error_msg = (
183- f"Tool '{ tool_name } ' not found.\n "
184- f"Available tools in config: { ', ' .join (config .tools .keys ())} \n "
185- f"Available tools in registry: { ', ' .join ([c .__name__ for c in ToolRegistry .list_items ()])} \n "
186- f" - Define the tool in the 'tools' section of your config\n "
187- f" - Or ensure the tool is registered in ToolRegistry"
188- )
189- logger .error (error_msg )
190- raise ValueError (error_msg )
191-
192- tools .append (tool_class )
217+ tools = cls ._resolve_tools (agent_def .tools , config )
218+ tools .extend (mcp_tools )
193219
194220 try :
195221 # Extract agent-specific parameters from agent_def (e.g., working_directory)
0 commit comments