Build a powerful AI agent that makes research reports

Build a powerful AI agent that makes research reports
AI can already analyze stock price data, but what about building research reports?
You may have heard of AI agents and agentic workflows—these are structured steps designed to achieve a specific goal. Each step is connected through specialized prompts that guide the AI’s reasoning and decision-making.
Agents represent the future of AI. Some have said 2025 is the year of agents.
In today’s newsletter, you’ll get Python code to build an AI agent that can take a topic and write a research report.
We’ll ask our agent to write a report on why the announcement of DeepSeek R1 rattled investors in Nvidia on January 27.
There’s a lot of code in today’s newsletter, but you can copy and paste it and use it over and over.
Let's go!
Build a powerful AI agent that makes research reports
AI is transforming how we collect and analyze data, especially in web search and report automation. Using LllamaIndex is a great way to build AI agents to build these reports.
Our system has three agents: ResearchAgent, WriteAgent, and ReviewAgent.
The ResearchAgent gathers information from the web, the WriteAgent compiles the findings into a report, and the ReviewAgent evaluates the report and provides feedback. These agents operate sequentially within the AgentWorkflow class.
To support research and writing, we build and use tools: web_search for gathering information, record_notes for capturing key points, write_report for generating the document, and review_report for quality assessment.
Let's see how it works with Python.
Imports and set up
Import the modules we need from LlamaIndex. We’ll use Tavily to search the internet and dotenv to load API keys from a .env file.
1import os
2from llama_index.llms.openai import OpenAI
3from llama_index.core.workflow import Context
4from llama_index.core.agent.workflow import (
5 FunctionAgent,
6 ReActAgent,
7 AgentWorkflow,
8 AgentInput,
9 AgentOutput,
10 ToolCall,
11 ToolCallResult,
12 AgentStream,
13)
14from tavily import AsyncTavilyClient
15from dotenv import load_dotenv
16
17load_dotenv()
18
19# Make sure your .env file has your OpenAI API key
20llm = OpenAI(model="gpt-4o")
Now we’re ready to build our research agent.
Create a tool to perform web searches
We set up an asynchronous function to perform web searches using the Tavily API. This function will take a query string as input and return search results.
async def search_web(query: str) -> str:
# Make sure your .env file has your Tavily API key
client = AsyncTavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
return str(await client.search(query))
The function is designed to be asynchronous, allowing other tasks to run while waiting for the search results. This will be used as a tool for our agent.
Build tools to record, write, and review notes and reports
We define several asynchronous functions to record notes, write reports, and review them. Each function interacts with a context object to store and retrieve the current state.
1async def record_notes(ctx: Context, notes: str, notes_title: str) -> str:
2 current_state = await ctx.get("state")
3 if "research_notes" not in current_state:
4 current_state["research_notes"] = {}
5 current_state["research_notes"][notes_title] = notes
6 await ctx.set("state", current_state)
7 return "Notes recorded."
8
9async def write_report(ctx: Context, report_content: str) -> str:
10 current_state = await ctx.get("state")
11 current_state["report_content"] = report_content
12 await ctx.set("state", current_state)
13 return "Report written."
14
15async def review_report(ctx: Context, review: str) -> str:
16 current_state = await ctx.get("state")
17 current_state["review"] = review
18 await ctx.set("state", current_state)
19 return "Report reviewed."
The record_notes
function checks if research notes exist in the current state and adds them if not. The write_report
function saves the report content to the state, while the review_report
function saves feedback on the report.
The context object ensures that the state is maintained consistently across different parts of the program.
Build AI agents to handle specific tasks in the workflow
We create three different agents: ResearchAgent, WriteAgent, and ReviewAgent. Each agent has a specific purpose and is equipped with a set of functions it can use.
1research_agent = FunctionAgent(
2 name="ResearchAgent",
3 description="Useful for searching the web for information on a given topic and recording notes on the topic.",
4 system_prompt=(
5 "You are the ResearchAgent that can search the web for information on a given topic and record notes on the topic. "
6 "Once notes are recorded and you are satisfied, you should hand off control to the WriteAgent to write a report on the topic. "
7 "You should have at least some notes on a topic before handing off control to the WriteAgent."
8 ),
9 llm=llm,
10 tools=[search_web, record_notes],
11 can_handoff_to=["WriteAgent"],
12)
13
14write_agent = FunctionAgent(
15 name="WriteAgent",
16 description="Useful for writing a report on a given topic.",
17 system_prompt=(
18 "You are the WriteAgent that can write a report on a given topic. "
19 "Your report should be in a markdown format. The content should be grounded in the research notes. "
20 "Once the report is written, you should get feedback at least once from the ReviewAgent."
21 ),
22 llm=llm,
23 tools=[write_report],
24 can_handoff_to=["ReviewAgent", "ResearchAgent"],
25)
26
27review_agent = FunctionAgent(
28 name="ReviewAgent",
29 description="Useful for reviewing a report and providing feedback.",
30 system_prompt=(
31 "You are the ReviewAgent that can review the write report and provide feedback. "
32 "Your review should either approve the current report or request changes for the WriteAgent to implement. "
33 "If you have feedback that requires changes, you should hand off control to the WriteAgent to implement the changes after submitting the review."
34 ),
35 llm=llm,
36 tools=[review_report],
37 can_handoff_to=["WriteAgent"],
38)
Each agent has a specific role to play in the workflow. The ResearchAgent is responsible for gathering information and recording notes.
The WriteAgent takes these notes and composes a report in markdown format. After writing, it seeks feedback from the ReviewAgent.
The ReviewAgent evaluates the report, providing approval or suggesting changes.
Execute the agent workflow to build the report
We set up an AgentWorkflow to organize the sequence of actions performed by the agents. The workflow starts with the ResearchAgent and maintains a state to track progress.
1agent_workflow = AgentWorkflow(
2 agents=[research_agent, write_agent, review_agent],
3 root_agent=research_agent.name,
4 initial_state={
5 "research_notes": {},
6 "report_content": "Not written yet.",
7 "review": "Review required.",
8 },
9)
10
11handler = agent_workflow.run(
12 user_msg=(
13 "Write me a report on why the announcement of the Deepseek R1 large language model rattled investors in Nvidia. "
14 "Briefly describe what Deepseek R1 is, why it made news globally, and why Nvidia's stock price tanked. "
15 )
16)
17
18current_agent = None
19current_tool_calls = ""
20async for event in handler.stream_events():
21 if (
22 hasattr(event, "current_agent_name")
23 and event.current_agent_name != current_agent
24 ):
25 current_agent = event.current_agent_name
26 print(f"\n{'='*50}")
27 print(f"🤖 Agent: {current_agent}")
28 print(f"{'='*50}\n")
29 elif isinstance(event, AgentOutput):
30 if event.response.content:
31 print("📤 Output:", event.response.content)
32 if event.tool_calls:
33 print(
34 "🛠️ Planning to use tools:",
35 [call.tool_name for call in event.tool_calls],
36 )
37 elif isinstance(event, ToolCallResult):
38 print(f"🔧 Tool Result ({event.tool_name}):")
39 print(f" Arguments: {event.tool_kwargs}")
40 print(f" Output: {event.tool_output}")
41 elif isinstance(event, ToolCall):
42 print(f"🔨 Calling Tool: {event.tool_name}")
43 print(f" With arguments: {event.tool_kwargs}")
The ResearchAgent is set as the starting point, guiding the initial data gathering. The initial state is defined to track the progress of notes, report content, and review status.
The workflow is executed by running it with a user message which asks the agent to perform a specific task.
After this run, you’ll see the agent output it’s status and a report explaining why the DeepSeek R1 announcement tanked Nvidia’s stock 17%.
Your next steps
This code is a framework for report building. Change the user message to request a report on a different topic. Alternatively, consider integrating additional tools for the agents to use.
