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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ LANGSMITH_TRACING=
SUPABASE_KEY=
SUPABASE_URL=
# Should be set to true for a production deployment on Open Agent Platform. Should be set to false otherwise, such as for local development.
GET_API_KEYS_FROM_CONFIG=false
GET_API_KEYS_FROM_CONFIG=false

# Setting it to true ensures that the think_tool tool can execute correctly. Otherwise, there is a very small probability that it will not be executed.
FORCE_THINK_TOOL=False
10 changes: 10 additions & 0 deletions src/open_deep_research/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ class Configuration(BaseModel):
"""Main configuration class for the Deep Research agent."""

# General Configuration
force_think_tool: bool = Field(
default=False,
metadata={
"x_oap_ui_config": {
"type": "boolean",
"default": False,
"description": "Require agents to call think_tool before executing other tools"
}
}
)
max_structured_output_retries: int = Field(
default=3,
metadata={
Expand Down
71 changes: 71 additions & 0 deletions src/open_deep_research/deep_researcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
lead_researcher_prompt,
research_system_prompt,
transform_messages_into_research_topic_prompt,
force_think_tool_supervisor_before_conduct_research_reminder,
force_think_tool_supervisor_after_conduct_research_reminder,
force_think_tool_researcher_after_call_search_tool_reminder,
has_mixed_think_tool_calls_reminder,
)
from open_deep_research.state import (
AgentInputState,
Expand All @@ -50,6 +54,8 @@
openai_websearch_called,
remove_up_to_last_ai_message,
think_tool,
has_mixed_think_tool_calls,
get_previous_tool_name,
)

# Initialize a configurable model that we will use throughout the agent
Expand Down Expand Up @@ -260,6 +266,17 @@ async def supervisor_tools(state: SupervisorState, config: RunnableConfig) -> Co
"research_brief": state.get("research_brief", "")
}
)

# Check if think_tool is mixed with other tools
if has_mixed_think_tool_calls(most_recent_message.tool_calls):
return Command(
goto="supervisor",
update={
"supervisor_messages": [
HumanMessage(content=has_mixed_think_tool_calls_reminder)
]
}
)

# Step 2: Process all tool calls together (both think_tool and ConductResearch)
all_tool_messages = []
Expand All @@ -285,6 +302,33 @@ async def supervisor_tools(state: SupervisorState, config: RunnableConfig) -> Co
if tool_call["name"] == "ConductResearch"
]

# Check if think_tool is called correctly before calling ConductResearch
if configurable.force_think_tool:
previous_tool_name = get_previous_tool_name(supervisor_messages)

# Ensure that think_tool is called correctly before calling ConductResearch
if conduct_research_calls:
if previous_tool_name != "think_tool":
return Command(
goto="supervisor",
update={
"supervisor_messages": [
HumanMessage(content=force_think_tool_supervisor_before_conduct_research_reminder)
]
}
)

# Ensure that think_tool is called correctly after calling ConductResearch
if previous_tool_name == "ConductResearch" and most_recent_message.tool_calls[0]["name"] != "think_tool":
return Command(
goto="supervisor",
update={
"supervisor_messages": [
HumanMessage(content=force_think_tool_supervisor_after_conduct_research_reminder)
]
}
)

if conduct_research_calls:
try:
# Limit concurrent research units to prevent resource exhaustion
Expand Down Expand Up @@ -463,6 +507,17 @@ async def researcher_tools(state: ResearcherState, config: RunnableConfig) -> Co
if not has_tool_calls and not has_native_search:
return Command(goto="compress_research")

# Check if think_tool is mixed with other tools
if has_mixed_think_tool_calls(most_recent_message.tool_calls):
return Command(
goto="researcher",
update={
"researcher_messages": [
HumanMessage(content=has_mixed_think_tool_calls_reminder)
],
}
)

# Step 2: Handle other tool calls (search, MCP tools, etc.)
tools = await get_all_tools(config)
tools_by_name = {
Expand All @@ -472,6 +527,22 @@ async def researcher_tools(state: ResearcherState, config: RunnableConfig) -> Co

# Execute all tool calls in parallel
tool_calls = most_recent_message.tool_calls

# Check if think_tool is called correctly
if configurable.force_think_tool:

previous_tool_name = get_previous_tool_name(researcher_messages)

# Ensure that think_tool is called correctly after calling other tools
if previous_tool_name and previous_tool_name != "think_tool" and tool_calls[0]["name"] != "think_tool":
return Command(
goto="researcher",
update={
"researcher_messages": [
HumanMessage(content=force_think_tool_researcher_after_call_search_tool_reminder)
]
}
)
tool_execution_tasks = [
execute_tool_safely(tools_by_name[tool_call["name"]], tool_call["args"], config)
for tool_call in tool_calls
Expand Down
23 changes: 22 additions & 1 deletion src/open_deep_research/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,25 @@
Remember, your goal is to create a summary that can be easily understood and utilized by a downstream research agent while preserving the most critical information from the original webpage.

Today's date is {date}.
"""
"""



force_think_tool_supervisor_before_conduct_research_reminder = (
"Before delegating any research with ConductResearch you must first call think_tool to "
"plan your approach. Reflect with think_tool, then decide whether to delegate."
)

force_think_tool_supervisor_after_conduct_research_reminder = (
"After calling ConductResearch, you must call think_tool to reflect on the results. "
"Use think_tool now to analyze progress and plan the next action."
)

force_think_tool_researcher_after_call_search_tool_reminder = (
"You must call think_tool to reflect on the latest findings before using other tools. "
"Use think_tool now to analyze progress and plan the next action."
)

has_mixed_think_tool_calls_reminder = (
"You cannot call think_tool in parallel with other tools. Please call think_tool first, then call the other tools."
)
18 changes: 18 additions & 0 deletions src/open_deep_research/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
AIMessage,
HumanMessage,
MessageLikeRepresentation,
ToolMessage,
filter_messages,
)
from langchain_core.runnables import RunnableConfig
Expand Down Expand Up @@ -923,3 +924,20 @@ def get_tavily_api_key(config: RunnableConfig):
return api_keys.get("TAVILY_API_KEY")
else:
return os.getenv("TAVILY_API_KEY")


def has_mixed_think_tool_calls(tool_calls: list[dict]) -> bool:
"""Return True if think_tool is mixed with other tools in the same turn."""
has_think = any(call["name"] == "think_tool" for call in tool_calls)
has_other = any(call["name"] != "think_tool" for call in tool_calls)
return has_think and has_other

def get_previous_tool_name(messages: list[object]) -> str | None:
"""Return the name of the most recent tool message before the latest message. If there is no previous tool message, return None."""
# Skip the most recent message (typically the current AI reply with tool calls)
for i in range(len(messages) - 1, -1, -1):
if isinstance(messages[i], ToolMessage):
return messages[i].name
if isinstance(messages[i], dict) and messages[i].get("type") == "tool":
return messages[i].get("name")
return None