Deploy an Agent with Agent Development Kit (ADK): Challenge Lab




Task 1 — Create the Search app & data store

  1. In Google Cloud Console, go to Agent Platform > Agents > Search.
  2. Click Create App (or similar "New App" button).
  3. Configure:
    • App type: Custom search (general)
    • App name: Paint Search
    • Company name: Cymbal Shops
    • Location: global
  4. When prompted to create/select a data store, click Create new data store:
    • Data source: Cloud Storage
    • Data type: Documents
    • Folder or File: File
    • File path: qwiklabs-gcp-04-1b866eb7d41d-bucket/Cymbal_Shops_Paint_Datasheets.pdf
    • Location: global
    • Data store name: Cymbal Paint
  5. Under advanced/parsing options:
    • Document parser: Layout Parser
    • Enable table annotation: On
    • Include ancestor headings in chunks: On
  6. Set Pricing model to General for both the App and the Data store.
  7. Click Create, then click Check my progress to verify.

Task 2 — Set up Cloud Shell environment

  1. Click Activate Cloud Shell in the console top bar → ContinueAuthorize.
  2. Open the Cloud Shell Editor (pencil icon), open the Explorer, click Open FolderOK to open your home directory.
  3. In the Terminal, run:
bash
gcloud storage cp -r gs://qwiklabs-gcp-04-1b866eb7d41d-bucket/adk_challenge_lab .
  1. Set up PATH and install dependencies:
bash
export PATH=$PATH:"/home/${USER}/.local/bin"
python3 -m pip install -r adk_challenge_lab/requirements.txt
python3 -m pip install chainlit==2.11.1
  1. Find your Search Engine ID: go to Agent Platform > Agents > Search, find "Paint Search" in the app list, copy its ID (looks like paint-search_1756...).
  2. Create the .env file — copy this into a text file, replace YOUR_ID, then paste into the terminal:
bash
cd ~/adk_challenge_lab
cat << EOF > .env
GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT=qwiklabs-gcp-04-1b866eb7d41d
GOOGLE_CLOUD_LOCATION=us-central1
RESOURCES_BUCKET=qwiklabs-gcp-04-1b866eb7d41d-bucket
MODEL=gemini-2.5-flash
SEARCH_ENGINE_ID=paint-search_XXXXXXXXXX
EOF
  1. Copy it into the agent folder:
bash
cp .env paint_agent/.env

Task 3 — Debug the agent (the AgentTool fix)

  1. Confirm the bug first:
bash
adk run paint_agent

Type hello, then yes — you should see the Multiple tools are supported only when they are all search tools error.

  1. Open adk_challenge_lab/paint_agent/agent.py in the editor.
  2. Identify the sub-agent using VertexAiSearchTool — it's search_agent (check sub_agents/search_agent/agent.py to confirm).
  3. Edit agent.py:
    • Add the import at the top:
python
     from google.adk.tools.agent_tool import AgentTool
  • Remove search_agent from the sub_agents=[...] list on root_agent.
  • Add it to tools=[...] instead:

 

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.adk.tools.agent_tool import AgentTool
from dotenv import load_dotenv
import google.auth
from google.adk.agents import Agent
from google.adk.tools import AgentTool
from google.adk.models import Gemini
from google.genai import types
import google.cloud.logging
import os

from .callback_logging import log_query_to_model, log_model_response

from .sub_agents.room_planner.agent import room_planner_agent
from .sub_agents.search_agent.agent import search_agent

from .tools import set_session_value


# Load env
load_dotenv()

RETRY_OPTIONS = types.HttpRetryOptions(initial_delay=1, max_delay=3, attempts=30)

# Configure logging to the Cloud
cloud_logging_client = google.cloud.logging.Client()
cloud_logging_client.setup_logging()

root_agent = Agent(
    name="paint_agent",
    model=Gemini(model=os.getenv("MODEL"), retry_options=RETRY_OPTIONS),
    instruction="""
    You represent the paint department of Cymbal Shops.

    Information about Cymbal Shops paint, including prices, is available to you
    through the 'search_agent' tool.

    - At the start of a conversation, let the user know you're here to
      help them find the right paint for their project. Ask them if they'd
      like to learn more about the different paint products offered by
      Cymbal Shops.
    - If they say yes, include information about all paint products including
      coverage rate and price.
    - If price and coverage rate aren't returned for some products, look them
      up individually.
    - After they have selected a paint product, use your set_session_value tool
      to  store their selection in the session dictionary with the key
      'SELECTED_PAINT', its coverage rate in 'COVERAGE_RATE', and its price
      per 2.5L container in 'PRICE'.
    - Transfer to the 'room_planner_agent'
    """,
    before_model_callback=log_query_to_model,
    after_model_callback=log_model_response,
    sub_agents=[room_planner_agent],
    tools=[
        set_session_value,
        AgentTool(agent=search_agent, skip_summarization=False),
    ],
)

  1. Save the file.
  2. Wait until the Cymbal Paint data store shows Ready on the Documents tab (Agent Platform > Agents > Search > Data Stores > Cymbal Paint), then wait a couple extra minutes.
  3. Re-run and test:
bash
adk run paint_agent

Ask it for prices of EcoGreens and Forever Paint. Click Check my progress.

  1. Type exit when done.

Task 4 — Implement shared state

  1. Open adk_challenge_lab/paint_agent/tools.py. Find set_session_value and implement it:
# Copyright 2025 Google LLC
# ... (Standard Header)

from google.adk.tools.agent_tool import AgentTool
from dotenv import load_dotenv
import google.auth
from google.adk.agents import Agent
from google.adk.models import Gemini
from google.genai import types
import google.cloud.logging
import os

from .callback_logging import log_query_to_model, log_model_response
from .sub_agents.room_planner.agent import room_planner_agent
from .sub_agents.search_agent.agent import search_agent
from .tools import set_session_value

# Load env
load_dotenv()

RETRY_OPTIONS = types.HttpRetryOptions(initial_delay=1, max_delay=3, attempts=30)

# Configure logging to the Cloud
cloud_logging_client = google.cloud.logging.Client()
cloud_logging_client.setup_logging()

root_agent = Agent(
    name="paint_agent",
    model=Gemini(model=os.getenv("MODEL"), retry_options=RETRY_OPTIONS),
    instruction="""
    You represent the paint department of Cymbal Shops.

    Information about Cymbal Shops paint, including prices, is available to you
    through the 'search_agent' tool.

    - At the start of a conversation, let the user know you're here to
      help them find the right paint for their project. Ask them if they'd
      like to learn more about the different paint products offered by
      Cymbal Shops.
    - If they say yes, include information about all paint products including
      coverage rate and price.
    - If price and coverage rate aren't returned for some products, look them
      up individually.
    - After they have selected a paint product, use your set_session_value tool
      to store their selection in the session dictionary with the key
      'SELECTED_PAINT', its coverage rate in 'COVERAGE_RATE', and its price
      per 2.5L container in 'PRICE'.
    - Transfer to the 'room_planner_agent'
    """,
    before_model_callback=log_query_to_model,
    after_model_callback=log_model_response,
    sub_agents=[room_planner_agent],
    tools=[
        set_session_value,
        AgentTool(agent=search_agent, skip_summarization=False),
    ],
)


  1. Open the agent.py for coverage_calculator_agent (under room_planner's sub-agents). Find the instruction string with ALL-CAPS placeholders like SELECTED_PAINT, COVERAGE_RATE, etc. Replace each with ADK's optional templating syntax:
Before: ...using the paint SELECTED_PAINT with coverage rate COVERAGE_RATE...
After:  ...using the paint {SELECTED_PAINT?} with coverage rate {COVERAGE_RATE?}...

Do this for every ALL-CAPS placeholder in that instruction.

  1. Save both files.
  2. Launch the dev UI:
bash
adk web --allow_origins "regex:https://.*\.cloudshell\.dev"

  1. You should now be able to have the following conversation with your agent:

    YouAgent Response
    hello[Offers to share information about Cymbal Shops' paints]
    yes[Shares information about paint products, i.e. Project Paint, EcoGreens, SureCoverage, Forever Paint.]
    I'd like to use EcoGreens[The State tab should show updated state values. Asks how many rooms and their names.]
    Just one room, my office[Asks you to select a color for your office.]
    Deep Ocean[Asks you for the room dimensions.]
    3m by 4m. 3m high. 1 door, 2 windows.[Confirms how many coats.]
    Two coats.[Calculates you will need coverage of 74 sq meters.]

  1. Open the web preview, select paint_agent, and run through the sample conversation in the doc (hello → yes → "I'd like to use EcoGreens" → room name → color → dimensions → coats). Confirm the State tab updates and the coverage calculation (74 sq meters) comes back correctly.
  2. Click Check my progress.
  3. Back in the terminal, press Ctrl+C to stop the server.

Task 5 — Deploy to Agent Runtime

  1. Make sure you're in the right directory:
bash
cd ~/adk_challenge_lab
  1. Deploy:
bash
adk deploy agent_engine paint_agent \
  --display_name "Paint Agent" 

This takes 5–10 minutes.

  1. While it deploys, go to IAM in the console and grant these roles to service-573697952121@gcp-sa-aiplatform-re.iam.gserviceaccount.com:
    • Agent Platform User
    • Discovery Engine User
  2. When deployment finishes, copy the printed agent resource name — you'll need it next.
  3. Click Check my progress.

Task 6. Query the deployed agent

  1. In the file adk_challenge_lab/chainlit_ui/app.py, find and update the following line with your deployed agent's resource name to load your remote agent:


agent = client.agent_engines.get(name='YOUR_AGENT_RESOURCE_NAME')


agent = client.agent_engines.get(name='projects/653349639632/locations/us-central1/reasoningEngines/9083023225859866624'


import chainlit as cl import vertexai from uuid import uuid4 from dotenv import load_dotenv import os from bs4 import BeautifulSoup import asyncio load_dotenv() project_id = os.environ["GOOGLE_CLOUD_PROJECT"] location = os.environ["GOOGLE_CLOUD_LOCATION"] bucket_name = f"gs://{project_id}-bucket" client = vertexai.Client( project=project_id, location=location, ) # Updated resource name for Task 6 agent = client.agent_engines.get(name='projects/653349639632/locations/us-central1/reasoningEngines/9083023225859866624') def convert_img_tags_to_chainlit_images(msg): img_list = [] if not msg.content: return msg soup = BeautifulSoup(msg.content, "html.parser") print("Converting img tags to chainlit images") found_imgs = soup.find_all("img") for img_tag in found_imgs: if img_tag.has_attr("src"): img = cl.Image(url=img_tag["src"], name="swatch", display="inline") img_list.append(img) # Remove the tag so it doesn't appear as a broken image in text img_tag.decompose() msg.elements = img_list # Only update content if there's actual text left, otherwise keep original # logic or a space to ensure msg.content isn't 'falsy' if elements exist cleaned_text = soup.get_text().strip() if cleaned_text: msg.content = cleaned_text elif img_list: # If we have images but no text, provide a small spacer so Chainlit displays it msg.content = " " return msg @cl.set_starters async def set_starters(): return [ cl.Starter( label="Painting Project Help", message="Tell me about Cymbal Shops' interior paints.", icon="/public/swatches.svg", ) ] @cl.on_chat_start async def on_chat_start(): print("A new chat session has started!") user_id = "user" try: # Use make_async to avoid blocking the event loop during session creation session_details = await cl.make_async(agent.create_session)(user_id=user_id) cl.user_session.set("user_id", user_id) cl.user_session.set("session_id", session_details["id"]) cl.user_session.set( "message_history", [{"role": "system", "content": "You are a helpful assistant."}], ) except Exception as e: print(f"Error starting chat: {e}") await cl.Message(content="Failed to initialize agent session. Please refresh the page.").send() @cl.on_message async def main(message: cl.Message): # Ensure session is ready, wait slightly if it's currently initializing for _ in range(10): user_id = cl.user_session.get("user_id") session_id = cl.user_session.get("session_id") if session_id: break await asyncio.sleep(0.5) else: await cl.Message(content="Session initialization is taking longer than expected. Please refresh.").send() return message_history = cl.user_session.get("message_history") message_history.append({"role": "user", "content": message.content}) msg = cl.Message(content="") try: async_stream = agent.async_stream_query( user_id=user_id, session_id=session_id, message=message.content, ) # Use the native async generator provided by ADK async for event in async_stream: print(f"DEBUG: Received event: {event}") # Check if the stream returned an explicit backend error if isinstance(event, dict): if "error" in event: await cl.Message(content=f"Agent Engine Error: {event['error']}").send() return # Handle payload errors like "code: 498, Event loop is closed" silently if "code" in event and "message" in event: print(f"Agent Backend Notification (Code {event.get('code')}): {event.get('message')}") continue # Check for standard content and parts if "content" in event and "parts" in event["content"]: for part in event["content"]["parts"]: if "text" in part: await msg.stream_token(part["text"]) # Post-process message convert_img_tags_to_chainlit_images(msg) # Check if we have content OR elements (images) actual_content = msg.content.strip() if msg.content else "" if actual_content or msg.elements: message_history.append({"role": "assistant", "content": msg.content}) await msg.update() else: # If the stream finished completely empty, optionally clean up the empty message await msg.remove() except Exception as e: print(f"Error querying agent: {e}") await cl.Message(content=f"An error occurred: {str(e)}").send()


 
  1. Run the UI with the following:

    cd ~/adk_challenge_lab/chainlit_ui
    chainlit run app.py

    Expected output:

    2025-08-25 12:30:00 - Your app is available at http://localhost:8000
  2. Click the link for http://localhost:8000 to open it in a new browser tab.

  3. Have the following conversation with your deployed agent:

    YouAgent Response
    hello[Offers to share information about Cymbal Shops’ paints]
    yes[Shares information about paint products, i.e. Project Paint, EcoGreens, SureCoverage, Forever Paint ]
    I'd like to use Forever Paint[Asks how many rooms and their names]
    Two rooms. The living room and a baby's room.[Asks you to select a color for your living room and baby's room.]
    "Sunlight through a canvas tent" for the baby's room and "Coffee Cream" for the living room.[Asks you for the room dimensions]
    The living room is 5m by 4m. 2.5m high. 1 door, 3 windows.[Asks number of coats.]
    Two coats.[Requests dimensions for baby's room.]
    The baby's room is 3m by 3m. 2.5m high. 1 door, 1 window.[Provides a one-coat estimate and confirms the number of coats.]
    Always two coats.[Calculates you will need coverage of 77 sq meters for the living room and 53 sq meters for the baby's room.]
  4. If you'd like to start a new conversation with your agent, you can click the icon in the upper left to Create a New Chat.

    Click Check my progress to verify the objective.

Congratulations!

In this lab, you have:

  • Built an agent with Agent Development Kit (ADK) made up of a root agent and sub-agents
  • Enabled agents with an Agent Search tool and Python functions used as tools
  • Stored agent output in a session state dictionary and retrieved values from the session state dictionary for subsequent agent instructions
  • Deployed your agent to Agent Runtime
  • Queried the agent deployed to Agent Runtime
run on cloud shell 

cd ~/adk_challenge_lab/chainlit_ui
chainlit run app.py




Post a Comment

Ad Space

Responsive Advertisement