Build a UX Research Agent with Gemini
Build an AI agent that searches the web and structures competitor analysis reports.
Introduction
30 Second Summary
Competitor research is one of the most time-consuming parts of UX work. You open dozens of tabs, copy findings into spreadsheets, and by the time you finish, half your data is already outdated.
In this project, you will build a UX Research Agent using Python and the Google Gemini API. Your agent searches the live web for current competitor information, saves structured findings using custom tools you define, and compiles everything into a formatted comparison report.
What You'll Build
You type a research question like "Compare how Figma and Miro handle real-time collaboration" and your agent autonomously searches the web, calls your custom tools to save each insight, and hands you a polished markdown comparison table you can paste straight into a research deck.
By the end of this project, you'll have:
- A working AI agent you can ask any competitor research question, and it pulls live web data instead of relying on outdated training knowledge.
- Hands-on experience with the agentic loop pattern, where you watch the agent decide which tools to call (search, save insights, format reports) without you telling it what to do.
- A structured markdown comparison table as output, ready to paste directly into your research deliverables.
- Secret Mission: Add a scoring tool that lets the agent rate competitors on UX criteria and export the full analysis to a file.
Are there any prerequisites?
None. This guide walks you through installing every tool you need, including Python, a code editor, and your free Gemini API key. A Google account is the only thing you need to bring.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to your UX research workflow.
Make Your First AI Research Query
Your environment is ready. You have Python, Cursor, and the Gemini SDK installed.
Before building a full agent, you need to see how a basic LLM call works. You will ask Gemini a competitor research question and see what happens when the model only has its training data. It cannot access the web, so its answers may be outdated or generic. This is the limitation your agent will solve.
In this step, get ready to:
- Create a Python script that sends a UX research question to the Gemini API.
- Run the script and observe the limitation of training-data-only responses.
Create your research script
The Interactions API is how you talk to Gemini programmatically. You send a prompt, and the model returns a text response. Your script will do exactly that in just a few lines.
- In Cursor, create a new file called researcher.py inside your ux-research-agent folder.
- Paste the following code into researcher.py:
import os
from google import genai
# Initialize the client (reads GEMINI_API_KEY from environment)
client = genai.Client()
interaction = client.interactions.create(
model="gemini-2.5-flash",
input="What are the key differences between Figma and Miro for UX research collaboration in 2026?",
)
print(interaction.output_text)
What does this code do?
- from google import genai imports the Google GenAI SDK you installed in Step 1.
- genai.Client() creates a client that automatically reads your GEMINI_API_KEY environment variable to authenticate.
- client.interactions.create() sends your prompt to the gemini-2.5-flash model and waits for a complete response.
- interaction.output_text gives you the model's text reply as a single string you can print.
- Save researcher.py.
Run the script and observe the limitation
Before you run this, think about what you expect. You are asking Gemini about competitor differences in 2026. The model has no way to search the internet right now. What kind of answer do you think it will give?
- Run your script by entering this command in your terminal:
python3 researcher.py
You should see a multi-paragraph text response printed in your terminal comparing Figma and Miro. The model gives you an answer, but look closely at what it says.
The response is based entirely on training data. It cannot tell you about recent feature launches, current pricing changes, or real user sentiment from this month. It might state things that were true a year ago but are no longer accurate.
Why does this matter?
For real UX research, you need current information. A competitor could have launched a major collaboration feature last week, changed their pricing tier yesterday, or deprecated an entire product line. Training data alone cannot catch this.
This gap between "has knowledge" and "has current knowledge" is exactly what your agent will solve in the next step by adding live web search.
Seeing an error instead of a response?
- If you see an authentication or API key error, your GEMINI_API_KEY environment variable may not be set in this terminal session. Run echo $GEMINI_API_KEY to check. If it prints nothing, re-export it with export GEMINI_API_KEY="your-key-here" using your actual key.
- If you see a ModuleNotFoundError for google, the SDK is not installed in this Python environment. Run pip3 install -U google-genai and try again.
- If the script hangs for more than 30 seconds, the API may be experiencing high load. Press Ctrl+C to cancel, wait a moment, and run it again.
Still stuck? Help me debug my researcher.py script that is failing to call the Gemini API.
✔️ Awesome, I've got everything!
Great. Double check you have saved researcher.py and that it ran successfully with a text response printed in your terminal.
ⓧ I'd like to double check the full code
Here is the complete researcher.py file you should have at this point:
import os
from google import genai
# Initialize the client (reads GEMINI_API_KEY from environment)
client = genai.Client()
interaction = client.interactions.create(
model="gemini-2.5-flash",
input="What are the key differences between Figma and Miro for UX research collaboration in 2026?",
)
print(interaction.output_text)
You have a working connection to the Gemini API. The response you got is useful, but it is stuck in the past. Next up, you will give your script the power to search the live web so it can find current competitor information.
Give Your Agent Custom Research Tools
Your agent can now search the web and return current competitor data with real citations. That is a huge upgrade over static training data.
The problem is that Google Search returns unstructured text. For a real competitor analysis, you need structured findings you can compare across categories. By defining custom Python functions as tools, you let the model decide when to call them and what arguments to pass. This is the core agent pattern.
In this step, get ready to:
- Define two custom tool functions that store research findings in a structured format.
- Build the agentic loop that lets the model call your tools autonomously.
- Run the agent and watch it decide which tools to call on its own.
Define custom tool functions
Your agent needs a way to store individual research insights as it discovers them. You will create two plain Python functions that the model can call whenever it finds something worth saving.
- Replace the entire contents of researcher.py with the following code:
import json
import os
from google import genai
# Initialize the client (reads GEMINI_API_KEY from environment)
client = genai.Client()
# Storage for research findings
findings = []
# --- Custom Tool Functions ---
def save_finding(competitor, category, finding):
findings.append({
"competitor": competitor,
"category": category,
"finding": finding
})
return f"Saved finding for {competitor} under {category}."
def get_all_findings():
if not findings:
return "No findings saved yet."
return json.dumps(findings, indent=2)
What does this code do?
- findings = [] creates a list that acts as in-memory storage for all the insights the agent discovers.
- save_finding() takes three arguments (competitor name, category, and the insight) and appends them as a dictionary to the list.
- get_all_findings() returns the full list as formatted JSON so the model can review what it has collected so far.
- Save researcher.py.
- Test that your functions work by running this command in the terminal:
python3 -c "from researcher import save_finding, get_all_findings; print(save_finding('Figma', 'Pricing', 'Has a free tier')); print(get_all_findings())"
You should see two lines of output: the confirmation message and a JSON array with your test finding.
Seeing an import error?
- Make sure your terminal is in the ux-research-agent folder where researcher.py lives.
- Check that GEMINI_API_KEY is still exported in this terminal session. If you opened a new terminal, re-run the export command from Step 1.
Still stuck? Help me debug this import error when running my researcher.py functions.
Declare your tools for the model
The model does not read your Python code directly. You need to describe each function in a JSON schema so the model knows what tools are available, what arguments they accept, and when to use them.
- Add the following tool declarations below your get_all_findings function in researcher.py:
# --- Tool Declarations ---
tools = [
{"type": "google_search"},
{
"type": "function",
"name": "save_finding",
"description": "Saves a research finding about a competitor. Call this for each specific insight discovered.",
"parameters": {
"type": "object",
"properties": {
"competitor": {"type": "string", "description": "The name of the competitor, e.g. Figma"},
"category": {"type": "string", "description": "The category, e.g. Collaboration Features, Pricing, Target Users"},
"finding": {"type": "string", "description": "The specific finding or insight"}
},
"required": ["competitor", "category", "finding"]
}
},
{
"type": "function",
"name": "get_all_findings",
"description": "Returns all saved research findings.",
"parameters": {"type": "object", "properties": {}}
}
]
What does this code do?
This tools list tells the model what it can do. It keeps google_search for live web access and adds your two custom functions with their parameter schemas.
The model reads the description fields to decide when each tool is appropriate. Good descriptions are the difference between an agent that uses tools well and one that ignores them.
- Save researcher.py and verify the declarations loaded by running:
python3 -c "from researcher import tools; print(f'{len(tools)} tools declared')"
You should see 3 tools declared printed in your terminal.
Seeing a syntax error?
- Check that every opening brace { and bracket [ has a matching close.
- Make sure the tools list is placed after the get_all_findings function and is not indented inside it.
Help me fix this syntax error in my tool declarations.
Build the agentic loop
Now for the core pattern that makes this an agent instead of a one-shot API call. Your code will send a request, check if the model wants to call a function, execute it, send the result back, and repeat until the model is done.
- Add the available functions map and the first part of your run_agent function below the tools list:
# --- Available Functions Map ---
available_functions = {
"save_finding": lambda **kwargs: save_finding(**kwargs),
"get_all_findings": lambda **kwargs: get_all_findings(),
}
# --- Main Agent Loop ---
def run_agent(research_question):
print(f"\nResearching: {research_question}\n")
print("-" * 50)
user_input = [
{"type": "text", "text": "You are a UX Research Agent. Search for competitor information and save key findings using save_finding."},
{"type": "text", "text": research_question}
]
previous_id = None
while True:
interaction = client.interactions.create(
model="gemini-2.5-flash",
input=user_input,
tools=tools,
previous_interaction_id=previous_id,
)
What is the agentic loop?
The available_functions dictionary maps function names (as strings) to your actual Python functions. When the model says "call save_finding", your code looks up that name in this map and executes it.
The while True loop is the heart of the agent. Each iteration sends a request to the model and waits for its response. The previous_interaction_id chains each turn so the model remembers what happened before.
- Add the function-call processing logic directly below the interactions.create call (still inside the while True block):
function_results = []
for step in interaction.steps:
if step.type == "function_call":
func = available_functions[step.name]
result = func(**step.arguments)
print(f"Called {step.name}({step.arguments})")
print(f" -> {str(result)[:100]}...")
print()
function_results.append({
"type": "function_result",
"name": step.name,
"call_id": step.id,
"result": [{"type": "text", "text": json.dumps(result)}],
})
if not function_results:
break
user_input = function_results
previous_id = interaction.id
print("-" * 50)
print("\nFinal Report:\n")
print(interaction.output_text)
# --- Run ---
if __name__ == "__main__":
question = input("Enter your research question: ")
run_agent(question)
How does the loop decide when to stop?
After each model response, the code checks every step for step.type == "function_call". If the model requested any function calls, the code executes them, packages the results, and sends them back for the next turn.
When the model has no more function calls to make (if not function_results: break), the loop exits. The model's final output_text is the summary it produces after using all the tools it needed.
Why is this pattern so important?
This loop (send request, check for tool calls, execute, send results back, repeat) is the exact same pattern behind every production AI agent. Whether it is a customer support agent, a coding assistant, or a data pipeline, the architecture is identical. You are learning the foundation here.
- Save researcher.py.
Before you run this, what do you think will happen? Will the agent call save_finding once, or multiple times?
- Run your agent by executing:
python3 researcher.py
- When prompted, enter: Research Figma and Miro. For each, save findings about their collaboration features, pricing, and target users.
Watch your terminal as the agent works. You should see multiple Called save_finding(...) lines printed as the model autonomously decides to store each insight it discovers. After it finishes calling tools, it prints a final summary report.
Agent not calling any tools?
- Make sure GEMINI_API_KEY is still set in your terminal. Run echo $GEMINI_API_KEY to check.
- Verify the available_functions dictionary is at the module level (not indented inside a function).
- If you see a timeout or rate limit error, wait 60 seconds and try again. The free tier has a rate limit of 500 requests per day.
Help me debug why my agent is not calling save_finding.
✔️ Awesome, I've got everything!
Great. Double check you have saved researcher.py and confirm your agent printed multiple Called save_finding(...) lines followed by a final summary.
ⓧ I'd like to double check the full code
Scroll up and confirm your researcher.py contains each of these sections in order:
- Imports (json, os, genai) and client initialization.
- The findings = [] storage list.
- The save_finding and get_all_findings functions.
- The tools list with 3 entries (google_search, save_finding, get_all_findings).
- The available_functions dictionary.
- The run_agent function with the while True loop.
- The if __name__ == "__main__" block at the bottom.
Your agent is now autonomously searching the web and saving structured findings. Next up, you will add a report generation tool that turns those raw findings into a formatted comparison table ready for your research deliverables.
Generate a Comparison Report
Your agent searches the web and saves structured findings. But right now, you're left reading through a list of individual data points with no synthesis.
A real competitor analysis needs a final step that compiles raw insights into something actionable. In this step, you'll add a report generation tool that formats everything into a markdown comparison table. The agent decides when it has enough data and calls the tool on its own.
In this step, get ready to:
- Add a generate_report tool that formats findings into a markdown comparison table.
- Update the system prompt so the agent follows a complete research workflow.
- Run the agent end-to-end and see a formatted comparison report.
Add the report generation function
Your agent saves individual findings, but it has no way to compile them into a deliverable. The generate_report function takes all saved findings and organizes them into a markdown table with competitors as columns and categories as rows.
- In researcher.py, add the following function below get_all_findings(). Start with the function definition and data extraction:
def generate_report(title):
"""Generates a formatted markdown comparison report from saved findings.
Args:
title: The title for the comparison report.
Returns:
A formatted markdown report string.
"""
if not findings:
return "No findings to report. Save findings first."
# Get unique competitors and categories
competitors = list(dict.fromkeys(f["competitor"] for f in findings))
categories = list(dict.fromkeys(f["category"] for f in findings))
- Continue the function by adding the table-building logic directly below:
# Build markdown table
report = f"# {title}\n\n"
report += "| Category | " + " | ".join(competitors) + " |\n"
report += "|" + "---|" * (len(competitors) + 1) + "\n"
for category in categories:
row = f"| {category} "
for competitor in competitors:
cell_findings = [
f["finding"] for f in findings
if f["competitor"] == competitor and f["category"] == category
]
row += "| " + "; ".join(cell_findings) if cell_findings else "| - "
row += " |\n"
report += row
report += f"\n*Report generated with {len(findings)} findings across {len(competitors)} competitors.*\n"
return report
What does this code do?
- dict.fromkeys(...) extracts unique competitor names and categories from all saved findings while preserving insertion order.
- The nested loop builds a markdown table row by row. Each cell contains all findings for that competitor-category pair, joined with semicolons.
- If no findings exist for a cell, it fills in a dash so the table stays aligned.
Register the tool with the agent
The agent can only call functions it knows about. You need to add a tool declaration (telling the model what the function does) and an entry in the available functions map (so your loop can execute it).
- In the tools list, add this declaration after the get_all_findings entry:
{
"type": "function",
"name": "generate_report",
"description": "Generates a formatted markdown comparison report from all saved findings. Call this after saving all findings.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The title for the comparison report"
}
},
"required": ["title"]
}
}
- In the available_functions dictionary, add this entry after the get_all_findings line:
"generate_report": lambda **kwargs: generate_report(**kwargs),
- Also update the get_all_findings tool description. Find this line in your tools list:
"description": "Returns all saved research findings.",
- Replace it with:
"description": "Returns all saved research findings. Use this to review what has been collected before generating a report.",
Why update this description?
Tool descriptions are how the model understands when to call each function. By mentioning "before generating a report", you hint to the model that it should review findings before creating the final report. These descriptions act as lightweight instructions that guide the agent's decision-making.
- Save researcher.py.
- Run your agent to confirm it still works by running:
python3 researcher.py
- Enter a research question like Compare Figma and Miro for UX research collaboration.
You should see the agent call save_finding multiple times. It might also call generate_report on its own, but without explicit instructions in the system prompt, it may not do so reliably every time.
Seeing an error about generate_report?
- Check that the function name in your available_functions dictionary matches exactly: "generate_report".
- Make sure the tool declaration is inside the tools list with a comma separating it from the previous entry.
- Confirm you have GEMINI_API_KEY still exported in your terminal session. If you opened a new terminal, run the export command again.
Help me debug my generate_report tool registration
Update the system prompt and run the complete agent
Your agent can call generate_report, but it needs clear instructions to follow a structured workflow every time. A system prompt tells the agent exactly what steps to follow: search first, save findings, then generate the report.
- In researcher.py, add this variable above the run_agent function (below the available_functions dictionary):
# --- System Prompt ---
system_prompt = """You are a UX Research Agent specialized in competitor analysis.
When given a research question:
1. Use Google Search to find current, factual information about each competitor.
2. For each key insight you find, call save_finding with the competitor name, a category, and the specific finding.
3. After researching all competitors, call generate_report to create a formatted comparison table.
Be thorough: look for collaboration features, pricing, target users, recent updates, and unique strengths.
Always cite specific details (dates, version numbers, pricing tiers) rather than vague statements."""
What does the system prompt do?
The system prompt gives your agent a repeatable workflow. Step 1 triggers Google Search grounding. Step 2 ensures raw data gets structured via save_finding. Step 3 ensures a formatted deliverable via generate_report.
Without this, the agent might skip steps or respond with unstructured text. The prompt turns an unpredictable chatbot into a reliable research tool.
Now update the run_agent function to use this system prompt instead of the inline string.
- Inside run_agent, find this line in the user_input list:
{"type": "text", "text": "You are a UX Research Agent. Search for competitor information and save key findings using save_finding."},
- Replace it with:
{"type": "text", "text": system_prompt},
- Save researcher.py.
Before you run this, what do you think will be different from the last run? Will the agent reliably call generate_report this time?
- Run the complete agent by running:
python3 researcher.py
- Enter a research question like Compare how Figma and Miro handle real-time collaboration features and pricing in 2026.
You should see the agent call save_finding multiple times as it gathers data, then call generate_report to produce a formatted markdown comparison table with competitors as columns and categories as rows.
Agent not calling generate_report?
- Make sure you replaced the old inline system prompt string with the system_prompt variable reference. The variable must be defined above run_agent so it's accessible inside the function.
- Check that step 3 in your system prompt explicitly mentions calling generate_report. The model follows these numbered instructions closely.
- Try running it again. LLM responses can vary between runs. Two or three attempts usually produce the expected workflow.
Help me get my agent to call generate_report reliably
✔️ Awesome, I've got everything!
Great. Double check you've saved researcher.py and your agent produces a markdown table on each run.
ⓧ I'd like to double check the full code
Here is the complete researcher.py at this point. The full code also includes docstrings on save_finding and get_all_findings. Add these if you haven't already.
Your file has these sections in order:
- Imports and client initialization.
- The findings list and three tool functions (save_finding, get_all_findings, generate_report) each with a docstring.
- The tools list with four entries: google_search, save_finding, get_all_findings, and generate_report.
- The available_functions map with three entries.
- The system_prompt variable.
- The run_agent function using system_prompt in user_input.
- The if __name__ == "__main__" block.
Scroll up and confirm each section is present and matches what you built in this step.
Your agent now runs a complete research workflow: it searches the web for live competitor data, structures findings using custom tools, and delivers a formatted comparison table you could paste into any research document. That's the full agentic loop pattern in action.
Secret mission
Score Competitors and Export to File
Your comparison table shows qualitative findings, but stakeholders want numbers. In this mission, you'll add a scoring tool that lets your agent rate competitors on UX criteria (1-5 scale), then export the full analysis to a shareable markdown file.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine and uses the Gemini API free tier. There are no ongoing costs or cloud resources that will charge you money. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- ux-research-agent project folder on your Desktop (contains researcher.py and competitor_report.md).
- Gemini API key (stored in Google AI Studio).
- google-genai Python package.
- Cursor (code editor application).
- Python 3 installed via Homebrew.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your research agent or want to build more AI projects with the same setup.
- Your ux-research-agent folder stays on your Desktop, ready for the next time you want to run the agent.
- Your Gemini API key remains active in Google AI Studio. It costs nothing on the free tier.
- Python, Homebrew, Cursor, and the google-genai package stay installed for future projects.
- The GEMINI_API_KEY environment variable only lasts for your current terminal session. You will need to re-export it next time you open a new terminal.
Pause - I'll come back to this later
Close your running tools to free up memory. Your files and API key stay intact for when you return.
- Close Cursor.
- Close your Terminal window. This also clears the GEMINI_API_KEY environment variable from memory.
- Your project files remain on your Desktop. When you come back, open the ux-research-agent folder in Cursor and re-export your API key.
Delete - I don't want to use this again
Remove all project resources and tools installed during this project. Follow each step below.
- Delete your project folder by running this command in Terminal:
rm -rf ~/Desktop/ux-research-agent
- Revoke your Gemini API key. Go to aistudio.google.com, click API Keys in the left menu, find the key you created, click the three-dot menu icon next to it, and delete it.
- Uninstall the google-genai package by running:
pip3 uninstall google-genai
- Remove Cursor by opening Finder, navigating to Applications, and dragging Cursor to the Trash.
- Uninstall Python 3 by running:
brew uninstall python
- Uninstall Homebrew by running:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
Keep Python and Homebrew?
Python and Homebrew are useful for many other projects beyond this one. If you plan to do any future coding on your Mac, consider keeping them installed and only deleting the project folder and API key.
Nice Work!
Nice Work!
Nice work! You just built a fully functional AI agent from scratch using Python and the Google Gemini API. Your agent autonomously searches the web, structures findings with custom tools, and produces professional comparison reports.
You've learned how to:
- Build a working AI agent from scratch using Python and the Google Gemini API, sending research questions and receiving structured responses.
- Use Google Search grounding to give your agent access to live web data instead of relying on outdated training data.
- Implement the agentic loop pattern with custom function calling, the same pattern used in production AI agents everywhere.
- Secret Mission: Added quantitative scoring with score_competitor and file export with export_report to generate stakeholder-ready research deliverables.
Ready to quiz yourself?