Skip to content

Commit 4f79b3c

Browse files
committed
Add support for passing tools through the Configuration
1 parent d859138 commit 4f79b3c

File tree

2 files changed

+44
-0
lines changed

2 files changed

+44
-0
lines changed

src/open_deep_research/configuration.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ class Configuration(BaseModel):
209209
}
210210
}
211211
)
212+
213+
# Custom tools configuration
214+
custom_tools: Optional[List[Any]] = Field(
215+
default=None
216+
)
212217

213218

214219
@classmethod

src/open_deep_research/utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
235267
async 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

Comments
 (0)