Multi-Agent System with Google ADK
Build AI agents that communicate via the A2A protocol with CI/CD.
Introduction
30 Second Summary
When you ask a customer service chatbot for help, you're often talking to one AI agent. But behind the scenes at companies like Google and Microsoft, dozens of specialized agents pass tasks between each other to get you an answer. Each agent has its own expertise, and they collaborate using standardized protocols.
In this project, you will build a multi-agent system where three AI agents discover each other and delegate tasks using the A2A (Agent-to-Agent) protocol.
What You'll Build
A multi-agent task delegation system powered by Google ADK where a Weather Agent, a GitHub Insights Agent, and an Orchestrator Agent communicate via the A2A protocol.
By the end of this project, you'll have:
- Two independent A2A server agents (Weather and GitHub Insights) with auto-generated Agent Cards that expose their capabilities to other agents.
- An orchestrator agent that discovers remote agents and intelligently routes user requests to the right specialist.
- A GitHub Actions CI/CD pipeline that automatically lints and tests your code on every push.
- Secret Mission: Add streaming support and a health-check endpoint that returns all discovered Agent Cards.
Are there any prerequisites?
You should have basic Python knowledge and some familiarity with REST APIs. You'll also need a GitHub account and VS Code installed on your machine.
Set Up Your Development Environment
Before you can build agents that talk to each other, you need a clean workspace with all the right packages installed.
In this step, you will create the project directory structure, set up a Python virtual environment, and install the Google ADK with A2A protocol support.
In this step, get ready to:
- Create the project directory and Python virtual environment.
- Install ADK and supporting packages.
- Verify the installation works.
Create the project directory structure and virtual environment
Your multi-agent system needs separate folders for each agent and a shared tests folder. You will create this structure first, then isolate your dependencies inside a virtual environment.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
Now check that you have the right version of Python installed. This project requires Python 3.10 or higher.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.10 or higher
You are good to go. Continue to the next instruction below.
ⓧ I see an older version
You need Python 3.10+ for ADK. Visit python.org/downloads and install the latest stable release.
On macOS, the system Python may still resolve to an older version even after installing a new one. Use the full path (e.g. /usr/local/bin/python3.12) or install via a version manager like pyenv.
ⓧ Command not found
Python is not installed. Visit python.org/downloads and download Python 3.12 or newer.
After installation, close and reopen your terminal, then run the version check again.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir multi-agent-a2a
cd multi-agent-a2a
- Create the subdirectories for each agent and the test folder by running this command:
mkdir weather_agent github_agent orchestrator tests
- Confirm the folders exist by running:
ls
You should see weather_agent, github_agent, orchestrator, and tests in the output.
- Create a Python virtual environment by running this command:
macOS
python3 -m venv .venv
Windows
python -m venv .venv
What is a virtual environment?
A virtual environment is an isolated Python installation that keeps this project's packages separate from your system Python. This prevents version conflicts between different projects on your machine.
- Activate the virtual environment by running this command:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
You should see (.venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (.venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.
On macOS, make sure you used source before the path. Without source, the script runs in a subshell and activation does not persist.
Help me troubleshoot
Install the project dependencies
Your project needs several packages: the Google ADK with A2A extras, HTTP libraries, a web server, a linter, and a test runner. You will define these in a requirements.txt file so they are easy to reinstall later.
- Create a file called requirements.txt in the root of your multi-agent-a2a folder. In VS Code, click File then New File.
- Name the file requirements.txt and paste the following code:
google-adk[a2a]
requests
httpx
uvicorn
pytest
ruff
python-dotenv
What does each package do?
- google-adk[a2a] installs the Google Agent Development Kit with A2A protocol support.
- requests and httpx are HTTP libraries for calling external APIs like Open-Meteo and GitHub.
- uvicorn is the ASGI web server that runs your A2A agents as HTTP services.
- pytest is the test runner for your CI/CD pipeline.
- ruff is a fast Python linter that enforces code quality.
- python-dotenv loads environment variables from .env files automatically.
- Save requirements.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before continuing.
ⓧ I'd like to double check the full code
google-adk[a2a]
requests
httpx
uvicorn
pytest
ruff
python-dotenv
- Create another file called ruff.toml in the same multi-agent-a2a folder. Click File then New File.
- Name it ruff.toml and paste this configuration:
line-length = 100
target-version = "py310"
[lint]
select = ["E", "F", "W"]
ignore = ["E501"]
What does this configure?
This tells ruff to target Python 3.10 compatibility, allow lines up to 100 characters, and check for common errors (E), undefined names (F), and warnings (W).
The E501 ignore lets lines exceed the default length limit without failing the lint.
- Save ruff.toml by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before continuing.
ⓧ I'd like to double check the full code
line-length = 100
target-version = "py310"
[lint]
select = ["E", "F", "W"]
ignore = ["E501"]
- Install all the packages by running this command in your terminal:
pip install -r requirements.txt
You should see output ending with Successfully installed followed by a list of package names.
Installation failing?
Make sure your virtual environment is active. You should see (.venv) in your terminal prompt. If not, re-run the activate command from the previous substep.
If you see permission errors, do not use sudo. Deactivate and recreate the virtual environment instead.
Help me fix this
Verify the installation
The ADK package includes a command-line tool called adk. You can confirm everything installed correctly by checking its version.
- Verify the ADK CLI is available by running:
adk --version
You should see a version number printed in the terminal. This confirms the Google ADK is installed and ready to use.
Command not found?
If you see an error, your virtual environment may not be active. Check for (.venv) in your terminal prompt and activate it again if needed.
If the environment is active but the command still fails, try reinstalling with pip install "google-adk[a2a]" and check for any error messages during installation.
Help me troubleshoot
Your environment is ready. Next up, you will get a Gemini API key and test that your agents can connect to Google's AI models.
Get Your Gemini API Key and Configure Environment
Your agents need a brain. The Gemini API from Google powers the reasoning behind each agent's decisions, letting them understand natural language and call tools intelligently.
In this step, you will get a free API key from Google AI Studio and configure your project to use it securely. You will also verify the key works before building any agents.
In this step, get ready to:
- Create a free Gemini API key from Google AI Studio.
- Configure your environment files to store the key securely.
- Test the API key with a quick connectivity check.
Create a free Gemini API key from Google AI Studio
Google AI Studio is the portal where you create and manage API keys for the Gemini API. The key is free and gives you access to models like gemini-2.0-flash.
- Open your browser and navigate to Google AI Studio.
- Sign in with your Google account.
- Click Get API key in the left sidebar.
- Click Create API key.
- Copy your new API key and save it somewhere safe (you will need it in a moment).
Why is this key free?
Google provides free access to the Gemini API for development and prototyping. The free tier is generous enough for learning projects like this one.
Can't create a key?
Make sure you are signed in to the correct Google account. If you see a permissions error, try creating the key in a new project by selecting Create API key in new project.
Help me troubleshoot my Gemini API key creation in Google AI Studio.
Create the environment files
Your API key is a secret. You should never commit it to version control. The standard approach is to store it in a .env file that gets excluded by .gitignore.
- Create a .env file in the root of your multi-agent-a2a folder by running:
echo GOOGLE_API_KEY=your-api-key-here > .env
- Open .env in VS Code and replace your-api-key-here with the actual API key you copied from Google AI Studio.
What does this file do?
The python-dotenv package (already installed) reads this file and loads GOOGLE_API_KEY into your environment at runtime. The Google GenAI SDK automatically picks up this variable when creating a client.
- Create the .env.example file so collaborators know which variables are needed, by running:
echo GOOGLE_API_KEY=your-api-key-here > .env.example
- Create a .gitignore file to protect your secrets and keep the repository clean by running:
echo .env > .gitignore
echo __pycache__/ >> .gitignore
echo *.pyc >> .gitignore
echo .venv/ >> .gitignore
What does this .gitignore do?
This tells Git to ignore your .env file (secrets), Python cache files (__pycache__/ and *.pyc), and your virtual environment folder (.venv/). None of these belong in a repository.
- Confirm all three files exist by running:
ls -a .env .env.example .gitignore
You should see all three files listed in the output.
Files not showing up?
Make sure you are in the multi-agent-a2a directory. Files starting with a dot are hidden by default. The -a flag shows hidden files.
Help me find my .env file in my project directory.
Test the API key with a minimal agent
Before building your full agents, confirm the key works. You will create a quick test script that calls the Gemini API directly.
- Create a file called test_connection.py in the root of your project by running:
import os
from dotenv import load_dotenv
from google import genai
# Load the API key from .env file
load_dotenv()
# Create a client using the GOOGLE_API_KEY environment variable
client = genai.Client(api_key=os.getenv("GOOGLE_API_KEY"))
# Make a simple request to verify connectivity
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="Say hello in one sentence."
)
print("API connection successful!")
print(f"Response: {response.text}")
What does this script do?
It loads your API key from .env using python-dotenv, creates a GenAI client, and sends a simple prompt to gemini-2.0-flash. If the key is valid, you get a response back.
- Run the test script:
python test_connection.py
You should see a success message followed by a short greeting from Gemini.
✔️ I see a response from Gemini
Your API key is working. You can delete test_connection.py now since it was just for verification.
- Clean up the test file by running:
rm test_connection.py
ⓧ I see an error
Check the following:
- Open .env and confirm GOOGLE_API_KEY= is followed by your actual key with no spaces or quotes around it.
- Make sure your virtual environment is still activated (you should see (.venv) in your terminal prompt).
- Verify the key was created correctly in Google AI Studio by going back to the Get API key page.
Help me debug my Gemini API connection error.
Your environment is locked and loaded. Next up, you will build your first A2A agent that fetches real-time weather data.
Build the Weather Agent
Your environment is configured and your Gemini API key is ready to go. Now it's time to build your first agent.
The Weather Agent will fetch live weather data from the Open-Meteo API and expose itself as an A2A server that other agents can discover and communicate with.
In this step, get ready to:
- Create a weather tool function that calls the Open-Meteo Geocoding and Forecast APIs.
- Expose the weather agent as an A2A server using to_a2a().
- Test the agent by verifying its Agent Card endpoint.
Create the weather agent with a get_weather tool
Your weather agent needs a tool function that fetches real weather data. The function will call two Open-Meteo endpoints: one to convert a city name into coordinates, and another to get the forecast at those coordinates.
- Create a weather_agent folder inside your multi-agent-a2a project by running this command:
mkdir weather_agent
- Create the agent.py file inside the weather_agent folder by running:
touch weather_agent/agent.py
- Confirm the file was created by running:
ls weather_agent
You should see agent.py in the output.
- Open weather_agent/agent.py in VS Code by clicking on it in the Explorer sidebar.
- Add the imports and the get_weather tool function by copying the following code into the file:
import requests
from google.adk import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
def get_weather(city: str) -> dict:
"""Get current weather for a city using Open-Meteo API.
Args:
city: The name of the city to get weather for.
Returns:
A dictionary with weather data or an error message.
"""
geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_params = {"name": city, "count": 1, "language": "en"}
try:
geo_response = requests.get(geocoding_url, params=geo_params, timeout=10)
geo_data = geo_response.json()
if "results" not in geo_data or len(geo_data["results"]) == 0:
return {"status": "error", "error_message": f"City '{city}' not found."}
location = geo_data["results"][0]
latitude = location["latitude"]
longitude = location["longitude"]
city_name = location["name"]
country = location.get("country", "Unknown")
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_params = {
"latitude": latitude,
"longitude": longitude,
"current": "temperature_2m,wind_speed_10m,relative_humidity_2m,weather_code",
"timezone": "auto",
}
weather_response = requests.get(weather_url, params=weather_params, timeout=10)
weather_data = weather_response.json()
current = weather_data["current"]
return {
"status": "success",
"report": {
"city": city_name,
"country": country,
"temperature_celsius": current["temperature_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"humidity_percent": current["relative_humidity_2m"],
"weather_code": current["weather_code"],
},
}
except Exception as e:
return {"status": "error", "error_message": str(e)}
What does this code do?
The get_weather function is a tool that your ADK agent can call. It takes a city name and does two things:
- Geocoding step: Calls the Open-Meteo Geocoding API to convert the city name into latitude and longitude coordinates.
- Forecast step: Sends those coordinates to the Forecast API and returns the current temperature, wind speed, humidity, and weather code.
- If anything goes wrong (city not found, network error), it returns a structured error dictionary so the agent can respond gracefully.
- Save weather_agent/agent.py.
Seeing red underlines on the imports?
Make sure your VS Code Python interpreter is set to the .venv virtual environment you created earlier. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Python: Select Interpreter, and choose the one inside your .venv folder.
help me fix import errors in my weather agent file
Expose the weather agent as an A2A server
A tool function alone isn't useful to other agents. You need to wrap it in an ADK Agent, then expose that agent as an A2A server so it can be discovered over the network.
- In weather_agent/agent.py, add the following code below the get_weather function (after the last except block):
weather_agent = Agent(
model="gemini-2.0-flash",
name="weather_agent",
description="An agent that provides current weather information for any city worldwide.",
instruction="""You are a weather assistant. When a user asks about the weather
in a city, use the get_weather tool to fetch real-time weather data.
Present the results clearly with temperature, wind speed, and humidity.
If the city is not found, let the user know politely.""",
tools=[get_weather],
)
a2a_app = to_a2a(weather_agent, port=8001)
What does this code do?
The Agent constructor creates an ADK agent powered by gemini-2.0-flash. The instruction field tells the model how to behave, and tools=[get_weather] gives it access to your weather function.
The to_a2a() function wraps the agent into a full A2A server. It auto-generates an Agent Card (a JSON file describing the agent's name, skills, and how to talk to it) and serves it at a well-known URL that other agents can discover.
What is an Agent Card?
An Agent Card is a standardized JSON document that describes what an agent can do, how to communicate with it, and where it lives on the network. It is served at /.well-known/agent-card.json so other agents can discover it automatically.
Think of it as a business card for your AI agent. Other agents read it to decide whether this agent can handle a particular task.
- Save weather_agent/agent.py.
Seeing a syntax error?
Make sure the triple-quoted instruction string uses three double quotes (""") on both ends. Also check that your Agent() call and to_a2a() call are outside and below the get_weather function, not indented inside it.
help me fix the syntax error in my weather agent
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete weather_agent/agent.py file. Make sure yours matches exactly:
import requests
from google.adk import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
def get_weather(city: str) -> dict:
"""Get current weather for a city using Open-Meteo API.
Args:
city: The name of the city to get weather for.
Returns:
A dictionary with weather data or an error message.
"""
geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_params = {"name": city, "count": 1, "language": "en"}
try:
geo_response = requests.get(geocoding_url, params=geo_params, timeout=10)
geo_data = geo_response.json()
if "results" not in geo_data or len(geo_data["results"]) == 0:
return {"status": "error", "error_message": f"City '{city}' not found."}
location = geo_data["results"][0]
latitude = location["latitude"]
longitude = location["longitude"]
city_name = location["name"]
country = location.get("country", "Unknown")
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_params = {
"latitude": latitude,
"longitude": longitude,
"current": "temperature_2m,wind_speed_10m,relative_humidity_2m,weather_code",
"timezone": "auto",
}
weather_response = requests.get(weather_url, params=weather_params, timeout=10)
weather_data = weather_response.json()
current = weather_data["current"]
return {
"status": "success",
"report": {
"city": city_name,
"country": country,
"temperature_celsius": current["temperature_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"humidity_percent": current["relative_humidity_2m"],
"weather_code": current["weather_code"],
},
}
except Exception as e:
return {"status": "error", "error_message": str(e)}
weather_agent = Agent(
model="gemini-2.0-flash",
name="weather_agent",
description="An agent that provides current weather information for any city worldwide.",
instruction="""You are a weather assistant. When a user asks about the weather
in a city, use the get_weather tool to fetch real-time weather data.
Present the results clearly with temperature, wind speed, and humidity.
If the city is not found, let the user know politely.""",
tools=[get_weather],
)
a2a_app = to_a2a(weather_agent, port=8001)
Test the weather agent as an A2A server
Your agent code is written. Now you need to run it as a server and confirm it's discoverable via its Agent Card endpoint.
- Start the weather agent server by running this command in your terminal:
uvicorn weather_agent.agent:a2a_app --host localhost --port 8001
What does this command do?
- uvicorn is the ASGI web server that hosts your agent.
- weather_agent.agent:a2a_app tells uvicorn to find the a2a_app object inside weather_agent/agent.py.
- --host localhost --port 8001 makes the server accessible at http://localhost:8001.
You should see output indicating the server is running, similar to Uvicorn running on http://localhost:8001.
- Open a new browser tab and navigate to:
http://localhost:8001/.well-known/agent-card.json
You should see a JSON response containing your agent's name, description, and capabilities. This is the auto-generated Agent Card that other agents will use to discover your weather agent.
Connection refused or page won't load?
Make sure the uvicorn server is still running in your terminal. If you see an error about the module not being found, confirm you are running the command from the multi-agent-a2a project root folder (the one that contains the weather_agent/ directory).
Also check that your .env file with GOOGLE_API_KEY is in the project root. The ADK agent needs it to initialize.
help me debug my weather agent server
- Once you've verified the Agent Card loads, stop the server by pressing Ctrl+C in your terminal.
Your first A2A agent is alive and discoverable on the network. Next up, you'll build a second agent that pulls GitHub repository insights and exposes itself the same way.
Build the GitHub Insights Agent
Your weather agent is up and running, serving live weather data to any A2A client that discovers it. Now you need a second specialist agent that brings a completely different skill to the table.
In this step, you will build a GitHub Insights agent that pulls real repository data from the GitHub REST API. This agent gives your future orchestrator the ability to answer questions about any public repository's stars, forks, language, and open issues.
In this step, get ready to:
- Create two tool functions that call the GitHub REST API.
- Define a GitHub insights agent and expose it as an A2A server.
- Verify the agent's Agent Card is accessible on port 8002.
Create the repository info and issues tools
The GitHub agent needs two tools: one to fetch general repository statistics, and one to fetch recent open issues. Both call the public GitHub REST API, which allows 60 unauthenticated requests per hour.
- Create a github_agent folder inside your multi-agent-a2a project directory by running this command:
mkdir github_agent
- Create a new file called agent.py inside the github_agent folder by running:
touch github_agent/agent.py
- Confirm the file was created by running:
ls github_agent/
You should see agent.py listed in the output.
- Open github_agent/agent.py in VS Code and add the imports and the first tool function by pasting the following code:
import requests
from google.adk import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
def get_repo_info(owner: str, repo: str) -> dict:
"""Get information about a GitHub repository.
Args:
owner: The owner (user or organization) of the repository.
repo: The name of the repository.
Returns:
A dictionary with repository information or an error message.
"""
url = f"https://api.github.com/repos/{owner}/{repo}"
headers = {"Accept": "application/vnd.github+json"}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 404:
return {"status": "error", "error_message": f"Repository '{owner}/{repo}' not found."}
data = response.json()
return {
"status": "success",
"repo": {
"full_name": data["full_name"],
"description": data.get("description", "No description"),
"stars": data["stargazers_count"],
"forks": data["forks_count"],
"open_issues": data["open_issues_count"],
"language": data.get("language", "Not specified"),
"created_at": data["created_at"],
"updated_at": data["updated_at"],
"html_url": data["html_url"],
},
}
except Exception as e:
return {"status": "error", "error_message": str(e)}
What does this code do?
The imports bring in the same ADK building blocks you used for the weather agent: Agent for defining the agent, and to_a2a for exposing it as an A2A server.
The get_repo_info function calls GET /repos/{owner}/{repo} on the GitHub API. It extracts key stats like stars, forks, language, and issue count into a clean dictionary the agent can present to users.
- Save github_agent/agent.py.
You won't see a visible change yet. The next chunk adds the second tool function.
- Below the get_repo_info function in github_agent/agent.py, add the get_open_issues function:
def get_open_issues(owner: str, repo: str, count: int = 5) -> dict:
"""Get the most recent open issues for a GitHub repository.
Args:
owner: The owner (user or organization) of the repository.
repo: The name of the repository.
count: Number of issues to retrieve (default 5, max 10).
Returns:
A dictionary with a list of open issues or an error message.
"""
count = min(count, 10)
url = f"https://api.github.com/repos/{owner}/{repo}/issues"
headers = {"Accept": "application/vnd.github+json"}
params = {"state": "open", "per_page": count, "sort": "created", "direction": "desc"}
try:
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code == 404:
return {"status": "error", "error_message": f"Repository '{owner}/{repo}' not found."}
data = response.json()
issues = []
for item in data:
if "pull_request" not in item:
issues.append({
"number": item["number"],
"title": item["title"],
"state": item["state"],
"created_at": item["created_at"],
"html_url": item["html_url"],
"labels": [label["name"] for label in item.get("labels", [])],
})
return {
"status": "success",
"repository": f"{owner}/{repo}",
"issues": issues,
"total_count": len(issues),
}
except Exception as e:
return {"status": "error", "error_message": str(e)}
What does this code do?
The get_open_issues function calls GET /repos/{owner}/{repo}/issues with filters for open issues sorted by creation date.
It caps the request at 10 issues maximum using min(count, 10). The loop also skips pull requests (GitHub's API returns them mixed in with issues) by checking for the pull_request key.
- Save github_agent/agent.py.
You won't see a change yet. The next chunk defines the agent and makes the file runnable.
Define the agent and expose it as an A2A server
With both tools ready, you can now create the ADK agent that uses them and wrap it as an A2A server. This is the same pattern you used for the weather agent, just pointing at a different port.
- At the bottom of github_agent/agent.py, add the agent definition and A2A exposure:
github_agent = Agent(
model="gemini-2.0-flash",
name="github_insights_agent",
description="An agent that provides insights about GitHub repositories including stats, open issues, and project health.",
instruction="""You are a GitHub repository analyst. When a user asks about a
GitHub repository, use the available tools to fetch real data.
Use get_repo_info for general repository statistics like stars, forks, and language.
Use get_open_issues to list recent open issues.
Present the information in a clear, structured format.
If a repository is not found, let the user know politely.""",
tools=[get_repo_info, get_open_issues],
)
a2a_app = to_a2a(github_agent, port=8002)
What does this code do?
The Agent definition gives the LLM its persona (github_insights_agent) and instructions for when to use each tool. The instruction field tells it to use get_repo_info for stats and get_open_issues for issues.
The to_a2a() call wraps the agent into a full A2A server on port 8002. It auto-generates an Agent Card at /.well-known/agent-card.json so other agents can discover this agent's capabilities.
- Save github_agent/agent.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before moving on to testing.
ⓧ I'd like to double check the full code
import requests
from google.adk import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
def get_repo_info(owner: str, repo: str) -> dict:
"""Get information about a GitHub repository.
Args:
owner: The owner (user or organization) of the repository.
repo: The name of the repository.
Returns:
A dictionary with repository information or an error message.
"""
url = f"https://api.github.com/repos/{owner}/{repo}"
headers = {"Accept": "application/vnd.github+json"}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 404:
return {"status": "error", "error_message": f"Repository '{owner}/{repo}' not found."}
data = response.json()
return {
"status": "success",
"repo": {
"full_name": data["full_name"],
"description": data.get("description", "No description"),
"stars": data["stargazers_count"],
"forks": data["forks_count"],
"open_issues": data["open_issues_count"],
"language": data.get("language", "Not specified"),
"created_at": data["created_at"],
"updated_at": data["updated_at"],
"html_url": data["html_url"],
},
}
except Exception as e:
return {"status": "error", "error_message": str(e)}
def get_open_issues(owner: str, repo: str, count: int = 5) -> dict:
"""Get the most recent open issues for a GitHub repository.
Args:
owner: The owner (user or organization) of the repository.
repo: The name of the repository.
count: Number of issues to retrieve (default 5, max 10).
Returns:
A dictionary with a list of open issues or an error message.
"""
count = min(count, 10)
url = f"https://api.github.com/repos/{owner}/{repo}/issues"
headers = {"Accept": "application/vnd.github+json"}
params = {"state": "open", "per_page": count, "sort": "created", "direction": "desc"}
try:
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code == 404:
return {"status": "error", "error_message": f"Repository '{owner}/{repo}' not found."}
data = response.json()
issues = []
for item in data:
if "pull_request" not in item:
issues.append({
"number": item["number"],
"title": item["title"],
"state": item["state"],
"created_at": item["created_at"],
"html_url": item["html_url"],
"labels": [label["name"] for label in item.get("labels", [])],
})
return {
"status": "success",
"repository": f"{owner}/{repo}",
"issues": issues,
"total_count": len(issues),
}
except Exception as e:
return {"status": "error", "error_message": str(e)}
github_agent = Agent(
model="gemini-2.0-flash",
name="github_insights_agent",
description="An agent that provides insights about GitHub repositories including stats, open issues, and project health.",
instruction="""You are a GitHub repository analyst. When a user asks about a
GitHub repository, use the available tools to fetch real data.
Use get_repo_info for general repository statistics like stars, forks, and language.
Use get_open_issues to list recent open issues.
Present the information in a clear, structured format.
If a repository is not found, let the user know politely.""",
tools=[get_repo_info, get_open_issues],
)
a2a_app = to_a2a(github_agent, port=8002)
Test the GitHub agent
Time to confirm the agent starts cleanly and publishes its Agent Card. You will run it with uvicorn the same way you ran the weather agent.
- If your weather agent is still running from the previous step, stop it first by pressing Ctrl+C in that terminal.
- Start the GitHub agent by running this command:
uvicorn github_agent.agent:a2a_app --host localhost --port 8002
What does this command do?
This tells uvicorn to import the a2a_app variable from github_agent/agent.py and serve it on port 8002. The A2A server handles all the protocol plumbing: JSON-RPC endpoints, task management, and the Agent Card.
You should see output indicating the server is running on http://localhost:8002.
- Open a second terminal (keep the agent running in the first one).
- Verify the Agent Card is accessible by running:
curl http://localhost:8002/.well-known/agent-card.json
You should see a JSON response containing the agent's name, description, and supported skills. The name field should show github_insights_agent.
Agent not starting or Agent Card not loading?
- Make sure your virtual environment is activated. You should see (.venv) in your terminal prompt.
- Check that port 8002 is not already in use. If the weather agent is still running on a different port, that is fine. But if something else is on 8002, stop it first.
- Verify you are running the command from the multi-agent-a2a project root (the parent of github_agent/).
- If you see an import error, double check that github_agent/agent.py has the correct import lines at the top.
Help me debug why my GitHub agent won't start on port 8002.
Your second A2A agent is live and advertising its skills. Next up, you will build an orchestrator agent that discovers both agents and routes user requests to the right specialist.
Build the Orchestrator Agent
You now have two independent agents running as A2A servers. Each one advertises its capabilities through an Agent Card. The missing piece is a central coordinator that discovers these agents and routes user requests to the right one.
In this step, you will build an orchestrator agent that consumes your remote A2A agents using their Agent Cards. The orchestrator does not call APIs directly. It delegates tasks to the specialized agents over the A2A protocol.
In this step, get ready to:
- Create the orchestrator agent that uses RemoteA2aAgent to consume both remote A2A agents.
- Configure the orchestrator with instructions for intelligent task routing.
- Run all three agents simultaneously and test multi-agent delegation.
Create the orchestrator agent that uses RemoteA2aAgent
The orchestrator connects to remote agents by reading their Agent Cards. It does not need to know the internal implementation of each agent. It only needs the URL where the Agent Card is served.
- Create a new folder called orchestrator inside your multi-agent-a2a project directory by running this command:
mkdir orchestrator
- Create the agent.py file inside the orchestrator folder by running:
touch orchestrator/agent.py
- Open orchestrator/agent.py in VS Code by clicking on it in the Explorer sidebar.
- Add the imports and remote agent connections by pasting the following code:
from google.adk import Agent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
weather_remote = RemoteA2aAgent(
name="weather_remote",
description="Remote agent that provides current weather information for any city worldwide.",
agent_card="http://localhost:8001/.well-known/agent-card.json",
)
github_remote = RemoteA2aAgent(
name="github_remote",
description="Remote agent that provides insights about GitHub repositories including stats and open issues.",
agent_card="http://localhost:8002/.well-known/agent-card.json",
)
What does this code do?
RemoteA2aAgent is ADK's built-in class for consuming remote A2A agents. Each instance points to a remote agent's Agent Card URL.
When the orchestrator starts, it fetches the Agent Card from each URL to learn the remote agent's name, description, and capabilities. It uses this information to decide which agent to delegate tasks to.
The agent_card parameter points to the well-known URL where each A2A server advertises itself. Port 8001 is your weather agent. Port 8002 is your GitHub agent.
Configure the orchestrator with routing instructions
The orchestrator needs instructions that tell it how to decide which remote agent should handle each request. You will configure a root agent that uses both remote agents as sub-agents.
- In orchestrator/agent.py, add the root agent definition below the two RemoteA2aAgent instances:
root_agent = Agent(
model="gemini-2.0-flash",
name="orchestrator_agent",
description="An orchestrator agent that coordinates weather and GitHub insights agents.",
instruction="""You are a helpful orchestrator that coordinates multiple specialized agents.
You have access to the following remote agents:
1. weather_remote: Use this for any weather-related questions. It can provide
current weather data for any city worldwide.
2. github_remote: Use this for any GitHub repository questions. It can provide
repository statistics, open issues, and project health information.
Rules:
- If the user asks about weather, delegate to weather_remote.
- If the user asks about a GitHub repository, delegate to github_remote.
- If the user asks about both, delegate to both agents and combine the results.
- Always present the final results clearly to the user.
- If you are unsure which agent to use, ask the user for clarification.""",
sub_agents=[weather_remote, github_remote],
)
What does this code do?
The root_agent is the entry point that ADK uses to start the orchestrator. The sub_agents parameter registers both remote agents as delegates.
When a user sends a message, the orchestrator's LLM reads the instruction text and decides which sub-agent to forward the request to. It can also combine results from multiple agents for complex queries.
The name root_agent is important. ADK's CLI tools look for a variable with this exact name when discovering agents in a package.
- Now create the __init__.py file that makes the orchestrator folder discoverable as a Python package by running:
touch orchestrator/__init__.py
- Open orchestrator/__init__.py and add this import:
from . import agent
Why is this init file needed?
ADK's CLI tools (adk run, adk web) discover agents by treating each subdirectory as a Python package. The directory must contain both __init__.py and agent.py files.
The import statement from . import agent ensures that the agent.py module loads when the package is imported. This makes root_agent discoverable by ADK.
- Save both orchestrator/agent.py and orchestrator/__init__.py.
- Confirm both files exist by running:
ls orchestrator/
- You should see agent.py and __init__.py listed in the output.
Don't see both files?
Make sure you ran the touch commands from your multi-agent-a2a project root, not from inside the orchestrator folder. Run pwd to check your current directory.
help me set up the orchestrator folder structure
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
from google.adk import Agent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
weather_remote = RemoteA2aAgent(
name="weather_remote",
description="Remote agent that provides current weather information for any city worldwide.",
agent_card="http://localhost:8001/.well-known/agent-card.json",
)
github_remote = RemoteA2aAgent(
name="github_remote",
description="Remote agent that provides insights about GitHub repositories including stats and open issues.",
agent_card="http://localhost:8002/.well-known/agent-card.json",
)
root_agent = Agent(
model="gemini-2.0-flash",
name="orchestrator_agent",
description="An orchestrator agent that coordinates weather and GitHub insights agents.",
instruction="""You are a helpful orchestrator that coordinates multiple specialized agents.
You have access to the following remote agents:
1. weather_remote: Use this for any weather-related questions. It can provide
current weather data for any city worldwide.
2. github_remote: Use this for any GitHub repository questions. It can provide
repository statistics, open issues, and project health information.
Rules:
- If the user asks about weather, delegate to weather_remote.
- If the user asks about a GitHub repository, delegate to github_remote.
- If the user asks about both, delegate to both agents and combine the results.
- Always present the final results clearly to the user.
- If you are unsure which agent to use, ask the user for clarification.""",
sub_agents=[weather_remote, github_remote],
)
from . import agent
Run all three agents and test multi-agent delegation
To test the full system, you need all three agents running at the same time. The weather and GitHub agents run as A2A servers with uvicorn. The orchestrator runs via the ADK CLI which connects to both of them.
- Open a new terminal in VS Code by pressing Ctrl+Shift+` (backtick).
- Make sure your virtual environment is active in this terminal.
- Start the weather agent by running:
uvicorn weather_agent.agent:a2a_app --host localhost --port 8001
- Open a second new terminal (Ctrl+Shift+` again).
- Activate your virtual environment and start the GitHub agent by running:
uvicorn github_agent.agent:a2a_app --host localhost --port 8002
- Open a third terminal (Ctrl+Shift+` again).
- Activate your virtual environment and run the orchestrator using the ADK CLI:
adk run orchestrator
What does adk run do?
The adk run command starts an interactive terminal session with your agent. It discovers the root_agent inside the orchestrator package and lets you type queries directly.
Type exit when you are done testing to close the session.
- Test a weather query by typing this when you see the [user]: prompt:
What's the weather in Tokyo?
- You should see the orchestrator delegate to the weather agent and return temperature, wind speed, and humidity data for Tokyo.
- Test a GitHub query by typing:
Tell me about the google/adk-python repository
- You should see repository statistics including stars, forks, and language information.
- Test multi-agent delegation by typing a combined query:
What's the weather in San Francisco and what are the latest issues on google/adk-python?
- The orchestrator should delegate to both agents and combine the results into a single response.
- Type exit to close the orchestrator session.
Seeing a connection error?
If the orchestrator cannot reach the remote agents, make sure both uvicorn processes are still running in their respective terminals. Check that port 8001 and port 8002 are not blocked by another process.
If you see a "No root_agent found" error, verify that orchestrator/__init__.py contains from . import agent and that your agent.py file defines a variable named exactly root_agent.
help me debug why my orchestrator cannot connect to the remote agents
Your multi-agent system is fully operational. Three independent agents are communicating over the A2A protocol without sharing any internal state. Next up, you will push this project to GitHub and set up a CI/CD pipeline to validate your code automatically.
Set Up GitHub Repository and CI/CD Pipeline
Your multi-agent system runs beautifully on your machine. But working code without automated quality checks is fragile. One typo in a future commit could break everything silently.
In this step, you'll write unit tests for your agent tools, create a GitHub Actions CI/CD pipeline that lints and tests your code on every push, and publish everything to a GitHub repository.
In this step, get ready to:
- Write unit tests for the weather and GitHub agent tools.
- Create a GitHub Actions CI/CD workflow with linting and testing.
- Push everything to GitHub and verify the pipeline passes.
Write unit tests for the agent tools
Automated tests exercise your tool functions with known inputs and verify the outputs are correct. This gives your CI pipeline something concrete to run.
- Create a tests directory inside your multi-agent-a2a folder by running this command:
mkdir tests
- Create a new file called test_tools.py inside the tests folder.
- Add the imports and weather agent tests by pasting this code into tests/test_tools.py:
import pytest
from weather_agent.agent import get_weather
from github_agent.agent import get_repo_info, get_open_issues
def test_get_weather_valid_city():
result = get_weather("London")
assert result["status"] == "success"
assert "report" in result
assert "temperature_celsius" in result["report"]
assert result["report"]["city"] == "London"
def test_get_weather_invalid_city():
result = get_weather("XyzNonexistentCity12345")
assert result["status"] == "error"
assert "error_message" in result
What do these tests do?
- test_get_weather_valid_city calls get_weather("London") and checks that the response contains a success status, a report object, and the city name.
- test_get_weather_invalid_city passes a nonsense city name and checks that the function returns an error status gracefully.
- Add the GitHub agent tests below the weather tests in the same file:
def test_get_repo_info_valid():
result = get_repo_info("google", "adk-python")
assert result["status"] == "success"
assert "repo" in result
assert result["repo"]["full_name"] == "google/adk-python"
assert "stars" in result["repo"]
def test_get_repo_info_invalid():
result = get_repo_info("nonexistent-user-xyz", "nonexistent-repo-xyz")
assert result["status"] == "error"
def test_get_open_issues_valid():
result = get_open_issues("google", "adk-python", count=3)
assert result["status"] == "success"
assert "issues" in result
assert result["total_count"] <= 3
def test_get_open_issues_respects_max():
result = get_open_issues("google", "adk-python", count=20)
assert result["status"] == "success"
assert result["total_count"] <= 10
What do these tests do?
- test_get_repo_info_valid queries the real google/adk-python repository and verifies it returns success with expected fields.
- test_get_repo_info_invalid sends a nonexistent repo name and confirms the function returns an error.
- test_get_open_issues_valid requests 3 issues and verifies the count stays within bounds.
- test_get_open_issues_respects_max passes 20 as the count, but the function caps it at 10. The test confirms this ceiling.
- Save tests/test_tools.py.
- Run the tests locally by running this command:
pytest tests/ -v
- You should see all 6 tests passing with green PASSED markers.
Tests failing or not found?
- Make sure you are running the command from the multi-agent-a2a root folder, not from inside the tests directory.
- Check that your virtual environment is activated. The pytest package was installed there.
- If you see import errors, confirm your file is named test_tools.py (not tests_tools.py) and the imports at the top match the module names exactly.
help me debug my failing pytest tests
✔️ Awesome, I've got everything!
Great. Double check you have saved tests/test_tools.py and all 6 tests pass.
ⓧ I'd like to double check the full code
import pytest
from weather_agent.agent import get_weather
from github_agent.agent import get_repo_info, get_open_issues
def test_get_weather_valid_city():
result = get_weather("London")
assert result["status"] == "success"
assert "report" in result
assert "temperature_celsius" in result["report"]
assert result["report"]["city"] == "London"
def test_get_weather_invalid_city():
result = get_weather("XyzNonexistentCity12345")
assert result["status"] == "error"
assert "error_message" in result
def test_get_repo_info_valid():
result = get_repo_info("google", "adk-python")
assert result["status"] == "success"
assert "repo" in result
assert result["repo"]["full_name"] == "google/adk-python"
assert "stars" in result["repo"]
def test_get_repo_info_invalid():
result = get_repo_info("nonexistent-user-xyz", "nonexistent-repo-xyz")
assert result["status"] == "error"
def test_get_open_issues_valid():
result = get_open_issues("google", "adk-python", count=3)
assert result["status"] == "success"
assert "issues" in result
assert result["total_count"] <= 3
def test_get_open_issues_respects_max():
result = get_open_issues("google", "adk-python", count=20)
assert result["status"] == "success"
assert result["total_count"] <= 10
Create the GitHub Actions CI workflow
GitHub Actions runs your linting and tests automatically every time you push code. The workflow definition lives in a special .github/workflows directory that GitHub recognizes.
- Create the workflow directory structure by running these commands:
mkdir .github
mkdir .github/workflows
- Create a new file called ci.yml inside .github/workflows.
- Paste the following workflow definition into .github/workflows/ci.yml:
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with ruff
run: |
ruff check .
- name: Run tests
run: |
pytest tests/ -v
What does this workflow do?
- on: push/pull_request triggers the pipeline whenever code is pushed to main or a pull request targets main.
- actions/checkout@v4 clones your repository into the runner.
- actions/setup-python@v5 installs Python 3.12 on the CI runner.
- ruff check . lints all Python files for syntax errors and style violations.
- pytest tests/ -v runs your unit tests with verbose output.
- Save .github/workflows/ci.yml.
Why linting and testing together?
Linting catches formatting problems and unused imports before they accumulate. Tests catch logic errors. Running both in one pipeline means a single push either passes everything or fails fast with a clear reason.
Initialize Git and push to GitHub
With tests and the CI workflow ready, it is time to version-control your project and push it to GitHub where the pipeline will run automatically.
- Initialize a Git repository by running this command:
git init
- Stage all your project files by running:
git add .
What about your API key?
Your .gitignore file already lists .env. Git will skip it automatically so your API key never reaches GitHub.
- Commit your files by running:
git commit -m "Initial commit: multi-agent A2A system"
- Open your browser and go to github.com.
- Click the + icon in the top-right corner, then select New repository.
- Enter multi-agent-a2a as the repository name.
- Leave the repository set to Public.
- Do NOT check any boxes for README, .gitignore, or license (you already have these locally).
- Click Create repository.
GitHub shows a quick setup page with commands. You will use the "push an existing repository" commands.
- Connect your local repository to GitHub by running this command (replace the URL with your own):
git remote add origin [[REPO_URL="https://github.com/YOUR_USERNAME/multi-agent-a2a.git"]]
- Rename your branch to main and push by running these commands:
git branch -M main
git push -u origin main
Push rejected or authentication failed?
- If Git asks for credentials, you may need to set up a Personal Access Token or SSH key. GitHub no longer accepts passwords for Git operations.
- If the push is rejected because the remote contains work you don't have, make sure you did NOT initialize the GitHub repo with a README.
help me authenticate with GitHub from the command line
- Back on GitHub, click the Actions tab at the top of your repository page.
- You should see a workflow run in progress or already completed with a green checkmark.
- Click into the run to see the lint-and-test job. Expand each step to confirm linting passed and all 6 tests show PASSED.
Pipeline failing on GitHub?
- Click the failed step in the Actions log to see the exact error message.
- If ruff check fails, run ruff check . locally to see the same errors and fix them before pushing again.
- If tests fail on GitHub but pass locally, it is likely a timing issue with the external APIs. Re-run the workflow from the Actions tab.
help me debug my failing GitHub Actions pipeline
Your multi-agent system is now backed by automated quality gates. Every future push to main triggers linting and tests before anything can break silently.
Secret mission
Add Streaming and Health Check
Your multi-agent system works, but in production you need visibility into agent health and the ability to stream responses in real time. Extend your orchestrator with a health-check endpoint that discovers all connected agents, and enable streaming on your A2A servers for live response delivery.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses free APIs (Open-Meteo, GitHub REST API, and the Gemini free tier). There are no ongoing cloud costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Running uvicorn processes (weather agent on port 8001, GitHub agent on port 8002).
- Local project folder on your Desktop (contains all agent code, virtual environment, and .env file).
- GitHub repository (created in Step 6).
- Gemini API key (created in Step 2 via Google AI Studio).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your multi-agent system.
- Your uvicorn servers will keep running until you close the terminal windows or press Ctrl+C.
- Your Gemini API key stays active on the free tier with no charges.
- Your GitHub repository remains available for future pushes and CI/CD runs.
Pause - I'll come back to this later
Shut down running processes to free up terminal sessions and ports. Your files and repository stay intact for when you return.
- In each terminal window running a uvicorn process, press Ctrl+C to stop the server.
- Deactivate your Python virtual environment by running:
deactivate
- Close any extra terminal windows you opened for this project.
- Your project folder and GitHub repository remain untouched and ready for next time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Stop any running uvicorn processes by pressing Ctrl+C in each terminal.
- Delete the local project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/multi-agent-adk
- On Windows, run this instead:
Remove-Item -Recurse -Force ~\Desktop\multi-agent-adk
- Delete your GitHub repository. Open the repository page on GitHub, click Settings, scroll down to the Danger Zone section, and click Delete this repository. Confirm by typing the repository name when prompted.
- Delete your Gemini API key. Go to the Google AI Studio API Keys page at aistudio.google.com/app/apikey, find the key you created for this project, and delete it.
Nice Work!
Nice Work!
You did it. You built a production-style multi-agent system from scratch where independent AI agents discover and communicate with each other using the A2A protocol.
Here's what you accomplished:
- Built two independent A2A server agents (Weather and GitHub Insights) with real external API integrations and auto-generated Agent Cards.
- Created an orchestrator agent that discovers remote agents via their Agent Cards and intelligently delegates tasks using RemoteA2aAgent.
- Set up a GitHub Actions CI/CD pipeline with automated linting and testing that validates your project on every push.
- Secret Mission: Added streaming support and a health-check endpoint for production readiness.
Ready to quiz yourself?