VibeStudio — เวิร์กโฟลว์แบบเป็น Agent หลายรูปแบบพร้อม ADK, Haunted Edition

1. บทนำ

VibeStudio, Haunted Edition

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

โมเดล Generative สามารถทำงานบ้านแต่ละอย่างได้ด้วยตัวเอง Gemini จะค้นคว้า ฟัง ดู และเขียน โมเดลรูปภาพจะวาด และ Veo จะแสดงผล สิ่งที่ AI ทำเองไม่ได้คือการจัดลำดับงานในแต่ละคืนตามลำดับ หยุดรอการตัดสินใจของคุณ ปฏิเสธคำสั่งที่ไม่ถูกต้องก่อนที่จะเสียเงิน รอการเรนเดอร์เป็นนาทีๆ โดยไม่เปิดอะไรค้างไว้ และบันทึกข้อมูลไว้

Codelab นี้จะเปลี่ยนช่วงเย็นนั้นให้เป็น ADK Workflow ซึ่งเป็นกราฟที่วาดขึ้นจากโหนดและขอบที่ทำงานด้วยตัวเอง โดยจะถามคุณเฉพาะเมื่อจำเป็นต้องใช้การตัดสินใจของคุณ และสิ้นสุดการทำงานแต่ละครั้งด้วยคลิปและโปสเตอร์บน VibeTube

พรอมต์ไม่สามารถรับประกันลำดับ การหยุด หรือการตรวจสอบได้ แต่กราฟทำได้

สิ่งที่คุณจะได้เรียนรู้

  • มี Agent 1 ราย และโมเดลรูปภาพเป็นเครื่องมือ Agent คือโมเดล คำสั่ง และเครื่องมือ เครื่องมือที่ 3 จะเรียกใช้โมเดลรูปภาพ และการเรียกใช้ครั้งแรกจะสิ้นสุดลงด้วยโปสเตอร์ จากนั้นก็เป็นประโยคที่ขับเคลื่อนส่วนที่เหลือของห้องทดลอง นั่นคือ พรอมต์ไม่สามารถรับประกันลำดับ การหยุด หรือการตรวจสอบได้
  • โครงสร้างที่ประกอบขึ้นเป็นกราฟ Workflow คือรายการขอบ คุณสร้าง Fan-out และ Join (ตัวอ่านแบบขนานที่มาบรรจบกันที่ JoinNode) และเราเตอร์ที่ตั้งชื่อทางออกของตัวเอง โดยมี Agent โหมดงานอยู่เบื้องหลังทางออกที่ถูกปฏิเสธซึ่งจะล้างทิศทางในลูปของการเรียกใช้เครื่องมือและกลับเข้าสู่เส้นทางหลัก
  • ไปป์ไลน์การนำเข้าข้อมูลแบบหลายโมดัล สิ่งที่ผู้ชมส่งมา 4 ประเภท โดยมีผู้อ่าน 1 คนต่อประเภท Gemini จะถอดเสียงบันทึกเสียง บรรยายภาพ ดูคลิปและฟังซาวด์แทร็ก และผู้อ่านทุกคนจะเขียนรูปร่างเส้นเดียวกันลงในคลังข้อมูล RAG Engine เดียวกัน เครื่องมืออ่านที่ 3 ใน Fan-out จะดึงข้อมูลตามความหมายและส่งสื่อกลับมาพร้อมกับข้อความที่ตัดตอนมา
  • การระงับ 2 ประเภท กลไกเดียว การวิ่งหยุดลงเมื่อเจอคน (RequestInput: ไพ่ 4 ใบ คุณตอบ) และเมื่อเจอเครื่องมือที่ช้า (Veo อยู่หลัง LongRunningFunctionTool: ใบเสร็จ) ทั้ง 2 อย่างนี้เป็นสิ่งเดียวกัน นั่นคือการเรียกแบบเปิดในที่เก็บเซสชัน ซึ่งกลับมาทำงานอีกครั้งด้วยรหัสจากแบบฟอร์มหรือจากกระบวนการอื่น The two branches of the media desk share the poster by name, in state.
  • ความทรงจำในแต่ละคืน Lifecycle Callback 2 รายการใน Memory Bank: before_model_callback จะเรียกคืนรสนิยมของครีเอเตอร์ก่อนที่ผู้เสนอจะอ่าน ส่วน after_agent_callback จะจดจำตัวเลือกของคืนนี้หลังจากผู้เขียนสคริปต์
  • การเปิดตัว กราฟภายในแอปที่มี ADK Runner, เหตุการณ์ที่เซิร์ฟเวอร์ส่งไปยังหน้าเว็บ, Cloud Run ที่มีเซสชันแอฟฟินิตี้ และ POST แบบหลายส่วน 1 รายการที่อัปโหลดคลิปไปยัง VibeTube โดยใช้โปสเตอร์เป็นภาพปก

สิ่งที่คุณจะสร้าง

แอปที่ทำงานอยู่ในเบราว์เซอร์: VibeStudio Workbench ซึ่งเป็นหน้าคู่หูที่มีตัวแก้ไขโค้ดในหน้าเว็บ ตัวตรวจสอบที่อ่านไฟล์ในดิสก์และเซสชันที่การเรียกใช้ของคุณเขียน และ adk web ที่ฝังอยู่ โดยทั้ง 7 เซสชันคือ 7 หน้าของ Codelab นี้ ซึ่งมีหมายเลขและตัวอักษรส่วนเดียวกันเพื่อให้ทั้ง 2 ส่วนสอดคล้องกัน แต่ละหน้าจะเปิดขึ้นในแถบที่ระบุเซสชันในประโยคเดียวและสิ่งที่คุณทำ 3 อย่าง ได้แก่ แก้ไข เรียกใช้ และยืนยัน อ่านส่วนนี้ จากนั้นทำการแก้ไขในเวิร์กเบนช์ การแก้ไขทุกครั้งจะสิ้นสุดด้วยแผงการยืนยันที่อ่านสิ่งที่เกิดขึ้นจริง

สิ่งที่ทำงานในแต่ละส่วน: VibeStudio Workbench, แบ็กเอนด์ และบริการ Google Cloud

สิ่งที่จะทำงานมี 3 อย่าง ได้แก่ Workbench (หน้าในพอร์ต 4600, เอดิเตอร์, ตัวตรวจสอบ), แบ็กเอนด์ของคุณ (Workflow ใน agent/ และแอปแซนด์บ็อกซ์ 1 แอปต่อเซสชันใน stage0_prompt/ ถึง stage6_video/ ซึ่ง adk web แสดง) และ Google Cloud (Gemini, โมเดลรูปภาพ, RAG Engine, Veo, Memory Bank ซึ่งทั้งหมดเข้าถึงผ่าน Vertex AI ด้วยข้อมูลเข้าสู่ระบบของคุณเอง)

เซสชันทั้ง 7 รายการ

เซสชัน

สิ่งที่คุณสร้าง

การแก้ไขของคุณ

1 · เรื่องราว

ยังไม่มีอะไรเลย — คืนนี้และกราฟที่กำลังจะเกิดขึ้น

—

2 · การสร้างรูปภาพ

ตัวแทนคนหนึ่งวาดโปสเตอร์แรก

POSTER_TOOL

3 · เวิร์กโฟลว์

ค่ำคืนที่เหมือนกราฟ: การแยกออกและการรวมเข้า เราเตอร์และลูปของมัน

FANOUT, ROUTER

4 · การนำเข้าข้อมูลหลายรูปแบบ

สิ่งที่ผู้ชมส่งมา อ่านเป็นคลังข้อมูลเดียว ผู้อ่านคนที่ 3

INGEST_READERS, RAG_MEDIA

5 · การสร้างวิดีโอ

การระงับ 2 ประเภท ได้แก่ โปสเตอร์ที่แชร์ตามชื่อในรัฐ

GATE, VIDEO_TOOL, DELIVER_RESPONSE, STATE_SHARE

6 · ความทรงจำ

สิ่งที่ช่องจดจำระหว่างคืนต่างๆ

MEMORY_RECALL, MEMORY_REMEMBER

7 · ทำให้ใช้งานได้และเผยแพร่

VibeStudio บน Cloud Run, ค่ำคืนบน VibeTube

รหัสกิจกรรมของห้อง แล้วเผยแพร่

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

ตำแหน่งของฟีเจอร์นี้

กราฟ ธีม และห้องใน VibeTube มาจาก VibeStudio เวอร์ชัน Haunted ของ Lin โดยนี่คือการตัดต่อแบบหลายโมดัล ซึ่งเน้นไปที่สิ่งที่ผู้ชมส่งมาและการหยุดชะงัก 2 ประเภทในรูปแบบอินเทอร์แอกทีฟหน้าเดียวของ Agent Valley Labs สตูดิโอคือฉากหลัง กลไกทุกอย่างที่อยู่เบื้องหลังคือกลไกทั่วไปที่คุณจะใช้สำหรับไปป์ไลน์การสนับสนุน ขั้นตอนการตรวจสอบเนื้อหา หรือสิ่งอื่นๆ ที่ต้องหยุดชั่วคราวเพื่อรอให้บุคคลดำเนินการและรอให้งานที่ใช้เวลานานเสร็จสิ้น

2. 🧰 การตั้งค่า

  • สิ่งที่ต้องมี — บัญชีที่สามารถชำระเงินได้, โปรเจ็กต์, ห้องทดลองที่ติดตั้ง, โมเดลที่พิสูจน์แล้วว่าตอบคำถามได้ และเวิร์กเบนช์ที่ทำงานอยู่เบื้องหลังในพอร์ตเดียว
  • เหตุผล - กระบวนการเดียวในพอร์ตเดียว: หน้าเว็บ, เอดิเตอร์, ตัวตรวจสอบ, /api/* และ adk web แชร์ต้นทางเดียวกัน ดังนั้นจึงไม่มีสิ่งใดในห้องทดลองนี้ที่ต้องใช้เทอร์มินัลที่ 2 หรือพอร์ตที่ 2
  • วิธี — โคลนสคริปต์ 2 รายการ ส่วนที่ 2 จะทำการเรียกโมเดลจริง เริ่มเวิร์กเบนช์ และเริ่มสร้างคลังข้อมูล RAG และคลังความทรงจำขณะที่คุณอ่าน เพื่อให้เซสชันที่ 4 และ 6 พร้อมใช้งาน

ทุกอย่างในห้องทดลองนี้ทำงานใน Cloud Shell โดยมีการติดตั้ง gcloud, Python, Node, ffmpeg และ git ไว้แล้ว และสามารถเข้าถึง Gemini, โมเดลรูปภาพ, Veo, RAG Engine และ Memory Bank ได้ทั้งหมดผ่าน Vertex AI โดยใช้ข้อมูลเข้าสู่ระบบของคุณเอง ไม่มีคีย์ API ในห้องปฏิบัติการนี้ และไม่มีอะไรให้ติดตั้งในแล็ปท็อปของคุณ แล็ปท็อปที่มี Google Cloud SDK, Python 3.12 และ Node 20 ขึ้นไปจะทำงานในลักษณะเดียวกัน

มีสิ่งหนึ่งที่ห้องทดลองนี้ให้คุณไม่ได้ นั่นคือโปรเจ็กต์ที่อยู่ในระบบคลาวด์ของ Google ที่มีการเรียกเก็บเงิน ขั้นตอนที่ 1 จะช่วยให้คุณมีบัญชีที่สามารถชำระเงินได้ ส่วนขั้นตอนที่ 4 จะเปลี่ยนบัญชีนั้นเป็นโปรเจ็กต์ ทำตามลำดับดังกล่าว เนื่องจากทุกขั้นตอนหลังจากขั้นตอนที่ 4 จะอ่านโปรเจ็กต์ที่ขั้นตอนที่ 4 กำหนดไว้

1 · สร้างบัญชีที่มีการเรียกเก็บเงิน (~1 นาที)

ลงชื่อเข้าใช้ด้วยบัญชี Google ที่เปิดใช้การเรียกเก็บเงิน บัญชีดังกล่าวใช้ได้ทั้งหมด หากไม่มี ให้เริ่มทดลองใช้ฟรี ซึ่งจะให้บัญชีสำหรับการเรียกเก็บเงินแก่คุณ ซึ่งเป็นสิ่งเดียวที่จำเป็นในขั้นตอนที่ 4 หากคุณอยู่ในห้องทดลองที่มีผู้สอน ให้แลกใช้เครดิตสำหรับเซสชันนี้ก่อน ซึ่งจะเป็นการสร้างบัญชีสำหรับการเรียกเก็บเงินให้คุณ

2 · เปิด Cloud Shell (~2 นาที)

👉🖥 คลิกเปิดใช้งาน Cloud Shell ที่ด้านบนของคอนโซล Google Cloud ซึ่งเป็นไอคอนรูปเทอร์มินัลในส่วนหัว

เปิดใช้งาน Cloud Shell จากส่วนหัวของคอนโซล Google Cloud

👉🖥 คลิกเปิดเครื่องมือแก้ไข (ไอคอนโฟลเดอร์เปิดที่มีดินสอ) ซึ่งจะเปิดเครื่องมือแก้ไข Cloud Shell พร้อมเครื่องมือสำรวจไฟล์ทางด้านซ้าย ซึ่งเป็นที่ที่คุณจะไปเมื่อต้องการอ่านทั้งไฟล์

เปิดเครื่องมือแก้ไข Cloud Shell

👉🖥 หากระบบแจ้งให้คุณให้สิทธิ์ในขั้นตอนใดก็ตามในวันนี้ ให้คลิกให้สิทธิ์เพื่อดำเนินการต่อ

👉🖥 เปิดเทอร์มินัลภายในเครื่องมือแก้ไข ซึ่งก็คือแท็บ 1 และทุกขั้นตอน 👉💻 จะทำงานที่นั่น แท็บที่ 2 คือ Workbench ซึ่งคุณจะเปิดในขั้นตอนที่ 7 หากเทอร์มินัลไม่ได้อยู่ที่ด้านล่างของหน้าจอ ให้เปิดด้วย View → Terminal

เปิดเทอร์มินัลใหม่ในเครื่องมือแก้ไข Cloud Shell

👉💻 ตรวจสอบว่าคุณลงชื่อเข้าใช้บัญชีใดอยู่ ซึ่งควรเป็นบัญชีจากขั้นตอนที่ 1

gcloud auth list

3 · รับห้องทดลอง (~1 นาที)

👉💻 โคลนที่เก็บ:

cd ~
git clone https://github.com/cuppibla/vibe-studio-multimodal
cd vibe-studio-multimodal

4 · สร้างโปรเจ็กต์ (~2 นาที)

👉💻 สคริปต์หนึ่งจะค้นหาบัญชีสำหรับการเรียกเก็บเงิน สร้างโปรเจ็กต์ในบัญชีนั้น ลิงก์การเรียกเก็บเงิน และชี้ Cloud Shell ไปที่โปรเจ็กต์นั้น

cd ~/vibe-studio-multimodal
./setup_project.sh

คุณจะเห็น ✓ สำหรับบัญชีสำหรับการเรียกเก็บเงินที่เลือก ✓ สำหรับโปรเจ็กต์ที่สร้าง และส่วนท้ายดังนี้

Project ready: vibe-studio-4271
  Next:  ./setup_codelab.sh

vibe-studio-4271 คือรหัสโปรเจ็กต์ของคุณ ซึ่งจะมีตัวเลขที่แตกต่างกัน โดยมี 4 สิ่งที่เกิดขึ้นในนั้น ได้แก่

billing

จะใช้บัญชี [YYYY-MM-DD] GDP Credit: ใหม่ล่าสุดหากคุณมี และจะใช้บัญชีสำหรับการเรียกเก็บเงินอื่นๆ ที่เปิดอยู่หากไม่มี

project

สร้าง vibe-studio-XXXX และลิงก์บัญชีสำหรับการเรียกเก็บเงินนั้นกับโปรเจ็กต์

เลือก

เขียนรหัสลงใน ~/project_id.txt และเรียกใช้ gcloud config set project ดังนั้นหลังจากนี้จะไม่มีการขอให้คุณพิมพ์รหัสอีก

waits

จะทำการสำรวจจนกว่าโปรเจ็กต์จะได้รับอนุญาตให้แสดงจริง โดยใช้เวลาสูงสุด 2 นาที

👉🖥 ตอนนี้โปรเจ็กต์เดียวกันจะอยู่ในคอนโซล โดยรหัสจะอยู่ในการ์ดข้อมูลโปรเจ็กต์บนแดชบอร์ด

รหัสโปรเจ็กต์ในแดชบอร์ดของคอนโซล

5 · ติดตั้งและตั้งค่าห้องทดลอง (~4 นาที)

👉💻

cd ~/vibe-studio-multimodal
./setup_codelab.sh

สคริปต์เดียวที่เรียกใช้ซ้ำได้อย่างปลอดภัย การเรียกใช้ครั้งที่ 2 จะเก็บคำตอบทั้งหมดที่คุณให้ไว้ในครั้งแรก โดยจะถามคำถาม 2 ข้อในช่วงเริ่มต้น และ Enter จะใช้ค่าเริ่มต้นสำหรับทั้ง 2 ข้อ ดังนี้

รหัสเหตุการณ์

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

ชื่อที่แสดง

ชื่อที่ห้องแชทให้เครดิตคุณ หรือกด Enter เพื่อใช้ชื่อที่สร้างจากบัญชีของคุณ

ทั้ง 2 รายการจะอยู่ใน .env และสามารถเปลี่ยนแปลงได้ในภายหลังใน .env หรือในหน้าเผยแพร่ จากนั้นระบบจะดำเนินการส่วนที่เหลือเองตามลำดับต่อไปนี้

uv env

ติดตั้ง uv หากไม่มี จากนั้น uv venv และ uv sync จะติดตั้งสิ่งที่ uv.lock ปักหมุดไว้ — google-adk, google-genai, vertexai, fastapi, uvicorn — ลงใน .venv

apis

เปิดใช้ API 6 รายการในโปรเจ็กต์ที่เลือกในขั้นตอนที่ 4 และยืนยันว่า API แต่ละรายการตอบกลับก่อนดำเนินการต่อ ซึ่งได้แก่ aiplatform (Gemini, โมเดลรูปภาพ, Veo, Memory Bank, RAG Engine), vectorsearch (พื้นที่เก็บข้อมูลเบื้องหลังคลังข้อมูล RAG), run, cloudbuild, artifactregistry (เซสชัน 7), cloudtrace (การติดตามของแอป)

account

ตรวจสอบว่าบัญชีบริการเริ่มต้นของ Compute ของโปรเจ็กต์มีอยู่และมีบทบาทที่บิลด์และบริการของเซสชัน 7 ต้องการ

.env

เขียน .env โดยชี้ทุกการเรียกไปยัง Vertex AI ในโปรเจ็กต์ของคุณ ซึ่งก็คือ Gemini ในปลายทาง global, Veo, Memory Bank และ RAG Engine ใน us-central1 และไม่มีคีย์ API ที่ใดเลย โดยจะเก็บ .env ก่อนหน้าไว้เป็น .env.bak และทุกอย่างที่คุณเพิ่มด้วยตนเองจะยังคงอยู่

veo · room

ยืนยันว่า veo-3.1-fast-generate-001 แสดงใน us-central1 (การอ่านข้อมูลเมตา ไม่มีการแสดงผล) และถาม VibeTube ว่าห้องของคุณเปิดอยู่หรือไม่

model

ทำการเรียก Gemini จริง 1 ครั้ง เพื่อให้โปรเจ็กต์ที่ไม่ดีล้มเหลวที่นี่ในเวลาตั้งค่า ไม่ใช่ภายในเซสชัน

starter

วางไฟล์ 9 ไฟล์ที่คุณจะแก้ไขไว้ในตำแหน่งจาก starter/ โดยคงบรรทัด TODO ไว้ เฉพาะบรรทัดที่ขาดหายไปเท่านั้น ระบบจะไม่เขียนทับการแก้ไขของคุณ

page

ติดตั้งและสร้าง web/ ซึ่งเป็นหน้าเวิร์กเบนช์ จากนั้นเริ่มเซิร์ฟเวอร์ในเบื้องหลังบนพอร์ต 4600

cloud

เริ่มสร้างคลังข้อมูล RAG Engine และ Memory Bank ในเบื้องหลัง เพื่อให้เซสชันที่ 4 และ 6 พร้อมใช้งาน

preflight

เรียกใช้ scripts/preflight.py และพิมพ์ลิงก์เพื่อเปิด

6 · อ่านข้อมูลก่อนเริ่ม (~1 นาที)

สิ่งสุดท้ายที่ขั้นตอนที่ 5 พิมพ์ออกมาคือการตรวจสอบก่อนเริ่มงาน โดยทุกบรรทัดเป็นการตรวจสอบจริง ไม่ใช่การคาดเดา

  ✓ python 3.12
  ✓ auth path A: Vertex via ADC (STUDIO_VERTEX=1)
  ✓ Google Cloud ADC (project vibe-studio-4271)
  ✓ Memory Bank SDK
  ✓ stage0_prompt loads
  ✓ stage1_fanout loads (0 edges)
  ✓ stage3_router loads (... edges)
  ✓ stage4_memory loads (... edges)
  ✓ stage5_rag loads (... edges)
  ✓ stage6_video loads (... edges)
  ✓ node and npm (build the learning center's page)
  ✓ learning center page built (web/dist)
  ✓ aiplatform.googleapis.com enabled (Gemini, Veo, Memory Bank, RAG Engine)
  ...
  - Memory Bank: still being created in the background
  - RAG corpus: still being created in the background
  ✓ session 7 runtime account 123456789-compute@developer.gserviceaccount.com has its roles
  ✓ learning center running on port 4600

PREFLIGHT GREEN

Open session 1 here:  https://4600-cs-...cloudshell.dev/step/story

stage1_fanout loads (0 edges) พูดถูก กราฟนั้นว่างเปล่า และเซสชันที่ 3 คือที่ที่คุณจะเติมข้อมูลลงไป บรรทัด - ทั้ง 2 บรรทัดก็ถูกต้องเช่นกัน โดยคลังข้อมูลจะใช้เวลา 1-2 นาทีในการแสดงผล และธนาคารจะใช้เวลาประมาณ 20 วินาที และคุณไม่จำเป็นต้องใช้ทั้ง 2 อย่างก่อนเซสชันที่ 4

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

cd ~/vibe-studio-multimodal
uv run python scripts/preflight.py

7 · เปิด Workbench (~1 นาที)

Workbench ทำงานอยู่แล้ว ขั้นตอนที่ 5 เริ่มต้นกระบวนการในเบื้องหลัง โดยมีกระบวนการ uvicorn 1 กระบวนการในพอร์ต 4600 ที่แสดงหน้าเว็บ ทุกเส้นทาง /api/* และ adk web ที่ต่อเชื่อมที่ /workbench ไม่มีอะไรให้เริ่ม และไม่มีอะไรในห้องทดลองนี้ที่ขอให้คุณรีสตาร์ท การแก้ไขจะปรากฏในการเรียกใช้ครั้งถัดไป

👉🖥 คลิกที่ลิงก์ที่ Preflight พิมพ์ไว้ หรือใน Cloud Shell: Web Preview → Change port → 4600 เรียกแท็บนั้นว่าแท็บ 2 โดยจะเปิดขึ้นในเซสชันที่ 1 · เรื่องราว

คำสั่ง 3 รายการที่คุณอาจต้องการใช้ในภายหลัง ซึ่งทั้งหมดมาจาก ~/vibe-studio-multimodal มีดังนี้

scripts/stop.sh

หยุด Workbench

scripts/start.sh

จะเริ่มทำงานในเบื้องหน้า โดยกด Ctrl+C เพื่อหยุดการทำงานและทุกกระบวนการที่เริ่มทำงาน

scripts/restart.sh --pull

git pull จากนั้นรีสตาร์ทในเบื้องหลัง ซิงค์ venv อีกครั้ง และสร้างหน้าเว็บใหม่เฉพาะเมื่อมีการเปลี่ยนแปลง

เลย์เอาต์ที่เก็บ

vibe-studio-multimodal/
├── agent/                  # the workflow and the platform clients
│   ├── graph.py            # the graph: three readers, the proposer, the gate, the router, the script, the desk
│   ├── desk.py             # render_desk and render_submit, the LongRunningFunctionTool
│   ├── deliver.py          # the delivery console: find the pending call, wait for Veo, answer by id
│   ├── schemas.py          # Direction, Directions, CleanedDirection, Script, each with a poster idea
│   ├── cleanup_tools.py    # find_policy_hits, suggest_replacement: the task agent's tools
│   ├── policy_words.txt    # the policy, as data; policy_replacements.txt, the approved stand-ins
│   ├── backlog.txt         # the creator's Halloween ideas; comments.md, the audience's comments
│   ├── audience/           # what else the audience sent: voice/*.mp3, photo/*.jpg, clip/*.mp4
│   ├── media/past/         # six past posters and captions.md, what Gemini sees in each
│   └── platform/           # config, audience (the readers), rag (RAG Engine), images, videogen (Veo), memory (Memory Bank)
├── stage0_prompt/ ... stage6_video/   # one sandbox app per session, runnable in adk web
├── server/ & web/          # the VibeStudio Workbench (FastAPI + React, port 4600)
├── vibestudio/             # the app deployed in session 7, with its own copy of agent/
├── checks/ & scripts/      # the hole registry, the verifiers, preflight, start/stop/restart
└── starter/                # the nine files you edit, as students receive them

3. 1 · เรื่องราว — ค่ำคืน และกราฟที่เกิดขึ้น

👉🖥 แท็บที่ 2 จะเปิดขึ้นในเซสชันที่ 1 · เรื่องราว ลองนึกภาพตามดูสิ งาน 6 อย่างที่เรียงรายอยู่รอบตัวคนๆ เดียว ได้แก่ ค้นคว้าเทรนด์ ค้นหาข้อมูลเก่าๆ เลือกทิศทาง ตรวจสอบนโยบาย เขียนสคริปต์ แสดงคลิป และวงจรนี้ก็วนซ้ำทุกคืน Gemini สามารถค้นคว้าข้อมูล เขียนสคริปต์ และสร้างงานที่ค้างอยู่ได้ด้วยตัวเอง Veo สามารถแสดงผลได้ด้วยตัวเอง ส่วนการเลือกและการตรวจสอบเป็นหน้าที่ของมนุษย์

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

คำศัพท์ ดังนั้น 6 หน้าถัดไปจะอ่านโดยไม่หยุด

คำ

ความหมายในที่นี้

agent

ADK Agent: โมเดล คำสั่ง เครื่องมือ เซสชัน 2

node

1 ขั้นตอนของกราฟ ซึ่งอาจเป็นฟังก์ชัน Python, Agent, การรวม หรือเราเตอร์

edge

ทูเพิลของโหนดที่ทำงานตามลำดับ โดยกราฟคือรายการของโหนดเหล่านั้น เซสชัน 3

session

บันทึกการเรียกใช้ 1 รายการใน runs/sessions.db: เหตุการณ์ สถานะ และการโทรที่ยังเปิดอยู่ เซสชัน 5

state

พจนานุกรมที่โหนดแชร์ตามชื่อคีย์ โหนดหนึ่งเขียน Event(state=...) ส่วนอีกโหนดประกาศพารามิเตอร์ที่มีชื่อเดียวกัน เซสชัน 5

ความทรงจำ

สิ่งที่คงอยู่หลังจบเซสชันคือ Memory Bank ผ่าน Callback เซสชัน 6

4. 2 · การสร้างรูปภาพ — Agent 1 ราย และโมเดลรูปภาพเป็นเครื่องมือ

เซสชันที่ 2 · การสร้างรูปภาพ

👉🖥 ใน Workbench ให้เปิดเซสชันที่ 2 · การสร้างรูปภาพ ก่อนที่จะมีกราฟ สิ่งที่ง่ายที่สุดที่อาจใช้ได้คือ Agent 1 รายการที่มีคำสั่งเป็นข้อความบรรยายตลอดทั้งคืน, ฟังก์ชัน Python 3 รายการเป็นมือ และการรันที่จบลงด้วยรูปภาพ

Agent คือโมเดล คำสั่ง และเครื่องมือ

stage0_prompt/agent.py กำหนด root_agent ด้วยช่อง 3 ช่องของตัวแทน ADK model เป็นโมเดล Gemini จาก agent/platform/config.py instruction คือพรอมต์ของระบบ: งาน 6 อย่างในคราวเดียว โดยงานสุดท้ายคือรูปภาพ tools มีฟังก์ชัน 3 อย่าง

root_agent = Agent(
    name="solo_channel", model=config.MODEL,
    tools=[check_trends, read_backlog, make_poster],
    instruction=(
        "You run the creator's cozy-spooky short-video channel, alone, every "
        "night until Halloween.\n"
        "When the creator gives you an idea (or nothing), do ALL of this:\n"
        "check what is trending. look at your backlog of ideas. propose a "
        "direction and agree on it with the creator. refuse blacklisted "
        "subjects (competitor, hateful, gore). then describe the video you "
        "would make: a title (<=60 chars) and 3 shots, one visual sentence "
        "each. then draw its poster: call make_poster once with one line that "
        "names the single image that sells the video, and tell the creator "
        "where the poster is.\n"
        "Ask the creator to confirm the direction before describing the "
        "video."))

โมเดลเป็นระบบปิด โดยจะให้เหตุผลตามน้ำหนักและโทเค็นในบริบทของโมเดล เครื่องมือคือฟังก์ชัน Python ที่โมเดลอาจขอ ADK จะอ่านซิกเนเจอร์และ Docstring ของแต่ละฟังก์ชันและประกาศให้กับโมเดล เมื่อโมเดลต้องการฟังก์ชันใดฟังก์ชันหนึ่ง ก็จะส่งเหตุการณ์ function_call ออกมา รันไทม์จะเรียกใช้ Python และเขียนเหตุการณ์ function_response กลับเข้าไปในบริบท และโมเดลจะดำเนินการต่อด้วยผลลัพธ์ที่อยู่ตรงหน้า

3 ฟังก์ชัน และ 1 ในนั้นคือการวาด

2 ใน 3 คนอ่านงานวิจัย ส่วนพรอมต์ที่ 3 จะขอให้โมเดลรูปภาพสร้างโปสเตอร์

def check_trends() -> dict:
    """Ten formats trending on the platform right now, with a heat score each."""
    from agent.trends import sample_trends
    return {"trends": sample_trends()}


def read_backlog() -> dict:
    """The creator's backlog: ideas they noted down to make someday."""
    from agent.graph import backlog_notes
    return {"backlog": backlog_notes()}


def make_poster(subject: str) -> dict:
    """Draw tonight's poster: one image of `subject` in the channel's cozy
    low-poly style. Returns where the PNG landed (path, url) in a few seconds."""
    ...

make_poster เรียกใช้ agent/platform/images.py ซึ่งเป็นโมเดลรูปภาพที่รวมเป็นฟังก์ชันธรรมดาในลักษณะเดียวกับที่ videogen.py รวม Veo: คำนำหน้าสไตล์บ้าน (รูปลักษณ์ฮาโลวีนแบบโลว์โพลีที่อบอุ่นของช่อง สีส้มฟักทองและสีครีมแสงเทียนตัดกับสีม่วงพลัมยามค่ำคืน) วัตถุ เฟรมแนวตั้ง 9:16, PNG 1 รายการที่มีขนาดต่ำกว่า runs/media/posters ซึ่งแสดงที่ /static/posters เมื่อใช้ STUDIO_REAL_IMAGES=0 ระบบจะคัดลอกโปสเตอร์ตัวแทนและแจ้งผลลัพธ์ prebaked เพื่อให้ห้องทดลองทำงานได้ทุกที่

MODEL = os.environ.get("STUDIO_IMAGE_MODEL", "gemini-2.5-flash-image")
ASPECT = "9:16"

def generate(subject: str, visual_style: str = "", name: str | None = None) -> dict:
    """One poster for `subject`. Returns where it landed: status, path, url,
    prompt, and prebaked=True when the stand-in was used instead of the model."""

การเรียกเดียวกันนี้เป็นโหนดในกราฟในเซสชันที่ 5

ลงมือปฏิบัติ: การแก้ไขเดียว

ตามที่จัดส่ง make_poster จะแสดงผลเป็น Dict ที่ระบุว่าข้ามโปสเตอร์ไปแล้ว 👉🖥 ในเครื่องมือแก้ไข Workbench ให้เรียกใช้โมเดลรูปภาพแทนบรรทัด TODO ดังนี้

    from agent.platform import images
    return images.generate(subject, name="solo")

บันทึก แถวการยืนยันจะอ่านไฟล์ ซึ่งก็คือเครื่องมือ 3 รายการใน Agent และการเรียกใช้โมเดลรูปภาพที่พร้อมใช้งาน 👉🖥 คลิกตรวจสอบการโหลดเวิร์กโฟลว์ แล้วคลิกเปิด adk web และส่งข้อความต่อไปนี้

tonight's idea: a tiny ghost learning to haunt a sock drawer

สิ่งที่จะเกิดขึ้นและเหตุผล

  • เหตุการณ์เครื่องมือ 2 รายการก่อนการตอบกลับ ตามด้วยคำถาม check_trends และ read_backlog ทำงานเนื่องจากคำสั่งบอกให้ตรวจสอบและดู และโมเดลไม่มีทั้ง 2 อย่างในน้ำหนัก การตอบกลับจะเสนอแนวทางและขอให้คุณยืนยัน เนื่องจากคำสั่งขอให้มีการตกลงก่อน
  • ข้อความที่ 2 จะได้รับทุกอย่าง ส่ง skip the questions, describe the video and draw the poster โมเดลจะอธิบายวิดีโอ เรียกใช้ make_poster 1 ครั้งพร้อมบรรทัดเรื่องที่เขียนเอง และบอกคุณว่า PNG อยู่ที่ใด แผงยืนยันจะแสดงโปสเตอร์ข้างแถว ซึ่งเป็นฉากฮาโลวีนในรูปลักษณ์ของช่องที่วาดในเวลาประมาณ 8 วินาที
  • เหตุใดจึงไม่ถือว่านี่เป็นช่องทาง การค้นคว้าข้อมูลจะถูกพับเป็นร้อยแก้วที่ไม่มีอะไรอ้างอิงได้ นโยบายได้รับการรับรองโดยโมเดลที่เขียนคำสั่ง และโปสเตอร์ได้รับการวาดก่อนที่จะมีการตรวจสอบใดๆ มีการพูดถึงข้อตกลงในข้อความเดียว ข้อความแจ้งไม่ใช่เกต ส่วนที่เหลือของห้องทดลองจะแยกงานทั้ง 6 งานนี้ออกเป็นกราฟที่วาดขึ้น

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

5. 3 · เวิร์กโฟลว์ — การกระจายและการรวม การกำหนดเส้นทางและการวนซ้ำ

เซสชัน 3 · เวิร์กโฟลว์

👉🖥 ใน Workbench ให้เปิดเซสชัน 3 · เวิร์กโฟลว์ ส่วน 3a และ 3b แต่ละส่วนสร้างขึ้นในแอปแซนด์บ็อกซ์เพื่อให้คุณเรียกใช้ใน adk web ได้ด้วยตัวเอง โดย Fan-out อยู่ใน stage1_fanout ส่วนเราเตอร์และลูปอยู่ใน stage3_router

Fan-out and join (3a)

ADK Workflow คือรายการขอบ แต่ละรายการคือเชน ซึ่งเป็นทูเพิลของโหนดที่ทำงานตามลำดับ เชน 2 รายการที่ออกจาก START จะทำงานพร้อมกัน เชนที่สิ้นสุดใน JoinNode จะรายงานไปยังเชนดังกล่าว และการรวมจะรอเชนทั้งหมดที่สิ้นสุดที่นั่นก่อนที่จะดำเนินการต่อ โดยส่งต่อ Dict 1 รายการที่คีย์ตามชื่อโหนด เป้าหมายที่เขียนเป็น dict จะแมปชื่อเส้นทางของเราเตอร์กับโหนด หน้า 3a ของโต๊ะทำงานจะวาดรูปทรงและมีแบบฝึกหัดเขียนขอบ

ประเภทโหนด

เขียนเป็น

การทำงาน

โหนดฟังก์ชัน

def scan_trends(node_input): ... return Event(output=...)

Python ธรรมดา และอาจเขียนสถานะที่แชร์กับ Event(state=...) ด้วย

โหนดการรวม

JoinNode(name="join_research")

รอจนกว่าเชนขาเข้าทุกรายการจะรายงาน จากนั้นส่งเอาต์พุตทั้งหมดเป็น Dict รายการเดียว

โหนด Agent

Agent(...)

การเรียกใช้โมเดล 1 ครั้งในโหมด single_turn, คำตอบที่มีโครงสร้าง, ไม่มีการสนทนา

เราเตอร์

return Event(output=..., route="OK")

โหนดฟังก์ชันที่การแสดงผลตั้งชื่อ Edge ขาออก

โหนดอินพุตจากมนุษย์

yield RequestInput(...)

ระงับกราฟจนกว่าจะมีผู้ตอบแบบฟอร์ม เซสชัน 5

การกระจายการวิจัย: ผู้อ่าน 2 คนจาก START เข้าสู่การรวม

กราฟแรกคือการวิจัยของเซสชันที่ 2 เป็นโหนด scan_trends และ read_backlog ใน agent/graph.py เป็นผู้อ่าน 2 คน โดย read_backlog จะเขียนไอเดียของคืนนี้ลงในสถานะที่แชร์ด้วย stage1_fanout/agent.py กำหนดการรวมและสร้าง Workflow จากรายการชื่อ EDGES ซึ่งว่างเปล่า (0 edges ของการตรวจสอบก่อนการดำเนินการ) 👉🖥 เติมด้วยเชน 2 รายการต่อไปนี้

EDGES = [(START, scan_trends, join_research),
         (START, read_backlog, join_research)]

👉🖥 เรียกใช้ใน adk web โหนด 2 โหนดสว่างขึ้นพร้อมกัน จากนั้นก็รวมกัน เปิดเหตุการณ์ของ join_research และอ่านพจนานุกรม 1 รายการที่มีคีย์ scan_trends และคีย์ read_backlog ไม่มีการเรียกใช้โมเดล ไม่มีอะไรจัดรูปแบบพจนานุกรมของโหนดรวมใหม่ โหนดที่อยู่ถัดจากโหนดรวมใน 3b คือ Agent และโหนด Agent จะได้รับ node_input เป็นข้อความ ดังนั้นพจนานุกรมจึงมาถึงในรูปแบบ JSON ที่โมเดลอ่านโดยตรง

เราเตอร์และลูป (3b)

Agent ภายใน Workflow จะทำงานในโหมด single_turn โดยจะรับเอาต์พุตของโหนดก่อนหน้าเป็นข้อความ ทำการเรียกโมเดล 1 ครั้ง และตอบกลับ 1 ครั้ง เมื่อใช้ output_schema คำตอบจะได้รับการตรวจสอบเทียบกับโมเดล Pydantic ก่อนที่โหนดถัดไปจะเห็น ดังนั้นกราฟจึงส่งผ่านออบเจ็กต์ที่พิมพ์ ไม่ใช่ข้อความธรรมดา propose_directions อ่านพจนานุกรมการวิจัยของ Join และเสนอผู้สมัคร 4 คน ซึ่งทุกคนเป็นฉากฮาโลวีน ผู้สมัคร 4 เป็นเหยื่อล่อ: ชื่อของผู้สมัครมีคำที่ช่องปฏิเสธ ดังนั้นเราเตอร์จึงมีบางอย่างให้จับ ผู้สมัครทุกคนจะมี poster_idea ซึ่งเป็นข้อความบรรทัดเดียวที่ตั้งชื่อรูปภาพเดี่ยวที่ขายวิดีโอ โดยผู้เขียนสคริปต์จะเก็บไว้และโต๊ะสื่อจะวาดภาพในเซสชันที่ 5

เราเตอร์คือโหนดฟังก์ชันที่ผลลัพธ์จะระบุชื่อ Edge ที่การเรียกใช้เลือกใช้ ไม่มีการเรียกใช้โมเดล: policy_check จะอ่าน agent/policy_words.txt ในเวลาที่ตัดสินใจ จับคู่ทั้งคำกับชื่อและมุมมองของคำสั่ง และกำหนดเส้นทางก่อนที่จะเขียนสคริปต์หรือใช้จ่ายเงิน นโยบายคือข้อมูล: แก้ไขไฟล์ แล้วการเรียกใช้ครั้งถัดไปจะอ่านไฟล์นั้น

def policy_check(node_input):
    """The policy gate: a router. Refused words in the direction send it to
    quarantine; a clean one goes to the scripter. Code, not a model."""
    chosen = _as_dict(node_input)
    bad = policy_hits(f"{chosen.get('title', '')} {chosen.get('angle', '')}")
    return Event(output=node_input, route="BLOCK" if bad else "OK")

ประตูทางเข้านโยบาย: เราเตอร์ที่มีทางออก 2 ทางซึ่งมีป้ายกำกับ

stage3_router/agent.py คือ Fan-out บวกกับ propose_directions และ choose_direction ซึ่งเป็นตัวแทนของประตูมนุษย์ที่คุณสร้างในเซสชันที่ 5 โดยจะเลือกผู้สมัครที่มีหมายเลขลงท้ายด้วยแนวคิดของคืนนี้ (..., pick 4) ไม่เช่นนั้นจะเลือกผู้สมัครคนแรก และเขียนตัวเลือกนั้นลงในสถานะ scripter เป็นตัวแทน single_turn ที่เปลี่ยนแนวทางที่ได้รับอนุมัติให้เป็น Script ซึ่งประกอบด้วยชื่อ คำอธิบาย แท็ก ประโยคเปิด ช็อต 3 ช็อต และไอเดียโปสเตอร์ โดยทุกช็อตเป็นรูปภาพฮาโลวีนในรูปลักษณ์ของช่อง

การวนซ้ำหลังจากการปฏิเสธการออก quarantine มาพร้อมกับ Agent โหมดงาน Agent มี mode: chat คือลูปการสนทนาที่ Agent ระดับรูทเรียกใช้กับบุคคล single_turn คือการเรียกใช้ 1 ครั้งพร้อมคำตอบที่มีโครงสร้าง และ task คือลูปของการเรียกใช้เครื่องมือที่ Agent เรียกใช้ด้วยตนเองจนกว่าจะเรียกใช้ finish_task ในตัว ซึ่ง ADK จะรับอาร์กิวเมนต์มาจาก output_schema Quarantine จะเรียกใช้ find_policy_hits ในทิศทางที่ถูกปฏิเสธ ขอให้ suggest_replacement แทนที่คำแต่ละคำด้วยคำที่ได้รับอนุมัติของช่อง (agent/policy_replacements.txt, ข้อมูลอีกครั้ง) เขียนใหม่ ตรวจสอบอีกครั้ง และจบด้วย CleanedDirection ที่พิมพ์ไว้ อ่านข้อความในหน้าเว็บ ไม่ต้องพิมพ์

quarantine = Agent(
    name="quarantine",
    model=config.MODEL,
    mode="task",
    instruction=QUARANTINE_INSTRUCTION,
    tools=[find_policy_hits, suggest_replacement],
    output_schema=CleanedDirection)

เมื่อจัดส่งแล้ว เชนจะทำงานตั้งแต่การเลือกไปจนถึงสคริปต์โดยตรง 👉🖥 วางเราเตอร์ไว้ระหว่างทั้ง 2 ฝั่ง โดยมีทางออก 2 ทางและทางกลับจากเขตกักกัน ซึ่งจะเปลี่ยนจาก 1 บรรทัดเป็น 3 บรรทัด ดังนี้

           (join_research, propose_directions, choose_direction, policy_check),
           (policy_check, {"OK": scripter, "BLOCK": quarantine}),
           (quarantine, scripter),

👉🖥 ใช้ทั้ง 2 เส้นทาง ส่งไอเดียตามที่เป็นอยู่: choose_direction รับตัวเลือกที่ 1, policy_check แสดง OK และผู้เขียนสคริปต์จะเขียนสคริปต์ที่มีโครงสร้าง คลิกเซสชันใหม่ แล้วส่งไอเดียอีกครั้งโดยมี , pick 4 ต่อท้าย: ผู้สมัคร 4 เป็นเหยื่อล่อ policy_check ปล่อย BLOCK ออกมา การกักบริเวณจะเรียกใช้เครื่องมือของตน เขียนคำที่ถูกปฏิเสธใหม่ เรียกใช้ finish_task และคำสั่งที่ได้รับการแก้ไขแล้วจะกลับไปที่ผู้เขียนสคริปต์ การอ่านแถวที่ยืนยันจะเรียกใช้ทั้ง 2 รายการจากที่เก็บเซสชัน

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

6. 4 · การนำเข้าข้อมูลหลายรูปแบบ — สิ่งที่ผู้ชมส่งมาจะถูกอ่านเป็นคลังข้อมูลเดียว

เซสชัน 4 · การนำเข้าข้อมูลหลายรูปแบบ

👉🖥 ใน Workbench ให้เปิดเซสชัน 4 · การนำเข้าข้อมูลหลายรูปแบบ ส่วน 4a และ 4b ผู้ชมไม่ได้แค่เขียนเท่านั้น โดยมีตัวอ่าน 1 ตัวต่อประเภทข้อมูลที่จะเปลี่ยนทุกอย่างที่ส่งมาให้เป็นข้อความที่มีรูปแบบเดียวกัน จากนั้นคลังข้อมูล RAG Engine 1 คลังจะเก็บข้อมูลทั้งหมดไว้ และตัวอ่านตัวที่ 3 ใน Fan-out จะค้นหาข้อมูลตามความหมายและส่งสื่อกลับมา

สิ่งที่ผู้ชมส่ง (4ก)

agent/comments.md มีความคิดเห็น 12 รายการที่เขียนไว้ใต้วิดีโอฮาโลวีน 6 รายการของช่อง agent/audience/ มีส่วนที่เหลือ ได้แก่ บันทึกเสียง 8 รายการ (voice/*.mp3) รูปภาพห้องของผู้ชมเอง 14 รูป (photo/*.jpg) และคลิป 6 คลิป (clip/*.mp4) ซึ่งเป็นภาพแพนช้าๆ เหนือรูปภาพพร้อมเสียงบรรยายของผู้ชม หน้าจอของ 4a เปิดขึ้นมาและแสดงทุกอย่าง ทั้งเล่นโน้ตเสียง แสดงรูปภาพ และเล่นคลิป

ตารางกริดของผู้ชมหลังจากการส่งข้อมูลเข้า: การ์ดทุกใบจะแสดงสิ่งที่ Gemini ได้ยินหรือเห็น

RAG Engine จะดึงข้อมูลข้อความ บันทึกเสียง รูปภาพ หรือคลิปจะเข้าร่วมคลังข้อมูลได้ในลักษณะเดียวกับที่ผู้โพสต์ในอดีตทำใน rag.caption_posters นั่นคือโมเดลจะเปลี่ยนข้อมูลดังกล่าวให้เป็นบรรทัดข้อความที่ตั้งชื่อไฟล์ บรรทัดนั้นจะกลายเป็นข้อความ และเมื่อข้อความกลับมา ไฟล์ก็จะกลับมาด้วย agent/platform/audience.py มีเครื่องอ่าน 1 เครื่องต่อประเภท แต่ละรายการจะส่งไฟล์ 1 ไฟล์และพรอมต์ 1 รายการไปยัง Gemini และเขียน 1 บรรทัดต่อไฟล์ ดังนี้

VOICE_PROMPT = ("Transcribe this voice note word for word inside double quotes. Then add: mood: and one word. "
                "Plain text on one line, nothing else.")
PHOTO_PROMPT = ("Two sentences: what is in the picture, the palette, the mood. Then: tags: and three short tags. "
                "Plain text on one line, no headings, no markdown.")
CLIP_PROMPT = ("Describe the shots in two sentences, then quote what you HEAR on the soundtrack as: heard: \"...\". "
               "Plain text on one line, no headings, no markdown.")


def _ask(path: pathlib.Path, mime: str, prompt: str) -> str:
    """One media file in, one line of text out."""
    from google.genai import types as gt
    client = images._client()
    resp = client.models.generate_content(
        model=config.MODEL,
        contents=[gt.Part.from_bytes(data=path.read_bytes(), mime_type=mime), prompt],
        config=gt.GenerateContentConfig(automatic_function_calling=gt.AutomaticFunctionCallingConfig(disable=True)))
    return " ".join((resp.text or "").split())


def transcribe_voice() -> pathlib.Path:
    """voice note -> text: Gemini listens."""
    files = sorted((AUDIENCE / "voice").glob("*.mp3"))
    lines = []
    for p in files:
        said = _ask(p, "audio/mpeg", VOICE_PROMPT)
        lines.append(f"- voice: {p.name} — {said}")
    return _write(AUDIENCE / "voice" / "transcripts.md", "voice", "What Gemini heard in the audience's voice notes", lines)

caption_photos ก็ทำแบบเดียวกันกับ image/jpeg และ PHOTO_PROMPT ส่วน watch_clips ก็ทำแบบเดียวกันกับ video/mp4 และ CLIP_PROMPT และ Gemini จะดูภาพและฟังเพลงประกอบในการโทรครั้งเดียว ผู้อ่านทุกคนเขียนรูปร่างบรรทัดเดียวกัน ดังนั้นการแยกส่วน การฝัง และการดึงข้อมูลจึงไม่ทราบว่าข้อความมาจากประเภทใด

- voice: note-03.mp3 — "I want a whole series of the cat judging costumes ..." mood: delighted
- photo: photo-01-ghost-drawer.jpg — A cream wooden drawer, striped socks, a paper-cup ghost ... tags: drawer, socks, cozy
- clip:  clip-05.mp4 — A hallway floor, a snail in a sheet and a round vacuum ... heard: "The snail won."
- poster: sock-ghost.png — A sheet ghost one inch above a striped sock ...

เครื่องอ่าน 1 เครื่องต่อประเภท, รูปร่างเส้น 1 แบบ, คลังข้อมูล 1 รายการ

คลังข้อมูล Vertex AI RAG Engine คือการดึงข้อมูลจากเอกสาร โดยระบบจะแบ่งคลังข้อมูลออกเป็นข้อความสั้นๆ ประมาณ 120 โทเค็น ข้อความแต่ละรายการจะฝังด้วย text-embedding-005 และคำค้นหาจะฝังในลักษณะเดียวกันและตอบด้วยข้อความที่ใกล้เคียงที่สุดตามความหมาย ไม่ใช่ตามคำที่ใช้ร่วมกัน "cozy little hauntings at home" จะค้นหาความคิดเห็นที่ขอให้มีเวทมนตร์เล็กๆ ในห้องเล็กๆ, บันทึกเสียงเกี่ยวกับค้างคาวที่กอดหลอดไฟ และคลิปที่มีซาวด์แทร็กที่บอกว่าผีเป็นถ้วยกระดาษ

corpus = rag.create_corpus(
    display_name="vibestudio-feedback",
    backend_config=rag.RagVectorDbConfig(
        rag_embedding_model_config=rag.RagEmbeddingModelConfig(
            vertex_prediction_endpoint=rag.VertexPredictionEndpoint(
                publisher_model="publishers/google/models/text-embedding-005"))))

rag.upload_file(
    corpus_name=corpus.name, path="agent/comments.md", display_name="comments.md",
    transformation_config=rag.TransformationConfig(
        chunking_config=rag.ChunkingConfig(chunk_size=120, chunk_overlap=20)))

การตั้งค่าได้เริ่มสร้างคลังข้อมูลแล้ว แถวการเชื่อมต่อของหน้า 4a จะระบุว่าคลังข้อมูลพร้อมใช้งานหรือไม่ และจะสร้างคลังข้อมูลหากการตั้งค่าทำไม่ได้ จากปุ่มของหน้าหรือเทอร์มินัล: python -m agent.platform.rag สร้างคลังข้อมูลและบันทึกไว้ใน runs/ragcorpus.json; python -m agent.platform.rag load --audience เรียกใช้โปรแกรมอ่านทุกโปรแกรมใน audience.READERS จากนั้นอัปโหลดสิ่งที่เขียนไว้ข้างความคิดเห็นและคำบรรยายของผู้โพสต์ โดยแต่ละไฟล์จะอยู่ภายใต้ชื่อของตัวเอง และรอให้ดัชนีตอบกลับ; python -m agent.platform.rag query "cozy little hauntings at home" แสดงการดึงข้อมูลที่พบข้อความที่ไม่มีคำใดตรงกับคำค้นหา

การแก้ไข เมื่อจัดส่ง READERS จะมีเพียง read_comments เท่านั้น โดยเครื่องอ่านสื่อ 3 เครื่องจะอยู่หลัง # 1 อัน 👉🖥 เรียกใช้การนำเข้าข้อมูลหนึ่งครั้งตามที่จัดส่ง: เอาต์พุตจะระบุว่า not read (no reader wired in audience.READERS): voice, photo, clip และระบบจะนำเข้าเฉพาะความคิดเห็นและคำบรรยายแทนเสียงเท่านั้น จากนั้นลบ # ดังนี้

    transcribe_voice, caption_photos, watch_clips,

👉🖥 บันทึกและเรียกใช้การนำเข้าอีกครั้ง โปรแกรมอ่านหน้าจอจะใช้เวลาประมาณ 3 นาทีในการอ่านทั้ง 40 รายการ โดยคอนโซลจะพูดว่า heard, saw และ watched ทีละบรรทัด และการ์ดทุกใบในตารางกริดจะแสดงข้อมูลตามบรรทัดที่อ่าน แถวตรวจสอบจะอ่านไฟล์ เอาต์พุตของผู้อ่าน และคลังข้อมูล โดยมีผู้อ่าน 4 คนที่เชื่อมต่ออยู่, บันทึกเสียง 8 รายการที่ได้ยิน, รูปภาพ 14 รูปที่เห็น, คลิป 6 คลิปที่ดู และไฟล์ 5 ไฟล์ในคลังข้อมูล

ผู้อ่านคนที่ 3 (4b)

ความคิดเห็นของกลุ่มเป้าหมายคือการวิจัยที่แชร์โดยการเรียกใช้ทุกครั้ง ดังนั้นจึงเข้าสู่กราฟในตำแหน่งเดียวกับการวิจัย นั่นคือเป็นผู้อ่านรายที่ 3 ในการรวม read_feedback ใน agent/graph.py จะนำแนวคิดของคืนนี้ไปใช้ ถามคลังข้อมูลเพื่อหาข้อความที่ใกล้เคียงที่สุด และรายงานไปยัง join_research ซึ่งรออยู่ 3 รายการ ขอบ (START, read_feedback, join_research) ส่งใน stage5_rag/agent.py ซึ่งเป็นรูปร่างที่สอนในเซสชัน 3 การแก้ไขของคุณอยู่ภายในโหนด การดึงข้อมูล ไม่ใช่การเรียกใช้โมเดล และเป็นแบบเชิงกำหนด: แนวคิดเดียวกันจะค้นหาข้อความเดียวกันในขณะที่ผู้เสนอแตกต่างกัน

def read_feedback(node_input):
    """The third reader: what the audience sent - comments, and what Gemini
    heard and saw in their voice notes, photos and clips - the passages
    nearest to tonight's idea, and the media those passages came with.
    Retrieval, not a model call."""
    from .platform import rag
    idea = idea_text(node_input)
    query = idea or "what viewers liked and what they complained about"
    try:
        hits = rag.retrieve(query)
    except Exception as e:
        print(f"  [rag] feedback unavailable ({str(e)[:80]})")
        return Event(output={"query": query, "feedback": [], "media": [],
                             "note": "no corpus connected - run: python -m agent.platform.rag"})
    return Event(output={"query": query, "feedback": [h["text"] for h in hits]})  # TODO: RAG_MEDIA

ทุกบรรทัดในคลังข้อมูลที่มาจากบันทึกเสียง รูปภาพ คลิป หรือโปสเตอร์จะระบุชื่อไฟล์ rag.media_in(hits) จะอ่านชื่อเหล่านั้นจากข้อความและเปลี่ยนเป็น URL ที่หน้าเว็บสามารถแสดงและเล่นได้ 👉🖥 ส่งสื่อกลับไปพร้อมกับข้อความ เพื่อให้สิ่งที่ผู้ชมส่งมาเดินทางไปพร้อมกับสิ่งที่พวกเขาพูด

    return Event(output={"query": query, "feedback": [h["text"] for h in hits],
                         "media": rag.media_in(hits)})

👉🖥 วิ่ง stage5_rag ด้วยไอเดีย เปิด read_feedback: คำค้นหาคือแนวคิดของคุณ เอาต์พุตมีข้อความที่ใกล้เคียงที่สุด 5 ข้อความและรายการ media ที่มี URL ต่อไฟล์ เปิด join_research: 3 คีย์ เปิด propose_directions: ผู้สมัคร 1 ถึง 3 อ้างอิง feedback ในหลักฐาน แผงยืนยันจะแสดงสิ่งที่ผู้อ่านดูใต้แถว ซึ่งได้แก่ คลิป บันทึกเสียง 3 รายการ และโปสเตอร์ผีในตู้เย็น

สิ่งที่ผู้อ่านคนที่ 3 ดู: สื่อที่มาพร้อมกับข้อความที่ดึงมา

7. 5 · การสร้างวิดีโอ - การระงับ 2 ประเภท กลไกเดียว

เซสชัน 5 · การสร้างวิดีโอ

👉🖥 ใน Workbench ให้เปิดเซสชัน 5 · การสร้างวิดีโอ ส่วน 5a และ 5b ซึ่งสร้างใน stage6_video ทั้งกราฟ การติดคลิปใช้เวลาไม่กี่นาทีและการติดโปสเตอร์ใช้เวลาไม่กี่วินาที และการวิ่งจะหยุด 2 ครั้งระหว่างทาง ครั้งแรกสำหรับคุณ และครั้งที่ 2 สำหรับ Veo The two stops are one mechanism, and the two branches of the desk share what they made by name.

การระงับ 2 ประเภท (5ก)

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

สำหรับ 1 คน · direction_gate

สำหรับเครื่องมือที่ทำงานช้า · render_desk

ผู้ที่หยุดการวิ่ง

direction_gate ให้ผลลัพธ์เป็น RequestInput

render_submit ส่งคืน {status: pending} และ LongRunningFunctionTool ทำให้หยุดได้

สิ่งที่รออยู่

บุคคล: การ์ด 1 ใน 4 ใบ

งานที่ช้า: การทำงานของ Veo, นาที

สิ่งที่อยู่ในเซสชัน

การเรียก adk_request_input แบบเปิด: ข้อความ แบบฟอร์ม เพย์โหลด

การเรียก render_submit แบบเปิด: ใบเสร็จที่มีชื่อการดำเนินการ

วิธีที่ระบบจะกลับมาทำงานต่อ

function_response ที่มีรหัสของการโทรนั้นซึ่งมีตัวเลือกของคุณ

function_response ที่มีรหัสของการโทรนั้น ซึ่งมี URL ของคลิป

ผู้ส่ง

adk web เมื่อคุณส่งแบบฟอร์ม

python -m agent.deliver จากกระบวนการอื่นในภายหลัง

ซึ่งเป็นที่ที่มันอาศัยอยู่ในระหว่างนั้น

runs/sessions.db · รหัสเซสชันเดียว · ไม่มีอะไรทำงาน

แถวเดียวกัน รหัสเดียวกัน · ไม่มีอะไรทำงาน

การระงับ 2 ประเภท ตารางเดียว

สำหรับบุคคลธรรมดา ในพรอมต์เดียว "ขอให้ครีเอเตอร์ยืนยัน" เป็นคำแนะนำ และเซสชันที่ 2 ได้พูดถึงเรื่องนี้ในข้อความเดียว ในเวิร์กโฟลว์ การหยุดชั่วคราวคือเครื่องมือ โหนดที่ให้ผลลัพธ์เป็น RequestInput จะระงับการเรียกใช้: ADK จะเขียนการเรียกแบบเปิดชื่อ adk_request_input ลงในเซสชันด้วย message, response_schema และ payload ของคุณ โดยกำหนด interrupt_id ให้ และส่งกลับไปยังผู้เรียก ไม่มีการรอชุดข้อความ สิ่งเดียวที่จะทำให้กลับมาทำงานต่อได้คือ function_response ที่มีรหัสนั้น ซึ่งได้รับการตรวจสอบเทียบกับสคีมาของคุณ ข้อความแชทไปยังเวิร์กโฟลว์คือเทิร์นใหม่ ไม่ใช่คำตอบ สคีมายังเป็นแบบฟอร์มด้วย โดย adk web จะแสดงผลสคีมา และแอป VibeStudio จะแสดงผลสคีมาเดียวกันเป็นการ์ด 4 ใบ direction_gate เขียนตัวเลือกไปยังสถานะแล้ว 👉🖥 วิธีหยุดการแจ้งเตือน

    yield RequestInput(
        message="Pick tonight's direction: 1, 2, 3 or 4.",
        response_schema={
            "type": "object",
            "properties": {
                "pick": {"type": "string", "enum": ["1", "2", "3", "4"]}}},
        payload={"candidates": cands})

คำตอบของคุณจะมาถึงที่ persist_direction เป็น node_input ซึ่งเป็น Dict ที่มีคีย์เดียวคือ pick ผู้สมัครทั้ง 4 รายมาถึงด้วยวิธีอื่น โดยเกตเวย์จะเขียนผู้สมัครไปยังสถานะที่แชร์ภายใต้ candidates และ ADK จะเชื่อมโยงคีย์สถานะกับพารามิเตอร์ที่มีชื่อเดียวกัน โหนดจะแก้ไขการเลือกเป็นผู้สมัคร 1 รายและเขียน direction, angle, hook และ user:prefs กลับด้วย Event(state=...) คีย์ที่มีคำนำหน้า user: จะคงอยู่ตลอดเซสชันสำหรับผู้ใช้รายนั้น

สำหรับเครื่องมือที่ทำงานช้า การเรนเดอร์ Veo ใช้เวลาหลายนาที และกราฟไม่ควรรอนานขนาดนั้น render_submit ใน agent/desk.py จะเริ่มงานและแสดงผลทันทีพร้อมใบเสร็จที่มีสถานะเป็น pending:

def render_submit(prompt: str) -> dict:
    """Submit one Veo render of `prompt`. Returns at once with a pending
    receipt; the clip is delivered later, to this call, by id."""
    receipt = videogen.start(f"{prompt} {videogen.NO_TEXT}")
    return {"status": "pending", "operation": receipt["operation"], "prompt": receipt["prompt"]}

เมื่อรวมอยู่ใน LongRunningFunctionTool ใบเสร็จนั้นจะมีความหมายต่อ ADK นั่นคือเทิร์นของ Agent สิ้นสุดลง เวิร์กโฟลว์จะหยุดชั่วคราวที่ render_desk และการโทรที่เปิดอยู่พร้อมรหัสและใบเสร็จจะเป็นแถวใน runs/sessions.db 👉🖥 ใน stage6_video/agent.py ให้รวมเครื่องมือไว้ดังนี้

    tools=[LongRunningFunctionTool(render_submit)])

ดำเนินการต่อตามรหัสจากกระบวนการอื่น agent/deliver.py จะค้นหาการเรียกที่รอดำเนินการในที่เก็บเซสชัน โพล Veo จนกว่าจะมีคลิป เขียนผลลัพธ์ลงใน runs/state.json และส่ง function_response ที่มีรหัสของการเรียกนั้นไปยังเซสชันเดียวกัน 👉🖥 สร้างส่วนคำตอบ:

    part = Part(function_response=FunctionResponse(
        id=row["call_id"], name=row["name"], response=response))

👉🖥 เปิด stage6_video จนกว่าจะหยุด 2 ครั้ง ส่งไอเดีย: การเรียกใช้จะหยุดที่แบบฟอร์มที่สร้างจากสคีมาของคุณ โดยมีผู้สมัคร 4 คนในเพย์โหลด พิมพ์ 2 ใน pick แล้วส่ง กราฟจะกลับมาทำงานต่อในเวลา persist_direction ลองทดสอบการหยุดทำงานที่นี่: ระงับที่แบบฟอร์ม หยุดเซิร์ฟเวอร์ (scripts/stop.sh ในแท็บ 1) เริ่มเซิร์ฟเวอร์อีกครั้ง (scripts/restart.sh) เปิดเซสชันอีกครั้ง รูปแบบยังคงอยู่ เพราะการหยุดชั่วคราวคือแถว ไม่ใช่ชุดข้อความ หลังจาก script_ready แล้ว render_desk จะเรียกใช้ render_submit รับใบเสร็จ ตอบกลับ WAITING และการเรียกใช้จะสิ้นสุดลง โดยเซสชันจะเก็บรหัสการเรียกใช้ไว้ เรียกใช้การแสดงโฆษณาจากคอนโซลหรือแท็บ 1 ของหน้าเว็บ

cd ~/vibe-studio-multimodal
uv run python -m agent.deliver status   # the pending renders, by session, call id and operation
uv run python -m agent.deliver          # wait for Veo, answer the newest pending call by id

👉🖥 คลิกรีเฟรช adk web: function_response จะติดตามการโทรที่รอดำเนินการ join_media จะเผยแพร่ และ store_release จะทำงาน แถวการยืนยัน 8 แถวจะอ่านจุดหยุด 2 จุดจากเซสชัน ได้แก่ การเรียกของเกตเวย์และคำตอบของคุณ ใบเสร็จของโปรแกรมแสดงผลและการนำส่ง

รัฐและโต๊ะทำงาน (5ข)

โต๊ะสื่อมีโครงสร้างแบบ 3a อีกครั้ง โดยมีการแยกสาขาและการรวมเข้าด้วยกัน โดยมีสาขาที่ช้า 1 สาขา script_ready ส่งสคริปต์ 2 วิธี render_desk ส่งคลิปและระงับการวิ่ง make_poster คือเครื่องมือเซสชันที่ 2 ในรูปแบบโหนด โดยposter_ideaของสคริปต์ สไตล์ของช่อง และสิ่งที่คลังความทรงจำรู้เกี่ยวกับรสนิยมของครีเอเตอร์ในเรื่องโปสเตอร์ (เชื่อมโยงจากสถานะเป็น memory_facts, เซสชันที่ 6) จะส่งไปยังโมเดลรูปภาพ และ PNG ขนาด 9:16 จะส่งกลับมาในไม่กี่วินาที join_media จะรอการดำเนินการที่ช้า และ store_release จะเขียนสิ่งที่พบลงในสถานะ

แชร์ตามชื่อ store_release ไม่เคยได้รับโปสเตอร์ โดยจะประกาศพารามิเตอร์ชื่อ poster_url และ ADK จะเชื่อมโยงพารามิเตอร์ดังกล่าวจากสถานะที่แชร์ เนื่องจากโหนดในสาขาอื่นเขียนคีย์นั้น ไม่มีเพย์โหลดข้ามระหว่างสาขา:

def store_release(node_input, poster_url: str = "", poster_path: str = ""):
    """`poster_url` and `poster_path` are not passed by anyone: ADK binds them
    from shared STATE because the parameter names match the keys make_poster
    wrote (the STATE_SHARE edit). The two branches of the desk never hand each
    other payloads; they share what they made by name."""
    render = state.load().get("render") or {}          # the delivered clip, written by deliver
    url, status = render.get("url") or "", render.get("status", "")
    ...
    if not poster_url:
        said += " · poster: not in state (did make_poster write poster_url to state? the STATE_SHARE edit)"
    yield Event(output={"status": status, "url": url, "poster_url": poster_url},
                state={"render_url": url, "render_status": status, "poster_url": poster_url},
                message=f"video: {said} · poster: {poster_url or 'none'}")

เมื่อจัดส่งแล้ว make_poster จะแสดงผลเป็นโปสเตอร์และข้อความ และ store_release จะแสดงข้อความว่า poster: not in state 👉🖥 เพิ่มการเขียนสถานะลงในผลลัพธ์สุดท้าย

                state={"poster_url": rec.get("url", ""), "poster_path": rec.get("path", "")},
                message=f"poster: {said}")

👉🖥 ดำเนินการอีกครั้งและส่งมอบ เปิดเหตุการณ์ของ make_poster: URL ของโปสเตอร์ในเอาต์พุตและเดลต้าสถานะที่มี poster_url เปิด store_release: ข้อความจะระบุชื่อผู้โพสต์ บล็อกเซสชันของการเรียกใช้นี้ของหน้าจะแสดงแท็บสถานะของแถว adk web เดียวกัน ซึ่งแสดงรหัสเซสชันที่การดำเนินการต่อต้องการ การเรียกที่เปิดอยู่ และคีย์ที่โหนดเขียน poster_url และอื่นๆ แผงยืนยันจะแสดงคลิปและโปสเตอร์เคียงข้างกัน

คลิปและโปสเตอร์อยู่เคียงข้างกันหลังจากการนำส่ง

8. 6 · ความทรงจำ - สิ่งที่ช่องจดจำได้ระหว่างคืนต่างๆ

เซสชัน 6 · ความทรงจำ

👉🖥 ใน Workbench ให้เปิดเซสชัน 6 · หน่วยความจำ ส่วน 6a และ 6b ซึ่งสร้างใน stage4_memory เซสชันที่ 5 เป็นเรื่องเกี่ยวกับสถานะของการวิ่ง 1 ครั้ง เซสชันนี้จะพูดถึงสิ่งที่ช่องจำได้ระหว่างการเรียกใช้ ซึ่งก็คือสิ่งที่กราฟได้ยินจากภายนอกผ่าน Callback 2 รายการ

Memory Bank (6ก)

Vertex AI Memory Bank คือความทรงจำระยะยาวที่มีการจัดการเกี่ยวกับบุคคล ข้อเท็จจริงจะอยู่ภายใต้ขอบเขต ชื่อแอป และรหัสผู้ใช้ รวมถึงอยู่ภายใต้หัวข้อที่คุณกำหนด เมื่อคุณส่งการสนทนากับ memories.generate ให้ NotebookLM ระบบจะดึงข้อมูลที่ตรงกับหัวข้อ ฝังข้อมูลนั้น และรวมเข้ากับข้อมูลที่มีอยู่แล้ว โดยจะสร้างข้อเท็จจริงใหม่ อัปเดตข้อมูลที่เปลี่ยนแปลง และรวมข้อมูลที่ซ้ำกัน memories.retrieve แสดงผลข้อเท็จจริงสำหรับขอบเขต ทั้ง 2 สายจะอยู่ใน agent/platform/memory.py

SCOPE = {"app_name": config.APP, "user_id": config.USER}
TOPICS = {
    "CREATOR_TASTE": "Which video directions this creator picks and passes on, "
                     "and how that preference changes over time.",
    "CHANNEL_RULES": "Standing instructions the creator states for every video "
                     "(style, subjects to avoid, format rules).",
    "VISUAL_STYLE": "What this creator approves and rejects in a poster: palettes, "
                    "framing, props and mood of the posters they shipped.",
}

หัวข้อที่ 3 คือรสนิยมของช่องในเรื่องรูปภาพ โดยโมเดลได้เขียนลงในหน่วยความจำนี้ในเซสชันที่ 5 ซึ่งมีการอธิบายโปสเตอร์ที่จัดส่งแล้วในประโยคเดียวและจดจำไว้ จากนั้นจึงวาดโปสเตอร์ถัดไปโดยใช้ประโยคนั้นในพรอมต์

การตั้งค่าเริ่มสร้างแบงก์แล้ว แถวการเชื่อมต่อของหน้า 6a จะระบุว่าแบงก์พร้อมใช้งานหรือไม่ จากปุ่มของหน้าหรือเทอร์มินัล: python -m agent.platform.bank สร้างคลังและหัวข้อต่างๆ และแคชชื่อทรัพยากรใน runs/memorybank.json; python -m agent.platform.bank load สร้างเซสชันที่ผ่านมา 4 เซสชัน (ยุคสัตว์ 2 ยุคที่มีกฎสไตล์ ยุคแกดเจ็ต ยุคแฟนตาซีล่าสุด) python -m agent.platform.bank list แสดงผลลัพธ์ของข้อความถอดเสียง ซึ่งก็คือข้อความสรุปข้อเท็จจริง ไม่ใช่ข้อความถอดเสียง

การติดต่อกลับ (6b)

หน่วยความจำไม่ใช่โหนด Callback คือฟังก์ชันที่คุณส่งไปยัง Agent โดย ADK จะเรียกใช้ฟังก์ชันนี้ในเวลาที่แน่นอนพร้อมบริบทแบบเรียลไทม์ และการแสดงผล None จะช่วยให้สิ่งต่างๆ ดำเนินไปตามปกติ 3 คู่: รอบเทิร์นของ Agent (before_agent_callback, after_agent_callback พร้อม CallbackContext), รอบการเรียกโมเดลแต่ละครั้ง (before_model_callback พร้อม LlmRequest, after_model_callback พร้อม LlmResponse), รอบการเรียกใช้เครื่องมือแต่ละครั้ง Guardrail, การตรวจวัดระยะไกล, แคช และการแทรกบริบทจะอยู่ที่นี่ โดยไม่มีโหนดในกราฟ

ใน stage4_memory/agent.py propose_directions จะอ่านคลังข้อมูลก่อนที่จะเรียกใช้โมเดล โดย recall_taste จะดึงข้อมูลข้อเท็จจริงมาต่อท้ายคำขอเป็นบล็อก MEMORY โดยเรียงลำดับจากเก่าที่สุดก่อน และบอกให้โมเดลปรับตัวเลือกที่ 1 ถึง 3 ให้สอดคล้องกับรสนิยมปัจจุบันและถือว่ากฎเป็นข้อจำกัด นอกจากนี้ยังเขียนข้อเท็จจริงลงในสถานะเป็น memory_facts ซึ่ง make_poster จะค้นหาได้ 👉🖥 เชื่อมต่อสาย:

    output_schema=Directions,
    before_model_callback=recall_taste)

ผู้เขียนสคริปต์จะเขียนสิ่งที่ครีเอเตอร์เลือกหลังจากถึงตาของตนเอง โดย remember_pick จะอ่าน direction จากสถานะ เขียนประโยค 1 ประโยคเกี่ยวกับตัวเลือกนั้น แล้วส่งให้ memories.generate 👉🖥 เชื่อมต่อสาย:

    output_schema=Script,
    after_agent_callback=remember_pick)

👉🖥 เรียกใช้ด้วยข้อความว่างเปล่าและเลือกการ์ด เปิดกิจกรรม propose_directions: คำขอมี 3 ยุค และผู้สมัคร 1 ถึง 3 มีแนวโน้มที่จะชอบรสนิยมปัจจุบัน ขณะที่เทรนด์แนะนำอย่างอื่น หลังจากสคริปต์ ให้คลิกแสดงคลัง: ความทรงจำมีการเปลี่ยนแปลงหรือเพิ่มเข้ามาสำหรับรายการที่เลือกในคืนนี้ เรียกใช้คืนที่ 2: ระบบจะจดจำคืนแรก

9. 7 · ติดตั้งใช้งานและเผยแพร่ — กราฟภายในแอป, ค่ำคืนใน VibeTube

เซสชัน 7 · ติดตั้งใช้งานและเผยแพร่

👉🖥 ใน Workbench ให้เปิดเซสชัน 7 · ติดตั้งใช้งานและเผยแพร่ ส่วน 7a และ 7b

ติดตั้งใช้งาน VibeStudio (7a)

ในแซนด์บ็อกซ์ adk web เป็นผู้ขับเคลื่อนกราฟ VibeStudio ขับเคลื่อนด้วย Runner ของ ADK ในเทรดผู้ปฏิบัติงานที่มีวนรอบเหตุการณ์ของตัวเอง และพับทุกเหตุการณ์ให้อยู่ในสถานะการเรียกใช้เดียวที่หน้าเว็บอ่านผ่านเหตุการณ์ที่เซิร์ฟเวอร์ส่งมา ซึ่งได้แก่ ผู้อ่าน การเลือกของคุณเป็นการ์ด 4 ใบ โปสเตอร์เมื่อมาถึง และการแสดงผลเมื่อส่ง คำตอบของ Gate และการแสดงผลของ Render เป็นการเรียกเดียวกัน ซึ่งเป็นส่วน function_response และแอปจะสำรวจ Veo เองแทนที่จะรอคอนโซล

self._svc = DatabaseSessionService(db_url=config.DB_URL)
self._runner = Runner(app_name=config.APP, agent=wf, session_service=self._svc)

async for ev in self._runner.run_async(user_id=config.USER, session_id=run_id, new_message=message):
    self._absorb(ev)    # fold the ADK event into the run state, publish one app event

part = Part(function_response=FunctionResponse(id=call_id, name=name, response=response))

vibestudio/server/agent/ เป็นสำเนาแบบไบต์ต่อไบต์ของ agent/ ของห้องปฏิบัติการ ซึ่งตรวจสอบโดย checks/verify_app.py รายการ Edge ที่สมบูรณ์ใน agent/graph.py คือทุกโครงสร้างของ Codelab นี้ในที่เดียว ซึ่งมี 13 โหนด โดยจะส่งมาแบบสมบูรณ์เพื่อให้คุณอ่าน ไม่ใช่พิมพ์

        (START, scan_trends, join_research),
        (START, read_backlog, join_research),
        (START, read_feedback, join_research),
        (join_research, propose_directions, direction_gate,
         persist_direction, policy_check),
        (policy_check, {"OK": scripter, "BLOCK": quarantine}),
        (quarantine, scripter),
        (scripter, script_ready),
        (script_ready, render_desk, join_media),
        (script_ready, make_poster, join_media),
        (join_media, store_release),

👉🖥 ติดตั้งใช้งานด้วยปุ่มติดตั้งใช้งานของ Workbench หรือ 👉💻 จากแท็บ 1 โดยทำดังนี้

cd ~/vibe-studio-multimodal
uv run python vibestudio/deploy.py

ซึ่งจะทำงานโดยมีค่าจาก .env ของคุณที่กรอกไว้ ดังนี้

gcloud run deploy vibestudio --source vibestudio \
  --project $GOOGLE_CLOUD_PROJECT --region us-central1 \
  --labels dev-tutorial-codelab=vibetube --allow-unauthenticated \
  --memory 2Gi --cpu 2 --timeout 3600 --concurrency 40 \
  --max-instances 1 --min-instances 1 --session-affinity \
  --set-env-vars GOOGLE_CLOUD_PROJECT=...,STUDIO_VERTEX=1,STUDIO_MEMORY_BANK=...,STUDIO_RAG_CORPUS=...,VIBETUBE_URL=...,VIBETUBE_EVENT=...,VIBETUBE_NAME=...,VIBETUBE_PROJECT=...

--source จะสร้างคอนเทนเนอร์ด้วย Cloud Build และทำให้ใช้งานได้ในขั้นตอนเดียว โดยการสร้างครั้งแรกจะใช้เวลา 4-5 นาที --session-affinity ที่มีอินสแตนซ์แบบวอร์ม 1 รายการจะเก็บการเรียกใช้ที่ถูกระงับไว้ในอินสแตนซ์ที่เก็บข้อมูลเซสชันของเซสชัน 5 อีกครั้ง ในการติดตั้งใช้งานขนาดใหญ่ขึ้น พื้นที่เก็บข้อมูลเซสชันจะย้ายไปที่ VertexAiSessionService และความสัมพันธ์จะหายไป บิลด์และบริการจะทำงานเป็นบัญชีบริการ Compute เริ่มต้นของโปรเจ็กต์ ซึ่งการตั้งค่าจะให้สิทธิ์ตามบทบาทที่จำเป็น

👉🖥 เปิด URL ของบริการและสร้างวิดีโอในแอป โดยเริ่มจากไอเดีย เลือก รอคลิปและโปสเตอร์ แท็บประวัติจะเก็บทั้ง 2 รายการไว้

เผยแพร่ (7b)

การเผยแพร่คือการส่ง POST แบบหลายส่วนจากแอปไปยังปลายทางของห้องแชทบน VibeTube ในรูปแบบ vibestudio/server/platform/publish.py คลิปคือวิดีโอ โปสเตอร์คือภาพปก และโปรไฟล์จะตั้งชื่อห้องและคุณ ดังนี้

url = f"{prof['platform_url']}/api/events/{prof['event_code']}/videos"
data = {"title": state.script["title"], "description": state.script["description"],
        "duration": "0:08", "displayName": prof["display_name"],
        "projectId": prof["project_id"]}        # one video per project and room: publishing again replaces it
files = {"videoFile": (clip.name, clip.open("rb"), "video/mp4"),
         "thumbnailFile": (poster.name, poster.open("rb"), "image/png")}   # the poster, not a frame
r = httpx.post(url, data=data, files=files, timeout=120)
video_id = r.json()["id"]                       # watch it at {platform}/e/{event}?v={video_id}

VibeTube จะเก็บวิดีโอไว้ 1 รายการต่อ projectId และห้อง และโปรเจ็กต์ที่แอปส่งคือโปรเจ็กต์ Google Cloud ดังนั้นการเผยแพร่อีกครั้งจะแทนที่วิดีโอของคุณแทนที่จะเพิ่มวิดีโอ ระบบจะไม่อัปโหลดโปสเตอร์ตัวแทน แต่จะใช้โปสเตอร์จริงเป็นภาพขนาดย่อแบบไบต์ต่อไบต์

การแก้ไขล่าสุดเป็นการกำหนดค่า ไม่ใช่บรรทัดโค้ด 👉🖥 ในหน้า 7b: URL ของ VibeStudio (กรอกข้อมูลจากการติดตั้งใช้งานของ 7a) รหัสกิจกรรมของห้องตามที่ VibeTube สะกด (รหัสที่คุณตั้งค่าไว้ เว้นแต่ผู้สอนจะเปิดรหัสอื่น) และชื่อของคุณ กดเผยแพร่ Workbench จะส่งต่อการคลิกไปยังแอป แอปจะโพสต์คืนนั้น และแถวด้านล่างจะอ่านผลลัพธ์จากแอปและจาก VibeTube โดยแอปจะตอบ คืนนั้นจะจบลงด้วยโปสเตอร์จริง ชื่อเวอร์ชันจะตั้งชื่อห้องเปิด VibeTube จะแสดงรหัสวิดีโอ วิดีโอจะอยู่บนผนัง และภาพขนาดย่อจะเป็นโปสเตอร์ของคุณ การ์ดผลการค้นหาจะแสดงภาพปกบน VibeTube ข้างโปสเตอร์ในแอป

เผยแพร่แล้ว: ภาพปกใน VibeTube คือโปสเตอร์ที่โมเดลรูปภาพวาด

10. สิ่งที่คุณสร้าง

👉🖥 ตอนจบของหน้า 7b จะวาดกราฟทั้งหมด ซึ่งมีโหนด 13 โหนดตั้งแต่แนวคิดไปจนถึงคลิปและโปสเตอร์ของคลิป วางเมาส์เหนือโหนดเพื่อดูว่าโหนดนั้นทำอะไรและเซสชันที่สร้างโหนดนั้นขึ้นมา

เซสชัน

สิ่งที่สอน

เขียนเป็น

2 · การสร้างรูปภาพ

Agent: โมเดล, วิธีการ, เครื่องมือ, โมเดลรูปภาพเป็นเครื่องมือ, สิ่งที่พรอมต์รับประกันไม่ได้

Agent(tools=[...]), function_call / function_response, images.generate

3 · เวิร์กโฟลว์

รายการขอบ การกระจายและการรวม เราเตอร์ที่มีลูปโหมดงานอยู่ด้านหลัง

Workflow START JoinNode Event(route=...) mode="task" finish_task

4 · การนำเข้าข้อมูลหลายรูปแบบ

ผู้อ่าน 1 คนต่อประเภท รูปทรงเส้น 1 รูป คลังข้อมูล 1 ชุด ผู้อ่านคนที่ 3 ตามความหมาย พร้อมสื่อ

generate_content พร้อมส่วนเสียง รูปภาพ และวิดีโอ rag.upload_file, rag.retrieve, media_in

5 · การสร้างวิดีโอ

การระงับ 2 ประเภท กลไกเดียว, พื้นที่เก็บข้อมูลเซสชัน, โปสเตอร์ที่แชร์ตามชื่อ

RequestInput, LongRunningFunctionTool, FunctionResponse(id=...), Event(state=...), การเชื่อมโยงพารามิเตอร์

6 · ความทรงจำ

ความทรงจำข้ามคืน, การโทรกลับ 2 ครั้ง

memories.generate / retrieve, before_model_callback, after_agent_callback

7 · ทำให้ใช้งานได้และเผยแพร่

กราฟภายในแอปบน Cloud Run, ค่ำคืนบน VibeTube

Runner(agent=wf), run_async, เหตุการณ์ที่เซิร์ฟเวอร์ส่ง, POST แบบหลายส่วน 1 รายการ

กฎ 5 ข้อที่กราฟปฏิบัติตามมีดังนี้

  1. ระงับแทนการรอ การหยุดชั่วคราวคือแถวในที่เก็บเซสชัน ไม่ใช่ชุดข้อความ
  2. กลับมาทำงานต่อในลักษณะเดิม โดยใช้รหัสของการโทรที่เปิดอยู่ ไม่ว่าจะเป็นบุคคลหรือกระบวนการที่ตอบรับก็ตาม
  3. แชร์ข้อมูลตามชื่อในสถานะ ไม่ใช่การส่งผ่านเพย์โหลดทุกรายการ
  4. ตัดสินใจด้วยโค้ดก่อนจ่ายเงินซื้อโมเดล นโยบายคือไฟล์ข้อความและเราเตอร์ และจะทำงานก่อนการแสดงผล
  5. อ่านทุกอย่างที่ไม่ใช่ข้อความให้เป็นข้อความก่อนที่จะเข้าถึงคลังข้อมูล โดยมีชื่อไฟล์อยู่ภายในบรรทัดเพื่อให้ไฟล์กลับมาได้

พรอมต์ไม่สามารถรับประกันลำดับ การหยุด หรือการตรวจสอบได้ แต่กราฟทำได้

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

  • VertexAiSessionService สำหรับเซสชันที่คงอยู่ได้นานกว่าอินสแตนซ์ และยกเลิก --session-affinity
  • เว็บฮุคที่ตอบรับการเรียกใช้ของโปรแกรมแสดงผลแทนที่จะเป็น Poller: FunctionResponse(id=...) เหมือนกัน แต่ผู้ส่งต่างกัน
  • RequestInput อีกครั้งก่อนเผยแพร่ สคีมาคือแบบฟอร์ม จึงเป็นโหนดเดียวและไม่มีโค้ดหน้าเว็บ
  • ความคิดเห็น บันทึกเสียง และคลิปใหม่ๆ ของผู้ชมจะได้รับการเพิ่มลงในคลังข้อมูลหลังจากเปิดตัวทุกครั้ง พร้อมกับโปสเตอร์ที่ส่งมาด้วย

ล้างข้อมูล

ทุกอย่างที่ห้องทดลองนี้สร้างขึ้นจะอยู่ในโปรเจ็กต์ที่สร้างขึ้นในขั้นตอนที่ 4 ดังนั้นวิธีล้างข้อมูลที่ประหยัดที่สุดคือการลบโปรเจ็กต์ ใน Cloud Shell ให้ทำดังนี้

gcloud projects delete $(cat ~/project_id.txt)

หากต้องการเก็บโปรเจ็กต์ไว้และนำเฉพาะส่วนที่ต้องชำระเงินออก ให้ทำดังนี้ บริการ Cloud Run (gcloud run services delete vibestudio --region us-central1), คลังข้อมูล RAG และคลังความทรงจำ (ชื่อทรัพยากรอยู่ใน runs/ragcorpus.json และ runs/memorybank.json และหน้า Agent Engine และ RAG Engine ของคอนโซลจะลบออก) และอิมเมจคอนเทนเนอร์ใน Artifact Registry