Build an AI Research Agent with Claude
Build a Python CLI agent that uses Claude to autonomously research any topic.
Introduction
30 Second Summary
Every day you spend time searching the web, reading multiple articles, and piecing together information on topics that matter to you. What if you could hand that job to an AI that searches, reads, and summarizes everything into a clean report in seconds?
In this project, you will build an AI research agent powered by Claude that autonomously searches the web for current information and delivers structured research briefs on any topic you give it.
What You'll Build
A Python command-line research agent that takes any topic, searches the web using Claude's built-in tools, and produces a structured report with a summary, key findings, recent developments, and sources.
By the end of this project, you'll have:
- A working AI research agent that searches the web and synthesizes findings into structured reports.
- Hands-on experience with Claude's tool use API, the exact pattern used in production AI systems.
- An interactive multi-turn conversation mode that remembers context and lets you ask follow-up questions.
- Secret Mission: Add a client-side tool that saves research reports to markdown files, learning the full agent loop pattern.
Are there any prerequisites?
You should be comfortable with basic Python (functions, variables, dictionaries, and imports) and know how to run commands in a terminal. No prior experience with AI APIs is needed.
Before We Start
Before We Start...
Set Up Your Project
Before you can build an AI research agent, you need a clean workspace with the right tools installed.
In this step, you'll create a project folder, set up a virtual environment to keep your dependencies isolated, and install the Anthropic Python SDK that connects your code to Claude.
In this step, get ready to:
- Create a project folder and virtual environment.
- Install the Anthropic SDK and set your API key.
- Create the research agent file with its initial setup code.
Create your project folder and install the SDK
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (the backtick key, above Tab).
What is a terminal?
The terminal is a text-based interface where you type commands to create folders, install packages, and run your code. VS Code has one built in so you never have to leave the editor.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir research-agent
cd research-agent
- Confirm you're in the right place by running:
pwd
You should see a path ending in research-agent.
- Create a virtual environment and activate it by running these commands:
python -m venv .venv
.venv\Scripts\activate
What is a virtual environment?
A virtual environment is an isolated space for your project's packages. It prevents conflicts with other Python projects on your machine.
After activation, your terminal prompt shows (.venv) at the beginning. That confirms the virtual environment is active.
Your terminal prompt should now start with (.venv).
Don't see (.venv) in your prompt?
Make sure you ran the activation command .venv\Scripts\activate (with a backslash, not a forward slash).
If PowerShell blocks the script, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned and try activating again.
help me activate my Python virtual environment in PowerShell
- Install the Anthropic SDK by running:
pip install anthropic
You should see output ending with a message like Successfully installed anthropic-....
Install failed or pip not found?
Make sure your virtual environment is active (you see (.venv) in the prompt). If pip is not recognized, try python -m pip install anthropic instead.
help me install the anthropic Python package
Set your API key
The Anthropic SDK needs your API key to authenticate requests to Claude. You'll store it as an environment variable so it stays out of your code.
- Set your API key by running this command (replace your-api-key-here with your actual Anthropic API key):
$env:ANTHROPIC_API_KEY="your-api-key-here"
Why an environment variable?
Environment variables keep secrets out of your source code. If you ever share or publish your code, your API key stays private because it lives in your terminal session, not in your files.
- Verify everything is working by running:
python -c "import anthropic; print(anthropic.__version__)"
You should see a version number printed (such as 0.109.2).
Seeing ModuleNotFoundError?
This means pip install anthropic didn't install into your active environment. Confirm (.venv) shows in your prompt, then run pip install anthropic again.
help me fix ModuleNotFoundError for the anthropic package
Create the research agent file
Now you'll create the Python file where all your agent code will live.
- In VS Code, click File in the top menu bar.
- Click New File.
- Type research_agent.py as the filename and press Enter.
- Save the file into your research-agent folder.
- Add the following code to research_agent.py:
import anthropic
# Create a client that reads your API key from the environment
client = anthropic.Anthropic()
What does this code do?
import anthropic loads the SDK you just installed so your script can talk to Claude.
anthropic.Anthropic() creates a client object that automatically reads ANTHROPIC_API_KEY from your environment. You never have to paste your key into the code.
- Save research_agent.py by pressing Ctrl+S.
- Verify the file exists by running this in your terminal:
ls
You should see research_agent.py listed in the output alongside the .venv folder.
Don't see research_agent.py?
Make sure you saved the file into the research-agent folder (the one you created on your Desktop). If it saved elsewhere, drag it into the correct folder or create it again.
help me find where my file saved in VS Code
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete research_agent.py file at this point:
import anthropic
client = anthropic.Anthropic()
Your environment is ready. Next up, you'll make your first API call to Claude and see how the Messages API works.
Make Your First API Call
Your environment is set up and your client is ready to communicate with Claude. The core of every AI application is the API call: you send a message in, and you get a response back.
In this step, you'll send your first message to Claude, explore the response object structure, and shape Claude's behavior with a system prompt.
In this step, get ready to:
- Send your first message to the Claude API.
- Explore the response object and extract the text.
- Add a system prompt to customize Claude's behavior.
Send your first message to Claude
The Messages API is how you communicate with Claude programmatically. Every API call needs at minimum a model ID, a token limit, and a list of messages.
- In research_agent.py, add the following code below the client = anthropic.Anthropic() line:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "What is quantum computing?"}],
)
print(response)
What does this code do?
- client.messages.create() sends a message to Claude and waits for a response.
- model="claude-sonnet-4-6" tells the API which Claude model to use. Sonnet 4.6 is a great balance of speed and intelligence for research.
- max_tokens=1024 caps how many tokens (roughly words) Claude can generate in its response.
- messages is a list of conversation turns. Each message has a role ("user" or "assistant") and content (the text).
- Save research_agent.py.
- Run the script by entering this command in your terminal:
python research_agent.py
You should see a large response object printed to your terminal with fields like id, model, content, stop_reason, and usage.
Seeing an authentication error?
- Make sure your ANTHROPIC_API_KEY environment variable is still set. If you opened a new terminal, you need to set it again.
- Check that your API key starts with sk-ant- and has no extra spaces.
- Confirm your virtual environment is activated (you should see (.venv) in your terminal prompt).
Why is my API call failing?
Extract the text from the response
The response object contains metadata about the API call. The actual text Claude generated lives inside response.content, which is a list of content blocks.
- In research_agent.py, find the line that says print(response) and add this line directly below it:
print(response.content[0].text)
What does this line do?
response.content is a list of content blocks returned by Claude. For a simple text response, the first block at index [0] contains the text.
Accessing .text on that block gives you just the clean string, without all the metadata surrounding it.
- Save research_agent.py and run it again:
python research_agent.py
Below the large response object, you should now see Claude's answer about quantum computing printed as clean, readable text.
Only seeing the response object?
- Make sure print(response.content[0].text) is on its own line below print(response), not inside the client.messages.create() call.
- Check that there are no typos in .content[0].text. The square brackets and dot notation must be exact.
Help me fix my response.content access pattern.
Add a system prompt
A system prompt tells Claude what role to play and how to behave across all messages in a conversation. This is how you shape Claude into a research assistant rather than a generic chatbot.
- In research_agent.py, add the following code between the client = anthropic.Anthropic() line and the response = client.messages.create( line:
system_prompt = (
"You are a helpful research assistant. Provide clear, accurate information."
)
What is a system prompt?
The system prompt sets Claude's overall behavior for the entire conversation. It runs before any user messages and shapes the tone, focus, and format of responses.
In later steps, you'll expand this prompt to instruct Claude to always search the web and format output with specific sections like Key Findings and Sources.
- Now find the response = client.messages.create( call and add system=system_prompt, as a new parameter. Your API call should now look like this:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": "What is quantum computing?"}],
)
- Save research_agent.py and run it again:
python research_agent.py
Claude's response should now be more structured and informative, reflecting its role as a research assistant.
Getting a parameter error?
- Make sure system=system_prompt is placed before messages= inside the client.messages.create() call.
- Check that system_prompt is defined above the API call, not below it.
- Ensure each parameter ends with a comma.
Why is my system prompt causing an error?
✔️ Awesome, I've got everything!
Great. Double check you've saved research_agent.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete research_agent.py file so far:
import anthropic
client = anthropic.Anthropic()
system_prompt = (
"You are a helpful research assistant. Provide clear, accurate information."
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": "What is quantum computing?"}],
)
print(response)
print(response.content[0].text)
You just made your first API call to Claude and shaped its behavior with a system prompt. Next up, you'll give Claude the ability to search the web for real-time information.
Add Web Search
Your agent can answer questions, but only from what it learned during training. Ask it about something that happened last week and it has no idea.
Claude's web search tool fixes this. It lets Claude search the internet for current information before responding, turning your script from a static chatbot into a research tool that can find real-time data.
In this step, get ready to:
- Give Claude access to web search by adding a tool to your API call.
- Observe the new response structure that includes search-related blocks.
- Write a helper function to extract just the text from Claude's response.
Add the web search tool to your API call
Claude's web search is a server-side tool. This means Anthropic's infrastructure handles the actual searching. You just tell Claude the tool exists, and it decides when to use it.
- In research_agent.py, find your client.messages.create() call and replace it with this updated version:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system_prompt,
# Give Claude access to web search
tools=[{"type": "web_search_20260209", "name": "web_search"}],
# Ask about something current to trigger a search
messages=[{"role": "user", "content": "What are the latest developments in quantum computing?"}],
)
What changed here?
- tools is a new parameter that tells Claude which tools it can use.
- web_search_20260209 is the tool type identifier for Claude's built-in web search.
- The message now asks about "latest developments" instead of a general definition. This encourages Claude to search the web for current information.
Observe the response block types
When Claude uses web search, the response contains several block types instead of just one text block. You need to update your print statements to see this new structure.
- In research_agent.py, find these two print lines at the bottom of your file:
print(response)
print(response.content[0].text)
- Replace both lines with this single print statement:
print(response.content)
- Save research_agent.py and run it:
python research_agent.py
You should see a list of content blocks printed to your terminal. Look for three distinct block types in the output.
What are the block types?
When Claude uses web search, response.content contains a mix of block types:
- server_tool_use is Claude deciding to search the web. This is Claude's internal decision, handled entirely by Anthropic's servers.
- web_search_tool_result contains the actual search results that came back.
- text is Claude's final answer after reading the search results.
You only need the text blocks to display the agent's response to the user. The other blocks are internal plumbing.
Seeing an error about the tool?
- Make sure the tools parameter is inside the client.messages.create() call, not outside it.
- Check that web_search_20260209 is spelled exactly as shown, including the date suffix.
- Verify your ANTHROPIC_API_KEY is still set in this terminal session.
Why isn't my web search tool working?
Write the extract_text() function
Printing the raw response.content is noisy. Your users only care about Claude's final answer, not the search plumbing.
- In research_agent.py, add this function between the client = anthropic.Anthropic() line and the system_prompt variable:
def extract_text(response):
"""Extract text content from a Claude response."""
text_parts = []
# Loop through all content blocks in the response
for block in response.content:
# Only keep blocks that are plain text (skip tool blocks)
if block.type == "text":
text_parts.append(block.text)
return "\n".join(text_parts)
What does this function do?
- It loops through every block in response.content.
- It checks each block's type attribute and only keeps blocks where block.type == "text".
- It joins all the text parts with newlines and returns a clean string.
- This pattern of filtering content blocks is how you cleanly display agent output in any LLM application.
- Now add a second print line at the bottom of your file, below the existing print(response.content) line:
print(extract_text(response))
- Save research_agent.py and run it again:
python research_agent.py
You should see the raw block list first, then a clean, readable research summary below it. The extract_text() output is what your users will actually see.
Only seeing tool blocks, no text?
- Make sure your extract_text() function checks block.type == "text" with a lowercase t.
- Verify the function is defined before the response = client.messages.create() call, not after it.
- If the response takes a while, that is normal. Web search adds a few seconds of latency.
Why is extract_text returning empty results?
✔️ Awesome, I've got everything!
Great. Double check you have saved research_agent.py before moving on.
ⓧ I'd like to double check the full code
import anthropic
client = anthropic.Anthropic()
def extract_text(response):
"""Extract text content from a Claude response."""
text_parts = []
for block in response.content:
if block.type == "text":
text_parts.append(block.text)
return "\n".join(text_parts)
system_prompt = (
"You are a helpful research assistant. Provide clear, accurate information."
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system_prompt,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=[{"role": "user", "content": "What are the latest developments in quantum computing?"}],
)
print(response.content)
print(extract_text(response))
Your agent can now search the web and you have a clean way to display its findings. Next up, you will build an interactive research loop so you can ask follow-up questions and have a real conversation with your agent.
Build the Multi-Turn Research Loop
Your agent can search the web and produce research summaries. But right now it only answers one question, then the script ends.
A real research assistant lets you ask follow-up questions. In this step, you will build an interactive loop that keeps the conversation going and remembers everything you have discussed so far.
In this step, get ready to:
- Create an interactive research loop that accepts user input and responds with web-searched answers.
- Add conversation history so Claude remembers earlier questions and answers.
- Test multi-turn research with follow-up questions.
Create the interactive research loop
The Messages API is stateless. Every time you call it, you must send the full conversation history for Claude to remember context. Your loop will collect user input, grow a history list, and pass that list on every call.
- In research_agent.py, find and delete everything below the extract_text() function. This includes the system_prompt variable, the response call, and both print() lines.
- Replace that code with the following run_research_loop() function and its call at the bottom:
def run_research_loop(client):
"""Run an interactive multi-turn research session."""
system_prompt = (
"You are a research assistant. When given a topic or question, use web search "
"to find current, accurate information. Provide well-structured responses."
)
# Stores every user and assistant message for context
conversation_history = []
print("\n=== AI Research Agent ===")
print("Enter a topic to research, ask follow-up questions, or type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
# Exit commands end the session
if user_input.lower() in ("quit", "exit", "q"):
print("\nResearch session ended. Goodbye!")
break
# Skip empty input
if not user_input:
continue
# Add the new user message to history
conversation_history.append({"role": "user", "content": user_input})
print("\nSearching and analyzing...\n")
# Send the FULL history so Claude remembers earlier turns
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=system_prompt,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=conversation_history,
)
# Extract the text and store the assistant turn in history
assistant_text = extract_text(response)
conversation_history.append({"role": "assistant", "content": response.content})
print(f"Agent:\n{assistant_text}\n")
print("-" * 50 + "\n")
run_research_loop(client)
What does this code do?
- conversation_history is a list that grows with each exchange. It stores every user message and every assistant response as dictionaries with role and content keys.
- The while True loop keeps prompting for input until the user types quit, exit, or q.
- Before calling the API, the user message is appended to history. After the response arrives, the assistant content is appended too. This way the next call includes the full conversation.
- messages=conversation_history passes every previous turn to Claude so it can reference earlier findings when answering follow-ups.
- Save research_agent.py.
Seeing a syntax error?
- Make sure the run_research_loop(client) call at the bottom is NOT indented. It must sit at the module level, outside the function definition.
- Check that the extract_text() function still exists above the new function. It is called inside the loop.
- Verify you deleted ALL of the old standalone code (the old system_prompt, response, and print lines). Leftover fragments cause IndentationError or NameError.
Help me debug a syntax error in my research_agent.py file after adding the run_research_loop function.
Understand how conversation history works
The key insight behind this loop is how the Messages API handles context. Claude does not remember previous calls on its own. Your code is responsible for passing the full history every time.
Why send the full history every time?
The Claude Messages API is stateless. Each request is independent. If you send only the latest question, Claude has no idea what came before.
By appending every user message and assistant response to conversation_history and passing the whole list as messages, you give Claude the full context. This lets it say things like "as I mentioned earlier" or dig deeper into a topic you raised three questions ago.
Why store response.content instead of just the text?
The line conversation_history.append({"role": "assistant", "content": response.content}) stores the full content blocks (including tool use blocks), not just the extracted text. The API expects the exact content structure back in subsequent turns. Storing response.content keeps the format compatible.
Test your multi-turn research agent
- Run your research agent by entering this command in the terminal:
python research_agent.py
- When you see the You: prompt, type a research topic. For example:
What are the latest breakthroughs in nuclear fusion energy?
- Wait for Claude to search the web and respond. You should see a structured answer with current information.
- Ask a follow-up question that references the first answer. For example:
Which of those projects is closest to commercial viability?
- Verify that Claude's follow-up answer references the projects it mentioned in its first response. This confirms conversation history is working.
- Type quit to end the session.
You should see the message Research session ended. Goodbye! and the script exits cleanly.
Claude not remembering your earlier questions?
- Confirm that conversation_history.append() is called BOTH before the API call (for the user message) and after (for the assistant response). Missing either one breaks the chain.
- Check that the messages parameter in client.messages.create() is set to conversation_history, not a new single-message list.
Help me figure out why my research agent is not remembering previous questions in the conversation.
✔️ Awesome, I've got everything!
Great. Make sure you have saved research_agent.py.
ⓧ I'd like to double check the full code
Here is the complete research_agent.py file at this point:
import anthropic
client = anthropic.Anthropic()
def extract_text(response):
"""Extract text content from a Claude response."""
text_parts = []
for block in response.content:
if block.type == "text":
text_parts.append(block.text)
return "\n".join(text_parts)
def run_research_loop(client):
"""Run an interactive multi-turn research session."""
system_prompt = (
"You are a research assistant. When given a topic or question, use web search "
"to find current, accurate information. Provide well-structured responses."
)
conversation_history = []
print("\n=== AI Research Agent ===")
print("Enter a topic to research, ask follow-up questions, or type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("\nResearch session ended. Goodbye!")
break
if not user_input:
continue
conversation_history.append({"role": "user", "content": user_input})
print("\nSearching and analyzing...\n")
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=system_prompt,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=conversation_history,
)
assistant_text = extract_text(response)
conversation_history.append({"role": "assistant", "content": response.content})
print(f"Agent:\n{assistant_text}\n")
print("-" * 50 + "\n")
run_research_loop(client)
Your research agent now holds a full conversation. It searches the web for fresh information on every question and uses the history to give coherent, context-aware follow-ups. Next, you will engineer the system prompt to produce polished, structured research reports.
Engineer the Research Output
Your research agent works, but its output is unpredictable. Sometimes Claude returns a wall of text. Sometimes it gives bullet points. A real research tool needs consistent, scannable output every time.
In this step, you will craft a system prompt that forces structured output and add a single-query mode so users can get instant results without entering the interactive loop.
In this step, get ready to:
- Craft a structured system prompt that enforces consistent research output formatting.
- Add a single-query mode so users can pass a topic as a command-line argument.
- Test both modes to confirm the agent delivers structured research reports.
Craft a structured system prompt
The quality of an AI agent's output depends heavily on its system prompt. A vague prompt gives inconsistent results. A structured prompt that specifies exact sections and formatting gives reliable, professional output every time.
- In research_agent.py, add the following research_topic() function below the extract_text() function:
def research_topic(client, topic):
"""Perform a single research query on a topic."""
system_prompt = (
"You are a research assistant. When given a topic, use web search to find "
"current, accurate information. Then provide a well-structured research brief:\n\n"
"## Summary\nA 2-3 sentence overview of the topic.\n\n"
"## Key Findings\n3-5 bullet points of the most important facts.\n\n"
"## Recent Developments\nWhat's new or changed recently.\n\n"
"## Sources\nList the sources you used.\n\n"
"Always search for information before responding. Be thorough but concise."
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=system_prompt,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=[{"role": "user", "content": f"Research this topic: {topic}"}],
)
return response
What does this code do?
The system_prompt is the key engineering here. It instructs Claude to always search before responding and specifies four exact sections: Summary, Key Findings, Recent Developments, and Sources.
The \n\n characters create line breaks inside the prompt string. Claude reads these as formatting instructions and mirrors that structure in its output.
Now update the system prompt inside run_research_loop() to match the same structured format. The interactive mode needs this too so follow-up questions stay consistent.
- In the run_research_loop() function, find the existing system_prompt assignment:
system_prompt = (
"You are a research assistant. When given a topic or question, use web search "
"to find current, accurate information. Provide well-structured responses."
)
- Replace it with this expanded version that specifies output structure for both initial research and follow-ups:
system_prompt = (
"You are a research assistant. When given a topic or question, use web search "
"to find current, accurate information. Provide well-structured responses with "
"clear headings and bullet points.\n\n"
"For the first question on a new topic, structure your response as:\n"
"## Summary\nA 2-3 sentence overview.\n\n"
"## Key Findings\n3-5 bullet points.\n\n"
"## Recent Developments\nWhat's new or changed.\n\n"
"## Sources\nYour sources.\n\n"
"For follow-up questions, provide focused answers with sources.\n"
"Always search for information before responding to ensure accuracy."
)
Why two different system prompts?
The research_topic() prompt is for one-shot queries where you always want the full structured report. The run_research_loop() prompt distinguishes between first questions (full structure) and follow-ups (focused answers). This gives each mode the appropriate behavior.
- Save research_agent.py.
Seeing a syntax error?
The prompt strings use escaped newlines (\n) inside regular strings. Make sure you have matching opening and closing parentheses for each system_prompt assignment. Each string line must end with a closing quote and the final line with a closing parenthesis.
Help me fix a syntax error in my system prompt string
Add a single-query mode with sys.argv
Right now, the only way to use your agent is to enter the interactive loop. A single-query mode lets users pass a topic directly as a command-line argument and get instant results.
- At the top of research_agent.py, find this line:
import anthropic
- Add import sys on the line below it, so your imports look like this:
import anthropic
import sys
Next, you need to wrap the client creation in a function and restructure how the script starts. This lets both modes share the same client setup.
- Find and delete this line near the top of your file (below the imports):
client = anthropic.Anthropic()
- In its place, add this create_client() function:
def create_client():
"""Create and return an Anthropic client."""
return anthropic.Anthropic()
Why wrap client creation in a function?
A bare client = anthropic.Anthropic() at module level runs the moment the file is imported. Wrapping it in a function gives you control over when the client is created and keeps the module importable without side effects.
Finally, replace the bare function call at the bottom of the file with a proper entry point that checks for command-line arguments.
- Find this line at the very bottom of your file:
run_research_loop(client)
- Replace it with this __main__ block that routes between single-query and interactive mode:
if __name__ == "__main__":
client = create_client()
if len(sys.argv) > 1:
# Single-query mode: topic passed as command-line argument
topic = " ".join(sys.argv[1:])
print(f"\nResearching: {topic}\n")
response = research_topic(client, topic)
print(extract_text(response))
else:
# Interactive mode: multi-turn research conversation
run_research_loop(client)
What does this code do?
- sys.argv is a list of everything the user typed on the command line. sys.argv[0] is always the script name, so sys.argv[1:] captures anything after it.
- If the user runs python research_agent.py quantum computing, then sys.argv[1:] is ["quantum", "computing"]. The " ".join() combines them back into a single topic string.
- If no arguments are passed, the script falls through to the else branch and starts the interactive loop.
- Save research_agent.py.
Seeing a NameError for client?
Make sure you deleted the old client = anthropic.Anthropic() line AND the old run_research_loop(client) line at the bottom. The __main__ block now creates the client via create_client().
Help me fix a NameError in my research agent
✔️ Awesome, I've got everything!
Great. Make sure you have saved research_agent.py before moving on to test both modes.
ⓧ I'd like to double check the full code
Here is the complete research_agent.py file at this point. Compare yours against it:
import anthropic
import sys
def create_client():
"""Create and return an Anthropic client."""
return anthropic.Anthropic()
def extract_text(response):
"""Extract text content from a Claude response."""
text_parts = []
for block in response.content:
if block.type == "text":
text_parts.append(block.text)
return "\n".join(text_parts)
def research_topic(client, topic):
"""Perform a single research query on a topic."""
system_prompt = (
"You are a research assistant. When given a topic, use web search to find "
"current, accurate information. Then provide a well-structured research brief:\n\n"
"## Summary\nA 2-3 sentence overview of the topic.\n\n"
"## Key Findings\n3-5 bullet points of the most important facts.\n\n"
"## Recent Developments\nWhat's new or changed recently.\n\n"
"## Sources\nList the sources you used.\n\n"
"Always search for information before responding. Be thorough but concise."
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=system_prompt,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=[{"role": "user", "content": f"Research this topic: {topic}"}],
)
return response
def run_research_loop(client):
"""Run an interactive multi-turn research session."""
system_prompt = (
"You are a research assistant. When given a topic or question, use web search "
"to find current, accurate information. Provide well-structured responses with "
"clear headings and bullet points.\n\n"
"For the first question on a new topic, structure your response as:\n"
"## Summary\nA 2-3 sentence overview.\n\n"
"## Key Findings\n3-5 bullet points.\n\n"
"## Recent Developments\nWhat's new or changed.\n\n"
"## Sources\nYour sources.\n\n"
"For follow-up questions, provide focused answers with sources.\n"
"Always search for information before responding to ensure accuracy."
)
conversation_history = []
print("\n=== AI Research Agent ===")
print("Enter a topic to research, ask follow-up questions, or type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit", "q"):
print("\nResearch session ended. Goodbye!")
break
if not user_input:
continue
conversation_history.append({"role": "user", "content": user_input})
print("\nSearching and analyzing...\n")
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=system_prompt,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=conversation_history,
)
assistant_text = extract_text(response)
conversation_history.append({"role": "assistant", "content": response.content})
print(f"Agent:\n{assistant_text}\n")
print("-" * 50 + "\n")
if __name__ == "__main__":
client = create_client()
if len(sys.argv) > 1:
topic = " ".join(sys.argv[1:])
print(f"\nResearching: {topic}\n")
response = research_topic(client, topic)
print(extract_text(response))
else:
run_research_loop(client)
Test both modes
Your agent now supports two ways to run. Single-query mode delivers a one-shot structured report. Interactive mode lets you dig deeper with follow-up questions.
- Test single-query mode by running this command in your terminal:
python research_agent.py quantum computing
- Wait for the response. You should see a structured report with clear ## Summary, ## Key Findings, ## Recent Developments, and ## Sources headings.
What should I see?
Claude searches the web, then prints a formatted report. The exact content varies because it pulls live data, but the structure should always follow the four sections you defined in the system prompt. The script exits automatically after printing.
- Now test interactive mode by running the script with no arguments:
python research_agent.py
- You should see the === AI Research Agent === banner and the You: prompt.
- Type a research topic and press Enter. Verify the response uses the structured format.
- Ask a follow-up question about the same topic. Verify that Claude references earlier context and searches for additional information.
- Type quit to exit the session.
Output not structured?
If Claude returns plain text without headings, double check that your system_prompt variable is being passed to the system parameter in the client.messages.create() call. Also verify the prompt text includes the section names (Summary, Key Findings, etc.) with the markdown heading markers.
Help me fix my research agent's output formatting
Your research agent is complete. It searches the web autonomously, delivers consistently structured reports, and supports both quick lookups and deep-dive research sessions. Next up, the Secret Mission will teach you the full client-side tool loop used in production AI agents.
Secret mission
Add a Client-Side Tool
Your research agent can search the web, but what about saving its findings to a file? In this mission, you'll add a client-side tool that lets Claude save research reports to markdown files. This teaches you the full tool_use agent loop: the exact pattern powering every production AI agent in 2026.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs from leaving files on your machine. The Anthropic API only charges when you actively make requests.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The research-agent project folder on your Desktop (contains your Python script, virtual environment, and any saved report files).
- The ANTHROPIC_API_KEY environment variable (set in your PowerShell session).
- Your Anthropic API key (stored in the Anthropic Console).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your research agent.
- Your research-agent folder stays on your Desktop, ready to use anytime.
- Your Anthropic API key remains active. You're only charged when you run the script and make API calls.
- The environment variable resets each time you close PowerShell. You'll need to set it again in a new session.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay intact for later.
- If your research agent is still running in the terminal, type quit to exit the interactive loop.
- Close the terminal in VS Code. This ends the PowerShell session and automatically clears the ANTHROPIC_API_KEY environment variable.
- Your project folder and virtual environment remain on your Desktop. When you come back, reactivate the virtual environment and reset the environment variable.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes your code, virtual environment, and any saved research reports.
- If your research agent is still running, type quit to exit.
- Close the terminal in VS Code to end the session and clear the environment variable.
- Delete the project folder by opening a new terminal and running the command for your operating system below.
macOS
cd ~/Desktop
rm -rf research-agent
Windows
cd ~\Desktop
Remove-Item -Recurse -Force research-agent
- To revoke your API key, sign in to the Anthropic Console and delete the key you created for this project from the API keys management page.
Nice Work!
Nice Work!
You just built a fully functional AI research agent that autonomously searches the web and delivers structured research briefs. This is the exact pattern powering production AI systems across the industry.
You've learned how to:
- Build a working AI research agent that autonomously searches the web and synthesizes findings using Claude's API with tool use.
- Manage multi-turn conversations with history tracking so Claude remembers context across follow-up questions.
- Engineer system prompts that produce structured, consistent output with clear sections and sources.
- Secret Mission: Implemented the full client-side tool use agent loop where Claude requests a tool call, your code executes it, sends the result back, and Claude continues.
Ready to quiz yourself?