@@ -232,6 +232,38 @@ def _find_first_mcp_error_nested(exc: BaseException) -> McpError | None:
232232 tool .coroutine = wrapped_mcp_coroutine
233233 return tool
234234
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+
235267async def load_mcp_tools (
236268 config : RunnableConfig ,
237269 existing_tool_names : set [str ],
@@ -292,6 +324,13 @@ async def get_all_tools(config: RunnableConfig):
292324 search_api = SearchAPI (get_config_value (configurable .search_api ))
293325 tools .extend (await get_search_tool (search_api ))
294326 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
295334 mcp_tools = await load_mcp_tools (config , existing_tool_names )
296335 tools .extend (mcp_tools )
297336 return tools
0 commit comments