Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/open_deep_research/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,19 @@ class Configuration(BaseModel):
}
}
)

custom_tools: Optional[List[Any]] = Field(
default=None
)
custom_tools_prompt: Optional[str] = Field(
default=None,
optional=True,
metadata={
"x_oap_ui_config": {
"type": "text",
"description": "Any additional instructions to pass along to the Agent regarding the custom tools that are available to it."
}
}
)

@classmethod
def from_runnable_config(
Expand Down
51 changes: 20 additions & 31 deletions src/open_deep_research/deep_researcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@
get_api_key_for_model,
get_model_token_limit,
get_notes_from_tool_calls,
get_search_tool,
get_today_str,
is_token_limit_exceeded,
openai_websearch_called,
remove_up_to_last_ai_message,
think_tool,
)

# Initialize a configurable model that we will use throughout the agent
Expand Down Expand Up @@ -179,8 +179,8 @@ async def supervisor(state: SupervisorState, config: RunnableConfig) -> Command[
"""Lead research supervisor that plans research strategy and delegates to researchers.

The supervisor analyzes the research brief and decides how to break down the research
into manageable tasks. It can use think_tool for strategic planning, ConductResearch
to delegate tasks to sub-researchers, or ResearchComplete when satisfied with findings.
into manageable tasks. It can use ConductResearch to delegate tasks to sub-researchers,
or ResearchComplete when satisfied with findings.

Args:
state: Current supervisor state with messages and research context
Expand All @@ -199,7 +199,7 @@ async def supervisor(state: SupervisorState, config: RunnableConfig) -> Command[
}

# Available tools: research delegation, completion signaling, and strategic thinking
lead_researcher_tools = [ConductResearch, ResearchComplete, think_tool]
lead_researcher_tools = [ConductResearch, ResearchComplete]

# Configure model with tools, retry logic, and model settings
research_model = (
Expand All @@ -223,12 +223,11 @@ async def supervisor(state: SupervisorState, config: RunnableConfig) -> Command[
)

async def supervisor_tools(state: SupervisorState, config: RunnableConfig) -> Command[Literal["supervisor", "__end__"]]:
"""Execute tools called by the supervisor, including research delegation and strategic thinking.
"""Execute tools called by the supervisor, including research delegation.

This function handles three types of supervisor tool calls:
1. think_tool - Strategic reflection that continues the conversation
2. ConductResearch - Delegates research tasks to sub-researchers
3. ResearchComplete - Signals completion of research phase
This function handles two types of supervisor tool calls:
1. ConductResearch - Delegates research tasks to sub-researchers
2. ResearchComplete - Signals completion of research phase

Args:
state: Current supervisor state with messages and iteration count
Expand Down Expand Up @@ -261,24 +260,10 @@ async def supervisor_tools(state: SupervisorState, config: RunnableConfig) -> Co
}
)

# Step 2: Process all tool calls together (both think_tool and ConductResearch)
# Step 2: Process all tool calls together
all_tool_messages = []
update_payload = {"supervisor_messages": []}

# Handle think_tool calls (strategic reflection)
think_tool_calls = [
tool_call for tool_call in most_recent_message.tool_calls
if tool_call["name"] == "think_tool"
]

for tool_call in think_tool_calls:
reflection_content = tool_call["args"]["reflection"]
all_tool_messages.append(ToolMessage(
content=f"Reflection recorded: {reflection_content}",
name="think_tool",
tool_call_id=tool_call["id"]
))

# Handle ConductResearch calls (research delegation)
conduct_research_calls = [
tool_call for tool_call in most_recent_message.tool_calls
Expand Down Expand Up @@ -366,8 +351,7 @@ async def researcher(state: ResearcherState, config: RunnableConfig) -> Command[
"""Individual researcher that conducts focused research on specific topics.

This researcher is given a specific research topic by the supervisor and uses
available tools (search, think_tool, MCP tools) to gather comprehensive information.
It can use think_tool for strategic planning between searches.
available tools (search, MCP tools, custom tools) to gather comprehensive information.

Args:
state: Current researcher state with messages and topic context
Expand All @@ -380,14 +364,17 @@ async def researcher(state: ResearcherState, config: RunnableConfig) -> Command[
configurable = Configuration.from_runnable_config(config)
researcher_messages = state.get("researcher_messages", [])

# Get all available research tools (search, MCP, think_tool)
# Get all available research tools (search, MCP, custom tools)
tools = await get_all_tools(config)
if len(tools) == 0:
raise ValueError(
"No tools found to conduct research: Please configure either your "
"search API or add MCP tools to your configuration."
)

search_tool = await get_search_tool(search_api=configurable.search_api)
search_tool_prompt = f"- **{search_tool[0].name}**: For conducting web searches to gather information" if search_tool else ""

# Step 2: Configure the researcher model with tools
research_model_config = {
"model": configurable.research_model,
Expand All @@ -398,7 +385,9 @@ async def researcher(state: ResearcherState, config: RunnableConfig) -> Command[

# Prepare system prompt with MCP context if available
researcher_prompt = research_system_prompt.format(
custom_tools_prompt=configurable.custom_tools_prompt or "",
mcp_prompt=configurable.mcp_prompt or "",
search_tool_prompt=search_tool_prompt,
date=get_today_str()
)

Expand Down Expand Up @@ -433,12 +422,12 @@ async def execute_tool_safely(tool, args, config):


async def researcher_tools(state: ResearcherState, config: RunnableConfig) -> Command[Literal["researcher", "compress_research"]]:
"""Execute tools called by the researcher, including search tools and strategic thinking.
"""Execute tools called by the researcher, including search tools, MCP and custom tools.

This function handles various types of researcher tool calls:
1. think_tool - Strategic reflection that continues the research conversation
2. Search tools (tavily_search, web_search) - Information gathering
3. MCP tools - External tool integrations
1. Search tools (tavily_search, web_search) - Information gathering
2. MCP tools - External tool integrations
3. Custom tools - Passed with the configuration
4. ResearchComplete - Signals completion of individual research task

Args:
Expand Down
33 changes: 6 additions & 27 deletions src/open_deep_research/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,14 @@
<Task>
Your focus is to call the "ConductResearch" tool to conduct research against the overall research question passed in by the user.
When you are completely satisfied with the research findings returned from the tool calls, then you should call the "ResearchComplete" tool to indicate that you are done with your research.
Research sub-agents have access to a wide range of tools to gather information and conduct research.
</Task>

<Available Tools>
You have access to three main tools:
You have access to these main tools:
1. **ConductResearch**: Delegate research tasks to specialized sub-agents
2. **ResearchComplete**: Indicate that research is complete
3. **think_tool**: For reflection and strategic planning during research

**CRITICAL: Use think_tool before calling ConductResearch to plan your approach, and after each ConductResearch to assess progress. Do not call think_tool with any other tools in parallel.**
</Available Tools>

<Instructions>
Expand All @@ -104,22 +103,11 @@
**Task Delegation Budgets** (Prevent excessive delegation):
- **Bias towards single agent** - Use single agent for simplicity unless the user request has clear opportunity for parallelization
- **Stop when you can answer confidently** - Don't keep delegating research for perfection
- **Limit tool calls** - Always stop after {max_researcher_iterations} tool calls to ConductResearch and think_tool if you cannot find the right sources
- **Limit tool calls** - Always stop after {max_researcher_iterations} tool calls to ConductResearch if you cannot find the right sources

**Maximum {max_concurrent_research_units} parallel agents per iteration**
</Hard Limits>

<Show Your Thinking>
Before you call ConductResearch tool call, use think_tool to plan your approach:
- Can the task be broken down into smaller sub-tasks?

After each ConductResearch tool call, use think_tool to analyze the results:
- What key information did I find?
- What's missing?
- Do I have enough to answer the question comprehensively?
- Should I delegate more research or call ResearchComplete?
</Show Your Thinking>

<Scaling Rules>
**Simple fact-finding, lists, and rankings** can use a single sub-agent:
- *Example*: List the top 10 coffee shops in San Francisco → Use 1 sub-agent
Expand All @@ -143,12 +131,11 @@
</Task>

<Available Tools>
You have access to two main tools:
1. **tavily_search**: For conducting web searches to gather information
2. **think_tool**: For reflection and strategic planning during research
You have access to these main tools:
{search_tool_prompt}
{custom_tools_prompt}
{mcp_prompt}

**CRITICAL: Use think_tool after each search to reflect on results and plan next steps. Do not call think_tool with the tavily_search or any other tools. It should be to reflect on the results of the search.**
</Available Tools>

<Instructions>
Expand All @@ -172,14 +159,6 @@
- You have 3+ relevant examples/sources for the question
- Your last 2 searches returned similar information
</Hard Limits>

<Show Your Thinking>
After each search tool call, use think_tool to analyze the results:
- What key information did I find?
- What's missing?
- Do I have enough to answer the question comprehensively?
- Should I search more or provide my answer?
</Show Your Thinking>
"""


Expand Down
70 changes: 38 additions & 32 deletions src/open_deep_research/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,37 +212,6 @@ async def summarize_webpage(model: BaseChatModel, webpage_content: str) -> str:
logging.warning(f"Summarization failed with error: {str(e)}, returning original content")
return webpage_content

##########################
# Reflection Tool Utils
##########################

@tool(description="Strategic reflection tool for research planning")
def think_tool(reflection: str) -> str:
"""Tool for strategic reflection on research progress and decision-making.

Use this tool after each search to analyze results and plan next steps systematically.
This creates a deliberate pause in the research workflow for quality decision-making.

When to use:
- After receiving search results: What key information did I find?
- Before deciding next steps: Do I have enough to answer comprehensively?
- When assessing research gaps: What specific information am I still missing?
- Before concluding research: Can I provide a complete answer now?

Reflection should address:
1. Analysis of current findings - What concrete information have I gathered?
2. Gap assessment - What crucial information is still missing?
3. Quality evaluation - Do I have sufficient evidence/examples for a good answer?
4. Strategic decision - Should I continue searching or provide my answer?

Args:
reflection: Your detailed reflection on research progress, findings, gaps, and next steps

Returns:
Confirmation that reflection was recorded for decision-making
"""
return f"Reflection recorded: {reflection}"

##########################
# MCP Utils
##########################
Expand Down Expand Up @@ -446,6 +415,38 @@ def _find_mcp_error_in_exception_chain(exc: BaseException) -> McpError | None:
tool.coroutine = authentication_wrapper
return tool

def load_custom_tools(config: RunnableConfig, existing_tool_names: set[str]) -> List[BaseTool]:
"""Load custom Python functions as tools"""
configurable = Configuration.from_runnable_config(config)
if not configurable.custom_tools:
return []

tools = []
for tool_func in configurable.custom_tools:
# Check if tool already exists
tool_name = getattr(tool_func, 'name', str(tool_func))
if tool_name in existing_tool_names:
warnings.warn(f"Tool {tool_name} already exists, skipping")
continue

# Ensure it's a proper LangChain tool
if isinstance(tool_func, BaseTool):
tools.append(tool_func)
elif callable(tool_func):
# If it's a callable but not a BaseTool, wrap it
try:
# If it's already decorated with @tool, it should be a BaseTool
if hasattr(tool_func, 'name') and hasattr(tool_func, 'description'):
tools.append(tool_func)
else:
warnings.warn(f"Tool {tool_name} is not properly decorated with @tool decorator, skipping")
except Exception as e:
warnings.warn(f"Error processing custom tool {tool_name}: {e}")
else:
warnings.warn(f"Invalid tool type for {tool_name}, must be callable or BaseTool")

return tools

async def load_mcp_tools(
config: RunnableConfig,
existing_tool_names: set[str],
Expand Down Expand Up @@ -576,7 +577,7 @@ async def get_all_tools(config: RunnableConfig):
List of all configured and available tools for research operations
"""
# Start with core research tools
tools = [tool(ResearchComplete), think_tool]
tools = [tool(ResearchComplete)]

# Add configured search tools
configurable = Configuration.from_runnable_config(config)
Expand All @@ -590,6 +591,11 @@ async def get_all_tools(config: RunnableConfig):
for tool in tools
}

# Add custom tools
custom_tools = load_custom_tools(config, existing_tool_names)
tools.extend(custom_tools)
existing_tool_names.update({tool.name for tool in custom_tools if hasattr(tool, "name")})

# Add MCP tools if configured
mcp_tools = await load_mcp_tools(config, existing_tool_names)
tools.extend(mcp_tools)
Expand Down