Build an Agentic Pitch Generator: Orchestration, Evaluation, and Analytics with Google Cloud

1. מבוא

באנר של הרצאת עומק בנושא סוכנים

איך יוצרים מנוע מרובה סוכנים (multi-agent) עם יכולות multimodal, שמקבל תדרוך בנושא מסוים ומארגן את יצירת הנכסים לרשתות החברתיות בצורה מושכת במיוחד. באמצעות הערכה לפיתוח סוכנים (ADK) של Google ופרוטוקול Agent2Agent‏ (A2A), תיצרו מיקרו-סוכנים אוטונומיים, תאגדו כללי מיתוג ככישורי סוכנים לשימוש חוזר, תפרסו שירות סוכנים ב-Cloud Run ותדרגו את הנכסים החזותיים שנוצרו בהתאם לתאימות למותג באמצעות פונקציות AI של BigQuery.

הפעולות שתבצעו:

ב-Codelab הזה תלמדו איך לבצע את המשימות הבאות:

  • תזמור של כמה סוכני ADK באמצעות תהליך עבודה דטרמיניסטי של גרף.
  • אפשר לארוז את הנחיות המותג ככישורים של סוכן שנטענים רק כשצריך.
  • מציידים סוכן בכלי ליצירת תמונות ששומר את הפלט שלו כארטיפקט.
  • לחשוף סוכן כשירות A2A ולקרוא לו מסוכן אחר כצומת מרוחק.
  • פורסים את Visual Director ב-Cloud Run ומפעילים את תהליך העבודה באמצעות A2A.
  • אפשר להשתמש בפונקציות AI ב-BigQuery כדי לתת ציון לתוצרים החזותיים המרכזיים שנוצרו, בהתאם להתאמה שלהם למותג.

הדרישות

  • דפדפן אינטרנט (כמו Chrome).
  • חשבון ב-Google Cloud ופרויקט ב-Google Cloud שהחיוב בו מופעל.

‫2. לפני שמתחילים

יצירת פרויקט ב-Google Cloud

  1. במסוף Google Cloud, בדף לבחירת הפרויקט, בוחרים פרויקט בענן או יוצרים פרויקט בענן.
  2. הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud. כך בודקים אם החיוב מופעל בפרויקט

הפעלת Cloud Shell

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

  1. פותחים את Google Cloud Shell בדפדפן (או לוחצים על Activate Cloud Shell בחלק העליון של מסוף Google Cloud):
    https://shell.cloud.google.com/
    
    אם מוצגות הודעות נוספות, לוחצים על המשך.ממתינים עד שהטרמינל והעורך ייטענו במלואם. הפעולה הזו עשויה להימשך דקה או שתיים.
  2. מגדירים את מזהה הפרויקט ב-Google Cloud ואת האזור (לדוגמה, us-central1) בסביבה על ידי הדבקת הפקודה הבאה בטרמינל (מחליפים את ואת בערכים שלכם):
    export PROJECT_ID="<YOUR_PROJECT_ID>"
    export REGION="<YOUR_REGION>"
    gcloud config set project $PROJECT_ID
    
    הדבקה בטרמינלאמורה להופיע הודעה דומה לזו:
    Your active configuration is: [cloudshell-####]
    Updated property [core/project]
    
  3. מפעילים את ממשקי ה-Cloud APIs הנדרשים של Google Cloud בפרויקט:
    gcloud services enable \
      compute.googleapis.com \
      cloudresourcemanager.googleapis.com \
      aiplatform.googleapis.com \
      storage.googleapis.com \
      storage-component.googleapis.com \
      bigqueryconnection.googleapis.com \
      bigquery.googleapis.com \
      run.googleapis.com \
      secretmanager.googleapis.com \
      cloudbuild.googleapis.com \
      artifactregistry.googleapis.com \
      iap.googleapis.com \
      iam.googleapis.com
    

שכפול המאגר

יש לנו תבנית קוד שתעזור לכם להתחיל את ה-Codelab. הוא קטן יחסית, אבל המאגר ב-GitHub שבו הוא נמצא גדול למדי. כדי להאיץ את השכפול ולחסוך במקום בדיסק, בדרך כלל נשתמש ב-git clone עם האפשרות --filter=blob:none, ב-git sparse-checkout ובעוד כמה פקודות.

זה נשמע מאוד מורכב, אבל Antigravity יכול לעשות את הכול בשבילנו!

קודם כול, נגדיר את Antigravity CLI כדי שיוכל לפתור את הבעיה שלנו באופן מיידי, ואז נפעיל אותו.

ב-Cloud Shell, מריצים את הפקודה הבאה:

source <(curl -fsSL https://github.com/GoogleCloudPlatform/devrel-demos/raw/refs/heads/vladkol/cloud-shell-configure-agy/codelabs/labs-scripts/bwg-multi-agent-pitch-generator/cloud_shell_configure_agy.sh)

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

Antigravity CLI

אנחנו צריכים רק את התיקייה codelabs/bwg-multi-agent-pitch-generator במאגר. נבקש מ-Antigravity להוריד רק את התיקייה הזו, ולשמור אותה באופן מקומי ב-~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/.

נותנים ל-Antigravity CLI את המשימה הבאה:

Download files from under `https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/codelabs/bwg-multi-agent-pitch-generator`
to folder `~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/`.
If `devrel-demos` folder already exists, rename it with a `.backup` suffix.

הנחיה ב-Antigravity CLI

‫Antigravity יבצע את המשימה. תתבקשו לתת הרשאות להפעלת כמה פקודות. אחרי שמורידים את הקוד, יוצאים מ-Antigravity CLI באמצעות הפקודה /exit:

/exit

יציאה מ-Antigravity CLI

פתיחת סביבת העבודה של המאגר

עכשיו נפתח את סביבת העבודה של הקוד ב-Cloud Shell. מריצים את הפקודה הזו במסוף של Cloud Shell:

cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/
cloudshell workspace .

הפעלת סקריפט ההגדרה

כדי לשפר את חוויית השימוש, מריצים את סקריפט ההגדרה מהמאגר. הוא מגדיר משתני סביבה, מתקין את agents-cli, יוצר את קטגוריית Cloud Storage לארטיפקטים, יוצר את החיבור למשאב BigQuery Cloud ומעניק את הרשאות ה-IAM הנדרשות לחשבון השירות שלו עם ניסיון חוזר אוטומטי.

סקריפט ההגדרה קורא גם ל-setenv.sh, כלי עזר קטן שמחלץ את המשתנים ששני הסוכנים צריכים – GOOGLE_CLOUD_PROJECT,‏ GOOGLE_CLOUD_REGION,‏ GOOGLE_CLOUD_LOCATION ו-GOOGLE_GENAI_USE_VERTEXAI – מ-PROJECT_ID ומ-REGION שייצאתם למעלה. הוא גם שומר את שני הערכים האלה בקובץ .env בספריית הבית, כדי שתוכלו לשחזר אותם מאוחר יותר.

  1. ב-Cloud Shell, הופכים את סקריפט ההגדרה לסקריפט שאפשר להפעיל ומריצים אותו:
    chmod +x codelab-setup.sh setenv.sh
    ./codelab-setup.sh
    
  2. ההרצה תימשך כדקה. אחרי ההצלחה תופיע ההודעה הבאה: "🎉 🦄 Script execution complete."הצלחה
  3. סקריפט ההגדרה פעל במעטפת משלו, ולכן המשתנים שלו לא הגיעו למסוף. טוענים אותם לסשן:
    ‫
    source ./setenv.sh
    
    הפרויקט, האזור והמיקום שלכם אמורים להופיע.

3. תזמור סוכנים באמצעות תהליך עבודה של גרף

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

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

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

יצירת תבנית לפרויקט הסוכן הראשון

  1. ב-Cloud Shell, יוצרים את הפרויקט על ידי הדבקת הפקודה הבאה בטרמינל:
    ‫
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator
    agents-cli create pitch-generator \
      --agent adk \
      --agent-directory pitch_generator \
      --deployment-target cloud_run \
      --session-type in_memory \
      --cicd-runner skip \
      --region <YOUR_REGION> \
      --auto-approve
    
    הפעולה הזו יוצרת פרויקט מלא וניתן להפעלה: סוכן, קובץ Dockerfile, בדיקות, Terraform לשימוש מאוחר יותר וקובץ .env עם תבנית .env.example שלו. יש שני חלקים שחשובים ל-Codelab הזה:
    • ‫pitch_generator/agent.py הוא הסוכן והקובץ שכותבים מחדש.
    • ‫pitch_generator/ הוא שם החבילה, שמוגדר על ידי --agent-directory.
    הסוכן קורא את הפרויקט והאזור שלו מהסביבה שייצאתם באמצעות setenv.sh, כך שאין צורך להגדיר כאן דבר.
  2. יחסי תלות של התקנות:
    cd pitch-generator
    agents-cli update -y && agents-cli install && uv sync --upgrade-package google-adk
    
    הערה: ה-scaffold גם מקשר את הפרויקט להצגת A2A, הפרוטוקול שבו נעשה שימוש בהמשך במעבדת התכנות הזו. בינתיים, אל תעשו כלום עם הקבצים האלה.

הסבר על התרשים

‫ADK v2.x תומך בתהליכי עבודה של גרפים. גרף מורכב מצמתים, שהם יחידות העבודה, ומקשתות שמקשרות ביניהם. צומת הוא בדרך כלל סוכן, אבל אפשר להשתמש גם בפונקציית Python רגילה. קצה מציין שכשאחד מהצמתים מסתיים, הצומת הבא מופעל.

הקצוות נכתבים כרשימה, ו-ADK קובע את הסדר:

edges=[
    ("START", creative_director),        ## the graph begins here
    (creative_director, copywriter),     ## then this
]

הקינון נותן יתרון למשמעות:

Edge

משמעות

(a, b, c)

שרשרת. a, ואז b, ואז c, אחד אחרי השני

(a, (b, c))

פיצול. a, ואז b ו-c בו-זמנית

((a, b), c)

מאוורר פנימי. c ממתין גם ל-a וגם ל-b

ב-fan-in, משתמשים ב-JoinNode, שמחכה לכל קודם ומעביר את התוצאות שלהם יחד.

במשימה הזו ניצור את תהליך העבודה הבא:

תרשים זרימה ראשוני של תהליך העבודה

פיתוח הפייפליין

  1. פותחים את קובץ הסוכן על ידי הדבקת הטקסט הבא במסוף:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator
    cloudshell edit pitch_generator/agent.py
    
    תראו את ה-scaffolded agent.py בעורך של Cloud Shell.💡 טיפ: בכל שלב, אפשר לבחור את הקוד ולהשתמש בסמל הנורה כדי לבקש מ-Gemini הסבר. אפשר גם לשאול את Antigravity CLI.איך משתמשים ב-Gemini Code Assist
  2. מחליפים את כל הקובץ ב-pitch_generator/agent.py בעורך בפקודה הבאה:
    import mimetypes
    import os
    
    from dotenv import load_dotenv
    from google.adk.agents import Agent
    from google.adk.agents.context import Context
    from google.adk.agents.remote_a2a_agent import (
       AGENT_CARD_WELL_KNOWN_PATH,
       RemoteA2aAgent,
    )
    from google.adk.apps import App
    from google.adk.events.event import Event
    from google.adk.models import Gemini
    from google.adk.workflow import JoinNode, Workflow
    from google.genai import types
    
    load_dotenv()
    
    MODEL = "gemini-3.8-flash"
    
    def _model() -> Gemini:
       return Gemini(
          model=MODEL,
          client_kwargs={"location": "global"},
          retry_options=types.HttpRetryOptions(attempts=3),
       )
    
    """ ⬇️ Configure visual-director RemoteA2aAgent agent below this comment """
    
    
    creative_director = Agent(
       name="creative_director",
       model=_model(),
       description="Turns a raw idea into a campaign concept.",
       instruction=(
          "You are the Creative Director. Turn the idea you are given into ONE "
          "punchy campaign concept line with the rationale explanation behind it."
       ),
       output_key="creative_director",
    )
    
    copywriter = Agent(
       name="copywriter",
       model=Gemini(model=MODEL, retry_options=types.HttpRetryOptions(attempts=3)),
       description="Writes social copy for a campaign concept.",
       instruction=(
          "You are the Copywriter. Write ONE short social caption for the "
          "campaign concept you are given. Under 25 words."
       ),
    )
    
    """ Waits for every node wired into it, then hands their outputs on together."""
    assemble = JoinNode(name="assemble")
    
    def package(node_input: dict):
       """Render the joined branches as the finished pitch.
       """
    
       pitch = "\n\n".join(
          [
                f"CONCEPT\n{node_input['creative_director']}",
                f"COPY\n{node_input['copywriter']}",
          ]
       )
       """ The first event is what the user sees. The second is the node's output,
       which any downstream node would receive."""
       yield Event(content=types.Content(role="model", parts=[types.Part(text=pitch)]))
       yield Event(output=pitch)
    
    """ we put creative_director before `assemble`  so that we can
       gather the `Concept` output at assemble"""
    root_agent = Workflow(
       name="pitch_generator",
       edges=[
          ("START", creative_director),
          (creative_director, copywriter),
          ((creative_director, copywriter), assemble),
          (assemble, package),
       ],
    )
    
    app = App(
       root_agent=root_agent,
       name="pitch_generator",
    )
    
  3. ‫agents-cli כולל ממשק משתמש מובנה לפיתוח אתרים שנקרא playground לבדיקה, בדומה ל-ADK. כדי להפעיל את Playground:
    agents-cli playground --port 8080
    
    תתבקשו לאשר את הפעלת הטלמטריה. מקלידים n ומקישים על Enter.
  4. לוחצים על Web Preview (תצוגה מקדימה של אתר), סמל העין בפינה השמאלית העליונה של הטרמינל, ובוחרים באפשרות Preview on port 8080 (תצוגה מקדימה ביציאה 8080).לוחצים על תצוגה מקדימה באתר
  5. יוצג הממשק הבא לצ'אט עם הסוכן. אפשר לנסות לשלוח את ההנחיה "Flying skateboards for cats" (סקייטבורדים מעופפים לחתולים). תופיע תשובה כמו בדוגמה שלמטה, שבה אפשר גם לבדוק את הגרף המלא של תהליך העבודה.תהליך עבודה לדוגמהבדיקת אופן יצירת התשובה ב-Playground מוצג כל שלב שהתרשים עבר, לפי הסדר: creative_director, אחר כך copywriter, אחר כך assemble, אחר כך package. הרצף הזה הוא הגרף שחיברתם, וכך תוכלו לאשר במהירות את הגרף שבניתם.
  6. חוזרים למסוף ומפסיקים את התהליך agents-cli playground שפועל על ידי הקשה על Ctrl+C פעמיים.

4. הקצאת סקיל לסוכן

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

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

מיומנות היא תיקייה שמכילה קובץ SKILL.md. המבנה של ספריית הכישורים נראה כמו בדוגמה הבאה (באמצעות visual_director, הסוכן הבא שתבנו):

visual_director/
   ...
   agent.py
   skills/
      brand-guidelines/
         SKILL.md          ## required: frontmatter + instructions
         references/       ## optional: longer supporting documents
         assets/           ## optional: templates, images, data
         scripts/          ## optional: executable helpers
   ...

נקודת הכניסה של מיומנות הסוכן היא הקובץ SKILL.md, שכולל קטע YAML frontmatter כמו זה שמוצג בהמשך:

---
name: brand-guidelines
description: The house brand rules for a key visual. Load this before writing art direction, so the visual looks like it came from this brand.
---

## Brand guidelines

Deep indigo and slate as the ground. One warm accent, amber or terracotta...

התווית היא frontmatter. זה כל מה שהסוכן רואה בהתחלה, ולכן בתיאור צריך לציין מתי כדאי לטעון את המיומנות. ‫body הוא התוכן, והוא לא נכלל בהקשר עד שהסוכן קורא ל-load_skill.

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

נתחיל לבנות את הסוכן השני, visual-director, באמצעות כישורי סוכן כמו שמוצג בתרשים הבא:

הוספת סקיל לסוכן השני

יצירת תשתית למנהל החזותי

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

bwg-multi-agent-pitch-generator/
├── pitch-generator/              ## built in the last task
│   ├── pitch_generator/
│   │   ├── agent.py              ## the graph you wrote
│   │   ├── app_utils/            ## generated, A2A wiring
│   │   └── fast_api_app.py       ## generated, serves the agent
│   └── pyproject.toml
└── visual-director/              ## created below
    └── ...                       ## the same shape again
  1. כדי ליצור את הפרויקט ולהתקין את יחסי התלות שלו, מדביקים את הטקסט הבא במסוף:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator
    agents-cli create visual-director \
      --agent adk \
      --agent-directory visual_director \
      --deployment-target cloud_run \
      --session-type in_memory \
      --cicd-runner skip \
      --region <YOUR_REGION> \
      --auto-approve
    cd visual-director
    agents-cli update -y && agents-cli install && uv sync --upgrade-package google-adk
    

כתיבת ספר המותג

  1. יוצרים את תיקיית המיומנות ופותחים את הקובץ:
    mkdir -p visual_director/skills/brand-guidelines
    cloudshell edit visual_director/skills/brand-guidelines/SKILL.md
    
  2. מדביקים את התוכן הבא בקובץ SKILL.md הפתוח בעורך ושומרים אותו:מיומנות
    ---
    name: brand-guidelines
    description: The house brand rules for a key visual — palette, light, composition, subject treatment, and what never appears. Load this before writing art direction or rendering an image, so the visual looks like it came from this brand rather than from a generic image model.
    ---
    
    ## Brand guidelines
    
    House style for campaign key visuals. Every rule here beats a preference in the
    brief. If the brief asks for something forbidden below, follow the brief for the
    subject and the guidelines for the treatment.
    
    ## Palette
    
    Deep indigo and slate as the ground. One warm accent — amber or terracotta —
    carrying the eye to the subject. Nothing neon, nothing fluorescent.
    
    At most three colors with any weight. A fourth reads as clutter.
    
    ## Light
    
    One dominant source, low and raking. Long shadows are wanted. Golden hour or a
    single hard studio key, never flat overhead light and never a ring light.
    
    Rim light is allowed on the subject only, and only to separate it from the
    ground.
    
    ## Composition
    
    Subject off-center, roughly a third in. Generous negative space on the opposite
    side — copy lands there later, so leave it genuinely empty.
    
    Eye level or slightly below. No top-down flat lays. No fisheye, no Dutch tilt.
    
    ## Subject
    
    One subject. If the idea implies a crowd, pick the one that carries it.
    
    Photographic realism with a shallow depth of field. Not illustration, not 3D
    render, not clay, not pixel art.
    
    ## Never
    
    - Text, logos, watermarks, or signage anywhere in the frame
    - Collage or split-screen layouts
    - Chrome, lens flare, bokeh sparkle
    - Cool-toned neon cyan or magenta
    
    ## Rendering the prompt
    
    When you turn art direction into an image prompt, state the palette, the light,
    and the composition explicitly. An image model will not infer them from the
    subject alone. Put the "Never" items in as plain description of what the frame
    does contain, not as negatives — image models handle a positive description far
    better than a list of prohibitions.
    

עדכון של visual_director/agent.py

מעדכנים את הקובץ agent.py ומגדירים אותו כך שהוא יכלול את המיומנות שהוגדרה קודם.

  1. פותחים את קובץ הסוכן:
    cloudshell edit ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/visual-director/visual_director/agent.py
    
  2. מחליפים את כל התוכן של הקובץ visual_director/agent.py בקוד הבא:
    import mimetypes
    from pathlib import Path
    
    from google import genai
    from google.adk.agents import Agent
    from google.adk.apps import App
    from google.adk.models import Gemini
    from google.adk.skills import load_skill_from_dir
    from google.adk.tools import ToolContext
    from google.adk.tools.skill_toolset import SkillToolset
    from google.genai import types
    MODEL = "gemini-3.8-flash"
    
    ## Configure the `brand-guidelines` skill for the agent
    BRAND_SKILL = load_skill_from_dir(Path(__file__).parent / "skills" / "brand-guidelines")
    
    ## Add definition of `generate_key_visual` tool below this comment
    
    root_agent = Agent(
       name="visual_director",
       model=Gemini(
          model=MODEL,
          client_kwargs={"location": "global"},
          retry_options=types.HttpRetryOptions(attempts=3),
       ),
       description="Turns a campaign concept into art direction and a key visual.",
       instruction="""You are the Visual Director on a campaign pitch team.
    
       Call `load_skill` for `brand-guidelines` before you write anything. The house
       style is not optional and it is not in this prompt.
    
       You are given a campaign concept. Then, in order:
    
       1. Write art direction for ONE key visual that sells it, obeying the brand
       guidelines: subject, composition, lighting, color, mood. Three or four
       sentences.
       2. Call `generate_key_visual` with exactly that art direction.
    
       Your final reply is the art direction itself and nothing else. No preamble, no
       alternatives, no questions back. Do not mention the skill, the tool, or the
       image file — the image travels on its own.""",
       tools=[
          SkillToolset(skills=[BRAND_SKILL]),
          ## Supply the `generate_key_visual` tool below this comment
       ],
    )
    
    app = App(
       root_agent=root_agent,
       name="visual_director",
    )
    
    הסוכן visual-director מוגדר עכשיו בצורה תקינה עם מיומנות של סוכן. לפני שתבדקו אותו, תציידו את הסוכן הזה בכלי ליצירת נכסי תמונות כאחת מהיכולות העיקריות שלו.

5. איך מציידים סוכן בכלי ליצירת תמונות

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

הכלי והארטיפקט שהוא יוצר

הטמעה והוספה של הכלי לסוכן

  1. פותחים את קובץ הסוכן:
    cloudshell edit ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/visual-director/visual_director/agent.py
    
  2. מעתיקים את הכלי generate_key_visual הבא לפני ההגדרה root_agent:
    async def generate_key_visual(art_direction: str, tool_context: ToolContext) -> dict:
       """Render the art direction as the campaign's key visual.
    
       Args:
          art_direction: The art direction to render, in full.
    
       Returns:
          The saved artifact's filename, version, size and media type.
       """
       IMAGE_MODEL = "gemini-3.1-flash-lite-image"
    
       response = await genai.Client(location="global").aio.models.generate_content(
          model=IMAGE_MODEL, contents=art_direction
       )
    
       ## The image arrives as inline bytes on one of the response parts.
       image = next(
          (
             part.inline_data
             for part in response.candidates[0].content.parts or []
             if part.inline_data
          ),
          None,
       )
       if image is None:
          raise ValueError(f"{IMAGE_MODEL} returned no image for: {art_direction[:120]}")
    
       filename = "key_visual" + (
          mimetypes.guess_extension(image.mime_type or "") or ".bin"
       )
       version = await tool_context.save_artifact(filename, types.Part(inline_data=image))
    
       ## Return the artifact filename and version
       return {
          "filename": filename,
          "version": version,
          "bytes": len(image.data),
          "mime_type": image.mime_type,
       }
    
  3. מוסיפים את generate_key_visual מתחת ל-SkillToolset כדי לצייד את הסוכן בכלי:
       tools=[
          SkillToolset(skills=[BRAND_SKILL]),
          ## Supply the `generate_key_visual` tool below this comment
          generate_key_visual,
       ],
    
  4. ‫agents-cli כולל ממשק משתמש מובנה לפיתוח אתרים שנקרא playground לבדיקה, בדומה ל-ADK. כדי להפעיל את Playground:
    agents-cli playground --port 8080
    
  5. לוחצים על Web Preview (תצוגה מקדימה של אתר), סמל העין בפינה השמאלית העליונה של הטרמינל, ובוחרים באפשרות Preview on port 8080 (תצוגה מקדימה ביציאה 8080).לוחצים על תצוגה מקדימה באתר
  6. יוצג הממשק הבא לצ'אט עם הסוכן. אפשר לנסות לשלוח את ההנחיה "Nine lives. ‫Zero gravity."תהליך עבודה לדוגמה אפשר לראות מהאינטראקציות איך visual_director הסוכן מבצע load_skill("brand-guidelines") כדי לקרוא את ספר המותג, ואז מבצע generate_key_visual כדי להתחיל ליצור את התמונה. התמונה שנוצרת מאוחסנת כארטיפקט, שמוצג גם בממשק המשתמש ואפשר לבדוק אותו בקלות.
  7. חוזרים למסוף ומפסיקים את התהליך agents-cli playground שפועל על ידי הקשה על Ctrl+C פעמיים.

6. ארכיטקטורה מרובת סוכנים עם פרוטוקול A2A

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

‫Agent2Agent‏ (A2A) הוא פרוטוקול פתוח שמאפשר לסוכן אחד להתקשר לסוכן אחר ברשת. החלק המרכזי הוא כרטיס הסוכן, מסמך JSON שהסוכן מפרסם על עצמו באמצעות כתובת URL ידועה, שכולל את השם, התיאור והיכולות שלו. המתקשר מאחזר את הכרטיס ויודע איך לתקשר עם הנציג שמאחוריו, ללא קוד מקור, יחסי תלות או שפה משותפים.

פרוטוקול A2A

‫agents-cli כבר הגדיר את זה כברירת מחדל. ה-scaffold שנוצר visual_director/app_utils/a2a.py, שבונה את הכרטיס מהסוכן ומטפל במסלולים, ו-visual_director/fast_api_app.py, שמציג אותם. צריך לבצע שינוי קטן כדי לאפשר לו גם לשלוח את הארטיפקטים בחזרה ללקוח.

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

הגדרת שרת A2A

הפיגום של שרת agents-cli A2A

הקוד והמודול הבאים כבר מסופקים כשמפעילים את סוכן visual-director באמצעות agents-cli. כדי לפתוח את הקבצים האלה, מדביקים את הפקודות הבאות בטרמינל:

cloudshell edit visual_director/fast_api_app.py
cloudshell edit visual_director/app_utils/a2a.py

‫visual_director/fast_api_app.py מפעיל את הסוכן ומטמיע את נתיבי ה-A2A בנתיב שנבנה משם האפליקציה:

await attach_a2a_routes(
    app,
    agent=root_agent,
    runner=runner,
    rpc_path=f"/a2a/{adk_app.name}",   ## becomes /a2a/visual_director
)

‫visual_director/app_utils/a2a.py יעשה את כל השאר, בשלושה מהלכים.

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

agent_card = await AgentCardBuilder(
    agent=agent,
    rpc_url=f"{resolved_app_url}{rpc_path}",
).build()

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

request_handler = DefaultRequestHandler(
    agent_executor=A2aAgentExecutor(runner=runner, force_new_version=True),
    task_store=task_store,
    agent_card=agent_card,
)

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

מסלול

מטרה

/a2a/visual_director/.well-known/agent-card.json

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

/a2a/visual_director

נקודת הקצה של JSON-RPC. לאן מועברות הבקשות בפועל

אפשרות להעביר פריטים מעבר לגבול

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

  1. פותחים את קובץ ה-A2A שנוצר:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/visual-director
    cloudshell edit visual_director/app_utils/a2a.py
    
  2. מאתרים את הייבוא AgentCardBuilder ומוסיפים את הייבוא A2aAgentExecutorConfig ואת הייבוא include_artifacts_in_a2a_event_interceptor מתחתיו:
    ## ...
    ## from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder
    ## Add the following two below `AgentCardBuilder` import
    from google.adk.a2a.executor.config import A2aAgentExecutorConfig
    from google.adk.a2a.executor.interceptors.include_artifacts_in_a2a_event import (
       include_artifacts_in_a2a_event_interceptor,
    )
    
  3. מחפשים את השורה agent_executor=A2aAgentExecutor(runner=runner), ומחליפים אותה בשורה הבאה (מוסיפים את A2aAgentExecutorConfig):
             agent_executor=A2aAgentExecutor(
                runner=runner,
                force_new_version=True,
                config=A2aAgentExecutorConfig(
                   execute_interceptors=[include_artifacts_in_a2a_event_interceptor]
                ),
             ),
    

הפעלת השירות וקריאת כרטיס ה-A2A שפורסם

  1. מפעילים את השרת ברקע. עליך להצהיר על APP_URL, כי זו הכתובת שהכרטיס מפרסם ככתובת שלו. מדביקים את הטקסט הבא בטרמינל:
    APP_URL=http://localhost:8801 nohup uv run uvicorn visual_director.fast_api_app:app --port 8801 > /tmp/visual-director.log 2>&1 &
    
  2. מחכים כמה שניות עד שהשירות יתחיל לפעול, ואז מאשרים שהוא פועל:
    sleep 10
    tail -2 /tmp/visual-director.log
    
    אלה הפרטים שיוצגו:
    INFO:     Application startup complete.
    INFO:     Uvicorn running on http://127.0.0.1:8801 (Press CTRL+C to quit)
    
  3. קוראים את הכרטיס שהסוכן מפרסם עכשיו:
    curl -s http://localhost:8801/a2a/visual_director/.well-known/agent-card.json | python3 -m json.tool
    
    בכרטיס מופיעים השם, התיאור, כתובת ה-URL והכישורים של הסוכן, כולל generate_key_visual. הוא נוצר ישירות מהגדרת הסוכן בקוד.
    {
       "name": "visual_director",
       "description": "Turns a campaign concept into art direction and a key visual.",
       "skills": [
            {
                "id": "visual_director",
                "name": "model",
                "tags": [
                    "llm"
                ]
            },
            {
                "id": "visual_director-generate_key_visual",
                "name": "generate_key_visual",
                "description": "Render the art direction as the campaign's key visual.\n\nArgs:\n   art_direction: The art direction to render, in full.\n\nReturns:\n   The saved artifact's filename, version, size and media type.",
                "tags": [
                    "llm",
                    "tools"
                ]
            }
       ],
       "preferredTransport": "JSONRPC",
       "protocolVersion": "0.3",
       "url": "http://localhost:8801/a2a/visual_director"
    }
    
    אתה מוכן להטמיע את סוכן visual-director בתהליך העבודה הראשי של pitch-generator.

7. הטמעת הסוכן המרוחק של A2A בתהליך העבודה

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

סוכן מרוחק עטוף בתג RemoteA2aAgent ומוצב בתרשים כמו כל צומת אחר. הקצוות שומרים על הצורה שלהם, והמתאם לא יודע אילו מהצמתים שלו הם מקומיים.

מכוונים את הכלי לתיאום אל השירות ומעדכנים את תהליך העבודה של התרשים

  1. פותחים את קובץ הסוכן:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator
    cloudshell edit pitch_generator/agent.py
    
  2. מוסיפים את שורות הייבוא האלה לחלק העליון של הקובץ, לצד שורות הייבוא הקיימות:
    import asyncio
    
    import subprocess
    import google.auth.transport.requests
    import google.oauth2.id_token
    import httpx
    from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part
    
  3. מוסיפים את RemoteA2aAgent ההגדרה הבאה מעל creative_director ההצהרה:
    VISUAL_DIRECTOR_URL = os.getenv("VISUAL_DIRECTOR_URL", "http://localhost:8801")
    
    ## For deployment on Cloud Run
    def _cloud_run_client() -> httpx.AsyncClient | None:
       """An httpx client that signs each request with a Cloud Run ID token.
    
       Cloud Run deploys private, so both the agent-card fetch and the A2A calls
       need one.
       """
       if not VISUAL_DIRECTOR_URL.startswith("https://"):
          return None
    
       async def sign(request: httpx.Request) -> None:
          token = None
          try:
             token = await asyncio.to_thread(
                   google.oauth2.id_token.fetch_id_token,
                   google.auth.transport.requests.Request(),
                   VISUAL_DIRECTOR_URL,
             )
          except Exception:
             token = subprocess.check_output(
                ["gcloud", "auth", "print-identity-token", "-q"],
                stderr=subprocess.PIPE
             ).decode().strip()
          if not token:
                raise Exception("Failed to fetch ID token")
          request.headers["Authorization"] = f"Bearer {token}"
    
       return httpx.AsyncClient(event_hooks={"request": [sign]}, timeout=600)
    
    ## Keep the approval's tool traffic out of the A2A request
    def _pitch_parts_only(part: types.Part):
       """Sends the Visual Director text and media only.
    
       RemoteA2aAgent replays the session on every call, and the Visual Director
       runs downstream of the human approval, so the approval's function_response
       would travel alongside the concept text. An ADK server reads a function
       response as "resume the paused invocation" and rejects a message that also
       carries text, so drop the tool parts here.
       """
       if part.function_call or part.function_response:
          return None
       return convert_genai_part_to_a2a_part(part)
    
    visual_director = RemoteA2aAgent(
       name="visual_director",
       description="Turns a campaign concept into art direction for one key visual.",
       agent_card=(
          f"{VISUAL_DIRECTOR_URL}/a2a/visual_director{AGENT_CARD_WELL_KNOWN_PATH}"
       ),
       httpx_client=_cloud_run_client(),
       genai_part_converter=_pitch_parts_only,
    )
    
  4. מחליפים את פונקציית Python של הצומת package בהגדרה הבאה:
    async def package(ctx: Context, node_input: dict):
       """Render the joined branches as the finished pitch."""
       empty = [name for name, value in node_input.items() if not value]
       if empty:
          raise ValueError(f"nothing reached the join from: {', '.join(empty)}")
    
       def _key_visual(ctx: Context) -> types.Blob | None:
          """The image the Visual Director sent back, or None if it sent none."""
          for event in reversed(ctx.session.events):
             if event.author != "visual_director" or not event.content:
                   continue
             for part in event.content.parts or []:
                   if part.inline_data and part.inline_data.data:
                      return part.inline_data
          return None
    
       image = _key_visual(ctx)
       if image is None:
          raise ValueError("the Visual Director returned art direction but no image")
    
       suffix = mimetypes.guess_extension(image.mime_type or "") or ".bin"
       filename = f"key_visual{suffix}"
       await ctx.save_artifact(filename, types.Part(inline_data=image))
    
       pitch = "\n\n".join(
          [
             f"CONCEPT\n{node_input['creative_director']}",
             f"COPY\n{node_input['copywriter']}",
             f"ART DIRECTION\n{node_input['visual_director']}",
             f"KEY VISUAL\n{filename}, {len(image.data)} bytes, {image.mime_type}",
          ]
       )
       yield Event(
          content=types.Content(
             role="model",
             parts=[types.Part(text=pitch), types.Part(inline_data=image)],
          )
       )
       yield Event(output=pitch)
    
  5. מוסיפים שער אישור אנושי לפני שהיצירה ממשיכה בהמשך השרשרת. ב-pitch_generator/agent.py, מגדירים את צמתי האישור מעל root_agent:
    @node(rerun_on_resume=False)
    async def approve_concept(ctx: Context):
        yield RequestInput(
            message="Please approve the campaign concept (yes/no).",
            response_schema=str
        )
    
    @node(rerun_on_resume=True)
    async def user_approval(ctx: Context):
       user_response = await ctx.run_node(approve_concept)
       if str(user_response).lower() in ("yes", "y"):
          approved_concept = f"## Approved Concept\n\n{ctx.session.state['creative_director']}"
          yield Event(
             content=types.Content(
                role="model",
                parts=[types.Part(text=approved_concept)]
             )
          )
       else:
          raise ValueError("User rejected the concept")
    
  6. בסוף, מעדכנים את הייבוא בחלק העליון כדי לכלול את RequestInput ו-node מ-google.adk.events ו-google.adk.workflow:
    ‫
    from google.adk.events import Event, RequestInput
    from google.adk.workflow import JoinNode, Workflow, node
    
    ולשנות את תהליך העבודה root_agent edges כדי לנתב דרך user_approval:
        edges=[
            ("START", creative_director),
            (creative_director, user_approval, (copywriter, visual_director)),
            ((creative_director, copywriter, visual_director), assemble),
            (assemble, package),
        ],
    
    כמה פרטים שחשוב לדעת:
    • אישור של אדם שבתהליך (HITL): approve_concept התוצאה היא RequestInput. כשתהליך העבודה מגיע לצומת הזה, הביצוע מושהה ומופעלת הפרעה למתקשר. הענפים הבאים (copywriter ו-visual_director) לא יפעלו עד שאישור המשתמש יאושר.
    • הסימן visual_director מופיע בקצוות בדיוק כמו copywriter, אבל אחד מהם הוא קריאה לפונקציה והשני הוא ברשת.
    • התמונה שמוחזרת נקראת מאירועי הסשן ולא מ-node_input, כי בשלב הזה רק טקסט מועבר לפלט של צומת. התמונה חצתה את הגבול דרך מיירט ה-A2A, אבל היא הגיעה כמצורפת לאירועים של הסוכן המרוחק, ולכן פונקציית העזר _key_visual מאחזרת אותה משם.

בדיקת השילוב

עכשיו נבדוק שאפשר ליצור תקשורת בין הבמאי הוויזואלי (שפועל ברקע) לבין כלי יצירת הצעות המכירה באמצעות פרוטוקול A2A.

  1. מוודאים שהתכונה 'במאי חזותי' עדיין פועלת ברקע:
    ‫
    curl -s -o /dev/null -w "visual director: %{http_code}\n" http://localhost:8801/a2a/visual_director/.well-known/agent-card.json
    
    אמור להתקבל כאן סטטוס visual director: 200.אם מתקבל סטטוס אחר, מריצים את הפקודה הבאה כדי להפעיל מחדש את השירות:
    fuser -k 8801/tcp 2>/dev/null || true
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/visual-director
    APP_URL=http://localhost:8801 nohup uv run uvicorn visual_director.fast_api_app:app --port 8801 > /tmp/visual-director.log 2>&1 &
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator
    sleep 10
    curl -s -o /dev/null -w "visual director: %{http_code}\n" http://localhost:8801/a2a/visual_director/.well-known/agent-card.json
    
  2. מתחילים את הניסיון:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator/pitch_generator
    agents-cli playground --port 8080
    
  3. לוחצים על Web Preview (תצוגה מקדימה של אתר), סמל העין בפינה השמאלית העליונה של הטרמינל, ובוחרים באפשרות Preview on port 8080 (תצוגה מקדימה ביציאה 8080).לוחצים על תצוגה מקדימה באתר
  4. יוצג הממשק לצ'אט עם הסוכן. נסו לשלוח את ההנחיה "Flying skateboards for cats" (סקייטבורדים מעופפים לחתולים).תהליך עבודה לדוגמהתהליך עבודה לדוגמהתוכלו לראות את האינטראקציה המלאה שבה תהליך העבודה מתחיל וכל סוכן מופעל. אחרי ש-creative_director מנסח את הרעיון, תהליך העבודה מושהה בשלב האישור ומבקש ממך לאשר: "יש לאשר את הרעיון לקמפיין (כן/לא)". כדי להמשיך, מזינים yes בשדה להזנת קלט.אחרי האישור, התרשים מתרחב: אפשר לראות מתי סוכן visual-director מתחיל לטעון את הסקיל, ובאמצע יצירת התמונות, סוכן copywriter מסיים ליצור את הכיתוב לרשתות החברתיות – מה שמוכיח ששני הסוכנים פועלים במקביל בלי להמתין זה לזה. אחרי ששני התהליכים מסתיימים, הצומת assemble מצטרף לתוצאות עם הרעיון שאושר שנוצר על ידי creative_director, וכך מסתיים תהליך העברת התוכן של הצעת הערך בפונקציה package.שני השירותים, שמקושרים באמצעות A2A

8. שמירת מפתחות השורות ב-Cloud Storage

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

ה-ADK מחליט לאן להעביר את הארטיפקטים בהפעלה. הקובץ app_utils/services.py שנוצר קורא משתנה סביבה יחיד:

def get_artifact_service():
    """Process-wide artifact service: GCS when a bucket is set, else in-memory."""
    if bucket := os.environ.get("LOGS_BUCKET_NAME"):
        return GcsArtifactService(bucket_name=bucket)
    return InMemoryArtifactService()

אם מגדירים את LOGS_BUCKET_NAME, כל השיחות ב-save_artifact יועברו ל-Cloud Storage.

שילוב של שירות ארטיפקטים עם קטגוריית GCS

יצירת מערך נתונים ב-BigQuery

  1. ב-Cloud Shell, מריצים את הפקודה הבאה כדי ליצור מערך נתונים ב-BigQuery:
    bq mk --dataset --location=<YOUR_REGION> --project_id=<YOUR_PROJECT_ID> bwg
    
  2. מפנים את מאגר הארטיפקטים של הרכז אל הקטגוריה:
    ‫
    export LOGS_BUCKET_NAME="<YOUR_PROJECT_ID>-bwg"
    
    ‫GcsArtifactService מקבל שם קטגוריה ללא הקידומת gs://. בדומה למשתנים האחרים, המשתנה הזה נמצא במעטפת, ומשימת הפריסה מעבירה אותו ל-Cloud Run.
  3. בודקים את התוכן של הדלי. בכרטיסייה חדשה בדפדפן, פותחים את כתובת ה-URL הבאה:
    https://console.cloud.google.com/storage/browser?project=<YOUR_PROJECT_ID>
    
    לוחצים על ה-bucket. יכול להיות שהיא תכיל תמונות שנוצרו בעבר, בהתאם לבדיקות הקודמות שלכם.

9. פריסת סוכן Visual Director ב-Cloud Run

עד עכשיו, כל הפעולות בוצעו במחשב אחד, והיה אפשר להגיע ל-Visual Director בכתובת localhost:8801. במשימה הזו תפרסו את סוכן הבמאי הוויזואלי כשירות Cloud Run משלו, ותאפשרו לסוכן ליצירת הצעות למכירה לתקשר איתו ברשת באמצעות פרוטוקול Agent2Agent‏ (A2A).

מנהל התוכן החזותי מקבל כתובת URL אמיתית של https://, כך ש-_cloud_run_client שכתבתם קודם מתחיל לחתום על בקשות במקום להחזיר None. בנוסף, הפריסה ב-Cloud Run היא פרטית כברירת מחדל.

שני הסוכנים כשירותי Cloud Run

פריסת Visual Director

  1. אם שירות הפועל ברקע המקומי וסביבת הפיתוח עדיין פועלים, צריך להפסיק אותם:
    fuser -k 8801/tcp 2>/dev/null || true
    fuser -k 8000/tcp 2>/dev/null || true
    
  2. פורסים את הסוכן. הפעולה הזו יוצרת מאגר ומעבירה אותו.⏰ התהליך יימשך כמה דקות.
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/visual-director
    source .env
    agents-cli deploy \
      --service-name visual-director \
      --project <YOUR_PROJECT_ID> \
      --region <YOUR_REGION> \
      --no-confirm-project \
      --labels dev-tutorial=bwg001-devlab-1 \
      --update-env-vars "LOGS_BUCKET_NAME=$LOGS_BUCKET_NAME,GOOGLE_GENAI_USE_VERTEXAI=true,GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_CLOUD_REGION=$GOOGLE_CLOUD_REGION"
    

הפעלת מחולל הצעות הערך שמשתמש בבמאי הוויזואלי שנפרס ב-Cloud Run

  1. קוראים את כתובת ה-URL של הבמאי החזותי ומריצים את מחולל הצעות המחיר שמשתמש בה:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator
    source .env
    export VISUAL_DIRECTOR_URL=$(gcloud run services list \
      --project <YOUR_PROJECT_ID> \
      --region <YOUR_REGION> \
      --flatten='spec.template.spec.containers[0].env' \
      --filter='metadata.name=visual-director AND spec.template.spec.containers[0].env.name=APP_URL' \
      --format='value(spec.template.spec.containers[0].env.value)')
    echo "Visual Director is at $VISUAL_DIRECTOR_URL"
    APP_URL=http://localhost:8000 nohup uv run uvicorn pitch_generator.fast_api_app:app --port 8000 > /tmp/visual-pitch_generator.log 2>&1 &
    

התקשרות לתהליך העבודה שנפרס

שני הנציגים תומכים בפרוטוקול A2A. המאגר כולל לקוח קטן להפעלת Pitch Generator באמצעות A2A.

צריך להגדיר את PITCH_GENERATOR_URL לכתובת ה-URL של מחולל הצעות המחיר שפועל באופן מקומי. בינתיים, הבמאי הוויזואלי פועל ב-Cloud Run, והמחולל של הצעות המכירה מתקשר איתו באמצעות A2A.

  1. מעתיקים את הלקוח לפרויקט ומפעילים פתרונות חכמים:
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator
    cp ../solution/pitch-generator/call_agent.py .
    PITCH_GENERATOR_URL=http://localhost:8000 uv run python call_agent.py "Flying skateboards for cats" --save-image ./key_visual.png
    
    מכיוון שתהליך העבודה מכיל שער אישור אנושי, הסקריפט מושהה אחרי יצירת טיוטת הרעיון:
    === HUMAN APPROVAL REQUIRED ===
    
    [DRAFT CONCEPT]
    Concept Line: "Gravity is a Suggestion."
    ...
    
    Please approve the campaign concept (yes/no):
    
    מקלידים yes ומקישים על Enter כדי לאשר ולהמשיך בתהליך העבודה. (אפשר גם להעביר את הערך -y או --auto-approve כדי לאשר באופן אוטומטי).
  2. פותחים את call_agent.py ובודקים איך הוא מתקשר עם הנציג באמצעות a2a-sdk:
    • ‫ClientFactory יוצרת טירגוט רשמי של Client A2A https://SERVICE_URL/a2a/pitch_generator.
    • ‫generate_pitch_async שולח SendMessageRequest ראשוני. כשהתשובה חוזרת עם TaskState.TASK_STATE_INPUT_REQUIRED, המודל מזהה את החלק adk_request_input, מציג את טיוטת הרעיון, מקבל את האישור שלכם ושולח בקשה לקורות חיים עם התשובה שלכם בחזרה למשימה.
    • לולאת הניתוח קוראת את הקטעים CONCEPT, COPY ו-ART DIRECTION מפריטי המידע של המשימה שהושלמה, ושומרת את תמונת הקובץ הבינארי ב-key_visual.png.
    אמורות להיות שלוש פיסקאות מודפסות, והתמונה אמורה להישלח ל-key_visual.png.
  3. מעלים את התמונה המרכזית לדלי, תחת קידומת שאפשר להפנות אליה את BigQuery:
    gcloud storage cp ./key_visual.png gs://<YOUR_PROJECT_ID>-bwg/key-visuals/cats.png
    
  4. יוצרים הצעה שנייה כדי שיהיה משהו להשוות אליו, ומעלים גם אותה:
    PITCH_GENERATOR_URL=http://localhost:8000 uv run python call_agent.py "A commuter bike built for rainy cities" --save-image ./bike.png
    gcloud storage cp ./bike.png gs://<YOUR_PROJECT_ID>-bwg/key-visuals/bike.png
    

10. הקצאת ניקוד לרכיבים החזותיים העיקריים בהתאם להתאמה למותג ב-BigQuery

עכשיו יש לכם תמונות בקטגוריה ואת הנחיות המיתוג שאמורות לעצב אותן. במשימה הזו, נשלב בין השניים: נתייחס לתמונות מ-BigQuery, ואז נבקש ממודל Gemini לדרג כל אחת מהן בהתאם להנחיות, בלי להעביר אף בייט מ-Cloud Storage.

שתי תכונות של BigQuery מבצעות את העבודה:

  • ‫ObjectRef הוא ערך שמצביע על אובייקט ב-Cloud Storage. ‫OBJ.MAKE_REF יוצרת אחת מ-URI בתוספת חיבור שמעניק ל-BigQuery גישה שהוקצתה לדלי. החיבור pitch-connection שנוצר על ידי סקריפט ההגדרה הוא החיבור הזה, וכבר יש בו את ההרשאות Storage Object Viewer ו-Agent Platform User.
  • ‫AI.SCORE מקבלת הנחיה ומחזירה FLOAT64. ההנחיה יכולה לכלול מחרוזות עם ערכים של ObjectRef, כך שהמודל יראה את קריטריון ההערכה ואת התמונה יחד.

מערכת BigQuery קוראת את הדלי

יצירת טבלה של רכיבים חזותיים מרכזיים

  1. פותחים את BigQuery Studio בכרטיסייה חדשה בדפדפן:
    https://console.cloud.google.com/bigquery?project=<YOUR_PROJECT_ID>
    
  2. לוחצים על Create new query (יצירת שאילתה חדשה) (סמל +), מדביקים את השאילתה הבאה ולוחצים על Run (הפעלה) (מחליפים את ואת במזהה הפרויקט ובאזור שלכם). הוא יוצר שורה אחת לכל תמונה מרכזית, ומשייך את הקונספט של הקמפיין להפניה לתמונה:
    ‫
    SET @@location = '<YOUR_REGION>';
    
    CREATE OR REPLACE TABLE bwg.key_visuals AS
    SELECT
      campaign,
      concept,
      OBJ.FETCH_METADATA(
        OBJ.MAKE_REF(uri, '<YOUR_REGION>.pitch-connection')
      ) AS key_visual
    FROM UNNEST([
      STRUCT(
        'cats' AS campaign,
        'Flying skateboards for cats' AS concept,
        'gs://<YOUR_PROJECT_ID>-bwg/key-visuals/cats.png' AS uri
      ),
      STRUCT(
        'bike',
        'A commuter bike built for rainy cities',
        'gs://<YOUR_PROJECT_ID>-bwg/key-visuals/bike.png'
      )
    ]);
    
    ‫BQOBJ.MAKE_REF יוצר את ההפניה מה-URI ומהחיבור. ‫OBJ.FETCH_METADATA ואז יוצר קשר עם Cloud Storage וממלא את הגודל, סוג התוכן והגנרציה של האובייקט. הקריאה השנייה היא גם הדרך הכי מהירה להוכיח שהחיבור פועל: אם ההרשאות שגויות, השדה details יחזיר את השגיאה במקום את המטא-נתונים.
  3. בודקים מה מאוחסן. מחליפים את השאילתה הקודמת בשאילתה הזו ומריצים אותה:
    SELECT campaign, concept, key_visual.uri, key_visual.details
    FROM bwg.key_visuals;
    
    הטבלה מכילה מצביע ומטא-נתונים, ולא את התמונה. הבייטים אף פעם לא יצאו מהמאגר.

דירוג כל תמונה בהתאם להנחיות המותג

הבמאי הוויזואלי קיבל הוראה לציית לפלטת צבעים, לכלל תאורה ולכלל קומפוזיציה. האם זה באמת קרה או לא – זו שאלה שדורשת שיקול דעת, ולכן יש את AI.SCORE.

  1. מריצים את השאילתה הבאה (מחליפים את באזור שלכם). הפרומפט הוא טאפל: הקריטריונים להערכה, הקונספט של הקמפיין וObjectRef של התמונה:
    SELECT
      campaign,
      concept,
      AI.SCORE(
        ('''You are the brand guardian reviewing a campaign key visual.
    Score the attached image from 1 to 10 on how closely it follows these house rules:
    - Palette: deep indigo and slate, with one warm accent of amber or terracotta. Nothing neon.
    - Light: one dominant low, raking source with long shadows. No flat overhead light.
    - Composition: subject off-center with generous empty negative space on the opposite side.
    - Subject: one subject only, photographic realism, shallow depth of field.
    - Never: text, logos, or watermarks anywhere in the frame.
    A 10 obeys every rule. A 1 ignores them. The campaign concept is: ''',
          concept,
          key_visual),
        connection_id => '<YOUR_REGION>.pitch-connection',
        endpoint => 'gemini-3.5-flash'
      ) AS brand_fit
    FROM bwg.key_visuals
    ORDER BY brand_fit DESC;
    
  2. הופכים את הניקוד לפסיקה שהצוות יכול לפעול לפיה:
    ‫
    CREATE OR REPLACE TABLE bwg.brand_review AS
    SELECT
      campaign,
      concept,
      brand_fit,
      IF(brand_fit >= 7, 'on brand', 'needs another pass') AS verdict
    FROM (
      SELECT
        campaign,
        concept,
        AI.SCORE(
          ('''Score this campaign key visual from 1 to 10 on how closely it follows the house
    brand rules: deep indigo and slate palette with a single warm accent and nothing neon; one
    low raking light source with long shadows; the subject off-center with empty negative space
    opposite; a single photographic subject; and no text, logos, or watermarks in the frame.
    The campaign concept is: ''',
              concept,
              key_visual),
          connection_id => '<YOUR_REGION>.pitch-connection',
          endpoint => 'gemini-3.5-flash'
        ) AS brand_fit
      FROM bwg.key_visuals
    );
    
    SELECT * FROM bwg.brand_review ORDER BY brand_fit DESC;
    
    ‫AI.SCORE מחזירה FLOAT64 בלבד, ולכן ערך הסף נמצא ב-SQL רגיל, שאפשר לשנות אותו בלי לגעת בהנחיה. הניקוד מחושב לכל שורה, כלומר אותם קריטריונים חלים על כל נכס שנוצר בקמפיין, ולא רק על שני הנכסים שיצרתם כאן.

11. השלבים הבאים

יצירת הערכות

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

אחד מהסוכנים שלנו כולל Human in the loop, כלומר הוא מחכה לקלט. תצטרכו לאפשר ל-Antigravity לבצע שינויים בקוד הנוכחי כדי שההתנהגות הזו לא תפריע להערכות האוטומטיות.

אחת הדרכים הטובות להתחיל היא לשאול את Antigravity CLI.

כדי להפעיל את ה-CLI, מקלידים את הפקודה הבאה ב-Cloud Shell:

cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/
agy

הנה הנחיה ראשונית:

Explore this repository (`pitch-generator` and `visual-director`), including their agent instructions, tools, and `SKILL.md` files. Design and implement a creative `agents-cli eval` evaluation suite under `tests/eval/` that stress-tests the agent's behavior.

במאגר המשוכפל יש גם דוגמה להערכות והוראות להפעלתן.

12. הסרת המשאבים

כדי להימנע מחיובים בחשבון Google Cloud על המשאבים שבהם השתמשתם ב-Codelab הזה, מומלץ למחוק את הפרויקט או למחוק את המשאבים הספציפיים שנוצרו במהלך ה-Codelab:

gcloud run services delete visual-director --region=<YOUR_REGION> --project=<YOUR_PROJECT_ID> --quiet
bq rm -r -f -d <YOUR_PROJECT_ID>:bwg
bq rm --connection --location=<YOUR_REGION> --project_id=<YOUR_PROJECT_ID> pitch-connection
gcloud storage rm -r gs://<YOUR_PROJECT_ID>-bwg

13. מעולה!

בשיעור Codelab הזה יצרתם מחולל הצעות לקמפיינים עם כמה סוכנים באמצעות ערכה לפיתוח סוכנים (ADK). חיברתם שלושה סוכנים לתהליך עבודה גרפי דטרמיניסטי, ארזתם הנחיות מיתוג ככישורי סוכן שנטענים לפי דרישה, והענקתם לסוכן כלי ליצירת תמונות ששומר את הפלט שלו כארטיפקט. לאחר מכן, פיצלתם את Visual Director לשירות A2A משלו, הטמעתם אותו בחזרה בתרשים כצומת מרוחק, פרסתם אותו ב-Cloud Run והפעלתם את כל תהליך העבודה באמצעות A2A ממתאם שפועל באופן מקומי. לבסוף, הפנית אל התמונות החזותיות שנוצרו מ-BigQuery באמצעות ObjectRef והשתמשת ב-AI.SCORE כדי להשוות אותן להנחיות המיתוג שלך.