Chapter 09
Deep Research
Deep Research
One of the classic cross-business Agentic use cases! This is huge.
from agents import Agent, WebSearchTool, trace, Runner, function_tool
from agents.model_settings import ModelSettings
from pydantic import BaseModel, Field
from dotenv import load_dotenv
import asyncio
from IPython.display import display, Markdown
from messenger import send_email, pushload_dotenv(override=True)# Constants
MODEL_NAME = "gpt-5.4-mini"
USE_EMAIL = True
HOW_MANY_SEARCHES = 5Strategy for the Deep Research Agent
We are going to do it the bulletproof way.
We are going to orchestrate with code: separate calls to Runner.run() for each step in the process.
We will use Structured Outputs at each point.
We will build 4 Agents:
- The Search Agent: searches the web for information
- The Planner Agent: given a question, comes up with a list of searches that should be made
- The Writer Agent: writes a robust report
- The Emailer Agent: crafts and sends an email
And then 4 python functions, 1 to call Runner.run() for each of the 4 agents.
Agent 1: The Search Agent
OpenAI Hosted Tools
https://openai.github.io/openai-agents-python/tools/#hosted-tools
A paid, quick approach to carrying out managed functionality on OpenAI's cloud.
Their docs surface these tools, but it's worth keeping in mind that they're costly and lock you in to the OpenAI ecosystem.
OpenAI offers the following hosted tools:
WebSearchTool lets an agent search the web.
FileSearchTool allows retrieving information from your OpenAI Vector Stores.
CodeInterpreterTool lets the LLM execute code in a sandboxed environment.
HostedMCPTool exposes a remote MCP server's tools to the model.
ImageGenerationTool generates images from a prompt.
ToolSearchTool lets the model load deferred tools, namespaces, or hosted MCP servers on demand.
Important note - API charge of WebSearchTool
This currently costs 1 cent per call for OpenAI WebSearchTool. That can add up to about $1 for the next 2 labs. We'll use free and low cost Search tools with other platforms, so feel free to skip running this if the cost is a concern. Also student Christian W. pointed out that OpenAI can sometimes charge for multiple searches for a single call, so it could sometimes cost more than 1 cent per call.
Costs are in the Tools section here: https://developers.openai.com/api/docs/pricing
INSTRUCTIONS = """
You are a research assistant. Given a search term, you search the web for that term and
produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 words.
Capture the main points and be succinct. Reply only with the summary.
"""
task = "Most popular AI Agent frameworks in 2026"
settings = ModelSettings(tool_choice="required")
tools = [WebSearchTool()]search_agent = Agent(name="Search Agent", instructions=INSTRUCTIONS, tools=tools, model=MODEL_NAME, model_settings=settings)result = await Runner.run(search_agent, task)
display(Markdown(result.final_output))As always, take a look at the trace
Agent 2: The Planner Agent
We will now use Structured Outputs, and include a description of the fields
class WebSearchItem(BaseModel):
reason: str = Field(description="Your reasoning for why this search is important to the query.")
query: str = Field(description="The search term to use for the web search.")
class WebSearchPlan(BaseModel):
searches: list[WebSearchItem] = Field(description="A list of web searches to perform to best answer the query.")WebSearchPlan.model_json_schema()# See note above about cost of WebSearchTool
INSTRUCTIONS = f"""
You are a research assistant. Given a user query, come up with a set of web searches
to perform to best answer the query. Output {HOW_MANY_SEARCHES} terms to query for.
"""
planner_agent = Agent(name="Planner Agent", instructions=INSTRUCTIONS, model=MODEL_NAME, output_type=WebSearchPlan)
result = await Runner.run(planner_agent, task)
result.final_outputAgent 3: The Writer Agent
INSTRUCTIONS = """
You are a senior researcher tasked with writing a cohesive report for a research query.
You will be provided with the original query, and some research.
Generate a comprehensive report based on the research and the query.
The final output should be in markdown format, and it should be lengthy and detailed. Aim
for 5-10 pages of content, at least 1000 words.
"""
class ReportData(BaseModel):
short_summary: str = Field(description="A short 2-3 sentence summary of the findings.")
markdown_report: str = Field(description="The final report")
follow_up_questions: list[str] = Field(description="Suggested topics to research further")
writer_agent = Agent(name="Writer Agent", instructions=INSTRUCTIONS, model=MODEL_NAME, output_type=ReportData)Agent 4: The email agent
@function_tool
def send_email_tool(subject: str, text_body: str, html_body: str) -> str:
"""
Send out an email with the given subject and body to all sales prospects
Args:
subject: The subject of the email
text_body: The body of the email as plain text
html_body: The HTML body of the email
"""
if USE_EMAIL:
send_email(subject, text_body, html_body)
else:
push(f"Subject: {subject}\n\n{text_body}")
return "Email sent successfully"send_email_tool.params_json_schemaINSTRUCTIONS = """
You are provided with a detailed report. Use your tool to send an email, converting the report into
a clean, well presented HTML email with an appropriate subject line.
"""
email_agent = Agent(name="Email Agent", instructions=INSTRUCTIONS, tools=[send_email_tool], model=MODEL_NAME)Now to Orchestrate by Code
The next 2 functions will plan and execute the search, using the Agents, with calls to Runner.run()
async def run_searches(query: str):
print("Planning searches...")
result = await Runner.run(planner_agent, f"Query: {query}")
searches = result.final_output.searches
print(f"Will perform {len(searches)} searches")
tasks = [search(item) for item in searches]
results = await asyncio.gather(*tasks)
print("Finished searching")
return results
async def search(item: WebSearchItem):
input_message = f"Search term: {item.query}\nReason for searching: {item.reason}"
result = await Runner.run(search_agent, input_message)
return result.final_outputThe next 2 functions write a report and email it
async def write_report(query: str, search_results: list[str]):
print("Thinking about report...")
input_message = f"Original query: {query}\nSummarized search results: {search_results}"
result = await Runner.run(writer_agent, input_message)
print("Finished writing report")
return result.final_output
async def send_report_email(report: ReportData):
print("Writing email...")
result = await Runner.run(email_agent, report.markdown_report)
print("Email sent")
return result.final_outputShowtime!
query ="Most popular AI Agent frameworks in 2026"
with trace("Research trace"):
print("Starting research...")
search_results = await run_searches(query)
report = await write_report(query, search_results)
await send_report_email(report)
print("Hooray!")