Lab 12 Solution: Building a Research Assistant with Web Search
Goal
This file contains the complete code for agent.py and main.py in the Research Assistant lab: two agents, called in sequence, since google_search can't share an agent with custom function tools.
research_assistant/agent.py
"""
Research Assistant with Web Grounding
Searches web, extracts key information, and formats a report.
"""
from datetime import datetime
from google.adk import Agent
from google.adk.tools import google_search
# --- Custom Tools ---
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('.')
# Filter for meaningful sentences
facts = [s.strip() for s in sentences if len(s.strip()) > 10][:num_facts]
return {"status": "success", "facts": facts}
# --- Agent 1: Search Specialist ---
# Only google_search -- it cannot be mixed with custom function tools in the
# same agent (a Gemini API restriction: a mixed tools list constructs fine in
# Python but fails at the first real model call with
# `400 INVALID_ARGUMENT: Multiple tools are supported only when they are all
# search tools.`).
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 ---
# Only the custom tools -- no google_search here.
formatter_agent = Agent(
model='gemini-3.5-flash',
name='formatter_agent',
instruction="""
You are a report formatter. The user will give you a topic and some research
findings as plain text.
1. Call extract_key_facts on the findings text to pull out the most important points.
2. Call format_research_notes with the topic and those facts to produce a final report.
Present the final formatted document as your answer, verbatim.
""",
tools=[extract_key_facts, format_research_notes],
)
research_assistant/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:
runner = InMemoryRunner(agent=agent, app_name=app_name)
await runner.session_service.create_session(app_name=app_name, user_id="student", session_id="s1")
final_text = ""
async for event in runner.run_async(
user_id="student",
session_id="s1",
new_message=types.Content(role="user", parts=[types.Part(text=message_text)]),
):
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
final_text = part.text
return final_text
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())
Testing the Solution
- Initialize the project:
uv init research_assistant --python 3.10
cd research_assistant
uv add "google-adk>=2.1.0" python-dotenv - Configure
.envfor Vertex AI. - Run the script:
uv run python main.py
Self-Reflection Answers
-
Why is
google_searchconsidered a "built-in" tool whileformat_research_notesis a "custom" tool?- Answer:
google_searchruns inside Google's own infrastructure, invoked directly by the model with no code of yours executing -- Google built, hosts, and maintains it.format_research_notesis a plain Python function you wrote and control entirely; the ADK just exposes it to the model as a callable tool via its signature and docstring.
- Answer:
-
Our
extract_key_factstool is very simple. How could you make it more robust?- Answer: A better approach would be to use a separate Agent node (perhaps a smaller model like Gemini Flash) to perform semantic extraction from the search results, instead of a naive
.split('.'). That agent could even be folded intoformatter_agentas a third specialist step, since it only needs text in and text out -- no built-in tools involved.
- Answer: A better approach would be to use a separate Agent node (perhaps a smaller model like Gemini Flash) to perform semantic extraction from the search results, instead of a naive
-
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?- Answer: A one-way string handoff can't support a back-and-forth. You'd need
formatter_agentto actually invokeresearch_agentas a tool call, not just receive its output as a static string -- for example, by wrappingresearch_agentas a sub-agent it can transfer control to and back, or by exposing acall_research_agent(question: str)function tool that internally runsresearch_agentand returns its answer. Either way, this pushes you from "sequential composition" into genuine multi-agent orchestration -- which is exactly what Module 15 covers next.
- Answer: A one-way string handoff can't support a back-and-forth. You'd need