สร้างเครื่องมือสร้างการเสนอขายแบบ Agent: การจัดกลุ่ม การประเมิน และการวิเคราะห์ด้วย Google Cloud

1. บทนำ

แบนเนอร์มาสเตอร์คลาสสำหรับเอเจนต์

ดูวิธีสร้างเครื่องมือแบบหลายโมดอลและหลายเอเจนต์ที่รับบรีฟหัวข้อดิบและจัดการการสร้างชิ้นงานโซเชียลที่ดึงดูดใจสูง การใช้ Agent Development Kit (ADK) ของ Google และโปรโตคอล Agent2Agent (A2A) คุณจะสร้าง Micro-Agent แบบอัตโนมัติ จัดทำกฎของแบรนด์เป็นทักษะของ Agent ที่นำกลับมาใช้ใหม่ได้ ติดตั้งใช้งานบริการ Agent ใน Cloud Run และให้คะแนนภาพหลักที่สร้างขึ้นเพื่อการปฏิบัติตามข้อกำหนดของแบรนด์ด้วยฟังก์ชัน AI ของ BigQuery

สิ่งที่คุณจะทำ

ใน Codelab นี้ คุณจะได้เรียนรู้วิธีทำงานต่อไปนี้

  • จัดระเบียบ Agent ADK หลายตัวด้วยเวิร์กโฟลว์กราฟที่กำหนดได้
  • จัดทำหลักเกณฑ์การใช้แบรนด์เป็นทักษะของตัวแทนที่จะโหลดเมื่อจำเป็นเท่านั้น
  • จัดเตรียมเครื่องมือสร้างรูปภาพให้กับเอเจนต์ซึ่งจะบันทึกเอาต์พุตเป็นอาร์ติแฟกต์
  • เปิดเผย Agent เป็นบริการ A2A และเรียกใช้จาก Agent อื่นเป็นโหนดระยะไกล
  • ติดตั้งใช้งาน Visual Director ใน Cloud Run และเรียกเวิร์กโฟลว์ผ่าน A2A
  • ให้คะแนนภาพหลักที่สร้างขึ้นเพื่อความเหมาะสมกับแบรนด์ใน BigQuery ด้วยฟังก์ชัน AI

สิ่งที่คุณต้องมี

  • เว็บเบราว์เซอร์ (เช่น Chrome)
  • บัญชี Google Cloud และโปรเจ็กต์ Google Cloud ที่เปิดใช้การเรียกเก็บเงิน

2. ก่อนเริ่มต้น

สร้างโปรเจ็กต์ Google Cloud

  1. ในคอนโซล Google Cloud ให้เลือกหรือสร้างโปรเจ็กต์ Google Cloud ในหน้าตัวเลือกโปรเจ็กต์
  2. ตรวจสอบว่าโปรเจ็กต์ที่อยู่ในระบบคลาวด์เปิดใช้การเรียกเก็บเงินแล้ว ดูวิธีตรวจสอบว่าได้เปิดใช้การเรียกเก็บเงินในโปรเจ็กต์แล้วหรือไม่

เริ่มต้น Cloud Shell

คุณจะใช้ Google Cloud Shell เพื่อดาวน์โหลดโค้ด เรียกใช้สคริปต์การตั้งค่า และติดตั้งใช้งานแอปพลิเคชัน

  1. เปิด Google Cloud Shell ในเบราว์เซอร์ (หรือคลิกเปิดใช้งาน Cloud Shell ที่ด้านบนของ คอนโซล Google Cloud)
    https://shell.cloud.google.com/
    
    คลิกดำเนินการต่อหากได้รับประกาศเพิ่มเติมรอจนกว่าทั้งเทอร์มินัลและเครื่องมือแก้ไขจะโหลดเสร็จสมบูรณ์ การดำเนินการนี้อาจใช้เวลา 2-3 นาที
  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. เปิดใช้ Google Cloud API ที่จำเป็นสำหรับโปรเจ็กต์ของคุณ
    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 จะดำเนินการตามงาน โดยจะขอสิทธิ์จากคุณเพื่อเรียกใช้คำสั่ง 2-3 รายการ เมื่อดาวน์โหลดโค้ดแล้ว ให้ออกจาก 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 และให้สิทธิ์ IAM ที่จำเป็นแก่บัญชีบริการที่มีการลองใหม่โดยอัตโนมัติ

สคริปต์การตั้งค่ายังเรียกใช้ setenv.sh ซึ่งเป็นตัวช่วยขนาดเล็กที่ได้ตัวแปรที่เอเจนต์ทั้ง 2 รายการต้องการ ได้แก่ GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_REGION, GOOGLE_CLOUD_LOCATION และ GOOGLE_GENAI_USE_VERTEXAI จาก PROJECT_ID และ REGION ที่คุณส่งออกไว้ข้างต้น นอกจากนี้ ยังบันทึกค่าทั้ง 2 ค่าลงในไฟล์ .env ในไดเรกทอรีหน้าแรกเพื่อให้คุณกู้คืนได้ในภายหลัง

  1. ใน Cloud Shell ให้ทำให้สคริปต์การตั้งค่าเรียกใช้งานได้ แล้วดำเนินการ
    chmod +x codelab-setup.sh setenv.sh
    ./codelab-setup.sh
    
  2. การดำเนินการนี้จะใช้เวลาประมาณ 1 นาที คุณควรเห็นข้อความนี้ซึ่งบ่งบอกว่าสำเร็จแล้ว "🎉 🦄 การเรียกใช้สคริปต์เสร็จสมบูรณ์"สำเร็จ
  3. สคริปต์การตั้งค่าทำงานในเชลล์ของตัวเอง ดังนั้นตัวแปรจึงไม่ถึงเทอร์มินัลของคุณ โหลดลงในเซสชันโดยทำดังนี้
    source ./setenv.sh
    
    คุณควรเห็นโปรเจ็กต์ ภูมิภาค และสถานที่ตั้งของคุณแสดงขึ้นมา

3. จัดการ Agent ด้วยเวิร์กโฟลว์กราฟ

ในงานนี้ คุณจะสร้างโปรเจ็กต์เครื่องมือสร้างการเสนอเพลงและสร้างเวิร์กโฟลว์กราฟที่เรียกใช้เอเจนต์ 2 รายและรวมเอาต์พุตของเอเจนต์ทั้ง 2 ราย

การเสนอเพลงต้องมีแนวคิด ซึ่งหมายถึงไอเดียและชิ้นงานที่ขายแนวคิดนั้น ในกรณีการใช้งานนี้ เราจะออกแบบเอเจนต์ที่มีความรับผิดชอบแตกต่างกันในขอบเขตของงาน เพื่อแสดงให้เห็นว่าคุณจะแยกงานที่ซับซ้อนและมอบหมายงานเหล่านั้นให้กับเอเจนต์ต่างๆ ที่มีความรับผิดชอบที่แตกต่างกันได้อย่างไร ซึ่งจะช่วยให้จัดการได้ง่ายขึ้น

การแบ่งงานประเภทนี้ระหว่างผู้เชี่ยวชาญต้องมีสิ่งที่จะประสานงานเมื่อเจ้าหน้าที่แต่ละคนทำงาน คุณสามารถเลือกให้ Agent ตัดสินใจว่าจะโทรหา Agent ย่อย แต่ละรายเมื่อใด หรือจะใช้วิธีการเชิงกำหนดก็ได้ ใน Codelab นี้ คุณจะใช้วิธีการที่แน่นอนกับเวิร์กโฟลว์กราฟ

วางโครงสร้างโปรเจ็กต์ Agent แรก

  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 นี้มี 2 ส่วนที่สำคัญ ดังนี้
    • pitch_generator/agent.py คือเอเจนต์และไฟล์ที่คุณเขียนใหม่
    • pitch_generator/ คือชื่อแพ็กเกจที่ตั้งค่าโดย --agent-directory
    Agent จะอ่านโปรเจ็กต์และภูมิภาคจากสภาพแวดล้อมที่คุณส่งออกด้วย setenv.sh จึงไม่ต้องกำหนดค่าใดๆ ที่นี่
  2. ติดตั้งทรัพยากร Dependency
    cd pitch-generator
    agents-cli update -y && agents-cli install && uv sync --upgrade-package google-adk
    
    หมายเหตุ: โครงสร้างยังเชื่อมต่อโปรเจ็กต์เพื่อแสดง A2A ซึ่งเป็นโปรโตคอลที่ใช้ในภายหลังใน Codelab นี้ด้วย โปรดอย่าแตะต้องไฟล์เหล่านั้นในตอนนี้

ทำความเข้าใจกราฟ

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))

แฟนเอาต์ (Fan-Out) a แล้ว b และ c พร้อมกัน

((a, b), c)

fan-in. c รอทั้ง a และ b

สำหรับ Fan-in คุณใช้ JoinNode ซึ่งจะรอทุกงานก่อนหน้าและส่งต่อเอาต์พุตพร้อมกัน

ในงานนี้ เราจะสร้างเวิร์กโฟลว์ต่อไปนี้

กราฟเวิร์กโฟลว์เริ่มต้น

สร้างไปป์ไลน์

  1. เปิดไฟล์ Agent โดยวางข้อความนี้ในเทอร์มินัล
    cd ~/devrel-demos/codelabs/bwg-multi-agent-pitch-generator/pitch-generator
    cloudshell edit pitch_generator/agent.py
    
    คุณจะเห็น 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 มี UI การพัฒนาเว็บในตัวที่เรียกว่า playground สำหรับการตรวจสอบ ซึ่งคล้ายกับ ADK วิธีเริ่มใช้ Playground
    agents-cli playground --port 8080
    
    ระบบจะถามว่าคุณต้องการเปิดใช้การวัดและส่งข้อมูลทางไกลหรือไม่ พิมพ์ n แล้วกด Enter
  4. คลิกตัวอย่างเว็บ ไอคอนรูปดวงตาที่ด้านขวาบนของเทอร์มินัล แล้วเลือกแสดงตัวอย่างในพอร์ต 8080คลิกตัวอย่างเว็บ
  5. คุณจะเห็นอินเทอร์เฟซต่อไปนี้เพื่อแชทกับตัวแทน ลองส่งพรอมต์ "สเกตบอร์ดบินได้สำหรับแมว" คุณจะเห็นคำตอบดังที่แสดงด้านล่าง ซึ่งคุณสามารถตรวจสอบกราฟเวิร์กโฟลว์แบบเต็มได้ด้วยเวิร์กโฟลว์การสาธิตตรวจสอบวิธีสร้างคำตอบนั้น สนามเด็กเล่นจะแสดงแต่ละขั้นตอนที่กราฟดำเนินการตามลำดับ ได้แก่ creative_director, copywriter, assemble และ package ลำดับดังกล่าวคือกราฟที่คุณต่อสาย และวิธีนี้จะช่วยให้คุณยืนยันกราฟที่เพิ่งสร้างได้อย่างรวดเร็ว
  6. กลับไปที่เทอร์มินัลและหยุดกระบวนการ agents-cli playground ที่กำลังทำงานโดยกด Ctrl+C 2 ครั้ง

4. มอบทักษะให้ Agent

ในงานนี้ คุณจะสร้างเอเจนต์ตัวที่ 2 และกำหนดหลักเกณฑ์การใช้แบรนด์เป็นชุดให้เอเจนต์ในรูปแบบของทักษะ

ไปป์ไลน์จะเขียนคำ ภาพต้องมีสไตล์ของตัวเอง ได้แก่ ชุดสี วิธีการให้แสง องค์ประกอบที่คุณใช้และองค์ประกอบที่คุณไม่เคยใช้ ซึ่งเป็นหน้ากฎ และหน้ากฎต้องมีกลไกที่ดีกว่าในการจัดการ วิธีหนึ่งในการทำเช่นนี้คือการจัดแพ็กเกจเป็น "ทักษะของเอเจนต์"

ทักษะคือโฟลเดอร์ที่มีไฟล์ 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 ซึ่งมีส่วน Frontmatter ของ YAML ดังที่แสดงด้านล่าง

---
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 คือป้ายกำกับ ซึ่งเป็นสิ่งที่เอเจนต์เห็นตั้งแต่แรกเริ่ม จึงเป็นเหตุผลที่คำอธิบายต้องระบุว่าเมื่อใดที่ควรโหลดทักษะ เนื้อหาคือเนื้อหา และจะอยู่นอกบริบทจนกว่าเอเจนต์จะเรียกใช้ load_skill

การแยกนั้นคือจุดประสงค์ พรอมต์จะสั้นลง เอเจนต์จะดึงกฎมาใช้เฉพาะเมื่อจำเป็น และการเปลี่ยนสไตล์หมายถึงการแก้ไขไฟล์เดียวแทนที่จะต้องค้นหาในสตริงคำสั่ง

เราจะเริ่มสร้างเอเจนต์ตัวที่ 2 ซึ่งก็คือ visual-director โดยใช้ทักษะของเอเจนต์ตามที่แสดงในแผนภาพด้านล่าง

มอบทักษะให้กับ Agent รายที่ 2

สนับสนุนผู้กำกับภาพ

คุณจะสร้างเอเจนต์เฉพาะทางเพื่อจัดการคอมโพเนนต์ภาพ visual-director ซึ่งเป็นโปรเจ็กต์แยกต่างหากเนื่องจากต่อมาจะกลายเป็นบริการของตัวเองเพื่อแสดงให้เห็นว่า Agent หลายตัวสื่อสารกันอย่างไรโดยใช้โปรโตคอล A2A คุณจะมีโปรเจ็กต์ 2 รายการอยู่ข้างกัน ดังนี้

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. เปิดไฟล์ Agent โดยทำดังนี้
    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",
    )
    
    ตอนนี้ Agent visual-director ได้รับการกำหนดค่าอย่างถูกต้องด้วยทักษะของ Agent แล้ว ก่อนทดสอบ คุณจะต้องติดตั้งเครื่องมือให้กับเอเจนต์นี้เพื่อสร้างชิ้นงานรูปภาพให้เป็นหนึ่งในความสามารถหลัก

5. ติดตั้งเครื่องมือสร้างรูปภาพให้กับเอเจนต์

ในงานนี้ คุณจะมอบเครื่องมือให้ Visual Director ซึ่งจะแสดงผลทิศทางงานศิลป์เป็นรูปภาพและบันทึกเป็นอาร์ติแฟกต์ อาร์ติแฟกต์คือข้อมูลไบนารีหรือเนื้อหาไฟล์ที่มีการตั้งชื่อและกำหนดเวอร์ชัน ซึ่ง AI Agent สร้าง แก้ไข หรือใช้ในระหว่างงาน

เครื่องมือและอาร์ติแฟกต์ที่เครื่องมือเขียน

ติดตั้งใช้งานและเพิ่มเครื่องมือลงใน Agent

  1. เปิดไฟล์ Agent โดยทำดังนี้
    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 เพื่อติดตั้งเครื่องมือให้ Agent
       tools=[
          SkillToolset(skills=[BRAND_SKILL]),
          ## Supply the `generate_key_visual` tool below this comment
          generate_key_visual,
       ],
    
  4. agents-cli มี UI การพัฒนาเว็บในตัวที่เรียกว่า playground สำหรับการตรวจสอบ ซึ่งคล้ายกับ ADK วิธีเริ่มใช้ Playground
    agents-cli playground --port 8080
    
  5. คลิกตัวอย่างเว็บ ไอคอนรูปดวงตาที่ด้านขวาบนของเทอร์มินัล แล้วเลือกแสดงตัวอย่างในพอร์ต 8080คลิกตัวอย่างเว็บ
  6. คุณจะเห็นอินเทอร์เฟซต่อไปนี้เพื่อแชทกับตัวแทน ลองส่งพรอมต์ "แมวมี 9 ชีวิต Zero gravity"เวิร์กโฟลว์การสาธิต คุณสามารถตรวจสอบจากการโต้ตอบได้ว่าvisual_directorเอเจนต์ดำเนินการload_skill("brand-guidelines")เพื่ออ่านทักษะหลักเกณฑ์การใช้แบรนด์อย่างไร จากนั้นจึงดำเนินการgenerate_key_visualเพื่อเริ่มสร้างรูปภาพ ระบบจะจัดเก็บรูปภาพที่ได้เป็นอาร์ติแฟกต์ ซึ่งจะแสดงใน UI และตรวจสอบได้ง่าย
  7. กลับไปที่เทอร์มินัลและหยุดกระบวนการ agents-cli playground ที่กำลังทำงานโดยกด Ctrl+C 2 ครั้ง

6. สถาปัตยกรรมแบบหลาย Agent ที่มีโปรโตคอล A2A

ในงานนี้ คุณจะเปิดเผย Visual Director เป็นบริการที่เอเจนต์อื่นๆ สามารถเรียกใช้ได้ และกำหนดค่าให้ตอบกลับด้วยรูปภาพที่สร้างขึ้น

Agent2Agent (A2A) เป็นโปรโตคอลแบบเปิดสำหรับ Agent หนึ่งในการเรียก Agent อื่นผ่านเครือข่าย ส่วนสำคัญคือการ์ด Agent ซึ่งเป็นเอกสาร JSON ที่ Agent เผยแพร่เกี่ยวกับตัวเองผ่าน URL ที่รู้จักกันดี โดยระบุชื่อ คำอธิบาย และความสามารถ ผู้โทรจะดึงข้อมูลการ์ดและรู้วิธีพูดคุยกับตัวแทนที่อยู่เบื้องหลังการ์ด โดยไม่มีการแชร์ซอร์สโค้ด การอ้างอิง หรือภาษา

โปรโตคอล A2A

agents-cli ได้เชื่อมต่อไว้ให้แล้วโดยค่าเริ่มต้น โครงสร้างที่สร้างขึ้น 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 จะเริ่ม Agent และติดตั้งเส้นทาง 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 จะจัดการส่วนที่เหลือให้เองใน 3 ตา

โดยจะสร้างการ์ด Agent จาก Agent เองก่อน ชื่อ คำอธิบาย และเครื่องมือทุกอย่างจะกลายเป็นรายการที่ผู้โทรสามารถอ่านได้

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,
)

ประการที่สาม จะติดตั้ง 2 เส้นทาง ได้แก่ เส้นทางหนึ่งที่แสดงการ์ด และอีกเส้นทางหนึ่งที่รับสาย

เส้นทาง

วัตถุประสงค์

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

การ์ด ผู้โทรจะอ่านข้อความนี้เป็นอันดับแรกเพื่อดูว่าตัวแทนทำอะไรได้บ้าง

/a2a/visual_director

ปลายทาง JSON-RPC ตำแหน่งที่คำขอจริงไป

อนุญาตให้อาร์ติแฟกต์ข้ามขอบเขต

โดยค่าเริ่มต้น คำตอบ A2A จะมีข้อความ รูปภาพเป็นอาร์ติแฟกต์ และอาร์ติแฟกต์จะอยู่ในที่เก็บของตัวแทนการแสดงโฆษณา เว้นแต่คุณจะกำหนดค่าให้รวมไว้

  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 โดยจะสร้างขึ้นจากคำจำกัดความของ Agent ในโค้ดโดยตรง
    {
       "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. ฝัง Agent ระยะไกลของ A2A ในเวิร์กโฟลว์

ในงานนี้ คุณจะเพิ่มตัวแทนระยะไกลลงในกราฟเป็นกิ่งที่ 3

ระบบจะห่อหุ้ม Agent ระยะไกลด้วย RemoteA2aAgent และวางไว้ในกราฟเหมือนกับโหนดอื่นๆ ขอบจะคงรูปร่างเดิม และผู้ประสานงานจะไม่ทราบว่าโหนดใดเป็นโหนดในพื้นที่

ชี้ให้ผู้ประสานงานเห็นบริการและอัปเดตเวิร์กโฟลว์กราฟ

  1. เปิดไฟล์ Agent โดยทำดังนี้
    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 จะดึงรูปภาพจากที่นั่น

ทดสอบการผสานรวม

ตอนนี้คุณจะทดสอบว่า Visual Director (ทำงานในเบื้องหลัง) และ Pitch Generator สามารถสื่อสารผ่านโปรโตคอล A2A ได้

  1. ตรวจสอบว่า Visual Director ยังทำงานอยู่เบื้องหลัง
    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. คลิกตัวอย่างเว็บ ไอคอนรูปดวงตาที่ด้านขวาบนของเทอร์มินัล แล้วเลือกแสดงตัวอย่างในพอร์ต 8080คลิกตัวอย่างเว็บ
  4. คุณจะเห็นอินเทอร์เฟซสำหรับแชทกับตัวแทน ลองส่งพรอมต์ "สเกตบอร์ดบินได้สำหรับแมว"เวิร์กโฟลว์การสาธิตเวิร์กโฟลว์การสาธิต คุณจะเห็นการโต้ตอบทั้งหมดที่เวิร์กโฟลว์เริ่มต้นและตัวแทนแต่ละรายทำงาน หลังจากที่ creative_director ร่างแนวคิดแล้ว เวิร์กโฟลว์จะหยุดชั่วคราวที่ขั้นตอนการอนุมัติและขอให้คุณยืนยันว่า "โปรดอนุมัติแนวคิดแคมเปญ (ใช่/ไม่ใช่)" ป้อน "yes" ในช่องป้อนข้อมูลเพื่อดำเนินการต่อ เมื่อได้รับอนุมัติแล้ว กราฟจะขยายออก คุณจะเห็นเมื่อเอเจนต์ visual-director เริ่มโหลดทักษะ และในระหว่างการสร้างรูปภาพ เอเจนต์ copywriter จะสร้างคำบรรยายบนโซเชียลมีเดียเสร็จ ซึ่งแสดงให้เห็นว่าเอเจนต์ทั้ง 2 ทำงานควบคู่กันไปโดยไม่ต้องรอซึ่งกันและกัน หลังจากทั้ง 2 อย่างเสร็จสิ้น โหนด assemble จะรวมผลลัพธ์เข้ากับแนวคิดที่ได้รับอนุมัติซึ่งสร้างโดย creative_director เพื่อสรุปการนำส่งเนื้อหาการเสนอขายในฟังก์ชัน packageบริการทั้ง 2 รายการที่เชื่อมต่อผ่าน A2A

8. จัดเก็บภาพที่สำคัญไว้ใน Cloud Storage

ที่ผ่านมาภาพหลักทุกภาพอยู่ในความทรงจำ เมื่อกระบวนการหยุด รูปภาพก็จะหายไป ซึ่งเหมาะสําหรับการทดลอง แต่ไม่เหมาะสําหรับแคมเปญ ในงานนี้ คุณจะให้เวิร์กโฟลว์มี Bucket ที่จะเขียน และสร้างชุดข้อมูล 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:// ตัวแปรนี้จะอยู่ใน Shell ของคุณเช่นเดียวกับตัวแปรอื่นๆ และงานการทำให้ใช้งานได้จะส่งตัวแปรนี้ไปยัง 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 ในงานนี้ คุณจะติดตั้งใช้งาน Agent ผู้กำกับภาพเป็นบริการ Cloud Run ของตัวเอง และอนุญาตให้เครื่องมือสร้างพิตช์พูดคุยกับ Agent ผ่านเครือข่ายโดยใช้โปรโตคอล Agent2Agent (A2A)

Visual Director จะได้รับ https:// URL จริง ดังนั้น _cloud_run_client ที่คุณเขียนไว้ก่อนหน้านี้จะเริ่มลงนามในคำขอแทนที่จะส่งคืน None นอกจากนี้ Cloud Run ยังติดตั้งใช้งานแบบส่วนตัวโดยค่าเริ่มต้นด้วย

ทั้ง 2 เอเจนต์เป็นบริการ Cloud Run

ติดตั้งใช้งาน Visual Director

  1. หยุดบริการที่ทำงานอยู่เบื้องหลังในเครื่องและ Playground หากยังทำงานอยู่ โดยทำดังนี้
    fuser -k 8801/tcp 2>/dev/null || true
    fuser -k 8000/tcp 2>/dev/null || true
    
  2. ติดตั้งใช้งาน Agent ซึ่งจะสร้างและพุชคอนเทนเนอร์ ⏰ การดำเนินการนี้จะใช้เวลาสักครู่
    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"
    

เรียกใช้เครื่องมือสร้างการเสนอเพลงที่ใช้ Visual Director ซึ่งติดตั้งใช้งานใน Cloud Run

  1. อ่าน URL ของ Visual Director แล้วเรียกใช้ Pitch Generator ที่ใช้ 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 &
    

เรียกใช้เวิร์กโฟลว์ที่ติดตั้งใช้งาน

โดยเอเจนต์ทั้ง 2 รายรองรับโปรโตคอล A2A ที่เก็บมีไคลเอ็นต์ขนาดเล็กสำหรับการเรียกใช้เครื่องมือสร้างการเสนอเพลงผ่าน A2A

โดยต้องตั้งค่า PITCH_GENERATOR_URL เป็น URL ของเครื่องมือสร้างการเสนอเพลงที่ทำงานในเครื่อง ในขณะเดียวกัน Visual Director จะทำงานใน Cloud Run และ Pitch Generator จะสื่อสารกับ Visual Director ผ่าน 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การกำหนดเป้าหมายhttps://SERVICE_URL/a2a/pitch_generator A2A อย่างเป็นทางการ
    • generate_pitch_async จะส่ง SendMessageRequest เริ่มต้น เมื่อการตอบกลับมี TaskState.TASK_STATE_INPUT_REQUIRED ระบบจะตรวจหาส่วน adk_request_input แสดงแนวคิดฉบับร่าง รวบรวมการอนุมัติของคุณ และส่งคำขอประวัติพร้อมการตอบกลับของคุณกลับไปยังงาน
    • ลูปการแยกวิเคราะห์จะอ่านส่วน CONCEPT, COPY และ ART DIRECTION จากอาร์ติแฟกต์ของงานที่เสร็จสมบูรณ์แล้ว และบันทึกรูปภาพไบนารีไปยัง key_visual.png
    คุณควรเห็นส่วนข้อความทั้ง 3 ส่วนที่พิมพ์ออกมา และรูปภาพที่บันทึกไว้ใน key_visual.png
  3. อัปโหลดภาพหลักไปยัง Bucket ภายใต้คำนำหน้าที่คุณชี้ BigQuery ไปได้
    gcloud storage cp ./key_visual.png gs://<YOUR_PROJECT_ID>-bwg/key-visuals/cats.png
    
  4. สร้างการเสนอเพลงที่ 2 เพื่อให้มีเพลงเปรียบเทียบ แล้วอัปโหลดด้วย
    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

ตอนนี้คุณมีรูปภาพในที่เก็บข้อมูลและหลักเกณฑ์การใช้แบรนด์ที่ควรจะกำหนดรูปแบบของรูปภาพ ในงานนี้ คุณจะรวมทั้ง 2 อย่างเข้าด้วยกัน นั่นคืออ้างอิงรูปภาพจาก BigQuery แล้วขอให้โมเดล Gemini ให้คะแนนแต่ละรูปภาพตามหลักเกณฑ์ โดยไม่ต้องย้ายไบต์เดียวออกจาก Cloud Storage

ฟีเจอร์ 2 อย่างของ 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. คลิกสร้างคําค้นหาใหม่ (ไอคอน +) จากนั้นวางและเรียกใช้คําค้นหาต่อไปนี้ (แทนที่ และ ด้วยรหัสโปรเจ็กต์และภูมิภาคของคุณ) โดยจะสร้าง 1 แถวต่อภาพหลัก 1 ภาพ ซึ่งจะจับคู่แนวคิดของแคมเปญกับข้อมูลอ้างอิงถึงรูปภาพ
    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 และกรอกข้อมูลขนาด ประเภทเนื้อหา และรุ่นของออบเจ็กต์ การเรียกครั้งที่ 2 ยังเป็นวิธีที่เร็วที่สุดในการพิสูจน์ว่าการเชื่อมต่อใช้งานได้ด้วย หากสิทธิ์ไม่ถูกต้อง ฟิลด์ 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 ธรรมดาที่คุณปรับได้โดยไม่ต้องแตะต้องพรอมต์ การให้คะแนนจะดำเนินการต่อแถว ซึ่งหมายความว่าระบบจะใช้เกณฑ์การให้คะแนนเดียวกันกับชิ้นงานทุกชิ้นที่แคมเปญสร้างขึ้น ไม่ใช่แค่ 2 ชิ้นงานที่คุณสร้างที่นี่

11. ขั้นตอนถัดไป

สร้างการประเมิน

การประเมินแบบ Agent หรือ "Evals" คือการทดสอบอัตโนมัติที่ใช้เพื่อวัดความสามารถของเอเจนต์ AI แบบอัตโนมัติในการให้เหตุผล วางแผน ใช้เครื่องมือ และทําเวิร์กโฟลว์แบบหลายขั้นตอนให้เสร็จสมบูรณ์ ซึ่งคล้ายกับการทดสอบหน่วยสำหรับเอเจนต์ แต่จะพิจารณาถึงลักษณะที่ไม่แน่นอนของเอเจนต์

เอเจนต์ตัวหนึ่งของเรามี Human in the loop ซึ่งหมายความว่าเอเจนต์จะรอรับอินพุต คุณจะต้องอนุญาตให้ Antigravity ทำการแก้ไขโค้ดปัจจุบันเพื่อไม่ให้ลักษณะการทำงานนี้ขัดขวางการประเมินอัตโนมัติ

วิธีที่ดีที่สุดวิธีหนึ่งในการเริ่มต้นใช้งานคือการถาม Antigravity CLI

พิมพ์คำสั่งต่อไปนี้ใน Cloud Shell เพื่อเริ่ม CLI

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 สำหรับทรัพยากรที่ใช้ในโค้ดแล็บนี้ ให้ลบโปรเจ็กต์ (แนะนำ) หรือลบทรัพยากรแต่ละรายการที่สร้างขึ้นระหว่างโค้ดแล็บ

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 นี้ คุณได้สร้างเครื่องมือสร้างการเสนอแคมเปญแบบหลาย Agent ด้วย Agent Development Kit คุณเชื่อมต่อเอเจนต์ 3 ตัวเข้ากับเวิร์กโฟลว์กราฟแบบดีเทอร์มินิสติก จัดทำหลักเกณฑ์การใช้แบรนด์เป็นทักษะของเอเจนต์ที่โหลดตามต้องการ และมอบเครื่องมือสร้างรูปภาพให้กับเอเจนต์ซึ่งจะบันทึกเอาต์พุตเป็นอาร์ติแฟกต์ จากนั้นคุณจะแยก Visual Director ออกเป็นบริการ A2A ของตัวเอง ฝังกลับเข้าไปในกราฟเป็นโหนดระยะไกล แล้วติดตั้งใช้งานใน Cloud Run และเรียกเวิร์กโฟลว์ทั้งหมดผ่าน A2A จากตัวประสานที่ทำงานในเครื่อง สุดท้าย คุณอ้างอิงภาพที่สำคัญที่สร้างขึ้นจาก BigQuery ด้วย ObjectRef และใช้ AI.SCORE เพื่อเปรียบเทียบกับหลักเกณฑ์การใช้แบรนด์ของคุณเอง