Google ADK: Modular AI Agent Development from Scratch

Welcome to this tutorial on the Google Agent Development Kit (ADK)! 🚀

ADK is a Python framework for building, deploying, and managing sophisticated AI agents. It provides the building blocks to create agents that can reason, use tools, and collaborate as a team.

In this notebook, we'll build a simple but powerful "Agent Team" for a weather assistant. We will cover:

  • Agents & Tools: The core reasoning engines and their capabilities.
  • Agent-to-Agent Communication: How to make agents collaborate and delegate tasks.
  • Session Management: How to manage conversational state and memory.

Step 1: Setup and API Keys

First, let's install the necessary libraries and set up our API keys. We'll use a .env file to securely manage them.

  1. Install the required packages:
    pip install google-adk python-dotenv
    
  2. Create a file named .env in the same directory as this notebook and add your Google API key:
    GOOGLE_API_KEY="YOUR_GEMINI_API_KEY"
    

Now, let's load the key in our notebook.

import os
import asyncio
from dotenv import load_dotenv
import logging

# Load environment variables from .env file
load_dotenv()

GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")

# Configure ADK to use the API key directly
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False"

# Define the default model to use across the notebook
MODEL = "gemini-2.5-flash"

# Suppress verbose SDK warning about non-text parts when tools/structured outputs are present
class _SuppressNonTextParts(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        try:
            msg = record.getMessage()
        except Exception:
            msg = str(record.msg)
        return "non-text parts in the response" not in msg

for _name in ("google", "google.genai"):
    _logger = logging.getLogger(_name)
    _logger.addFilter(_SuppressNonTextParts())

if not GOOGLE_API_KEY:
    print("API key not found. Please create a .env file and add your GOOGLE_API_KEY.")
else:
    print("Successfully loaded API key.")
    print(f"Using model: {MODEL}")
Successfully loaded API key.
Using model: gemini-2.5-flash

Step 2: Define Tools and Specialist Agents

Tools are Python functions that give agents their capabilities. An agent's LLM relies on the function's docstring to understand what the tool does and when to use it, making clear docstrings essential.

A robust approach for complex tasks is to build an Agent Team with multiple, specialized agents. We'll create two specialist agents: one for weather and another for greetings.

from google.adk.agents import Agent
from typing import Optional

# --- Define Tools ---
def get_weather(city: str) -> dict:
    """Retrieves the current weather report for a specified city."""
    print(f"--- Tool: get_weather called for city: {city} ---")
    mock_db = {
        "new york": {"report": "The weather in New York is sunny with a temperature of 25°C."},
        "london": {"report": "It's cloudy in London with a temperature of 15°C."},
    }
    return mock_db.get(city.lower(), {"report": f"Sorry, I don't have weather for {city}."})


def say_hello(name: Optional[str] = None) -> str:
    """Provides a simple friendly greeting."""
    print(f"--- Tool: say_hello called ---")
    return f"Hello, {name}!" if name else "Hello there!"

# --- Define Specialist Agents ---
weather_agent = Agent(
    name="WeatherAgent",
    model=MODEL,
    description=(
        "Provides weather information for specific cities. Use this for any query that mentions weather, "
        "forecast, temperature, rain, sun, or a city/location."
    ),
    instruction=(
        "You are a helpful weather assistant. Always use the get_weather tool to obtain information. "
        "Respond concisely with the weather report."
    ),
    tools=[get_weather],
)

greeting_agent = Agent(
    name="GreetingAgent",
    model=MODEL,
    description=(
        "Handles only pure greetings such as 'hi', 'hello', or 'hey' with no other request. "
        "Do not handle any other topics (e.g., weather)."
    ),
    instruction=(
        "You are the Greeting Agent. Respond ONLY to standalone greetings (hi/hello/hey) with a short greeting. "
        "If the user asks about anything else (e.g., weather, cities, temperatures, or questions), do NOT answer; "
        "leave it to other agents."
    ),
    tools=[say_hello],
)

print("✅ Specialist agents and tools defined.")
✅ Specialist agents and tools defined.

Step 3: Build the Agent Team with a Root Agent

Next, we create a root agent that acts as a manager. This agent receives user requests and, based on its instructions and the description of its sub-agents, decides whether to handle the request itself or delegate it to the appropriate specialist.

By providing a list of sub_agents to the root agent, ADK enables this automatic delegation, which is a key feature for building modular and scalable agent systems.

# The root agent orchestrates the team
root_agent = Agent(
    name="RootAgent",
    model=MODEL,
    instruction=(
        "You are the coordinator of an agent team. Route each user request to the most appropriate sub-agent.\n"
        "Routing rules:\n"
        "- If the user mentions weather, forecast, temperature, a city/location, or asks about conditions, delegate to WeatherAgent.\n"
        "- If the user gives a standalone greeting (hi/hello/hey) with no other request, delegate to GreetingAgent.\n"
        "- Do NOT answer yourself; always delegate to a sub-agent.\n"
        "- Never treat a weather-related question as a greeting."
    ),
    # Key step: Link the sub-agents to enable delegation
    sub_agents=[weather_agent, greeting_agent]
)
print(f"✅ Root Agent '{root_agent.name}' created with sub-agents: {[sa.name for sa in root_agent.sub_agents]}")
✅ Root Agent 'RootAgent' created with sub-agents: ['WeatherAgent', 'GreetingAgent']

Step 4: Run the Agent and Manage the Session

To interact with our agent team, we use a Runner. The runner orchestrates the flow of information and uses a SessionService to manage conversational memory, allowing the agent to remember previous turns. InMemorySessionService is perfect for getting started.

Since ADK's run_async method is asynchronous, we'll use await to execute it in our Jupyter environment.

from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
import sys
import io
import contextlib

runner = Runner(
    agent=root_agent,
    session_service=InMemorySessionService(),
    app_name="weather_tutorial_app"
)

class _FilterStream(io.TextIOBase):
    def __init__(self, underlying, substrings):
        self._underlying = underlying
        self._substrings = tuple(substrings)

    def write(self, s):
        try:
            text = str(s)
        except Exception:
            text = s
        if any(sub in text for sub in self._substrings):
            return len(text)
        return self._underlying.write(text)

    def flush(self):
        return self._underlying.flush()

async def call_agent(query: str, session_id: str):
    """Helper function to run an asynchronous query against the agent."""
    print(f"\n>>> User: {query}")

    # Filter noisy stderr warnings about non-text parts when tools are used
    filter_stream = _FilterStream(sys.stderr, ["non-text parts in the response"])
    with contextlib.redirect_stderr(filter_stream):
        # Use run_async to get events and collect only the final final-response
        final_text = None
        async for event in runner.run_async(
            user_id="user_123",
            session_id=session_id,
            new_message=types.Content(role='user', parts=[types.Part(text=query)])
        ):
            if event.is_final_response() and event.content:
                # capture text but don't print yet to avoid duplicates
                for part in event.content.parts:
                    if getattr(part, "text", None):
                        final_text = part.text
                        break
        if final_text:
            print(f"<<< Agent: {final_text}")

# Let's run a multi-turn conversation!
async def main():
    session_id = "session_abc"
    user_id = "user_123"

    # Ensure the session exists before running; required by InMemorySessionService
    await runner.session_service.create_session(
        app_name="weather_tutorial_app",
        user_id=user_id,
        session_id=session_id,
    )

    print("--- Starting conversation ---")

    # The root agent should delegate this to the GreetingAgent
    await call_agent("Hello there", session_id)

    # The root agent should delegate this to the WeatherAgent
    await call_agent("What is the weather in New York?", session_id)

# In a Jupyter Notebook, you can run the top-level async function with await
await main()
--- Starting conversation ---

>>> User: Hello there
--- Tool: say_hello called ---
<<< Agent: Hello there!

>>> User: What is the weather in New York?
--- Tool: get_weather called for city: New York ---
<<< Agent: The weather in New York is sunny with a temperature of 25°C.

Conclusion

Congratulations! You've successfully built a multi-agent team with the Google ADK.

You've learned the essential concepts of defining tools, creating specialist agents, and using a root agent to delegate tasks. This modular design is fundamental for building more complex and capable agent systems.

From here, you can explore more advanced topics like adding memory with SessionState, implementing safety guardrails with callbacks, or connecting to real-world APIs.