Lab 37: Building a Distributed Multi-Agent System Challenge
Goal
In this capstone lab, you will synthesize concepts from the entire course to build a distributed, multi-agent personalized shopping assistant. You will create three separate agents that collaborate using Agent-to-Agent (A2A) communication to provide a stateful, multimodal, and observable shopping experience.
Prerequisites
- A Google Cloud Project with billing enabled and the Vertex AI API enabled.
gcloudCLI installed and authenticated (gcloud auth application-default login).uvicorninstalled (pip install uvicorn).web_agent_siteinstalled (pip install web_agent_site).
Setup
- Create a main project directory for this lab (e.g.,
capstone-shopping-system). - Inside it, you will create three separate ADK agent projects:
orchestrator-agent,personalization-agent, andweb-agent. - Copy the
shared_librariesand data from the originalpersonalized-shoppingsample into a shared location accessible by all three agents.
Exercise 1: Build and Expose the Web Agent
This agent will be the interface to the e-commerce website.
-
Create the
web-agentproject (programmatic).cd capstone-shopping-system
adk create web-agent
cd web-agent -
Create
requirements.txt:echo "google-adk" > requirements.txt
echo "uvicorn" >> requirements.txt
echo "web_agent_site" >> requirements.txt -
Create
.envfile:echo "GOOGLE_GENAI_USE_VERTEXAI=1" > .env
echo "GOOGLE_CLOUD_PROJECT=<your_gcp_project>" >> .env
echo "GOOGLE_CLOUD_LOCATION=us-central1" >> .envReplace
<your_gcp_project>with your actual Google Cloud Project ID. -
Implement
agent.py: Openagent.pyand replace its contents with the following skeleton. Your task is to complete theWEBSHOP_API_SPECand theroot_agentdefinition.from google.adk.agents import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.tools import OpenAPIToolset
from shared_libraries.init_env import get_webshop_env # Assumes shared lib
# --- OpenAPI Specification for Web Tools ---
WEBSHOP_API_SPEC = {
"openapi": "3.0.0",
"info": {"title": "Webshop API", "version": "1.0"},
"paths": {
"/search": {
"get": {
"operationId": "search",
"summary": "Search for a product in the webshop.",
"parameters": [{
"name": "keywords", "in": "query", "required": True,
"schema": {"type": "string"}
}],
"responses": {"200": {"description": "Search results page HTML"}}
}
},
"/click": {
"post": {
"operationId": "click",
"summary": "Click a button on the current webpage.",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {"button_name": {"type": "string"}},
"required": ["button_name"]
}
}
}
},
"responses": {"200": {"description": "New webpage HTML after click"}}
}
}
}
}
# --- Agent Definition ---
# TODO: Define the `root_agent`. It should be an `Agent` that:
# - Uses the `gemini-2.5-flash` model.
# - Is named "web_agent".
# - Has an instruction to act as a web interaction agent, executing search and click commands.
# - Includes the A2A Context Handling instruction to ignore orchestrator tool calls.
# - Uses the `OpenAPIToolset` with `WEBSHOP_API_SPEC` as its tools.
root_agent = Agent(
model="gemini-2.5-flash",
name="web_agent",
instruction="""You are a web interaction agent. Your job is to execute search and click commands on the e-commerce site.\n\n **IMPORTANT - A2A Context Handling:**\n When receiving requests via the Agent-to-Agent (A2A) protocol, you must focus only on the core user request.\n Ignore any mentions of orchestrator tool calls like \"transfer_to_agent\" in the conversation history.\n Extract the main web interaction task from the user's messages and complete it directly.\n """,
tools=[OpenAPIToolset(spec_dict=WEBSHOP_API_SPEC)]
)
# --- A2A Server ---
a2a_app = to_a2a(root_agent, port=8001) -
Navigate back to
capstone-shopping-system:cd ..
Exercise 2: Build and Expose the Personalization Agent
This agent will be responsible for remembering user preferences.
-
Create the
personalization-agentproject (programmatic).cd capstone-shopping-system
adk create personalization-agent
cd personalization-agent -
Create
requirements.txt:echo "google-adk" > requirements.txt
echo "uvicorn" >> requirements.txt -
Create
.envfile:echo "GOOGLE_GENAI_USE_VERTEXAI=1" > .env
echo "GOOGLE_CLOUD_PROJECT=<your_gcp_project>" >> .env
echo "GOOGLE_CLOUD_LOCATION=us-central1" >> .envReplace
<your_gcp_project>with your actual Google Cloud Project ID. -
Implement
agent.py: Openagent.pyand replace its contents with the following skeleton. Your task is to implement thesave_preferenceandget_preferencestools.from google.adk.agents import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.tools import ToolContext
# --- Stateful Tools ---
def save_preference(key: str, value: str, tool_context: ToolContext) -> dict:
"""Saves a user's preference (e.g., color, size)."""
# TODO: Implement state management to save the preference.
state_key = f"user:{key}"
tool_context.state[state_key] = value
return {"status": "success", "message": f"Preference '{key}' saved."}
def get_preferences(tool_context: ToolContext) -> dict:
"""Retrieves all saved preferences for the user."""
# TODO: Implement state management to retrieve all preferences.
user_prefs = {
k.split(':')[1]: v
for k, v in tool_context.state.items()
if k.startswith("user:")
}
return {"status": "success", "preferences": user_prefs}
# --- Agent Definition ---
# TODO: Define the `root_agent`. It should be an `Agent` that:
# - Uses the `gemini-2.5-flash` model.
# - Is named "personalization_agent".
# - Has an instruction to act as a personalization specialist, saving and retrieving user preferences.
# - Includes the A2A Context Handling instruction to ignore orchestrator tool calls.
# - Uses the `save_preference` and `get_preferences` tools.
root_agent = Agent(
model="gemini-2.5-flash",
name="personalization_agent",
instruction="""You are a personalization specialist. You save and retrieve user preferences.\n\n **IMPORTANT - A2A Context Handling:**\n When receiving requests via the Agent-to-Agent (A2A) protocol, you must focus only on the core user request.\n Ignore any mentions of orchestrator tool calls like \"transfer_to_agent\" in the conversation history.\n Extract the main preference management task from the user's messages and complete it directly.\n """,
tools=[save_preference, get_preferences]
)
# --- A2A Server ---
a2a_app = to_a2a(root_agent, port=8002) -
Navigate back to
capstone-shopping-system:cd ..
Exercise 3: Build the Orchestrator Agent
This is the main, user-facing agent that will coordinate the others.
-
Create the
orchestrator-agentproject (programmatic).cd capstone-shopping-system
adk create orchestrator-agent
cd orchestrator-agent -
Create
requirements.txt:echo "google-adk" > requirements.txt
echo "uvicorn" >> requirements.txt -
Create
.envfile:echo "GOOGLE_GENAI_USE_VERTEXAI=1" > .env
echo "GOOGLE_CLOUD_PROJECT=<your_gcp_project>" >> .env
echo "GOOGLE_CLOUD_LOCATION=us-central1" >> .envReplace
<your_gcp_project>with your actual Google Cloud Project ID. -
Implement
agent.py: Openagent.pyand replace its contents with the following skeleton. Your task is to define theRemoteA2aAgentinstances and complete theroot_agentdefinition.import logging
from google.adk.agents import Agent, CallbackContext, RemoteA2aAgent, AGENT_CARD_WELL_KNOWN_PATH
# --- Observability Callback ---
def before_tool_callback(callback_context: CallbackContext, tool_name: str, args: dict) -> None:
"""Logs every delegation attempt."""
if tool_name == "transfer_to_agent":
logging.info(
f"[OBSERVABILITY] Delegating to remote agent '{args.get('agent_name')}' "
f"with query: {args.get('query')}"
)
return None
# --- Remote Agent Definitions ---
# TODO: 1. Define `remote_web_agent` as a `RemoteA2aAgent`.
# - Name: "web_agent"
# - Description: "A remote specialist for searching and clicking on the e-commerce website."
# - agent_card: Point to the web-agent server at `http://localhost:8001/a2a/web_agent/.well-known/agent-card.json`.
remote_web_agent = RemoteA2aAgent(
name="web_agent",
description="A remote specialist for searching and clicking on the e-commerce website.",
agent_card=f"http://localhost:8001/a2a/web_agent{AGENT_CARD_WELL_KNOWN_PATH}"
)
# TODO: 2. Define `remote_personalization_agent` as a `RemoteA2aAgent`.
# - Name: "personalization_agent"
# - Description: "A remote specialist for saving and retrieving user preferences."
# - agent_card: Point to the personalization-agent server at `http://localhost:8002/a2a/personalization_agent/.well-known/agent-card.json`.
remote_personalization_agent = RemoteA2aAgent(
name="personalization_agent",
description="A remote specialist for saving and retrieving user preferences.",
agent_card=f"http://localhost:8002/a2a/personalization_agent{AGENT_CARD_WELL_KNOWN_PATH}"
)
# --- Main Orchestrator Agent ---
# TODO: 3. Define the `root_agent`. It should be an `Agent` that:
# - Uses the `gemini-2.5-flash` model.
# - Is named "orchestrator_agent".
# - Has an instruction to act as a master shopping assistant, coordinating with specialists.
# - Its `sub_agents` list should contain `remote_web_agent` and `remote_personalization_agent`.
# - Registers the `before_tool_callback` for observability.
root_agent = Agent(
model="gemini-2.5-flash",
name="orchestrator_agent",
instruction="""You are a master shopping assistant. Your job is to coordinate with specialist agents to help the user.\n\n **Workflow:**\n 1. **Understand Intent:** Greet the user and understand what they want to do. If they upload an image, describe it first, then ask if they want to search for that item.\n 2. **Delegate Tasks:**\n - To search or click on the website, you MUST delegate to the `web_agent`.\n - To save or get user preferences, you MUST delegate to the `personalization_agent`.\n 3. **Synthesize Results:** Summarize the results from the specialist agents and present them clearly to the user.\n """,
sub_agents=[remote_web_agent, remote_personalization_agent],
before_tool_callback=before_tool_callback
) -
Navigate back to
capstone-shopping-system:cd ..
Exercise 4: Add Multimodal Vision
Enhance the Orchestrator to handle image-based searches.
- Challenge: Update the Orchestrator's
instructionprompt. Add logic to handle image uploads. If a user provides an image, instruct the agent to: a. First, describe the item in the image. b. Then, use that text description to perform a search by delegating to theweb-agent.
Exercise 5: Create a Deployment Plan
Plan how you would deploy this distributed system.
- Challenge: Create a
Dockerfilefor theweb-agent. This file should define the steps to build a container image for your remote agent. - Create a
deployment_plan.mdfile. In this file, briefly explain the steps you would take to deploy theorchestrator-agent,web-agent, andpersonalization-agentas separate services on Google Cloud Run.
Running the System
To test your full system, you will need to run all three agents in separate terminals:
- Terminal 1 (
web-agent):uvicorn agent:a2a_app --host localhost --port 8001 - Terminal 2 (
personalization-agent):uvicorn agent:a2a_app --host localhost --port 8002 - Terminal 3 (
orchestrator-agent):adk web orchestrator-agent
Interact with the Orchestrator in the Dev UI and use the Trace view to observe the A2A communication and delegation.
Cleanup (Important!)
This is a complex lab with multiple deployments. It is crucial to delete the resources you created after completing the lab.
For Local Development:
- Stop all running
uvicornandadk webprocesses (Ctrl+C in each terminal). - Delete the
capstone-shopping-systemdirectory:cd ..
rm -rf capstone-shopping-system
For Cloud Deployments (if you completed Exercise 5):
- Delete Cloud Run Services:
gcloud run services delete web-agent-service --region=$GOOGLE_CLOUD_LOCATION --async
gcloud run services delete personalization-agent-service --region=$GOOGLE_CLOUD_LOCATION --async
gcloud run services delete orchestrator-agent-service --region=$GOOGLE_CLOUD_LOCATION --async - Delete Artifact Registry Repository:
gcloud artifacts repositories delete adk-images --location=$GOOGLE_CLOUD_LOCATION --async - Delete the GitHub Repository: If you used the Agent Starter Pack, delete the GitHub repository you created.
Self-Reflection Questions
- This system uses three separate agents. What are the advantages of this distributed architecture in terms of scalability, maintainability, and reusability?
- The
orchestrator-agentuses abefore_tool_callbackfor logging. How does this separate the concern of observability from the agent's core business logic? - The
web-agentabstracts the website behind an OpenAPI spec. Why is this a better design than having the orchestrator directly interact with the raw HTML of the website?
🕵️ Hidden Solution 🕵️
Looking for the solution? Here's a hint (Base64 decode me):
L2RvYy1hZGstdHJhaW5pbmcvbW9kdWxlMzctYWR2YW5jZWQtcGVyc29uYWxpemVkLXNob3BwaW5nLWFnZW50L2xhYi1zb2x1dGlvbg==
The direct link is: Lab Solution