A single LLM agent works well until the job spans several distinct skills. The proven pattern is a coordinator agent that delegates to specialized sub-agents — much like a manager routing work to departments. Google’s Agent Development Kit (ADK) is a code-first Python framework built for exactly this: you define agents and plain-Python tools, wire specialists under a coordinator, and ADK handles the LLM-driven delegation between them.

In this tutorial you will build a travel concierge: a coordinator agent that routes weather questions to a weather specialist and currency questions to a currency specialist, each backed by its own tool. You will run it in the terminal and the ADK web UI, then cover the deterministic parts (the tools and the agent wiring) with pytest, no API key required.

Inspired by Google’s “Intro to multi-agent systems with ADK.” This article uses the current google-adk Python package and the Gemini API.

Prerequisites

  • Python 3.10 or newer (ADK requires 3.10+). Check with python3 --version.
  • uv 0.5 or newer — the package and project manager used throughout. Install it from docs.astral.sh/uv, then verify with uv --version.
  • A Gemini API key from Google AI Studio (a free tier is available). You will place it in a git-ignored .env file.
  • Familiarity with Python functions and type hints. ADK reads a tool function’s signature and docstring to tell the model how to call it.

We use uv (not pip) for every dependency and run step. If you have not used uv before, the only commands you need appear inline in each step.

Step 1: Scaffold the project and lock down hygiene

Create the project workspace and a .gitignore before any other files. This is critical here because the project stores a secret API key in .env, which must never be committed.

Create the files

mkdir -p travel-concierge
cd travel-concierge
touch .gitignore

Add the code: .gitignore

# Secrets — never commit the API key
.env

# ADK runtime artifacts (session DB created by `adk run`/`adk web`)
.adk/

# Python
__pycache__/
*.py[cod]
.venv/

# uv
.uv/

# Tooling caches
.pytest_cache/
.ruff_cache/
.mypy_cache/

# OS / editor noise
.DS_Store
*.log

Detailed breakdown

  • .env is listed first and on its own line because it holds your GOOGLE_API_KEY. Ignoring it before the file exists guarantees the secret can never land in a commit.
  • .adk/ is a per-agent directory ADK creates the first time you run adk run or adk web (it holds a local session database). It is a runtime artifact, so it is ignored rather than committed.
  • __pycache__/ and *.py[cod] keep compiled bytecode out of version control — it is regenerated on every run and is machine-specific.
  • .venv/ is the virtual environment uv creates; it is reproducible from the lockfile, so it should never be committed.
  • .pytest_cache/ is written by pytest between runs; the others appear only if you add those tools later, but ignoring them now prevents accidental commits.

Step 2: Initialize the project with uv

Turn the folder into a managed uv project. This generates a pyproject.toml and a pinned uv.lock.

Create the file

uv init --name travel-concierge --python 3.10

uv init creates pyproject.toml, a sample main.py (which you will not use), and a README.md. Delete the sample file:

rm -f main.py

Add the code: pyproject.toml (generated, shown for reference)

[project]
name = "travel-concierge"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []

uv init creates an application-style project, so there is no [build-system] block — you do not need one to run the agents.

Detailed breakdown

  • requires-python = ">=3.10" matches ADK’s minimum and is enforced by uv when it resolves dependencies, so contributors on older Pythons fail fast.
  • dependencies = [] is empty for now; the next step fills it via uv add rather than hand-editing, which keeps pyproject.toml and uv.lock in sync.
  • readme = "README.md" points at the file uv init generated; the agent package and tests you add by hand in later steps.

Step 3: Add the ADK dependency

Install Google’s Agent Development Kit. The PyPI package is google-adk.

Install the dependency

uv add google-adk

Detailed breakdown

  • uv add google-adk pulls in ADK and its dependencies, including the google-genai SDK used to talk to Gemini. After it runs, google-adk appears under [project].dependencies in pyproject.toml.
  • ADK ships a CLI (adk) for running and serving agents, which you will use in Step 6. Because it is installed into the project environment, you invoke it with uv run adk ....
  • uv records exact versions in uv.lock, so uv sync reproduces the same dependency set anywhere.

Step 4: Add your Gemini API key

ADK loads credentials from a .env file in the agent package. Create the agent package directory and its .env now.

Create the file

mkdir -p travel_concierge
touch travel_concierge/.env

Note the underscore: the package directory is travel_concierge (a valid Python module name), inside the project folder travel-concierge.

Add the code: travel_concierge/.env

# travel_concierge/.env
GOOGLE_GENAI_USE_VERTEXAI=FALSE
GOOGLE_API_KEY=paste-your-key-from-ai-studio-here

Detailed breakdown

  • GOOGLE_GENAI_USE_VERTEXAI=FALSE tells ADK to use the Gemini Developer API (the AI Studio key) rather than Vertex AI. Set it to TRUE only if you are authenticating through Google Cloud / Vertex instead.
  • GOOGLE_API_KEY is the secret from AI Studio. Replace the placeholder with your real key. Because .env matches the .gitignore entry from Step 1, it is never committed — verify with git status (it should not appear).
  • ADK automatically discovers and loads this .env when it starts an agent from the travel_concierge package, so your tools and agents see the key without any manual load_dotenv() call.

Step 5: Define the tools and the multi-agent system

This is the core of the tutorial. A single module defines two plain-Python tools, two specialist agents, and the coordinator that delegates to them.

Create the files

touch travel_concierge/__init__.py
touch travel_concierge/agent.py

Add the code: travel_concierge/__init__.py

from . import agent

Detailed breakdown

  • This one line makes the package import its agent module on load. ADK’s CLI discovers a package’s coordinator by importing the package and reading agent.root_agent, so this re-export is what lets adk run travel_concierge find your system.

Add the code: travel_concierge/agent.py

"""A multi-agent travel concierge built with Google's ADK."""

from google.adk.agents import Agent

MODEL = "gemini-2.5-flash"

# --- Tools: plain Python functions the specialists can call ---

_WEATHER = {
    "paris": "18°C and partly cloudy",
    "tokyo": "24°C and clear",
    "new york": "21°C and rainy",
}

_RATES = {  # value of 1 USD in the target currency
    "EUR": 0.92,
    "JPY": 156.0,
    "GBP": 0.79,
}


def get_weather(city: str) -> dict:
    """Return the current weather for a city.

    Args:
        city: Name of the city, for example "Paris".
    """
    report = _WEATHER.get(city.lower())
    if report is None:
        return {"status": "error", "message": f"No weather data for {city}."}
    return {"status": "ok", "city": city, "report": report}


def convert_currency(amount: float, to_currency: str) -> dict:
    """Convert an amount of US dollars into another currency.

    Args:
        amount: The amount in USD.
        to_currency: ISO code of the target currency, for example "EUR".
    """
    rate = _RATES.get(to_currency.upper())
    if rate is None:
        return {"status": "error", "message": f"Unsupported currency {to_currency}."}
    return {
        "status": "ok",
        "amount_usd": amount,
        "to_currency": to_currency.upper(),
        "converted": round(amount * rate, 2),
    }


# --- Specialist sub-agents ---

weather_agent = Agent(
    name="weather_agent",
    model=MODEL,
    description="Answers questions about the current weather in a city.",
    instruction=(
        "You report current weather. Call the get_weather tool with the city the "
        "user mentions, then summarize the result in one short sentence."
    ),
    tools=[get_weather],
)

currency_agent = Agent(
    name="currency_agent",
    model=MODEL,
    description="Converts amounts of US dollars into other currencies.",
    instruction=(
        "You convert US dollars to other currencies. Call the convert_currency "
        "tool, then state the converted amount clearly."
    ),
    tools=[convert_currency],
)


# --- Coordinator (root) agent ---

root_agent = Agent(
    name="travel_concierge",
    model=MODEL,
    description="A travel concierge that coordinates specialist agents.",
    instruction=(
        "You are a travel concierge. Delegate weather questions to weather_agent "
        "and currency conversions to currency_agent. If a request needs both, "
        "handle them one at a time. Do not answer weather or currency questions "
        "yourself — always delegate."
    ),
    sub_agents=[weather_agent, currency_agent],
)

Detailed breakdown

  • from google.adk.agents import Agent imports ADK’s LLM-backed agent class. The same class is used for both specialists and the coordinator — what makes one a coordinator is that it has sub_agents.
  • get_weather / convert_currency are ordinary functions returning a dict. ADK reads each signature and docstring to build the schema the model sees, so the type hints and the Args: docstring matter — they are how the model learns the tool’s parameters. Returning a {"status": ...} dict is the ADK convention for signaling success or a recoverable error back to the model.
  • weather_agent and currency_agent are specialists: each has a narrow instruction, a description, and exactly one tool. The description is critical — the coordinator uses it (not the instruction) to decide which sub-agent should handle a request.
  • root_agent is the coordinator. Passing sub_agents=[...] enables ADK’s LLM-driven delegation (AutoFlow): the model reads the children’s descriptions and transfers control to the best fit. Its instruction explicitly forbids answering directly, which keeps routing behavior predictable.
  • root_agent is the required name ADK’s CLI looks for when it loads the package. Renaming it would break adk run/adk web.

Step 6: Run the multi-agent system

ADK can run your coordinator in the terminal or in a local web UI. Both load the .env you created, so make sure your real key is in place first.

Run in the terminal

uv run adk run travel_concierge

This starts an interactive session. Try prompts that exercise each specialist:

What's the weather in Tokyo?
How much is 50 USD in EUR?

The coordinator delegates the first prompt to weather_agent and the second to currency_agent. Press Ctrl+C to exit.

Run in the web UI

uv run adk web

This serves a local UI (default http://localhost:8000) where you pick the travel_concierge agent from a dropdown and chat with it. The UI also shows an event trace so you can watch the coordinator transfer control to a sub-agent.

Detailed breakdown

  • adk run travel_concierge loads the package, reads root_agent, and starts a read-eval-print loop against Gemini. Run it from the project root (the parent of the travel_concierge folder) so ADK can import the package by name.
  • adk web launches a FastAPI-based dev server that discovers every agent package in the current directory. Use it to inspect the delegation trace visually — invaluable when a coordinator routes to the wrong specialist.
  • Both commands make live Gemini API calls and require a valid key in .env. If you see an authentication error, re-check GOOGLE_API_KEY and that GOOGLE_GENAI_USE_VERTEXAI=FALSE.

Step 7: Add the test dependency

You can validate everything except the live model calls (the tool logic and the agent wiring) without an API key. Add pytest for that.

Install the dev dependency

uv add --dev pytest

Detailed breakdown

  • pytest is the test runner. Tests live in a tests/ directory and use a test_ prefix so pytest discovers them automatically.
  • --dev records it under [dependency-groups].dev, keeping it out of the runtime dependency set.
  • These tests are synchronous and call the tool functions directly and inspect the agent objects, so no pytest-asyncio and no API key are needed.

Step 8: Configure pytest

Tell pytest where the source lives and scope discovery to tests/.

Create the file

touch pytest.ini

Add the code: pytest.ini

[pytest]
pythonpath = .
testpaths = tests

Detailed breakdown

  • pythonpath = . adds the project root to sys.path, so from travel_concierge import agent resolves when tests run from tests/.
  • testpaths = tests scopes discovery to the tests/ directory, so pytest does not import the agent package as a test module.

Step 9: Write the tests

Test the deterministic surface: the tools’ behavior and the coordinator’s wiring.

Create the files

mkdir -p tests
touch tests/__init__.py
touch tests/test_agents.py

Add the code: tests/test_agents.py

"""Tests for the travel concierge tools and agent wiring (no API key needed)."""

from travel_concierge.agent import (
    convert_currency,
    currency_agent,
    get_weather,
    root_agent,
    weather_agent,
)


def test_get_weather_known_city():
    result = get_weather("Paris")
    assert result["status"] == "ok"
    assert "18°C" in result["report"]


def test_get_weather_unknown_city():
    assert get_weather("Atlantis")["status"] == "error"


def test_convert_currency_known_rate():
    result = convert_currency(100, "EUR")
    assert result["status"] == "ok"
    assert result["converted"] == 92.0


def test_convert_currency_unsupported():
    assert convert_currency(100, "XYZ")["status"] == "error"


def test_coordinator_delegates_to_both_specialists():
    sub_agent_names = {sub.name for sub in root_agent.sub_agents}
    assert sub_agent_names == {"weather_agent", "currency_agent"}


def test_each_specialist_has_one_tool():
    assert len(weather_agent.tools) == 1
    assert len(currency_agent.tools) == 1

Detailed breakdown

  • The tool tests call get_weather and convert_currency directly and assert on both the success and error branches. Because the tools are pure functions over in-memory tables, these run instantly and never touch the network.
  • test_coordinator_delegates_to_both_specialists reads root_agent.sub_agents and confirms the coordinator is wired to exactly the two specialists by name — catching a missing or misnamed sub-agent without any model call.
  • test_each_specialist_has_one_tool asserts each specialist exposes a single tool. It checks the count rather than identity because ADK may wrap a function in a tool object internally; the count is the stable, version-independent signal.
  • This suite gives you fast feedback on the structure of the system; live end-to-end behavior is validated separately with adk run in Step 6.

Run the tests

uv run pytest -v

You should see six passing tests.

Step 10: Add a Makefile with a help-first default

A Makefile makes the workflow discoverable. Running plain make must print a help screen listing every target.

Create the file

touch Makefile

Add the code: Makefile

.DEFAULT_GOAL := help

.PHONY: help install run web test

help:  ## Show this help screen
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
		| awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2}'

install:  ## Sync runtime and dev dependencies
	uv sync

run:  ## Run the concierge in the terminal
	uv run adk run travel_concierge

web:  ## Serve the agents in the ADK web UI
	uv run adk web

test:  ## Run the test suite
	uv run pytest -v

Detailed breakdown

  • .DEFAULT_GOAL := help makes bare make run the help target, satisfying the requirement that make with no arguments prints a help screen.
  • The help target scans the Makefile for targets annotated with a ## description comment and prints them in aligned, colored columns. New annotated targets appear automatically.
  • .PHONY declares these are commands, not files, so make always runs them.
  • run / web / test wrap the ADK and pytest commands so contributors do not have to remember the exact invocation; each goes through uv run to stay inside the managed environment.

Validate the default target

make

Confirm the output lists help, install, run, web, and test, each with its description.

Troubleshooting

  • ModuleNotFoundError: No module named 'google.adk' — you ran outside the managed environment. Prefix commands with uv run, or run uv sync first.
  • Authentication / 401 / “API key not valid”travel_concierge/.env is missing or has a bad key. Confirm GOOGLE_API_KEY is set and GOOGLE_GENAI_USE_VERTEXAI=FALSE.
  • adk run cannot find the agent — run it from the project root (the parent of travel_concierge), and make sure travel_concierge/__init__.py contains from . import agent.
  • The coordinator answers directly instead of delegating — strengthen each sub-agent’s description (the coordinator routes on descriptions) and keep the coordinator’s instruction explicit about always delegating.
  • pytest collects zero tests — confirm the files live in tests/ and start with test_, and that pytest.ini has testpaths = tests.

Recap

You built a multi-agent system with Google’s Agent Development Kit:

  • Scaffolded a uv-managed project with hygiene (.gitignore ignoring .env) in place first.
  • Stored a Gemini API key in a git-ignored .env that ADK loads automatically.
  • Wrote two plain-Python tools and two specialist agents, then composed them under a coordinator using sub_agents for LLM-driven delegation.
  • Ran the system in the terminal (adk run) and the web UI (adk web).
  • Validated the tools and the agent wiring with pytest — no API key required.
  • Added a help-first Makefile so the workflow is self-documenting.

Next improvements

  • Add deterministic orchestration with ADK’s workflow agents — SequentialAgent, ParallelAgent, and LoopAgent — when you need a fixed pipeline instead of LLM-driven routing.
  • Share state between agents by writing to and reading from the session state, so a specialist can build on another’s result.
  • Expose agents across processes or languages with the A2A (Agent-to-Agent) protocol, letting a coordinator delegate to remotely hosted specialists.
  • Deploy to production on Vertex AI Agent Engine or Cloud Run once the system works locally.

Sources