@@ -232,6 +232,38 @@ def _find_first_mcp_error_nested(exc: BaseException) -> McpError | None:
232
232
tool .coroutine = wrapped_mcp_coroutine
233
233
return tool
234
234
235
+ def load_custom_tools (config : RunnableConfig , existing_tool_names : set [str ]) -> List [BaseTool ]:
236
+ """Load custom Python functions as tools"""
237
+ configurable = Configuration .from_runnable_config (config )
238
+ if not configurable .custom_tools :
239
+ return []
240
+
241
+ tools = []
242
+ for tool_func in configurable .custom_tools :
243
+ # Check if tool already exists
244
+ tool_name = getattr (tool_func , 'name' , str (tool_func ))
245
+ if tool_name in existing_tool_names :
246
+ warnings .warn (f"Tool { tool_name } already exists, skipping" )
247
+ continue
248
+
249
+ # Ensure it's a proper LangChain tool
250
+ if isinstance (tool_func , BaseTool ):
251
+ tools .append (tool_func )
252
+ elif callable (tool_func ):
253
+ # If it's a callable but not a BaseTool, wrap it
254
+ try :
255
+ # If it's already decorated with @tool, it should be a BaseTool
256
+ if hasattr (tool_func , 'name' ) and hasattr (tool_func , 'description' ):
257
+ tools .append (tool_func )
258
+ else :
259
+ warnings .warn (f"Tool { tool_name } is not properly decorated with @tool decorator, skipping" )
260
+ except Exception as e :
261
+ warnings .warn (f"Error processing custom tool { tool_name } : { e } " )
262
+ else :
263
+ warnings .warn (f"Invalid tool type for { tool_name } , must be callable or BaseTool" )
264
+
265
+ return tools
266
+
235
267
async def load_mcp_tools (
236
268
config : RunnableConfig ,
237
269
existing_tool_names : set [str ],
@@ -292,6 +324,13 @@ async def get_all_tools(config: RunnableConfig):
292
324
search_api = SearchAPI (get_config_value (configurable .search_api ))
293
325
tools .extend (await get_search_tool (search_api ))
294
326
existing_tool_names = {tool .name if hasattr (tool , "name" ) else tool .get ("name" , "web_search" ) for tool in tools }
327
+
328
+ # Add custom tools
329
+ custom_tools = load_custom_tools (config , existing_tool_names )
330
+ tools .extend (custom_tools )
331
+ existing_tool_names .update ({tool .name for tool in custom_tools if hasattr (tool , "name" )})
332
+
333
+ # Keep MCP tools last
295
334
mcp_tools = await load_mcp_tools (config , existing_tool_names )
296
335
tools .extend (mcp_tools )
297
336
return tools
0 commit comments