VibeStudio:使用 ADK 的多模態 Agentic Workflows (萬聖節版)

1. 簡介

VibeStudio, Haunted Edition

你在 VibeTube 經營一個溫馨又恐怖的頻道,每晚都會發布一部 Shorts,直到萬聖節為止。觀眾希望在小房間裡看到小魔法,並透過各種方式傳送給你,包括留言、語音記事、自家鬧鬼抽屜的相片,以及黃昏時門廊的短片。待辦清單中有很多想法。每天晚上,他都要做同樣的工作:研究動態消息中的熱門話題、梳理待辦事項、閱讀觀眾傳送的內容、選擇方向、檢查是否符合頻道政策、撰寫腳本、繪製海報、渲染短片、發布。

生成式模型可以單獨完成每項工作。Gemini 負責研究、聆聽、觀察和撰寫;圖像模型負責繪圖;Veo 負責算繪。他們無法獨自完成的工作包括:依序執行夜間作業、停下來等待你做出決定、在造成損失前拒絕錯誤的方向、等待幾分鐘進行算繪而不開啟任何內容,以及留下記錄。

本程式碼實驗室會將那個晚上變成 ADK Workflow:由節點和邊緣組成的繪製圖表,會自行執行,只在需要您判斷時詢問,並在每次執行結束時,在 VibeTube 上顯示短片和海報。

提示無法保證順序、停止或檢查。圖表可以。

學習目標

  • 一個代理,並將圖片模型做為工具。Agent包含模型、指令和工具。第三個工具會呼叫圖片模型,第一次執行結束時會產生海報。接著是實驗室其餘部分的核心概念:提示詞無法保證順序、停止或檢查。
  • 圖表的結構。Workflow 是邊緣清單。您會建立扇出和彙整 (平行讀取器在 JoinNode 匯合),以及命名自身出口的路由器,並在拒絕出口後方設定工作模式代理,在工具呼叫迴圈中清除方向,然後重新加入主線。
  • 多模態擷取管道。觀眾傳送了四種內容,每種內容都由一位讀者處理:Gemini 轉錄語音記事、為相片加上說明文字、觀看短片並聆聽音軌,每位讀者都會將相同的線條形狀寫入一個 RAG Engine 語料庫。第三個讀取器會根據語意擷取資訊,並將媒體和段落一併傳回。
  • 兩種停權,一種機制。跑步時,會因為有人而停下 (RequestInput:四張卡片,你回答),也會因為工具速度慢而停下 (Veo 位於 LongRunningFunctionTool 後方:收據)。兩者都是指在工作階段儲存庫中,透過 ID 從表單或其他程序恢復的開放式通話。媒體服務台的兩個分支機構會依名稱和州/省分享海報。
  • 跨夜記憶。Memory Bank 的兩個生命週期回呼:before_model_callback 在提案者讀取前,回憶創作者的喜好;after_agent_callback 在腳本編寫者之後,記住今晚的選擇。
  • 版本。圖表顯示應用程式內含 ADK Runner、伺服器傳送事件至頁面、Cloud Run 啟用工作階段相依性,以及一個多部分 POST 要求,將短片上傳至 VibeTube,並以海報做為縮圖。

建構目標

瀏覽器中執行的應用程式:VibeStudio Workbench,這是一個隨附頁面,內含網頁內程式碼編輯器、驗證器 (可讀取磁碟上的檔案和執行作業寫入的工作階段),以及嵌入式 adk web。本程式碼實驗室的七個頁面就是七個課程,編號和部分字母相同,因此兩者保持一致。每個頁面都會開啟一個橫幅,用一句話說明課程內容,以及您在該課程中要執行的三項操作:編輯、執行、驗證。請先閱讀這裡的章節,然後在工作台中編輯。每次編輯後,驗證面板都會顯示實際情況。

各項服務的執行位置:VibeStudio Workbench、後端和 Google Cloud 服務

系統會執行三項作業:Workbench (通訊埠 4600 上的頁面、編輯器、驗證器)、後端 (agent/ 中的 Workflow,以及 stage0_prompt/ 至 stage6_video/ 中每個工作階段一個沙箱應用程式,由 adk web 提供),以及 Google Cloud (Gemini、圖片模型、RAG Engine、Veo、Memory Bank,全部透過 Vertex AI 使用您的憑證存取)。

七場會議

工作階段

建構內容

你的編輯內容

1 · 故事

目前還沒有任何內容,只有夜晚和圖表

—

2 · 圖像生成

一個代理繪製第一張海報

POSTER_TOOL

3 · Workflows

夜晚就像圖表:扇出和彙整、路由器和迴圈

FANOUT、ROUTER

4 · 多模態擷取

觀眾傳送的內容,讀入一個語料庫;第三位讀者

INGEST_READERS、RAG_MEDIA

5 · 影片生成

兩種停權方式;海報上列出州名

GATE、VIDEO_TOOL、DELIVER_RESPONSE、STATE_SHARE

6 · 記憶體

頻道在不同夜晚記住的資訊

MEMORY_RECALL、MEMORY_REMEMBER

7 · 部署及發布

在 Cloud Run 執行 VibeStudio;在 VibeTube 享受夜晚

房間的活動代碼,然後發布

如何使用這個程式碼實驗室。本課程以概念為主:每堂課都會先說明概念,然後顯示變更後的程式碼,並逐步解說。系統不會要求您在這裡編輯檔案,而是在工作台中進行編輯,每次只會提供一個編輯項目,旁邊有提示,下方有驗證面板。閱讀課程內容,然後按照頁面指示操作。

所在位置

VibeTube 上的圖表、主題和房間來自 Lin 的 VibeStudio 鬧鬼版;這是多模態剪輯,重新聚焦於觀眾傳送的內容和兩種懸念,採用 Agent Valley 實驗室的單頁互動格式。工作室是場景;其下的每個機制都是您用於支援管道、內容審查流程或任何其他必須暫停以等待人員和等待緩慢工作的普通機制。

2. 🧰 設定

  • 內容:可付款的帳戶、專案、已安裝的實驗室、經證實可回答的模型,以及在背景中透過一個通訊埠執行的 Workbench。
  • 原因:一個通訊埠上只有一個程序,網頁、編輯器、驗證器、/api/* 和 adk web 共用一個來源,因此這個實驗室不需要第二個終端機或第二個通訊埠。
  • 做法:複製、兩個腳本。第二個會實際呼叫模型、啟動 Workbench,並在您閱讀時開始建立 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 Cloud 專案。步驟 1 會建立可支付費用的帳戶,步驟 4 則會將該帳戶轉換為專案。請依序執行,因為步驟 4 之後的每個步驟都會讀取步驟 4 確定的專案。

1 · 取得已啟用計費功能的帳戶 (~1 分鐘)

請使用已啟用計費功能的 Google 帳戶登入。任何這類帳戶都適用。如果沒有,請開始免費試用,這樣就能取得帳單帳戶,完成步驟 4。如果您參加的是講師指導的實驗室,請先兌換本課程的抵免額,系統會為您建立帳單帳戶。

2 · 開啟 Cloud Shell (~2 分鐘)

👉🖥 點選 Google Cloud 控制台頂端的「啟用 Cloud Shell」,也就是標題列中的終端機圖示。

從 Google Cloud 控制台標題列啟用 Cloud Shell

👉🖥 點選「開啟編輯器」 (資料夾和鉛筆圖示)。Cloud Shell 編輯器隨即開啟,左側會顯示檔案總管,您隨時可以前往該處讀取整個檔案。

開啟「Cloud Shell Editor」(Cloud Shell 編輯器)。

👉🖥 如果今天在任何時候看到授權提示,請點選「授權」繼續操作。

👉🖥 在編輯器中開啟終端機,也就是分頁 1,所有 👉💻 步驟都會在該處執行。分頁 2 是工作台本身,您會在步驟 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 是專案 ID,您的專案 ID 數字會不同。其中會發生四件事:

billing

會採用最新的 [YYYY-MM-DD] GDP Credit: 帳戶 (如有),否則會採用任何其他已啟用的帳單帳戶

project

建立 vibe-studio-XXXX 並連結帳單帳戶

選取

將 ID 寫入 ~/project_id.txt 並執行 gcloud config set project,因此之後不會再要求您輸入 ID

waits

輪詢,直到專案實際允許提供服務為止,最多兩分鐘

👉🖥 控制台現在會顯示同一個專案,專案 ID 位於資訊主頁的「專案資訊」資訊卡:

控制台資訊主頁上的專案 ID

5 · 安裝並設定實驗室 (~4 分鐘)

👉💻

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

一個指令碼,可安全地再次執行,第二次執行會保留您第一次提供的所有答案。系統會在接近開頭處詢問兩個問題,按下 Enter 鍵即可採用預設值:

活動代碼

第 7 堂課中發布影片的 VibeTube 聊天室。輸入老師提供的聊天室代碼,或按下 Enter 鍵進入 sandbox (隨時開放所有人加入)。代碼區分大小寫,且聊天室只接受在指定時間內上傳的內容。指令碼會指出你命名的聊天室是否開放。

顯示名稱

聊天室顯示的名稱,或按下 Enter 鍵,使用帳戶名稱。

兩者都會顯示在.env中,你之後可以在.env或發布頁面中變更。接著,系統會依序自動完成其餘步驟:

uv env

如果缺少 uv,則會安裝 uv,然後 uv venv 和 uv sync 會將 uv.lock 固定的內容 (google-adk、google-genai、vertexai、fastapi、uvicorn) 安裝到 .venv

apis

在步驟 4 選取的專案中啟用六個 API,並確認每個 API 都有回應,然後再繼續:aiplatform (Gemini、圖片模型、Veo、Memory Bank、RAG Engine)、vectorsearch (RAG 語料庫背後的儲存庫)、run、cloudbuild、artifactregistry (工作階段 7)、cloudtrace (應用程式的追蹤記錄)

帳戶

確認專案的預設運算服務帳戶存在,且具備第 7 堂課建構作業和服務所需的角色

.env

寫入 .env,將每次呼叫指向專案中的 Vertex AI (global 端點上的 Gemini、us-central1 中的 Veo、Memory Bank 和 RAG Engine),而且完全不使用 API 金鑰;先前的 .env 會保留為 .env.bak,您手動新增的任何內容都會保留

veo · room

確認 veo-3.1-fast-generate-001 是在 us-central1 放送 (中繼資料讀取,不顯示),並詢問 VibeTube 你的聊天室是否開放

model

會向 Gemini 發出一次實際呼叫,因此不良專案會在設定時失敗,而不是在工作階段內

starter

將要編輯的九個檔案從 starter/ 放到適當位置,並保留其 TODO 行 (僅限缺少的行);您的編輯內容絕不會遭到覆寫

page

安裝並建構 web/ (工作台頁面),然後在背景啟動伺服器,並使用通訊埠 4600

cloud

在背景建立 RAG Engine 語料庫和記憶庫,因此工作階段 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 堂課填入資料。- 兩行也正確,語料庫需要一兩分鐘才能啟動,銀行則需要約二十秒,而且在第 4 堂課之前都不需要這兩者。

👉💻 隨時可以再次執行,查看進度。這項指令會讀取檔案、專案和執行中的伺服器,而不是檢查清單:

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

7 · 開啟工作台 (~1 分鐘)

工作台已在執行中。步驟 5 在背景啟動了這個程序,也就是在通訊埠 4600 上執行一個 uvicorn 程序,提供網頁、每個 /api/* 路由,以及掛接在 /workbench 的 adk web。您不需要啟動任何項目,本實驗室也不會要求您重新啟動:編輯內容會在下次執行時生效。

👉🖥 點選預檢列印的連結,或在 Cloud Shell 中依序點選「網頁預覽」→「變更通訊埠」→「4600」。我們稱之為分頁 2。系統會開啟「Session 1 · The story」。

以下三個指令稍後可能會用到,全部來自 ~/vibe-studio-multimodal:

scripts/stop.sh

停止工作台

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 會開啟「Session 1 · The story」。請看:六項家務圍繞著一個人,分別是研究趨勢、梳理待辦事項、選擇方向、檢查政策、撰寫腳本、渲染短片,而這個循環每晚都會重複。Gemini 可以獨自完成研究、待辦事項和腳本;Veo 可以獨自生成影片;選擇和檢查則由人類負責。

圓環下方是實驗室其餘部分建構的圖表,共有十三個節點,從構想到短片和海報。將滑鼠游標懸停在節點上,即可查看其功能和建構該節點的工作階段。這裡不需要編輯任何內容,頁面的「Generate the first poster」(生成第一張海報) 按鈕就是工作階段 2。

詞彙,所以接下來六頁會不間斷地朗讀:

字詞

這裡的意思

agent

ADK Agent:模型、指令、工具。工作階段 2

node

圖表中的一個步驟,例如 Python 函式、代理、join、路由器

edge

依序執行的節點元組;圖表是這些節點的清單。工作階段 3

工作階段

runs/sessions.db 中單次執行的記錄:事件、狀態,以及任何未關閉的呼叫。講座 5

州/省

節點依鍵名共用的字典;節點寫入 Event(state=...),另一個節點則宣告同名參數。講座 5

記憶體

工作階段結束後仍保留的內容:Memory Bank (透過回呼)。講座 6

4. 2 · 圖像生成:一個代理,並以圖像模型做為工具

第 2 堂課 · 圖像生成

👉🖥 在工作台中,開啟Session 2 · 圖像生成。在圖表之前,最簡單的做法是:一個 Agent,其指令是整晚的散文,三個 Python 函式做為其手,以及一個以圖片結尾的執行。

代理是由模型、指令和工具組成

stage0_prompt/agent.py 使用 ADK 代理的三個欄位定義 root_agent。model 是 agent/platform/config.py 的 Gemini 模型。instruction 是系統提示:一口氣完成六項工作,最後一項是圖片。tools 包含三個函式。

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 事件寫回內容,模型則會繼續處理結果。

三個函式,其中一個會繪製

其中兩人會閱讀研究報告。第三個函式會要求圖像模型生成海報:

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 框架、runs/media/posters 下的一個 PNG,在 /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 傳回的字典會指出已略過海報。👉🖥 在 Workbench 編輯器中,將 TODO 行替換為呼叫圖片模型的程式碼:

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

。驗證資料列讀取檔案:代理上有三個工具,圖像模型呼叫已就緒。👉🖥 點選「Check the workflow loads」,然後點選「Open adk web」並傳送:

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

預期結果和原因

  • 回覆前有兩個工具事件,接著是問題。check_trends 和 read_backlog 執行,因為指令要求檢查和查看,而模型權重中沒有這兩項。回覆中提出方向並請您確認,因為指令要求先取得同意。
  • 第二則訊息會收到所有內容。傳送 skip the questions, describe the video and draw the poster。模型會描述影片、使用自行撰寫的主旨行呼叫 make_poster 一次,並告知 PNG 檔案的儲存位置。驗證面板顯示海報,旁邊有幾列文字:萬聖節場景,風格與頻道一致,約八秒內繪製完成。
  • 為什麼這不是管道。研究成果融入散文,無法引用。這項政策是由撰寫指示的模型認證,而海報是在任何檢查之前繪製。雙方在訊息中達成協議。提示文字並非閘道。本實驗室的其餘部分會將這六項工作拆解成圖表。

頁面最後是「ADK 代理的組成要素」,可做為日後參考:模型、推論時使用的情境、執行動作時使用的工具和協作者、包裝每個呼叫的回呼,以及外部的工作階段和記憶體。

5. 3 · 工作流程:展開和彙整、路由器和迴圈

課程 3 · Workflows

👉🖥 在工作台中,開啟「Session 3 · Workflows」的 3a 和 3b 部分。每個部分都是在沙箱應用程式中建構,因此您可以在 adk web 中單獨執行:stage1_fanout 中的展開、stage3_router 中的路由器及其迴圈。

擴散傳遞及彙整 (3a)

ADK Workflow 是邊緣清單。每個項目都是一個鏈結,也就是依序執行的節點元組。從 START 離開的兩個鏈結會同時執行。以 JoinNode 結尾的鏈結會向其回報,而 join 會等待所有在該處結尾的鏈結,然後再繼續,並傳遞一個以節點名稱為鍵的字典。以 dict 形式編寫的目標會將路由器的路徑名稱對應至節點。工作手冊的 3a 頁面會繪製形狀,並提供寫出邊數的練習。

節點種類

寫法

用途

函式節點

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

純 Python。也可能使用 Event(state=...) 寫入共用狀態。

join 節點

JoinNode(name="join_research")

等待每個傳入鏈結回報,然後將所有輸出內容以一個字典的形式傳遞。

代理節點

Agent(...)

在 single_turn 模式下呼叫模型一次,取得結構化答案,不進行對話。

路由器

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

函式節點,其傳回值會命名傳出邊緣。

手動輸入節點

yield RequestInput(...)

暫停圖表,直到有人填寫表單為止。工作階段 5。

研究展開:兩個讀取器從 START 進入彙整

第一個圖表是第 2 堂課的研究,以節點表示。agent/graph.py 中的 scan_trends 和 read_backlog 是兩個讀取器;read_backlog 也會將今晚的想法寫入共用狀態。stage1_fanout/agent.py 定義彙整,並從名為 EDGES 的清單建構 Workflow,該清單一開始是空的 (預檢的 0 edges)。👉🖥 請填入兩個鏈結:

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

👉🖥 在 adk web 中執行。兩個節點同時亮起,然後加入;開啟 join_research 的事件,並讀取一個包含 scan_trends 鍵和 read_backlog 鍵的字典。未呼叫模型。沒有任何項目會重新格式化 join 的 dict:3b 中 join 後方的節點是代理,而代理節點會接收其 node_input 做為訊息,因此 dict 會以 JSON 格式傳送,模型會直接讀取。

路由器和迴圈 (3b)

Workflow 內的 Agent 會以 single_turn 模式執行:接收前一個節點的輸出內容做為訊息、呼叫模型一次,並回覆一次。使用 output_schema 時,系統會先根據 Pydantic 模型驗證答案,再傳送至下一個節點,因此圖表會傳遞類型物件,而非文字。propose_directions 讀取聯合研究字典,並提出四個候選方案,每個方案都是萬聖節場景。候選 4 是誘餌:標題含有頻道拒絕的字詞,因此路由器會捕捉到。每位候選人都會攜帶 poster_idea,其中一行會列出宣傳影片的單一圖片;編劇會保留這張圖片,媒體部門會在第 5 節課中繪製。

路由器是函式節點,其傳回值會指定執行作業採用的邊緣。不呼叫模型: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")

政策閘:有兩個標示出口的路由器

stage3_router/agent.py 是扇出加上 propose_directions 和 choose_direction,代表你在第 5 堂課中建立的人類門:它會挑選編號以今晚想法 (..., pick 4) 結尾的候選人,否則會挑選第一個,並將挑選結果寫入狀態。scripter是 single_turn 代理,可將核准的方向轉化為Script:標題、說明、標記、開場白、三個鏡頭和海報構想,每個鏡頭都是頻道風格的萬聖節圖片。

拒絕離開的迴圈。quarantine 是以任務模式代理的形式提供。Agent 包含 mode:chat 是根代理與使用者執行的對話迴圈,single_turn 是具有結構化答案的單一呼叫,task 則是代理自行執行的工具呼叫迴圈,直到呼叫內建的 finish_task 為止,其引數由 ADK 從 output_schema 衍生。隔離區會針對拒絕的方向呼叫 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)

出貨時,鏈結會從挑選器直接連到腳本編寫器。👉🖥 將路由器放在兩者之間,並設定兩個出口和從隔離區返回的路徑;三條線取代一條線:

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

👉🖥 執行這兩項路徑。直接傳送構想:choose_direction 採用候選 1,policy_check 輸出 OK,腳本撰寫工具則會撰寫結構化腳本。點選「新工作階段」,然後再次傳送構想,並在結尾加上 , pick 4:候選 4 是誘餌,policy_check 發出 BLOCK,隔離區呼叫工具、重寫遭拒的字詞、呼叫 finish_task,然後將清理後的指示傳回給腳本編寫者。驗證資料列會從工作階段儲存庫讀取兩次執行作業。

工作階段 2 提示中的每個句子現在都有一個節點:研究是兩個讀取器和一個彙整節點,拒絕是路由器和工作代理,腳本則是代理節點。在整個圖表中,替身選擇是兩個函式節點:direction_gate 和 persist_direction,您會看到這兩個節點會一直執行,直到工作階段 5 讓門停止為止。與創作者的協議仍是暫時的,研究也尚未聽取觀眾意見。

6. 4 · 多模態擷取:將觀眾傳送的內容讀取到一個語料庫中

第 4 堂課 · 多模態擷取

👉🖥 在工作台中,開啟「Session 4 · Multimodal ingestion」的 4a 和 4b 部分。觀眾不只寫下評論,每種媒體都有一個讀取器,可將傳送的所有內容轉換成相同格式的文字,並由一個 RAG Engine 語料庫保存所有內容,而扇出中的第三個讀取器則會依據含義搜尋,並傳回媒體。

觀眾傳送的內容 (4a)

agent/comments.md 包含十二則留言,分別來自該頻道六部萬聖節影片。agent/audience/ 則包含其餘內容:八則語音記事 (voice/*.mp3)、十四張檢視者房間的照片 (photo/*.jpg) 和六個短片 (clip/*.mp4),以及一張緩慢平移的照片,並配上檢視者的旁白。4a 頁面開啟,所有內容一覽無遺:每則語音記事、每張相片、每段短片。

擷取後的觀眾格狀檢視畫面:每張資訊卡都會顯示 Gemini 聽到的內容或看到的畫面

RAG Engine 會擷取文字。語音記事、相片或短片只能像過去在 rag.caption_posters 發布的貼文一樣加入語料庫:模型會將其轉換成一串文字,並以該文字命名檔案,這串文字會變成一段文字,當這段文字返回時,檔案也會隨之返回。agent/platform/audience.py 每個種類都有一個讀取器。每個工作都會傳送一個檔案和一個提示給 Gemini,並為每個檔案寫入一行:

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

每種一個讀取器、一個線條形狀、一個語料庫

語料庫。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;三個媒體讀卡機位於一個 # 後方。👉🖥 執行一次擷取作業:輸出內容會顯示 not read (no reader wired in audience.READERS): voice, photo, clip,且只有留言和字幕會進入。然後刪除 #:

    transcribe_voice, caption_photos, watch_clips,

👉🖥 儲存並再次執行擷取作業。讀取器需要約三分鐘才能讀取四十個項目;控制台會一次顯示一行 heard、saw 和 watched,格線中的每張卡片都會填入其行。驗證資料列會讀取檔案、讀取器輸出內容和語料庫:四個讀取器已連線、八則語音記事已聽取、十四張相片已查看、六個短片已觀看、語料庫中有五個檔案。

第三位讀者 (4b)

觀眾意見回饋是研究資料,每次執行都會分享,因此會進入研究資料所在的圖表:做為第三個讀取器進入 join。agent/graph.py 中的 read_feedback 會擷取今晚的想法,向語料庫要求最接近的段落,並回報給 join_research,後者會等待三個段落。邊緣 (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) 會從段落中讀取這些名稱,並將其轉換為網頁可顯示及播放的網址。👉🖥 將媒體與文字一起傳回,讓觀眾傳送的內容與他們說的話一起傳送:

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

👉🖥 執行 stage5_rag 並提供構想。開啟 read_feedback:查詢是您的構想,輸出內容包含五個最接近的段落,以及一個 media 清單,其中包含每個檔案的網址。開啟 join_research:三個按鍵。開啟 propose_directions:候選字詞 1 到 3 在證據中引用了 feedback。驗證面板顯示讀者在資料列下方查看的內容:一個短片、三則語音記事,以及一張冰箱燈光中的鬼魂海報。

第三位讀者查看的內容:擷取段落隨附的媒體

7. 5 · 影片生成:兩種停權,一種機制

第 5 堂課 · 影片生成

👉🖥 在工作台中,開啟 stage6_video 內建的「Session 5 · 影片生成」(第 5a 和 5b 部分),也就是整個圖表。剪輯片段需要幾分鐘,海報則需要幾秒鐘,而執行作業會在途中停止兩次:一次是為您,一次是為 Veo。這兩個停靠站是同一個機制,而服務台的兩個分支會依名稱共用所建立的內容。

兩種停權類型 (5a)

不需等待。必須停止的執行作業會將開放呼叫寫入其工作階段並結束;無論是誰接聽該呼叫,無論是人員還是程序,都會繼續執行。頁面會開啟一個表格:

個人 · direction_gate

用於慢速工具 · render_desk

停止執行作業的使用者

direction_gate 會產生 RequestInput

render_submit 會傳回 {status: pending};LongRunningFunctionTool 會將其設為停靠點

等待的內容

一個人:四張卡片之一

Veo 的作業需要幾分鐘

工作階段中的內容

adk_request_input 呼叫:訊息、表單、酬載

開啟 render_submit 呼叫:含有作業名稱的回條

如何恢復

function_response,其中包含該呼叫的 ID 和您的選擇

含有該通話 ID 的 function_response,並帶有片段的網址

寄件者

adk web,提交表單時

python -m agent.deliver,來自另一個程序,稍後

同時

runs/sessions.db · 一個工作階段 ID · 沒有任何執行作業

同一列、相同 ID · 什麼都不會執行

兩種停權,一個表格

個人:在單一提示中,「要求創作者確認」是建議做法,而工作階段 2 在一則訊息中就略過了這個步驟。在工作流程中,暫停功能是由引擎控制。產生 RequestInput 的節點會暫停執行:ADK 會將名為 adk_request_input 的開放呼叫寫入工作階段,並附上 message、response_schema 和 payload,然後為其提供 interrupt_id,並傳回呼叫端。沒有執行緒等待。只有包含該 ID 且通過結構定義驗證的 function_response,才能恢復工作流程。傳送至工作流程的即時通訊訊息是新的對話,而非答案。結構定義也是表單:adk web 會算繪結構定義,而 VibeStudio 應用程式會將相同的結構定義算繪為四張資訊卡。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})

您的答案會以 node_input 的形式傳送至 persist_direction,這是一個包含一個鍵 pick 的字典。四位候選人則透過另一種方式抵達:閘道將他們寫入 candidates 下方的共用狀態,而 ADK 將狀態索引鍵繫結至同名參數。節點會將選項解析為一個候選項目,並使用 Event(state=...) 寫回 direction、angle、hook 和 user:prefs。前置字元為 user: 的鍵會在該使用者的所有工作階段中保留。

工具速度太慢。Veo 算繪需要幾分鐘的時間,圖表不應等待這麼久。agent/desk.py 中的 render_submit 會啟動工作,並立即傳回狀態為 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 來說,這代表代理的回合結束,工作流程在 render_desk 暫停,而包含 ID 和收據的開放呼叫會成為 runs/sessions.db 中的一列。👉🖥 在 stage6_video/agent.py 中納入工具:

    tools=[LongRunningFunctionTool(render_submit)])

Resume by id, from another process. agent/deliver.py 會在工作階段儲存庫中尋找待處理的呼叫,輪詢 Veo 直到短片存在,將結果寫入 runs/state.json,並將含有該呼叫 ID 的 function_response 傳送至同一個工作階段。👉🖥 建立回覆部分:

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

👉🖥 執行 stage6_video,直到停止兩次。傳送一個想法:執行作業會停止在根據結構定義建立的表單上,酬載中有四個候選項目。在 pick 中輸入 2 並提交,圖表就會在 persist_direction 恢復。請在這裡嘗試終止測試:在表單處暫停、停止伺服器 (分頁 1 中的 scripts/stop.sh)、重新啟動伺服器 (scripts/restart.sh),然後重新開啟工作階段。表單仍會保留,因為暫停是列,而非執行緒。script_ready 之後,render_desk 會呼叫 render_submit、取得收據、回覆 WAITING,然後結束執行;工作階段會保留呼叫 ID。從頁面的控制台或分頁 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

👉🖥 按一下「Refresh adk web」:function_response 會在待處理呼叫後執行,join_media 釋放,store_release 執行。八個驗證資料列會讀取工作階段中的兩個停靠點:閘道的呼叫和您的回覆、轉譯器的收據和傳送。

州和書桌 (5b)

媒體服務台再次採用 3a 的結構,也就是扇出和彙整,並有一個緩慢的分支。script_ready 會透過兩種方式傳送腳本。render_desk提交剪輯片段,執行作業暫停。make_poster 是工作階段 2 工具,以節點形式呈現:腳本的 poster_idea、頻道的風格,以及 Memory Bank 掌握的創作者海報品味 (從狀態繫結為 memory_facts,工作階段 6) 會傳送至圖片模型,幾秒後就會傳回 9:16 PNG。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。👉🖥 將狀態寫入新增至最終 yield:

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

👉🖥 再次執行並提交。開啟 make_poster 的事件:輸出內容中的海報網址,以及 poster_url 的狀態差異。開啟 store_release:訊息中會指出海報名稱。頁面的「This run's session」區塊會顯示與 adk web 的「State」分頁相同的資料列:繼續執行所需的階段 ID、開啟的呼叫,以及節點寫入的索引鍵等 poster_url。驗證面板會並排顯示短片和海報。

短片和海報並排顯示

8. 6 · 記憶:頻道在不同夜晚記住的內容

第 6 堂課 · 記憶體

👉🖥 在工作台中,開啟 stage4_memory 內建的「Session 6 · Memory」(第 6a 和 6b 部分)。第 5 堂課介紹了單次執行的狀態。本課程將說明管道在執行期間記住的內容:圖表透過兩個回呼從外部聽到的內容。

Memory Bank (6a)

Vertex AI Memory Bank 是代管的長期記憶體,可儲存使用者的相關資訊。事實會存在於範圍 (應用程式名稱和使用者 ID) 和您定義的主題中。當你提供 memories.generate 的對話時,系統會擷取符合主題的內容、嵌入並整合現有資訊:建立新事實、更新變更的事實、合併重複的事實。memories.retrieve 會傳回範圍的事實。這兩項呼叫都位於 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.",
}

第三個主題是頻道的圖片風格。在第 5 堂課中,發布代理將資訊寫入記憶體:用一句話描述已出貨的海報並記住,然後在下一個海報的提示詞中使用這句話。

設定作業已開始建立銀行;6a 頁面的連線列會顯示是否已建立。透過頁面按鈕或終端機:python -m agent.platform.bank 會建立資料庫和主題,並將資源名稱快取到 runs/memorybank.json;python -m agent.platform.bank load 會植入四個過去的工作階段 (兩個有樣式規則的動物時代、一個小工具時代、一個最近的奇幻時代);python -m agent.platform.bank list 會顯示轉錄稿的內容:整合的事實陳述,而非轉錄稿。

回呼 (6b)

記憶體不是節點。回呼是傳遞至 Agent 的函式;ADK 會在固定時間點呼叫回呼,並提供即時情境,傳回 None 則可讓正常作業繼續進行。三組:代理回覆前後 (before_agent_callback、after_agent_callback,搭配 CallbackContext)、每次模型呼叫前後 (before_model_callback 搭配 LlmRequest、after_model_callback 搭配 LlmResponse)、每次工具呼叫前後。護欄、遙測、快取和情境注入會放在這裡,不會在圖表中顯示為節點。

在 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,寫下一個關於選擇的句子,然後交給 memories.generate。👉🖥 連結:

    output_schema=Script,
    after_agent_callback=remember_pick)

👉🖥 執行時不輸入訊息,並挑選一張卡片。開啟 propose_directions 事件:要求包含三個時代,候選項目 1 到 3 偏向目前的品味,而趨勢則顯示其他內容。在腳本編寫器之後,按一下「顯示記憶庫」:記憶體已變更或新增,用於今晚的精選內容。第二晚執行時,會記住第一晚的結果。

9. 7 · 部署及發布:應用程式中的圖表、VibeTube 上的夜晚

第 7 堂課 · 部署及發布

👉🖥 在工作台中,開啟「Session 7 · Deploy and publish」(第 7 堂課:部署及發布) 的 7a 和 7b 部分。

部署 VibeStudio (7a)

在沙箱中,adk web 驅動了圖表。VibeStudio 透過 ADK 的 Runner 驅動,在具有專屬事件迴圈的背景工作執行緒上執行,並將每個事件摺疊成一個執行狀態,頁面會透過伺服器傳送事件讀取該狀態:讀取器、你選擇的四張卡片、海報到達時、轉譯時。閘道的回覆和轉譯器的傳送作業是同一個呼叫,也就是 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 檢查。agent/graph.py 中的完整邊緣清單包含本程式碼實驗室的所有結構,共十三個節點;清單已填妥,您只需閱讀,不需輸入:

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

👉🖥 使用工作台的「Deploy」按鈕部署,或 👉💻 透過分頁 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 建構容器,並在一個步驟中部署容器;第一次建構作業需要四到五分鐘。--session-affinity 搭配一個暖執行個體,會在保留該執行個體的執行個體上保留暫停的執行作業:工作階段 5 的工作階段儲存區。在較大型的部署作業中,工作階段存放區會移至 VertexAiSessionService,而相似度會消失。建構作業和服務會以專案的預設運算服務帳戶身分執行,設定程序會授予該帳戶所需角色。

👉🖥 開啟服務網址,在應用程式中執行一晚:輸入構想、選擇一個選項,然後等待短片和海報。「記錄」分頁會保留這兩項資訊。

發布 (7b)

發布作業是從應用程式向 VibeTube 上的聊天室端點傳送多部分 POST,格式為 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 會為每個projectId和聊天室保留一部影片,而應用程式傳送的專案是您的 Google Cloud 專案,因此再次發布會取代影片,而非新增影片。系統不會上傳替代海報,而是會將實際海報做為縮圖,逐位元組上傳。

最後一項編輯是設定,而非程式碼。👉🖥 在 7b 頁面:VibeStudio 網址 (從 7a 的部署作業填入)、聊天室的活動代碼 (拼寫方式與 VibeTube 相同,即您在設定時提供的代碼,除非講師已開啟其他代碼),以及您的姓名。按一下「發布」。工作台會將點擊動作傳送至應用程式,應用程式會發布夜晚,下方的資料列則會讀取應用程式和 VibeTube 的結果:應用程式會回答,夜晚會以實際海報結束,產品代號會命名開放式房間,VibeTube 會傳回影片 ID,影片會顯示在牆上,縮圖則是您的海報。結果資訊卡會顯示 VibeTube 上的縮圖,旁邊則是應用程式中的海報。

發布:VibeTube 上的縮圖是圖片模型繪製的海報

10. 建構內容

👉🖥 7b 頁面的結尾會繪製整個圖表,從構想到短片和海報,共有十三個節點;將滑鼠游標懸停在節點上,即可查看其功能和建立該節點的工作階段。

工作階段

所學內容

寫法

2 · 圖像生成

代理:模型、指令、工具;圖像模型做為工具;提示無法保證的事

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

3 · Workflows

邊緣清單、扇出和彙整、後方有工作模式迴圈的路由器

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

4 · 多模態擷取

每種讀者一個,一個線條形狀,一個語料庫;第三個讀者按意義,與媒體

generate_content,包含音訊、圖片和影片部分,rag.upload_file、rag.retrieve、media_in

5 · 影片生成

兩種暫停機制,一種機制;工作階段儲存區;依名稱共用的海報

RequestInput、LongRunningFunctionTool、FunctionResponse(id=...)、Event(state=...)、參數繫結

6 · 記憶體

跨夜記憶,兩次回呼

memories.generate / retrieve、before_model_callback、after_agent_callback

7 · 部署及發布

Cloud Run 上的應用程式內圖表;VibeTube 上的夜晚

Runner(agent=wf)、run_async、伺服器傳送事件、一個多部分 POST

圖表遵循的五項規則:

  1. 暫停而非等待。暫停是工作階段儲存庫中的一列,而非執行緒。
  2. 以相同方式繼續所有作業:根據開放呼叫的 ID,無論是人員或程序回覆。
  3. 在狀態中依名稱共用資料,而不是透過每個酬載執行緒。
  4. 先用程式碼測試,再決定是否付費使用模型。這項政策是文字檔案和路由器,會在轉譯前執行。
  5. 在非文字內容進入語料庫前,將其轉換為文字,並在行內加入檔案名稱,以便擷取檔案。

提示無法保證順序、停止或檢查。圖表可以。

後續步驟

  • VertexAiSessionService:用於工作階段存續時間超過執行個體的情況,並捨棄 --session-affinity。
  • 回應轉譯呼叫的 Webhook (而非輪詢器):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 中的容器映像檔。