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-adkPython 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
.envfile. - 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
.envis listed first and on its own line because it holds yourGOOGLE_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 runadk runoradk 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 environmentuvcreates; it is reproducible from the lockfile, so it should never be committed..pytest_cache/is written bypytestbetween 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 byuvwhen it resolves dependencies, so contributors on older Pythons fail fast.dependencies = []is empty for now; the next step fills it viauv addrather than hand-editing, which keepspyproject.tomlanduv.lockin sync.readme = "README.md"points at the fileuv initgenerated; 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-adkpulls in ADK and its dependencies, including thegoogle-genaiSDK used to talk to Gemini. After it runs,google-adkappears under[project].dependenciesinpyproject.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 withuv run adk .... uvrecords exact versions inuv.lock, souv syncreproduces 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 foldertravel-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=FALSEtells ADK to use the Gemini Developer API (the AI Studio key) rather than Vertex AI. Set it toTRUEonly if you are authenticating through Google Cloud / Vertex instead.GOOGLE_API_KEYis the secret from AI Studio. Replace the placeholder with your real key. Because.envmatches the.gitignoreentry from Step 1, it is never committed — verify withgit status(it should not appear).- ADK automatically discovers and loads this
.envwhen it starts an agent from thetravel_conciergepackage, so your tools and agents see the key without any manualload_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
agentmodule on load. ADK’s CLI discovers a package’s coordinator by importing the package and readingagent.root_agent, so this re-export is what letsadk run travel_conciergefind 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 Agentimports 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 hassub_agents.get_weather/convert_currencyare ordinary functions returning a dict. ADK reads each signature and docstring to build the schema the model sees, so the type hints and theArgs: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_agentandcurrency_agentare specialists: each has a narrowinstruction, adescription, and exactly one tool. Thedescriptionis critical — the coordinator uses it (not the instruction) to decide which sub-agent should handle a request.root_agentis the coordinator. Passingsub_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_agentis the required name ADK’s CLI looks for when it loads the package. Renaming it would breakadk 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_conciergeloads the package, readsroot_agent, and starts a read-eval-print loop against Gemini. Run it from the project root (the parent of thetravel_conciergefolder) so ADK can import the package by name.adk weblaunches 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-checkGOOGLE_API_KEYand thatGOOGLE_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
pytestis the test runner. Tests live in atests/directory and use atest_prefix sopytestdiscovers them automatically.--devrecords 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-asyncioand 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 tosys.path, sofrom travel_concierge import agentresolves when tests run fromtests/.testpaths = testsscopes discovery to thetests/directory, sopytestdoes 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_weatherandconvert_currencydirectly 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_specialistsreadsroot_agent.sub_agentsand 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_toolasserts 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 runin 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 := helpmakes baremakerun thehelptarget, satisfying the requirement thatmakewith no arguments prints a help screen.- The
helptarget scans theMakefilefor targets annotated with a## descriptioncomment and prints them in aligned, colored columns. New annotated targets appear automatically. .PHONYdeclares these are commands, not files, somakealways runs them.run/web/testwrap the ADK andpytestcommands so contributors do not have to remember the exact invocation; each goes throughuv runto 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 withuv run, or runuv syncfirst.- Authentication / 401 / “API key not valid” —
travel_concierge/.envis missing or has a bad key. ConfirmGOOGLE_API_KEYis set andGOOGLE_GENAI_USE_VERTEXAI=FALSE. adk runcannot find the agent — run it from the project root (the parent oftravel_concierge), and make suretravel_concierge/__init__.pycontainsfrom . 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. pytestcollects zero tests — confirm the files live intests/and start withtest_, and thatpytest.inihastestpaths = tests.
Recap
You built a multi-agent system with Google’s Agent Development Kit:
- Scaffolded a
uv-managed project with hygiene (.gitignoreignoring.env) in place first. - Stored a Gemini API key in a git-ignored
.envthat ADK loads automatically. - Wrote two plain-Python tools and two specialist agents, then composed
them under a coordinator using
sub_agentsfor 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
Makefileso the workflow is self-documenting.
Next improvements
- Add deterministic orchestration with ADK’s workflow agents —
SequentialAgent,ParallelAgent, andLoopAgent— 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.