סוכנים בהיקף נרחב: ארכיטקטורה מרובת סוכנים עם פרוטוקול A2A ב-Agent Runtime ושילוב של ADK

1. מבוא

ככל שסוכני ה-AI מקבלים יותר אחריות, קשה יותר לתחזק, להרחיב ולפתח סוכן יחיד שמבצע את כל הפעולות. יכולות שונות דורשות לעיתים קרובות אסטרטגיות פריסה שונות, מחזורי עדכון שונים או אפילו צוותים שונים שאחראים עליהן.

  • פרוטוקול A2A (Agent2Agent) פותר את בעיית התקשורת – הוא קובע תקן לאופן שבו סוכנים מגלים את היכולות של סוכנים אחרים ומשתפים פעולה בין מסגרות וארגונים.
  • Gemini Enterprise Agent Platform Runtime פותרת את בעיית הפריסה – פלטפורמה מנוהלת ללא שרת (serverless) שמארחת את הסוכנים שלכם עם תמיכה מובנית ב-A2A, התאמה אוטומטית לעומס, נקודות קצה מאובטחות, הפעלת סשנים רציפה ואפס ניהול תשתית.

הם מאפשרים לכם ליצור סוכנים מיוחדים, לפרוס אותם כשירותי A2A שניתנים לגילוי ולשלב אותם במערכות מרובות סוכנים.

מה תפַתחו

סוכן הזמנות שמנהל הזמנות לשולחנות במסעדות (יצירה, בדיקה וביטול) באמצעות מצב סשן של ADK שמנוהל על ידי סשנים של Gemini Enterprise Agent Platform. אתם פורסים את הסוכן הזה ב-Gemini Enterprise Agent Platform Runtime, שם הוא הופך לזמין לגילוי באמצעות כרטיס הסוכן של פרוטוקול A2A. לאחר מכן משדרגים את סוכן שירות הלקוחות של המסעדה Foodie Finds (מתוך הסדנה המקדימה , אל דאגה אם לא ביקרתם בסדנה – הכנו מאגר התחלתי בשבילכם) כדי להשתמש בסוכן ההזמנות כסוכן משנה מרוחק מסוג A2A. התוצאה: מערכת מרובת סוכנים שבה המנהל מנתב שאילתות לגבי התפריט ל-MCP Toolbox ובקשות להזמנות לסוכן A2A מרחוק.

143fadef342e67a6.jpeg

מה תלמדו

  • פיתוח סוכן ADK שמשתמש בשירות מנוהל של סשנים כדי לנהל נתוני הזמנות
  • חשיפת סוכן ADK כשרת A2A עם כרטיסי סוכן ומיומנויות
  • פריסת סוכן A2A בסביבת זמן הריצה של סוכן Gemini Enterprise
  • שימוש בסוכן A2A מרוחק מסוכן ADK אחר באמצעות RemoteA2aAgent וטיפול בבקשה מאומתת
  • בדיקה מצטברת של מערכות מרובות סוכנים: A2A מקומי, A2A שנפרס, שילוב חלקי, פריסה מלאה

דרישות מוקדמות

2. הגדרת הסביבה – המשך מה-codelab הקודם

התיאורים שאנחנו מספקים ב-Codelab הזה הם המשך של ה-Codelab הזה בנושא דרישות מוקדמות: Agentic RAG עם ADK,‏ MCP Toolbox ו-Cloud SQL . אפשר להמשיך לעבוד מנקודת הסיום של ה-codelab הקודם

אפשר להתחיל לבנות בספריית העבודה הקודמת של ה-codelab ( ספריית העבודה צריכה להיות build-agent-adk-toolbox-cloudsql). כדי למנוע בלבול, נשנה את שם הספרייה לאותו שם ספרייה שבו אנחנו משתמשים כשאנחנו מתחילים מחדש

mv ~/build-agent-adk-toolbox-cloudsql ~/adk-a2a-agent-runtime-starter
cloudshell workspace ~/adk-a2a-agent-runtime-starter && cd ~/adk-a2a-agent-runtime-starter
source .env

מוודאים שקובצי המפתח מה-codelab הקודם נמצאים במקום:

echo "--- Restaurant Agent ---"
cat restaurant_agent/agent.py | head -5
echo ""
echo "--- Toolbox Config ---"
cat tools.yaml | head -5

אמור להופיע הקובץ restaurant_agent/agent.py עם הייבוא LlmAgent, והקובץ tools.yaml עם ההגדרה של כלי ניהול ההרשאות.

בשלב הבא, נאתחל מחדש את סביבת Python

rm -rf .venv
uv sync

כמו כן, צריך לוודא שמסד הנתונים מוכן:

uv run python scripts/verify_seed.py

אם תעקבו אחרי כל פרטי הבדיקה מה-codelab הקודם, יכול להיות שתראו פלט כמו זה

Menu Items: 16/15
Embeddings: 16/15

✗ Database not ready

זה בסדר. בבדיקה של מסד הנתונים לא נלקחים בחשבון הנתונים הנוספים שהזנתם בבדיקה של הטמעת הנתונים. אם יש לכם 15 נתונים או יותר, הכול בסדר.

הפעלת API נדרש

לאחר מכן, נצטרך לוודא שהפעלנו את ה-API הנדרש כדי ליצור אינטראקציה עם Gemini Enterprise Agent Platform

gcloud services enable \
  cloudresourcemanager.googleapis.com

כבר אמורים להיות לכם הקבצים והתשתית הדרושים כדי להמשיך לקטע הבא: A2A Protocol and Gemini Enterprise Agent Runtime !

3. הגדרת סביבה – התחלה חדשה עם מאגר המתחילים

בשלב הזה מכינים את סביבת Cloud Shell, מגדירים את הפרויקט ב-Google Cloud ומשכפלים את מאגר התבניות.

פתיחת Cloud Shell

פותחים את Cloud Shell בדפדפן. ‫Cloud Shell מספקת סביבה שהוגדרה מראש עם כל הכלים שדרושים ל-Codelab הזה. כשמוצגת בקשה, לוחצים על Authorize

אחר כך לוחצים על View (תצוגה) -> Terminal (טרמינל) כדי לפתוח את הטרמינל.הממשק אמור להיראות בערך כך:

86307fac5da2f077.png

זה יהיה הממשק הראשי שלנו, סביבת הפיתוח המשולבת (IDE) בחלק העליון והטרמינל בחלק התחתון

הגדרת ספריית העבודה

משכפלים את מאגר המתחילים, כל הקוד שכותבים ב-codelab הזה נמצא כאן:

rm -rf ~/adk-a2a-agent-runtime-starter
git clone https://github.com/alphinside/adk-a2a-agent-runtime-starter.git
cloudshell workspace ~/adk-a2a-agent-runtime-starter && cd ~/adk-a2a-agent-runtime-starter

יוצרים את קובץ .env מהתבנית שסופקה:

cp .env.example .env

כדי לפשט את הגדרת הפרויקט במסוף, מורידים את סקריפט הגדרת הפרויקט הזה לספריית העבודה:

curl -sL https://raw.githubusercontent.com/alphinside/cloud-trial-project-setup/main/setup_verify_trial_project.sh -o setup_verify_trial_project.sh

מריצים את הסקריפט. הסקריפט מאמת את החשבון לחיוב שלכם בתקופת הניסיון, יוצר פרויקט חדש (או מאמת פרויקט קיים), שומר את מזהה הפרויקט בקובץ .env בספרייה הנוכחית ומגדיר את הפרויקט הפעיל ב-gcloud.

bash setup_verify_trial_project.sh && source .env

הסקריפט:

  1. אימות שיש לכם חשבון לחיוב עם תקופת ניסיון פעילה
  2. בודקים אם יש פרויקט קיים ב-.env (אם יש)
  3. יצירת פרויקט חדש או שימוש חוזר בפרויקט קיים
  4. קישור החשבון לחיוב בתקופת הניסיון לפרויקט
  5. שומרים את מזהה הפרויקט ב-.env
  6. הגדרת הפרויקט כפרויקט פעיל ב-gcloud

כדי לוודא שהפרויקט מוגדר בצורה נכונה, בודקים את הטקסט הצהוב שליד ספריית העבודה בהנחיה של טרמינל Cloud Shell. מזהה הפרויקט צריך להופיע בו.

5c515e235ee1179f.png

הפעלת API נדרש

לאחר מכן, נצטרך לוודא שהפעלנו את ה-API הנדרש כדי ליצור אינטראקציה עם Gemini Enterprise Agent Platform

gcloud services enable \
  aiplatform.googleapis.com \
  cloudresourcemanager.googleapis.com

הגדרה של תשתית Starter

קודם נצטרך להתקין יחסי תלות של Python באמצעות uv, שהוא מנהל מהיר של חבילות ופרויקטים של Python שנכתב ב-Rust ( מסמכי uv). בשיעור Codelab הזה אנחנו משתמשים בו כדי לשמור על מהירות ופשטות בתחזוקת פרויקט Python.

uv sync

לאחר מכן, מריצים את סקריפט ההגדרה המלא, שיוצר את מכונת Cloud SQL, מטמיע נתונים ופורס את שירות Toolbox שישמש כמצב ההתחלתי של סוכן המסעדה.

bash scripts/full_setup.sh > logs/full_setup.log 2>&1 &

4. מושג: פרוטוקול Agent2Agent‏ (A2A) ו-Gemini Enterprise Agent Runtime

לפני שמתחילים לבנות, כדאי להקדיש רגע להסבר על שתי הטכנולוגיות העיקריות שמוצגות ב-codelab הזה כדי להרחיב את האפליקציה האג'נטית שלנו.

פרוטוקול Agent2Agent‏ (A2A)

פרוטוקול Agent2Agent‏ (A2A) הוא תקן פתוח שנועד לאפשר תקשורת ושיתוף פעולה חלקים בין סוכני AI. בזמן ש-MCP (Model Context Protocol) מקשר בין סוכנים לבין כלים ונתונים, פרוטוקול A2A מקשר בין סוכנים לבין סוכנים אחרים. כך הם יכולים לגלות את היכולות של כל אחד מהם, להעביר משימות ולשתף פעולה בין מסגרות וארגונים.

5586b67d0437d79f.png

ההבדל העיקרי בין עטיפת סוכן ככלי (באמצעות MCP) לבין חשיפתו באמצעות A2A: כלים הם חסרי מצב ומבצעים פונקציות יחידות, בעוד שסוכני A2A יכולים להסיק מסקנות, לשמור על מצב ולטפל באינטראקציות רב-שלביות כמו משא ומתן או הבהרה. סוכן שנחשף באמצעות A2A שומר על היכולות המלאות שלו, ולא מצטמצם לקריאה לפונקציה.

פרוטוקול A2A מגדיר שלושה מושגים מרכזיים:

  1. כרטיס הסוכן – מסמך JSON שמתאר מה הסוכן עושה, מהן המיומנויות שלו ומהי נקודת הקצה שלו. סוכנים אחרים מאחזרים את הכרטיס הזה כדי לגלות יכולות.
  2. הודעה – בקשה של משתמש או נציג שנשלחת לנקודת קצה של A2A ומפעילה משימה.
  3. משימה – יחידת עבודה עם מחזור חיים (הוגשה ← מתבצעת ← הושלמה/נכשלה) וארטיפקטים שמכילים את התוצאות.

e7e3224d05b725f0.jpeg

מידע נוסף זמין במאמר מה זה A2A?

Gemini Enterprise Agent Platform Runtime

Agent Runtime הוא שירות מנוהל במלואו ב-Google Cloud לפריסה, להרחבה ולניהול של סוכני AI בסביבת ייצור עם תכונות אבטחה ברמת Enterprise (למשל, VPC Service Controls,‏ CMEK). השירות מטפל בתשתית, כך שאתם יכולים להתמקד בלוגיקה של הסוכן.

8ecbfbce8f0b9557.png

‫Agent Runtime מספק:

  • פריסה מנוהלת – פריסת סוכנים שנוצרו באמצעות ADK,‏ LangGraph או כל מסגרת Python באמצעות קריאה אחת ל-SDK
  • אירוח A2A – פריסת סוכנים כנקודות קצה שתואמות ל-A2A עם הצגה אוטומטית של כרטיס הסוכן וגישה מאומתת
  • סשנים מתמשכיםVertexAiSessionService שומר את היסטוריית השיחות והסטטוס של הבקשות
  • התאמה אוטומטית לעומס – התאמה לעומס מאפס כדי לטפל בתעבורת נתונים, ללא ניהול תשתית
  • ניראות (observability) – מעקב, רישום ביומן ומעקב מובנים באמצעות חבילת הניראות של Google Cloud
  • ועוד הרבה תכונות אחרות. פרטים נוספים מופיעים במאמר הזה.

ב-Codelab הזה נסביר איך פורסים את סוכן ההזמנות ל-Agent Runtime. במהלך תהליך הפריסה, קוד הסוכן עובר סריאליזציה (pickling) והעלאה. Agent Runtime מספקת נקודת קצה ללא שרת (serverless) שמשרתת את פרוטוקול A2A – סוכנים אחרים (או לקוחות) מקיימים איתה אינטראקציה באמצעות קריאות HTTP רגילות, שעוברות אימות באמצעות פרטי כניסה ל-Google Cloud.

5. איך בונים סוכן הזמנות

בשלב הזה יוצרים סוכן ADK חדש שמטפל בהזמנות במסעדות באמצעות מצב סשן. הנציג תומך בשלוש פעולות – יצירה, בדיקה וביטול – עם מספר הטלפון כמפתח החיפוש. כל נתוני ההזמנות נמצאים במצב הסשן של ADK

יצירת תשתית לסוכן

משתמשים ב-adk create כדי ליצור את מבנה ספריית הסוכן עם המודל הנכון והגדרת הפרויקט:

source .env
uv run adk create reservation_agent \
    --model gemini-3.5-flash \
    --project ${GOOGLE_CLOUD_PROJECT} \
    --region ${GOOGLE_CLOUD_LOCATION}

הפעולה הזו יוצרת ספרייה בשם reservation_agent/ עם הקבצים __init__.py,‏ agent.py ו-.env שהוגדרו מראש למודל Gemini ב-Agent Platform.

adk-a2a-agent-runtime-starter/
├── reservation_agent/
│   ├── __init__.py
│   ├── agent.py
│   └── .env
├── logs
├── scripts
└── ...

בשלב הבא, נעדכן את קוד הסוכן

כתיבת קוד הסוכן

פותחים את קובץ הסוכן שנוצר:

cloudshell edit reservation_agent/agent.py

אחר כך מחליפים את התוכן בתוכן הבא:

# reservation_agent/agent.py
import os
from functools import cached_property
from typing import Any

from google.adk.agents import LlmAgent
from google.adk.models.google_llm import Gemini
from google.adk.tools import ToolContext
from google.genai import Client, types

# App-scoped state prefix ensures reservations persist across all sessions.
# See https://adk.dev/sessions/state/ for state scope details.
STATE_PREFIX = "app:reservation:"


class GeminiGlobal(Gemini):
    """Gemini with location pinned to 'global'.

    A2aAgent.set_up() on Agent Platform Runtime SDK overwrites GOOGLE_CLOUD_LOCATION with the Agent Platform Runtime
    region (e.g. us-central1), but the Gemini 3-family endpoint requires 'global'.
    """

    @cached_property
    def api_client(self) -> Client:
        project = os.getenv("GOOGLE_CLOUD_PROJECT")
        return Client(
            project=project,
            location="global",
            http_options=types.HttpOptions(
                headers=self._tracking_headers(),
                retry_options=self.retry_options,
            ),
        )


def create_reservation(
    phone_number: str,
    name: str,
    party_size: int,
    date: str,
    time: str,
    tool_context: ToolContext,
) -> dict:
    """Create a new restaurant reservation.

    Args:
        phone_number: Customer's phone number, used as the reservation ID.
        name: Name for the reservation.
        party_size: Number of guests.
        date: Reservation date (e.g., '2025-07-15' or 'this Friday').
        time: Reservation time (e.g., '7:00 PM').

    Returns:
        Confirmation of the reservation.
    """
    reservation = {
        "name": name,
        "party_size": party_size,
        "date": date,
        "time": time,
        "status": "confirmed",
    }
    tool_context.state[f"{STATE_PREFIX}{phone_number}"] = reservation
    return {
        "status": "confirmed",
        "message": f"Reservation created for {name}, party of {party_size} on {date} at {time}. Phone: {phone_number}.",
    }


def check_reservation(phone_number: str, tool_context: ToolContext) -> dict:
    """Look up an existing reservation by phone number.

    Args:
        phone_number: The phone number used when the reservation was created.
        tool_context: ADK tool context for state access.

    Returns:
        The reservation details, or a message if not found.
    """
    reservation = tool_context.state.get(f"{STATE_PREFIX}{phone_number}")
    if reservation:
        return {"found": True, "reservation": reservation}
    return {"found": False, "message": f"No reservation found for {phone_number}."}


def cancel_reservation(phone_number: str, tool_context: ToolContext) -> dict:
    """Cancel an existing reservation by phone number.

    Args:
        phone_number: The phone number used when the reservation was created.
        tool_context: ADK tool context for state access.

    Returns:
        Confirmation of cancellation, or a message if not found.
    """
    key = f"{STATE_PREFIX}{phone_number}"
    reservation = tool_context.state.get(key)
    if not reservation:
        return {
            "success": False,
            "message": f"No reservation found for {phone_number}.",
        }
    if reservation.get("status") == "cancelled":
        return {
            "success": False,
            "message": f"Reservation for {phone_number} is already cancelled.",
        }
    reservation["status"] = "cancelled"
    tool_context.state[key] = reservation
    return {
        "success": True,
        "message": f"Reservation for {reservation['name']} ({phone_number}) has been cancelled.",
    }


root_agent = LlmAgent(
    name="reservation_agent",
    model=GeminiGlobal(model="gemini-3.5-flash"),
    instruction="""You are a friendly reservation assistant for "Foodie Finds" restaurant.
You help diners create, check, and cancel table reservations.

When a diner wants to make a reservation, collect these details:
- Name for the reservation
- Phone number (used as the reservation ID)
- Party size (number of guests)
- Date
- Time

Always confirm the details before creating the reservation.
When checking or cancelling, ask for the phone number if not provided.
Be concise and professional.""",
    tools=[create_reservation, check_reservation, cancel_reservation],
)

6. הכנת הגדרת השרת A2A

הגדרת כרטיס סוכן A2A

כרטיס הסוכן הוא תיאור מובנה של היכולות של הסוכן – סוכנים ולקוחות אחרים משתמשים בו כדי להבין מה הסוכן עושה. יוצרים את הגדרות הכרטיס:

cloudshell edit reservation_agent/a2a_config.py

מעתיקים את הקוד הבא אל reservation_agent/a2a_config.py:

# reservation_agent/a2a_config.py
from a2a.types import AgentSkill
from vertexai.preview.reasoning_engines.templates.a2a import create_agent_card

reservation_skill = AgentSkill(
    id="manage_reservations",
    name="Restaurant Reservations",
    description="Create, check, and cancel table reservations at Foodie Finds restaurant",
    tags=["reservations", "restaurant", "booking"],
    examples=[
        "Book a table for 4 on Friday at 7pm",
        "Check reservation for 555-0101",
        "Cancel my reservation, phone number 555-0101",
    ],
    input_modes=["text/plain"],
    output_modes=["text/plain"],
)

agent_card = create_agent_card(
    agent_name="Reservation Agent",
    description="Handles restaurant table reservations — create, check, and cancel bookings for Foodie Finds restaurant.",
    skills=[reservation_skill],
)

יצירת רכיב ההפעלה A2A

ה-executor מגשר בין פרוטוקול A2A לבין סוכן ADK. הוא מקבל בקשות A2A, מריץ אותן דרך סוכן ADK ומחזיר תוצאות כמשימות A2A:

cloudshell edit reservation_agent/executor.py

מעתיקים את הקוד הבא אל reservation_agent/executor.py:

# reservation_agent/executor.py
import os
from typing import NoReturn

import vertexai
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import TaskState, TextPart, UnsupportedOperationError
from a2a.utils import new_agent_text_message
from a2a.utils.errors import ServerError
from google.adk.artifacts import InMemoryArtifactService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService, VertexAiSessionService
from google.genai import types

from reservation_agent.agent import root_agent as reservation_agent


class ReservationAgentExecutor(AgentExecutor):
    """Bridge between the A2A protocol and the ADK reservation agent.

    Uses InMemorySessionService for local testing, VertexAiSessionService
    when deployed to Agent Runtime (detected via GOOGLE_CLOUD_AGENT_ENGINE_ID).
    """

    def __init__(self) -> None:
        self.agent = None
        self.runner = None

    def _init_agent(self) -> None:
        if self.agent is not None:
            return

        self.agent = reservation_agent
        engine_id = os.environ.get("GOOGLE_CLOUD_AGENT_ENGINE_ID")

        if engine_id:
            project = os.environ.get("GOOGLE_CLOUD_PROJECT")
            location = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1")
            vertexai.init(project=project, location=location)
            session_service = VertexAiSessionService(
                project=project, location=location, agent_engine_id=engine_id,
            )
            app_name = engine_id
        else:
            session_service = InMemorySessionService()
            app_name = self.agent.name

        self.runner = Runner(
            app_name=app_name,
            agent=self.agent,
            artifact_service=InMemoryArtifactService(),
            session_service=session_service,
            memory_service=InMemoryMemoryService(),
        )

    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        if self.agent is None:
            self._init_agent()

        query = context.get_user_input()
        updater = TaskUpdater(event_queue, context.task_id, context.context_id)
        user_id = context.message.metadata.get("user_id", "a2a-user") if context.message.metadata else "a2a-user"

        if not context.current_task:
            await updater.submit()
        await updater.start_work()

        try:
            session = await self._get_or_create_session(context.context_id, user_id)
            content = types.Content(role="user", parts=[types.Part(text=query)])

            async for event in self.runner.run_async(
                session_id=session.id, user_id=user_id, new_message=content,
            ):
                if event.is_final_response():
                    parts = event.content.parts
                    answer = " ".join(p.text for p in parts if p.text) or "No response."
                    await updater.add_artifact([TextPart(text=answer)], name="answer")
                    await updater.complete()
                    break
        except Exception as e:
            await updater.update_status(
                TaskState.failed, message=new_agent_text_message(f"Error: {e!s}"),
            )
            raise

    async def _get_or_create_session(self, context_id: str, user_id: str):
        app_name = self.runner.app_name
        if context_id:
            session = await self.runner.session_service.get_session(
                app_name=app_name, session_id=context_id, user_id=user_id,
            )
            if session:
                return session
        session = await self.runner.session_service.create_session(
            app_name=app_name, user_id=user_id, session_id=context_id,
        )
        return session

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> NoReturn:
        raise ServerError(error=UnsupportedOperationError())

ה-executor מזהה אוטומטית את הסביבה שלו: אם הערך GOOGLE_CLOUD_AGENT_ENGINE_ID מוגדר (Agent Runtime מוסיף את הערך הזה בזמן הפריסה), הוא משתמש ב-VertexAiSessionService עבור סשנים מתמשכים. באופן מקומי, המערכת חוזרת ל-InMemorySessionService.

הספרייה reservation_agent צריכה לכלול עכשיו:

reservation_agent/
├── __init__.py
├── agent.py
├── a2a_config.py
├── executor.py
└── .env

7. הכנת סוכן A2A באמצעות Agent Platform SDK ובדיקה מקומית

בשלב הזה, סוכן ההזמנות עוטף את עצמו כסוכן שתואם ל-A2A באמצעות המחלקה A2aAgent של Agent Platform SDK ( שם ה-SDK עדיין משתמש במונח vertex לצורך תאימות לאחור), ואז בודק את זרימת פרוטוקול A2A המלאה באופן מקומי – אחזור כרטיס הסוכן, שליחת הודעה ואחזור המשימה. זהו אותו אובייקט A2aAgent שפורסים ל-Agent Runtime בשלב הבא.

הוספת יחסי תלות

מתקינים את Agent Platform SDK עם תמיכה ב-Agent Runtime וב-ADK, וגם את A2A SDK:

uv add "google-cloud-aiplatform[agent_engines,adk]==1.149.0" "a2a-sdk==0.3.26"

הסבר על הרכיבים של פריסת A2A

כדי לעטוף סוכן ADK עבור A2A, צריך שלושה רכיבים:

  1. כרטיס הסוכן – כרטיס ביקור שמתאר את היכולות, הכישורים וכתובת נקודת הקצה של הסוכן. סוכנים אחרים משתמשים בתיאור הזה כדי להבין מה הסוכן שלכם עושה.
  2. Agent Executor – הגשר בין פרוטוקול A2A לבין הלוגיקה של סוכן ADK. הוא מקבל בקשות A2A, מריץ אותן דרך סוכן ADK ומחזיר תוצאות כמשימות A2A.
  3. A2aAgent – המחלקה ב-Agent Platform SDK שמשלבת את הכרטיס ואת רכיב ההפעלה ליחידה שניתן לפרוס.

יצירת סקריפט הבדיקה

יוצרים את הסקריפט הבא כדי לבדוק באופן מקומי

cloudshell edit scripts/test_a2a_agent_local.py

מעתיקים את הקוד הבא אל scripts/test_a2a_agent_local.py:

# scripts/test_a2a_agent_local.py
import asyncio
import json
import os
from pprint import pprint

from dotenv import load_dotenv
from starlette.requests import Request
from vertexai.preview.reasoning_engines import A2aAgent

from reservation_agent.a2a_config import agent_card
from reservation_agent.executor import ReservationAgentExecutor

load_dotenv()


# --- Helper functions for building mock requests ---

def receive_wrapper(data: dict):
    async def receive():
        byte_data = json.dumps(data).encode("utf-8")
        return {"type": "http.request", "body": byte_data, "more_body": False}
    return receive

def build_post_request(data: dict = None, path_params: dict = None) -> Request:
    scope = {"type": "http", "http_version": "1.1", "headers": [(b"content-type", b"application/json")], "app": None}
    if path_params:
        scope["path_params"] = path_params
    return Request(scope, receive_wrapper(data))

def build_get_request(path_params: dict) -> Request:
    scope = {"type": "http", "http_version": "1.1", "query_string": b"", "app": None}
    if path_params:
        scope["path_params"] = path_params
    async def receive():
        return {"type": "http.disconnect"}
    return Request(scope, receive)


# --- Helper: poll for task completion ---

async def wait_for_task(a2a_agent, task_id, max_retries=30):
    """Poll on_get_task until the task reaches a terminal state."""
    for _ in range(max_retries):
        request = build_get_request({"id": task_id})
        result = await a2a_agent.on_get_task(request=request, context=None)
        state = result.get("status", {}).get("state", "")
        if state in ["completed", "failed"]:
            return result
        await asyncio.sleep(1)
    return result


def print_task_answer(result):
    """Extract and print the answer from task artifacts."""
    print(f"Status: {result.get('status', {}).get('state')}")
    for artifact in result.get("artifacts", []):
        if artifact.get("parts") and "text" in artifact["parts"][0]:
            print(f"Answer: {artifact['parts'][0]['text']}")


# --- Local test ---

async def main():
    # Create and set up the A2A agent locally

    a2a_agent = A2aAgent(agent_card=agent_card, agent_executor_builder=ReservationAgentExecutor)
    a2a_agent.set_up()

    # 1. Get agent card
    print("=" * 50)
    print("1. Retrieving agent card...")
    print("=" * 50)
    request = build_get_request(None)
    card_response = await a2a_agent.handle_authenticated_agent_card(request=request, context=None)
    print(f"Agent: {card_response.get('name')}")
    print(f"Skills: {[s.get('name') for s in card_response.get('skills', [])]}")

    # 2. Create a reservation
    print("\n" + "=" * 50)
    print("2. Creating a reservation...")
    print("=" * 50)
    message_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Book a table for 2 on Saturday at 6pm. Name: Bob, Phone: 555-0202"}],
            "role": "ROLE_USER",
        },
    }
    request = build_post_request(message_data)
    response = await a2a_agent.on_message_send(request=request, context=None)
    task_id = response["task"]["id"]
    context_id = response["task"].get("contextId")
    print(f"Task ID: {task_id}")

    # 3. Wait for result
    print("\n" + "=" * 50)
    print("3. Waiting for task result...")
    print("=" * 50)
    result = await wait_for_task(a2a_agent, task_id)
    print_task_answer(result)

    # 4. Check the reservation (same context for session continuity)
    print("\n" + "=" * 50)
    print("4. Checking the reservation...")
    print("=" * 50)
    check_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Check the reservation for 555-0202"}],
            "role": "ROLE_USER",
            "contextId": context_id,
        },
    }
    request = build_post_request(check_data)
    check_response = await a2a_agent.on_message_send(request=request, context=None)
    check_result = await wait_for_task(a2a_agent, check_response["task"]["id"])
    print_task_answer(check_result)

    # 5. Cancel the reservation
    print("\n" + "=" * 50)
    print("5. Cancelling the reservation...")
    print("=" * 50)
    cancel_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Cancel the reservation for 555-0202"}],
            "role": "ROLE_USER",
            "contextId": context_id,
        },
    }
    request = build_post_request(cancel_data)
    cancel_response = await a2a_agent.on_message_send(request=request, context=None)
    cancel_result = await wait_for_task(a2a_agent, cancel_response["task"]["id"])
    print_task_answer(cancel_result)

    print("\n" + "=" * 50)
    print("All tests passed!")
    print("=" * 50)


if __name__ == "__main__":
    asyncio.run(main())

סקריפט הבדיקה מייבא את כרטיס הסוכן ואת רכיב ההפעלה שיצרתם בשלב הקודם – ללא כפילות. הכלי ייצור A2aAgent מקומי, ידמה קריאות לפרוטוקול A2A באמצעות בקשות HTTP מדומות ויאמת את כל שלוש פעולות ההזמנה.

מכיוון שלא מוגדר GOOGLE_CLOUD_AGENT_ENGINE_ID באופן מקומי, המבצע משתמש ב-InMemorySessionService. כשפורסים את אותו רכיב Executor ל-Agent Runtime, הוא עובר אוטומטית ל-VertexAiSessionService עבור סשנים מתמשכים.

הרצת הבדיקה

PYTHONPATH=. uv run python scripts/test_a2a_agent_local.py

הפלט מחולק לחמישה שלבים:

  1. כרטיס הסוכן – מאחזר את היכולות והמיומנויות של הסוכן
  2. Create reservation (יצירת הזמנה) – מזמין שולחן ומחזיר משימה עם האישור
  3. Get task result – אחזור של המשימה שהושלמה עם התשובה
  4. Check reservation (בדיקת הזמנה) – חיפוש ההזמנה לפי מספר הטלפון
  5. ביטול ההזמנה – מבטל את ההזמנה ומאשר את הביטול

דוגמה לפלט כמו שמוצג בהמשך

==================================================
1. Retrieving agent card...
==================================================
Agent: Reservation Agent
Skills: ['Restaurant Reservations']

==================================================
2. Creating a reservation...
==================================================
Task ID: f7f7004d-cfea-49c2-b57d-5bca9959e193

==================================================
3. Waiting for task result...
==================================================
Status: TASK_STATE_COMPLETED
Answer: Your reservation for Bob, party of 2, on Saturday at 6:00 PM has been confirmed. The phone number associated is 555-0202.

==================================================
4. Checking the reservation...
==================================================
Status: TASK_STATE_COMPLETED
Answer: I found a reservation for Bob, party of 2, on Saturday at 6:00 PM. The reservation status is confirmed.

==================================================
5. Cancelling the reservation...
==================================================
Status: TASK_STATE_COMPLETED
Answer: Your reservation for Bob (555-0202) has been cancelled.

==================================================
All tests passed!
==================================================

בשלב הזה, ודאתם שכרטיס הסוכן A2A מתאר את הכישורים הנכונים, שכל שלושת פעולות ההזמנה פועלות דרך זרימת ההודעות/המשימות של פרוטוקול A2A, ושמצב השיחה נשמר בין ההודעות באותו ההקשר.

8. פריסת סוכן ההזמנות ל-Agent Runtime

בשלב הזה, סוכן ההזמנות נפרס ב-Gemini Enterprise Agent Platform Runtime – פלטפורמה ללא שרתים בניהול מלא, שמארחת את הסוכן וחושפת אותו כנקודת קצה מאובטחת של A2A. אחרי הפריסה, כל לקוח מורשה יכול לגלות את הסוכן ולקיים איתו אינטראקציה באמצעות נקודות קצה סטנדרטיות של HTTP A2A.

יצירת קטגוריית ביניים

יוצרים קטגוריה של Cloud Storage לצורך הכנת Agent Runtime. Agent Runtime משתמש בדלי הזה כדי להעלות את הקוד והתלות של הסוכן במהלך הפריסה:

STAGING_BUCKET="${GOOGLE_CLOUD_PROJECT}-adk-a2a-agent-runtime"
gsutil mb -l $REGION -p $GOOGLE_CLOUD_PROJECT gs://$STAGING_BUCKET 2>/dev/null || echo "Bucket already exists"
echo "STAGING_BUCKET=$STAGING_BUCKET" >> .env
source .env

יצירת סקריפט הפריסה

בשלב הבא, צריך להכין את סקריפט הפריסה

cloudshell edit scripts/deploy_a2a_agent_runtime.py

מעתיקים את הקוד הבא אל scripts/deploy_a2a_agent_runtime.py:

# scripts/deploy_a2a_agent_runtime.py
import os
from pathlib import Path

import vertexai
from dotenv import load_dotenv
from google.genai import types
from vertexai.preview.reasoning_engines import A2aAgent

from reservation_agent.a2a_config import agent_card
from reservation_agent.executor import ReservationAgentExecutor

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
STAGING_BUCKET = os.environ.get("STAGING_BUCKET", f"{PROJECT_ID}-adk-a2a-agent-runtime")
BUCKET_URI = f"gs://{STAGING_BUCKET}"

a2a_agent = A2aAgent(
    agent_card=agent_card,
    agent_executor_builder=ReservationAgentExecutor,
)


def main():
    vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)
    client = vertexai.Client(
        project=PROJECT_ID,
        location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    print("Deploying Reservation Agent to Agent Runtime...")
    print("This may take 3-5 minutes.")

    remote_agent = client.agent_engines.create(
        agent=a2a_agent,
        config={
            "display_name": agent_card.name,
            "description": agent_card.description,
            "requirements": [
                "google-cloud-aiplatform[agent_engines,adk]==1.149.0",
                "a2a-sdk==0.3.26",
                "google-adk==1.29.0",
                "cloudpickle",
                "pydantic"
            ],
            "extra_packages": [
                "./reservation_agent",
            ],
            "http_options": {
                "api_version": "v1beta1",
            },
            "staging_bucket": BUCKET_URI,
        },
    )

    resource_name = remote_agent.api_resource.name
    print(f"\nDeployment complete!")
    print(f"Resource name: {resource_name}")

    env_path = Path(".env")
    lines = env_path.read_text().splitlines() if env_path.exists() else []
    lines = [l for l in lines if not l.startswith("RESERVATION_AGENT_RESOURCE_NAME=")]
    lines.append(f"RESERVATION_AGENT_RESOURCE_NAME={resource_name}")
    env_path.write_text("\n".join(lines) + "\n")
    print("Written RESERVATION_AGENT_RESOURCE_NAME to .env")


if __name__ == "__main__":
    main()

סקריפט הפריסה מייבא את אותם agent_card וReservationAgentExecutor שבהם נעשה שימוש בבדיקה המקומית – אין כפילות בקוד. Agent Runtime מבצע סריאליזציה (pickling) של אובייקט A2aAgent יחד עם התלות שלו לצורך פריסה. בסוף סקריפט הפריסה, הערך RESERVATION_AGENT_RESOURCE_NAME ייכתב לקובץ .env

פריסה ל-Agent Runtime

מריצים את סקריפט הפריסה:

PYTHONPATH=. uv run python scripts/deploy_a2a_agent_runtime.py

הפריסה נמשכת 3-5 דקות. הסקריפט מספק נקודת קצה בלי שרת (serverless) ב-Agent Runtime שמארחת את סוכן ההזמנות. אחרי פריסה מוצלחת, יוצג פלט דומה לזה שמופיע בהמשך

Deploying Reservation Agent to Agent Runtime...
This may take 3-5 minutes.

Deployment complete!
Resource name: projects/your-project-number/locations/us-central1/reasoningEngines/your-agent-deployment-unique-id
Written RESERVATION_AGENT_RESOURCE_NAME to .env

אפשר לראות את הסוכן שנפרס במסוף Cloud. מחפשים את Agent Platform בסרגל החיפוש במסוף.

af3751f461e4708c.png

אחר כך, בכרטיסייה הימנית, מעבירים את העכבר מעל Agents ובוחרים באפשרות Deployments.

8a9c7fd127e60aca.png

האפליקציה Reservation Agent תופיע ברשימת הפריסות כמו שמוצג בהמשך

a38b46bcb6c8e4db.png

בדיקת הסוכן הפעיל

עכשיו אפשר לבדוק את הסוכן שפרסנו. כדי לעשות זאת, צריך ליצור סקריפט בדיקה לסוכן שפרסנו:

cloudshell edit scripts/test_a2a_agent_runtime.py

מעתיקים את הקוד הבא אל scripts/test_a2a_agent_runtime.py:

# scripts/test_a2a_agent_runtime.py
import asyncio
import os
import time

import vertexai
from a2a.types import TaskState
from dotenv import load_dotenv
from google.genai import types

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
RESOURCE_NAME = os.environ["RESERVATION_AGENT_RESOURCE_NAME"]


async def main():
    vertexai.init(project=PROJECT_ID, location=REGION)
    client = vertexai.Client(
        project=PROJECT_ID, location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    agent = client.agent_engines.get(name=RESOURCE_NAME)

    # 1. Get agent card
    print("=" * 50)
    print("1. Retrieving agent card...")
    print("=" * 50)
    card = await agent.handle_authenticated_agent_card()
    print(f"Agent: {card.name}")
    print(f"URL: {card.url}")
    print(f"Skills: {[s.name for s in card.skills]}")

    # 2. Send a reservation request
    print("\n" + "=" * 50)
    print("2. Sending reservation request...")
    print("=" * 50)
    message_data = {
        "messageId": "msg-remote-001",
        "role": "user",
        "parts": [{"kind": "text", "text": "Book a table for 3 on Sunday at noon. Name: Carol, Phone: 555-0303"}],
    }
    response = await agent.on_message_send(**message_data)

    task_object = None
    for chunk in response:
        if isinstance(chunk, tuple) and len(chunk) > 0 and hasattr(chunk[0], "id"):
            task_object = chunk[0]
            break

    task_id = task_object.id
    print(f"Task ID: {task_id}")
    print(f"Status: {task_object.status.state}")

    # 3. Poll for result
    print("\n" + "=" * 50)
    print("3. Waiting for result...")
    print("=" * 50)
    result = None
    for _ in range(30):
        try:
            result = await agent.on_get_task(id=task_id)
            if result.status.state in [TaskState.completed, TaskState.failed]:
                break
        except Exception:
            pass
        time.sleep(1)

    print(f"Final status: {result.status.state}")
    if result.artifacts:
        for artifact in result.artifacts:
            if artifact.parts and hasattr(artifact.parts[0], "root") and hasattr(artifact.parts[0].root, "text"):
                print(f"Answer: {artifact.parts[0].root.text}")

    print("\n" + "=" * 50)
    print("Remote agent test passed!")
    print("=" * 50)


if __name__ == "__main__":
    asyncio.run(main())

עכשיו נריץ את הבדיקה

source .env
uv run python scripts/test_a2a_agent_runtime.py

בפלט מוצג כרטיס הסוכן עם מיומנות 'הזמנות למסעדות', ואחריו השלמת המשימה עם אישור הזמנה.

==================================================
1. Retrieving agent card...
==================================================
Agent: Reservation Agent
URL: https://us-central1-aiplatform.googleapis.com/v1beta1/projects/your-project-id/locations/us-central1/reasoningEngines/your-agent-unique-id/a2a
Skills: ['Restaurant Reservations']

==================================================
2. Sending reservation request...
==================================================
Task ID: b34585d0-5f03-4cb0-85a3-40710a0d224d
Status: TaskState.completed

==================================================
3. Waiting for result...
==================================================
Final status: TaskState.completed
Answer: Your reservation for Carol, party of 3 on Sunday at noon with phone number 555-0303 is confirmed.

==================================================
Remote agent test passed!
==================================================

סוכן ההזמנות פועל עכשיו בהצלחה כנקודת קצה מנוהלת של A2A ב-Agent Runtime.

9. שילוב של סוכן הזמנות A2A עם סוכן ראשי של מסעדה

בשלב הזה, סוכן המסעדה משודרג כך שישתמש בסוכן ההזמנות שנפרס כסוכן משנה מרוחק מסוג A2A. הכלי לניהול תזמור רץ באופן מקומי, בעוד שהסוכן להזמנות רץ ב-Agent Runtime – שילוב חלקי שמאמת את החיבור בין סוכן לסוכן לפני פריסה מלאה.

פתרון בעיות בכתובת ה-URL של כרטיס הסוכן A2A

כדי לגלות את היכולות של סוכן ההזמנות, RemoteA2aAgent צריך את כתובת ה-URL של הכרטיס שלו. יוצרים סקריפט שמחלץ את כתובת ה-URL הזו מ-Agent Runtime וכותב אותה ל-.env של סוכן המסעדה:

cloudshell edit scripts/resolve_agent_card_url.py

מעתיקים את הקוד הבא אל scripts/resolve_agent_card_url.py:

# scripts/resolve_agent_card_url.py
import asyncio
import os
from pathlib import Path

import vertexai
from dotenv import load_dotenv
from google.genai import types

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
RESOURCE_NAME = os.environ["RESERVATION_AGENT_RESOURCE_NAME"]


async def main():
    vertexai.init(project=PROJECT_ID, location=REGION)
    client = vertexai.Client(
        project=PROJECT_ID, location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    agent = client.agent_engines.get(name=RESOURCE_NAME)
    card = await agent.handle_authenticated_agent_card()
    card_url = f"{card.url}/v1/card"

    print(f"Agent: {card.name}")
    print(f"Card URL: {card_url}")

    # Write to restaurant_agent/.env
    # Write to both restaurant_agent/.env (for adk web) and root .env (for Cloud Run deploy)
    for env_path in [Path("restaurant_agent/.env"), Path(".env")]:
        lines = env_path.read_text().splitlines() if env_path.exists() else []
        lines = [l for l in lines if not l.startswith("RESERVATION_AGENT_CARD_URL=")]
        lines.append(f"RESERVATION_AGENT_CARD_URL={card_url}")
        env_path.write_text("\n".join(lines) + "\n")
        print(f"Written RESERVATION_AGENT_CARD_URL to {env_path}")


if __name__ == "__main__":
    asyncio.run(main())

מריצים את הסקריפט כדי לאכלס את הקובץ .env בכתובת ה-URL של כרטיס הנציג

uv run python scripts/resolve_agent_card_url.py
source .env

עדכון הסוכן של המסעדה

פותחים את קובץ הסוכן של המסעדה:

cloudshell edit restaurant_agent/agent.py

לאחר מכן, מחליפים את התוכן בגרסה המעודכנת שכוללת את סוכן ההזמנות המרוחק כסוכן משנה:

# restaurant_agent/agent.py
import os

import httpx
from google.adk.agents import LlmAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.auth import default
from google.auth.transport.requests import Request as AuthRequest
from toolbox_adk import ToolboxToolset

TOOLBOX_URL = os.environ.get("TOOLBOX_URL", "http://127.0.0.1:5000")
RESERVATION_AGENT_CARD_URL = os.environ.get("RESERVATION_AGENT_CARD_URL", "")

toolbox = ToolboxToolset(TOOLBOX_URL)


class GoogleCloudAuth(httpx.Auth):
    """Auto-refreshing Google Cloud authentication for httpx.

    Refreshes the access token before each request if expired,
    so long-running agents never hit 401 errors.
    """

    def __init__(self):
        self.credentials, _ = default(
            scopes=["https://www.googleapis.com/auth/cloud-platform"]
        )

    def auth_flow(self, request):
        # Refresh the token if it is expired or missing
        if not self.credentials.valid:
            self.credentials.refresh(AuthRequest())
            
        request.headers["Authorization"] = f"Bearer {self.credentials.token}"
        yield request


reservation_remote_agent = RemoteA2aAgent(
    name="reservation_agent",
    description="Handles restaurant table reservations — create, check, and cancel bookings. Delegate to this agent when the user wants to book a table, check a reservation, or cancel a reservation.",
    agent_card=RESERVATION_AGENT_CARD_URL,
    httpx_client=httpx.AsyncClient(auth=GoogleCloudAuth(), timeout=60),
)

root_agent = LlmAgent(
    name="restaurant_agent",
    model="gemini-3.5-flash",
    instruction="""You are a friendly and knowledgeable concierge at "Foodie Finds," a restaurant. Your job:
- Help diners browse the menu by category or cuisine type.
- Provide full details about specific dishes, including ingredients, price, and dietary information.
- Recommend dishes based on natural language descriptions of what the diner is craving.
- Add new menu items when asked.
- For reservation requests (booking, checking, or cancelling tables), delegate to the reservation_agent.

When a diner asks about a specific dish by name or cuisine, use the get-item-details tool.
When a diner asks for a specific category or cuisine type, use the search-menu tool.
When a diner describes what kind of food they want — by flavor, texture, dietary needs, or cravings — use the search-menu-by-description tool for semantic search.

When in doubt between search-menu and search-menu-by-description, prefer search-menu-by-description — it searches dish descriptions and finds more relevant matches.
If a dish is not available (available is false), let the diner know and suggest similar alternatives from the search results.
Be conversational, knowledgeable, and concise.""",
    tools=[toolbox],
    sub_agents=[reservation_remote_agent],
)

השינויים העיקריים לעומת הגרסה הקודמת:

  • GoogleCloudAuth — handler מותאם אישית של httpx.Auth שמבצע רענון של אסימון הגישה ל-Google Cloud לפני כל בקשה. ה-Agent Runtime דורש קריאות מאומתות מ-A2A, והתוקף של הטוקנים פג אחרי פרק זמן מסוים.
  • RemoteA2aAgent קורא את RESERVATION_AGENT_CARD_URL מ-.env (שנכתב על ידי סקריפט הפתרון) ומשתמש ב-httpx_client המאומת
  • רשום כסוכן משנה – רכיב ה-Orchestrator של ADK מעביר אליו באופן אוטומטי בקשות להזמנות
  • עדכנו את ההוראות כך שיכללו מידע על מתן הרשאה לניהול הזמנות

בדיקה מקומית של הסוכן המשולב

הנציג למתחילים דרש שילוב עם MCP Toolbox, והקובץ הנדרש כבר היה אמור להינתן מ-codelab קודם או ממאגר המתחילים. אנחנו רק צריכים לוודא שתהליך השימוש בארגז הכלים פועל בצורה תקינה.

אם TOOLBOX_URL ב-.env כבר מצביע על שירות Cloud Run (מה-codelab הקודם או אולי מ-full_setup.sh של מאגר המתחילים), אפשר לדלג על השלב הזה – הסוכן יתחבר ל-Toolbox שנפרס.

אם אתם צריכים Toolbox מקומי, כדאי לבדוק אם הוא כבר פועל לפני שמתחילים מופע חדש:

if curl -s http://127.0.0.1:5000/api/toolsets > /dev/null 2>&1; then
  echo "Toolbox already running on port 5000"
else
  set -a; source .env; set +a
  ./toolbox --config=tools.yaml > logs/toolbox.log 2>&1 &
  echo "Toolbox started"
fi

אחר כך אפשר לנסות ליצור אינטראקציה עם נציג המסעדה דרך ממשק המשתמש של ADK web dev

uv run adk web --allow_origins "regex:https://.*\.cloudshell\.dev" --port 8080

פותחים את ממשק המשתמש האינטרנטי של ADK באמצעות תצוגה מקדימה באינטרנט של Cloud Shell (לוחצים על לחצן התצוגה המקדימה באינטרנט, משנים את היציאה ל-8080) ואז בוחרים באפשרות restaurant_agent

65a055b70ab52aa8.png

כדי לבדוק שיחה מעורבת:

שאילתת תפריט

What Italian dishes do you have?

בקשה להזמנה

I want to create reservation under name Bob, phone number 123456

בדיקת ההזמנה

יצירת סשן חדש ( התחלת שיחה חדשה):

Check the reservation for 123456

92cef3bc7671129a.png16bfd60f202dcaa7.pngc5326bbf6fa778e2.png

מפסיקים את התהליך של adk web באמצעות Ctrl+C פעמיים. בשלב הבא, נשלים את המערכת על ידי פריסה מלאה של הסוכן

10. פריסת סוכן המסעדה המעודכן ב-Cloud Run

בשלב הזה, סוכן המסעדה נפרס מחדש ב-Cloud Run עם שילוב A2A, וכך הושלמה הפריסה של מערכת מרובת סוכנים.

מתן הרשאות גישה ל-Agent Runtime

לחשבון השירות של Cloud Run צריכה להיות הרשאה להפעיל את Agent Runtime. מקצים את התפקיד roles/aiplatform.user לחשבון השירות שמוגדר כברירת מחדל ב-Compute Engine:

PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role="roles/aiplatform.user"

פריסה ב-Cloud Run

בהגדרה הזו, אנחנו מניחים ששירות הסוכן של המסעדה כבר קיים מה-codelab הקודם או מהרצת הפקודה scripts/full_setup.sh אם מתחילים מאפס. הפעולה הזו תפרוס מחדש עם הקוד המעודכן (שילוב חדש של RemoteA2aAgent) ותוסיף את כתובת ה-URL של כרטיס סוכן ההזמנות כמשתנה סביבה חדש – משתני סביבה קיימים (TOOLBOX_URL,‏ GOOGLE_CLOUD_PROJECT וכו') יישמרו:

gcloud run deploy restaurant-agent \
  --source . \
  --region=$REGION \
  --allow-unauthenticated \
  --update-env-vars="RESERVATION_AGENT_CARD_URL=$RESERVATION_AGENT_CARD_URL" \
  --min-instances=0 \
  --max-instances=1 \
  --memory=1Gi \
  --port=8080

בדיקת המערכת אחרי הפריסה המלאה

מקבלים את כתובת ה-URL של השירות שפרסתם:

AGENT_URL=$(gcloud run services describe restaurant-agent --region=$REGION --format='value(status.url)')
echo "Agent URL: $AGENT_URL"

פותחים את כתובת ה-URL בדפדפן. ממשק המשתמש האינטרנטי של ADK נטען – זהו אותו ממשק שבו השתמשתם באופן מקומי, ועכשיו הוא פועל ב-Cloud Run.

אתם יכולים לשוחח בצ'אט עם הנציג

שאילתת תפריט

What spicy dishes do you have?

בקשה להזמנה

Book a table for 4 on Friday at 7pm. Name: Eve, Phone: 555-0505

בדיקת ההזמנה

יצירת סשן חדש ( התחלת שיחה חדשה):

Check reservation for 555-0505

69ae9a7c35255fc.png55145841338ec9b3.png

מערכת מרובת סוכנים נפרסה במלואה. סוכן המסעדה ב-Cloud Run מתאם בין שני שירותי בק-אנד: MCP Toolbox לפעולות שקשורות לתפריט וסוכן ההזמנות A2A ב-Agent Runtime.

‫11. מעולה!

יצרתם ופרסתם מערכת מרובת סוכנים באמצעות פרוטוקול A2A ב-Google Cloud.

מה למדתם

  • יצירת סוכן ADK שמשתמש במצב סשן (ToolContext) כדי לנהל נתוני הזמנות בלי מסד נתונים
  • פריסת סוכן A2A ב-Agent Runtime באמצעות Agent Platform SDK
  • השתמשתם בסוכן A2A מרוחק מסוכן ADK אחר באמצעות RemoteA2aAgent כסוכן משנה
  • בדיקה של המערכת באופן מצטבר: A2A מקומי → פריסת A2A → שילוב חלקי → פריסה מלאה

השלבים הבאים

  • כדאי לעיין במקורות מידע נוספים כדי ללמוד איך ליצור ולפרוס סוכני AI ברמת הארגון באמצעות Gemini Enterprise Agent Ready‏ (GEAR)

פינוי נפח

כדי להימנע מחיובים בחשבון Google Cloud, מוחקים את המשאבים שנוצרו ב-codelab הזה.

gcloud projects delete $GOOGLE_CLOUD_PROJECT

אפשרות 2: מחיקת משאבים ספציפיים

# Delete the Agent Runtime deployment
uv run python -c "
import vertexai
from google.genai import types
vertexai.init(project='$GOOGLE_CLOUD_PROJECT', location='$REGION')
client = vertexai.Client(
    project='$GOOGLE_CLOUD_PROJECT', location='$REGION',
    http_options=types.HttpOptions(api_version='v1beta1'),
)
agent = client.agent_engines.get(name='$RESERVATION_AGENT_RESOURCE_NAME')
agent.delete(force=True)
print('Agent Runtime deployment deleted.')
"

# Delete Cloud Run services
gcloud run services delete restaurant-agent --region=$REGION --quiet
gcloud run services delete toolbox-service --region=$REGION --quiet

# Delete Cloud SQL instance
gcloud sql instances delete $DB_INSTANCE --quiet

# Delete GCS staging bucket
gsutil rm -r gs://$STAGING_BUCKET