Lab 12: Building a Research Assistant with Web Search Challenge
Goal
In this lab, you will build a Research Assistant that can access up-to-date information from the internet and process it using custom Python logic. Since google_search can't share an agent with custom function tools (a Gemini API restriction, not an ADK one -- see the README), you'll build this as two agents called in sequence: a search specialist, then a formatter that receives its output.
Prerequisites
- Agent Platform: While
google_searchcan work with AI Studio keys, the ADK standardizes on Agent Platform for grounding in enterprise scenarios. Ensure your.envis configured correctly (refer to Module 2).
Step 1: Create and Prepare the Project
We will use the uv workflow to initialize our research project.
-
Initialize the project:
uv init research_assistant --python 3.10
cd research_assistant
uv add "google-adk>=2.1.0" python-dotenv -
Configure Authentication: Ensure your
.envfile has your project ID and location set for Agent Platform.
Step 2: Define the Two Agents
Exercise: Create agent.py. The custom tools and the research_agent are provided below. Your task is to complete formatter_agent.
# In agent.py
from datetime import datetime
from google.adk import Agent
from google.adk.tools import google_search
# --- Custom Tools (Provided) ---
def format_research_notes(topic: str, findings: str) -> dict:
"""Formats research findings into a structured document."""
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
document = f"""
# Research Report: {topic}
Generated: {timestamp}
## Findings
{findings}
""".strip()
return {"status": "success", "document": document}
def extract_key_facts(text: str, num_facts: int = 5) -> dict:
"""Extracts key sentences from a block of text."""
sentences = text.split('.')
facts = [s.strip() for s in sentences if len(s.strip()) > 10][:num_facts]
return {"status": "success", "facts": facts}
# --- Agent 1: Search Specialist (Provided) ---
# Only google_search -- it cannot be mixed with the custom tools below.
research_agent = Agent(
model='gemini-3.5-flash',
name='research_agent',
instruction=(
"You are a research assistant. Use google_search to find current "
"information on the topic you're given, then summarize the key "
"findings in a few plain-text sentences."
),
tools=[google_search],
)
# --- Agent 2: Formatter ---
# TODO: Define `formatter_agent`.
# 1. Use 'gemini-3.5-flash'.
# 2. Add `extract_key_facts` and `format_research_notes` to tools (do NOT
# add google_search here -- that's the whole point of splitting these up).
# 3. Write an instruction telling it to: first call extract_key_facts on the
# findings text it's given, then call format_research_notes with the
# topic and those facts, then present the final document as its answer.
formatter_agent = Agent(...)
Step 3: Orchestrate the Two Agents
Since these are two separate agents, you need a small script to run one after the other, passing the first agent's output as the second agent's input -- exactly like the programmatic execution pattern from Module 6.
Exercise: Create main.py and complete the run_agent TODO.
# In main.py
import asyncio
from google.adk.runners import InMemoryRunner
from google.genai import types
from agent import research_agent, formatter_agent
async def run_agent(agent, app_name: str, message_text: str) -> str:
# TODO: Implement this helper:
# 1. Create an InMemoryRunner for `agent`.
# 2. Create a session (user_id="student", session_id="s1" is fine).
# 3. Call run_async with a user message built from `message_text`.
# 4. Return the text of the final event you see (there's no need to
# check is_final_response() here -- just keep the latest text seen).
...
async def main():
topic = "the latest AI developments from Google"
findings = await run_agent(research_agent, "research_app", f"Research this topic: {topic}")
print("--- RESEARCH FINDINGS ---")
print(findings)
report = await run_agent(formatter_agent, "formatter_app", f"Topic: {topic}\n\nFindings: {findings}")
print("\n--- FINAL REPORT ---")
print(report)
if __name__ == "__main__":
asyncio.run(main())
Run it with:
uv run python main.py
Observe the output: the first agent grounds itself in real web results beyond its training data; the second agent never touches google_search at all, only your custom tools -- yet the final report still incorporates the first agent's findings, because you're the one passing the data between them.
Having Trouble?
If you get stuck, you can find the complete, working code in the lab-solution.md file.
Lab Summary
You have successfully built a two-agent pipeline that bridges the knowledge gap of LLMs using built-in grounding tools. You have learned:
- How to easily enable web search using the
google_searchbuilt-in tool. - Why
google_searchcan't share an agent with custom function tools, and how to work around that with sequential composition instead. - How to write instructions that guide each agent through its own focused part of a larger research-and-formatting workflow.
Self-Reflection Questions
- Why is
google_searchconsidered a "built-in" tool whileformat_research_notesis a "custom" tool? - What are the benefits of having the model perform the search inside its own environment rather than you writing a Python script to scrape Google results?
main.pypassesfindingsbetween the two agents as a plain string. What would you have to change if you instead wantedformatter_agentto be able to askresearch_agentfollow-up questions?
🕵️ Hidden Solution 🕵️
Looking for the solution? Here's a hint (Base64 decode me):
L2RvYy1hZGstdHJhaW5pbmcvbW9kdWxlMTItYnVpbHQtaW4tdG9vbHMtZ3JvdW5kaW5nL2xhYi1zb2x1dGlvbg==
The direct link is: Lab Solution