Level: Beginner Env: Google Cloud Shell (browser terminal, no local install required)

What you'll build

A single conversational agent that answers weather and time questions using two custom tools you write yourself - then you'll deploy it to Cloud Run and call it over HTTP like a real service.

Learning objectives

By the end you'll be able to:

Make sure you completed the one-time setup before starting: APIs enabled, GOOGLE_API_KEY or Vertex vars exported, venv active, adk --version works. Do this in Cloud Shell, in the google-adk/ folder.

gcloud config set project YOUR_PROJECT_ID

gcloud services enable \
  aiplatform.googleapis.com \
  generativelanguage.googleapis.com \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com \
  iam.googleapis.com

python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install google-adk requests

adk --version

Pick one Gemini backend:

Option A - Gemini Developer API (simplest):

export GOOGLE_API_KEY="YOUR_GEMINI_API_KEY"
export GOOGLE_GENAI_USE_VERTEXAI=FALSE

Option B - Vertex AI (uses your GCP project's quota/billing):

gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
export GOOGLE_CLOUD_LOCATION="global"
export GOOGLE_GENAI_USE_VERTEXAI=TRUE

Both options work identically for this workshop - the agent's Python code never changes, only these environment variables do.

adk create my_agent

This generates:

my_agent/
  __init__.py     # from . import agent
  agent.py        # your root_agent lives here
  .env            # env vars auto-loaded by ADK (API key / Vertex config)

Open it in the Cloud Shell Editor (pencil icon, top-right) - it's just VS Code in your browser.

Drop your backend choice into my_agent/.env - pick ONE:

Option A - Gemini Developer API:

GOOGLE_API_KEY=YOUR_GEMINI_API_KEY
GOOGLE_GENAI_USE_VERTEXAI=FALSE

Option B - Vertex AI:

GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID
GOOGLE_CLOUD_LOCATION=us-central1
GOOGLE_GENAI_USE_VERTEXAI=TRUE

(Requires gcloud auth application-default login to have already run.)

Nothing else in this workshop changes based on which one you pick - every tool and every line of agent.py you write from here is backend-agnostic.

Open the generated agent.py. Every ADK agent boils down to four things:

my_agent/agent.py

from google.adk.agents import Agent

root_agent = Agent(
    model="gemini-2.5-flash",       # which LLM powers it
    name="root_agent",              # unique id
    description="...",              # used by OTHER agents to decide to route to this one
    instruction="...",              # the system prompt - the agent's "job description"
    tools=[],                       # python functions the model can call
)

The instruction is doing the heavy lifting: it tells the model what it's allowed to do, and - crucially - when to reach for a tool instead of guessing an answer.

A "tool" in ADK is just a plain Python function with a docstring. The docstring is the tool's API contract - the model reads it to decide whether and how to call it. Add this to agent.py:

import requests

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

    Args:
        city: Name of the city, e.g. "Bengaluru". Case-insensitive.

    Returns:
        On success: {"status": "ok", "city": "<City>", "weather": "<summary>"}
        On failure: {"status": "error", "error_message": "<details>"}
    """
    try:
        response = requests.get(f"https://wttr.in/{city}?format=3", timeout=10)
        response.raise_for_status()
        return {"status": "ok", "city": city.title(), "weather": response.text.strip()}
    except requests.RequestException as e:
        return {"status": "error", "error_message": f"Failed to fetch weather: {e}"}

Two rules that matter a lot more than they look like they should:

  1. Type hints are required - ADK uses them to build the function schema the model sees.

  2. Always return a dict with a status key - it gives the model (and the instruction) a clean way to branch on success/failure instead of guessing from free text.

from datetime import datetime
from zoneinfo import ZoneInfo

_CITY_TZ = {
    "bengaluru": "Asia/Kolkata",
    "london": "Europe/London",
    "new york": "America/New_York",
    "tokyo": "Asia/Tokyo",
}

def get_current_time(city: str) -> dict:
    """Return the current local time for a given city.

    Args:
        city: Name of the city, e.g. "Tokyo". Case-insensitive.

    Returns:
        On success: {"status": "ok", "city", "current_time"}.
        On failure: {"status": "error", "error_message"} if unsupported.
    """
    tz_name = _CITY_TZ.get(city.strip().lower())
    if tz_name is None:
        return {
            "status": "error",
            "error_message": f"No timezone for '{city}'. Supported: {', '.join(sorted(_CITY_TZ))}.",
        }
    now = datetime.now(ZoneInfo(tz_name))
    return {"status": "ok", "city": city.title(), "current_time": now.strftime("%Y-%m-%d %H:%M:%S %Z")}
root_agent = Agent(
    model="gemini-2.5-flash",
    name="root_agent",
    description="A helpful assistant that answers weather and time questions.",
    instruction=(
        "You are a friendly assistant. "
        "Use the get_weather tool for weather questions and the "
        "get_current_time tool for time questions. "
        "If a tool returns status 'error', apologize and pass along the "
        "error_message so the user knows which cities are supported. "
        "Never invent weather or time data - always call a tool."
    ),
    tools=[get_weather, get_current_time],
)

Option A - Web UI (recommended for the trace panel):

adk web --allow_origins "regex:https://.*\.cloudshell\.dev"

Cloud Shell will offer a Web Preview on port 8000 - click it, pick my_agent from the dropdown, and chat. Open the trace/events panel and watch the model decide to call get_weather - that decision is the whole point of the demo.

Option B - Terminal chat:

adk run my_agent

Try:

Do this yourself, ~10 min. Add a third tool of your own choosing. Ideas:

Requirements: type hints, a docstring, and a status key in the return dict. Add it to tools=[...] and update the instruction to mention it.

Use the command matching the backend you picked earlier.

Option A - Gemini Developer API:

adk deploy cloud_run my_agent -- \
  --region=us-central1 \
  --set-env-vars=GOOGLE_API_KEY=YOUR_GEMINI_API_KEY,GOOGLE_GENAI_USE_VERTEXAI=FALSE

Option B - Vertex AI:

# One-time per project: let the Cloud Run service account call Vertex AI.
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $(gcloud config get-value project) \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role="roles/aiplatform.user"

PROJECT_ID=$(gcloud config get-value project)

adk deploy cloud_run my_agent \
  --with_ui \
  --region=us-central1 \
  --set-env-vars="GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global,GOOGLE_GENAI_USE_VERTEXAI=TRUE" \
  --allow-unauthenticated

Note there's no API key anywhere in Option B - the deployed service authenticates as itself. This builds a container (via Cloud Build), pushes it to Artifact Registry, and deploys it to Cloud Run. Note the printed service URL, e.g.:

https://adk-default-service-name-XXXXXXXXXX.us-central1.run.app

Create a session:

export SERVICE_URL="https://adk-default-service-name-XXXXXXXXXX.us-central1.run.app"

curl -X POST "$SERVICE_URL/apps/my_agent/users/u_123/sessions/s_1" \
  -H "Content-Type: application/json" \
  -d '{}'

Send a message:

curl -X POST "$SERVICE_URL/run" \
  -H "Content-Type: application/json" \
  -d '{
    "appName": "my_agent",
    "userId": "u_123",
    "sessionId": "s_1",
    "newMessage": {
      "role": "user",
      "parts": [{"text": "What is the weather in London?"}]
    }
  }'

You should get back the full event stream, including the tool call and its result - same thing you saw in the local trace panel, just over the wire.

Avoids ongoing cost:

gcloud run services delete adk-default-service-name --region=us-central1
arrow_forwardContinue to Workshop 2: Multi-Agent Systems