‫VibeStudio – תהליך עבודה מבוסס-סוכן מולטי-מודאלי עם ADK, מהדורת ליל כל הקדושים

1. מבוא

‫VibeStudio, מהדורת ליל כל הקדושים

יש לך ערוץ נעים ומפחיד ב-VibeTube: סרטון Shorts אחד בכל לילה עד ליל כל הקדושים. הקהל רוצה קסם קטן בחדרים קטנים, והוא שולח אותו בכל דרך אפשרית – כתגובות, כהודעות קוליות, כתמונות של המגירות הרדופות שלהם וכקליפים של המרפסת שלהם בשקיעה. בצבר המשימות יש הרבה רעיונות. כל ערב מוקדש לאותן מטלות: לחקור מה מעסיק את הפיד, לסרוק את צבר המשימות, לקרוא את מה שהקהל שלח, לבחור כיוון, לבדוק אותו מול מדיניות הערוץ, לכתוב את התסריט, לצייר את הפוסטר, לעבד את הקליפ, לפרסם.

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

ב-Codelab הזה, הערב הזה הופך ל-ADK Workflow: תרשים מצויר של צמתים וקצוות שפועל בעצמו, שואל אתכם רק איפה נדרש שיקול הדעת שלכם, ומסיים כל הפעלה עם קליפ והפוסטר שלו ב-VibeTube.

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

מה תלמדו

  • סוכן אחד, ומודל התמונות ככלי. Agent הוא מודל, הוראה וכלים. הכלי השלישי מפעיל את מודל התמונות, וההרצה הראשונה מסתיימת ביצירת פוסטר. ואז המשפט שמניע את שאר ה-Lab: הנחיה לא יכולה להבטיח סדר, עצירה או בדיקה.
  • המבנים שמהם מורכב תרשים. ‫Workflow היא רשימת קצוות. אתם בונים את הפיצול והאיחוד (קוראים מקבילים שנפגשים ב-JoinNode) ונתב שנותן שם ליציאה שלו, עם סוכן במצב משימה מאחורי היציאה שנדחתה, שמנקה כיוון בלולאה של קריאות לכלים ומצטרף מחדש לקו הראשי.
  • צינור עיבוד נתונים מולטי-מודאלי. ארבעה סוגים של דברים שהקהל שלח, קורא אחד לכל סוג: Gemini מתמלל את ההודעות הקוליות, מוסיף כתוביות לתמונות, צופה בקליפים ושומע את הפסקולים שלהם, וכל קורא כותב את אותה צורת שורה בקורפוס אחד של מנוע RAG. קורא שלישי בתהליך ה-fan-out מאחזר לפי משמעות ומחזיר את המדיה עם הפסקאות.
  • שני סוגים של השעיה, מנגנון אחד. הריצה נעצרת בגלל אדם (RequestInput: ארבעה קלפים, אתם עונים) ובגלל כלי איטי (Veo מאחורי LongRunningFunctionTool: קבלה). שניהם אותו דבר: קריאה פתוחה במאגר הסשנים, שחודשה לפי המזהה שלה, מהטופס או מתהליך אחר. שני הסניפים של דלפק המדיה משתפים את הכרזה לפי שם, במדינה.
  • זיכרון לאורך כמה לילות. שתי קריאות חוזרות במחזור החיים של Memory Bank: ‏before_model_callback מאחזר את הטעם של היוצר לפני שהמציע קורא, after_agent_callback זוכר את הבחירה של הערב אחרי התסריטאי.
  • הגרסה. התרשים בתוך אפליקציה עם ADK Runner, אירועים שנשלחו מהשרת לדף, Cloud Run עם זיקה לסשן (session affinity) ו-POST אחד עם כמה חלקים שמעלה את הקליפ ל-VibeTube עם הפוסטר כתמונה ממוזערת.

מה תפַתחו

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

מה פועל ואיפה: VibeStudio Workbench, הקצה העורפי ושירותי Google Cloud

שלושה דברים פועלים: Workbench (הדף ביציאה 4600, כלי העריכה, כלי האימות), ה-backend שלכם (Workflow ב-agent/ ואפליקציית ארגז חול אחת לכל סשן ב-stage0_prompt/ עד stage6_video/, שמוצגת על ידי adk web), ו-Google Cloud (‫Gemini, מודל התמונות, RAG Engine, ‏Veo, ‏Memory Bank – כולם נגישים דרך Vertex AI עם פרטי הכניסה שלכם).

שבעת המפגשים

סשן

מה אתם מפתחים

העריכות שלך

‫1 · הסיפור

עדיין אין כלום – הלילה, והתרשים שהוא הופך להיות

—

‫2 · יצירת תמונות

סוכן אחד מצייר את הכרזה הראשונה

POSTER_TOOL

‫3 · תהליכי עבודה

הלילה כגרף: הפיצול והחיבור, הנתב והלולאה שלו

FANOUT, ROUTER

‫4 · הטמעה מולטי-מודאלית

מה שהקהל שלח, נקרא בקורפוס אחד; הקורא השלישי

INGEST_READERS, RAG_MEDIA

‫5 · יצירת סרטונים

שני סוגים של השעיה; הפוסטר ששותף לפי שם במדינה

GATE, VIDEO_TOOL, DELIVER_RESPONSE, STATE_SHARE

‫6 · זיכרון

מה נשמר בערוץ בין הלילות

MEMORY_RECALL, MEMORY_REMEMBER

‫7 · פריסה ופרסום

‫VibeStudio ב-Cloud Run; הלילה ב-VibeTube

את קוד האירוע של החדר, ולפרסם

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

איפה המידע הזה נמצא

התרשים, העיצוב והחדר ב-VibeTube מגיעים מ-Lin's Haunted Edition של VibeStudio. זהו החיתוך המולטי-מודאלי, שמתמקד במה שהקהל שלח ובשני סוגי ההשהיה, בפורמט האינטראקטיבי של דף אחד של מעבדות Agent Valley. הסטודיו הוא רקע; כל מנגנון שמתחתיו הוא המנגנון הרגיל שבו הייתם משתמשים עבור צינור עיבוד נתונים לתמיכה, תהליך בדיקת תוכן או כל דבר אחר שצריך להשהות עבור אדם ולהמתין למשימה איטית.

2. 🧰 הגדרה

  • מה – חשבון שיכול לשלם, פרויקט, ה-Lab שהותקן, המודל שהוכח שהוא עונה וסביבת העבודה שפועלת ברקע ביציאה אחת.
  • למה – תהליך אחד ביציאה אחת: הדף, הכלי לעריכה, כלי האימות, /api/* ו-adk web חולקים מקור, כך ששום דבר בשיעור ה-Lab הזה לא דורש מסוף שני או יציאה שנייה.
  • איך — שכפול, שני סקריפטים. החלק השני מבצע קריאה אמיתית למודל, מפעיל את סביבת העבודה ומתחיל ליצור את מאגר ה-RAG ואת ה-Memory Bank בזמן שאתם קוראים, כדי שהם יהיו מוכנים בסשנים 4 ו-6.

כל מה שקורה ב-Lab הזה פועל ב-Cloud Shell – gcloud, ‏ Python, ‏ Node, ‏ ffmpeg ו-git כבר מותקנים שם, ואפשר לגשת ל-Gemini, למודל התמונות, ל-Veo, ל-RAG Engine ול-Memory Bank דרך Vertex AI באמצעות פרטי הכניסה שלכם. בשיעור ה-Lab הזה אין מפתח API, ואין צורך להתקין שום דבר במחשב הנייד. מחשב נייד עם Google Cloud SDK, ‏ Python 3.12 ו-Node 20+‎ פועל באותו אופן.

יש דבר אחד שהשיעור הזה לא יכול לתת לכם: פרויקט בענן של Google עם חיוב. בשלב 1 יוצרים חשבון שדרכו אפשר לשלם על הפרויקט, ובשלב 4 הופכים את החשבון הזה לפרויקט. חשוב לבצע את השלבים לפי הסדר – כל שלב אחרי שלב 4 קורא את הפרויקט שנקבע בשלב 4.

‫1 · יצירת חשבון עם חיוב (כדקה)

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

‫2 · פתיחת Cloud Shell (כ-2 דקות)

👉🖥 לוחצים על Activate Cloud Shell בחלק העליון של מסוף Google Cloud – זה הסמל בצורת מסוף בכותרת.

הפעלת Cloud Shell מהכותרת של מסוף Google Cloud

👉🖥 לוחצים על Open Editor (סמל תיקייה פתוחה עם סמל עיפרון). הפעולה הזו פותחת את Cloud Shell Editor עם סייר קבצים בצד ימין – לשם עוברים כשרוצים לקרוא קובץ שלם.

פתיחת Cloud Shell Editor

👉🖥 אם תתבקשו לאשר בשלב כלשהו היום, לחצו על Authorize כדי להמשיך.

👉🖥 פותחים טרמינל בתוך הכלי לעריכת קוד – כלומר כרטיסייה 1, וכל שלב 👉💻 פועל שם. כרטיסייה 2 היא סביבת העבודה עצמה, ואתם פותחים אותה בשלב 7. אם הטרמינל עדיין לא מופיע בתחתית המסך, פותחים אותו באמצעות View → Terminal:

פותחים טרמינל חדש ב-Cloud Shell Editor.

👉💻 בודקים לאיזה חשבון מחוברים – זה צריך להיות החשבון משלב 1:

gcloud auth list

‫3 · קבלת ה-Lab (כדקה)

👉💻 משכפלים את המאגר:

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 הוא מזהה הפרויקט – בפרויקט שלכם יהיו ספרות אחרות. יש שם ארבעה דברים:

חיוב

אם יש לך חשבון [YYYY-MM-DD] GDP Credit:, המערכת תבחר את החשבון החדש ביותר. אחרת, היא תבחר חשבון אחר לחיוב שפתוח.

פרויקט

יוצר את vibe-studio-XXXX ומקשר אליו את החשבון לחיוב

בוחרים

כותב את המזהה ב-~/project_id.txt ומריץ את gcloud config set project, כך שלא תתבקשו להקליד אותו שוב

waits

מבצעת סקרים עד שהפרויקט מקבל אישור להצגת מודעות, למשך עד שתי דקות

👉🖥 אותו פרויקט נמצא עכשיו במסוף – המזהה שלו נמצא בכרטיס Project info במרכז הבקרה:

מזהה הפרויקט בלוח הבקרה של המסוף

‫5 · התקנה והגדרה של ה-Lab‏ (כ-4 דקות)

👉💻

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

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

קוד אירוע

החדר ב-VibeTube שבו הסרטון שלכם פורסם בסשן 7. מקלידים את קוד החדר שקיבלתם מהמדריך או המדריכה, או לוחצים על Enter כדי להיכנס לחדר sandbox, שפתוח לכולם בכל שעה. הקודים תלויי אותיות רישיות, ואפשר להעלות קבצים לחדר רק כשהוא פתוח. הסקריפט מציין אם החדר שציינתם פתוח.

השם המוצג

את השם שמופיע בקרדיט של החדר, או לוחצים על Enter כדי להשתמש בשם שמופיע בחשבון.

שניהם מגיעים אל .env ואפשר לשנות אותם מאוחר יותר, ב-.env או בדף הפרסום. לאחר מכן, המערכת מבצעת את שאר הפעולות בעצמה, לפי הסדר הבא:

uv env

מתקין את uv אם הוא חסר, ואז uv venv ו-uv sync מתקינים בדיוק את מה ש-uv.lock מצמיד – google-adk, ‏ google-genai, ‏ vertexai, ‏ fastapi, ‏ uvicorn – לתוך .venv

apis

מפעיל 6 ממשקי API בפרויקט שנבחר בשלב 4, ומאשר את התשובה של כל אחד מהם לפני שהוא ממשיך: aiplatform (‫Gemini, מודל התמונות, Veo, ‏Memory Bank, ‏RAG Engine), ‏vectorsearch (החנות שמאחורי קורפוס RAG), ‏run, ‏cloudbuild, ‏artifactregistry (סשן 7), ‏cloudtrace (המעקבים של האפליקציה)

חשבון

מוודא שחשבון השירות שמשמש כברירת מחדל ב-Compute Engine קיים בפרויקט, ושהוא כולל את התפקידים שנדרשים ל-build ולשירות של סשן 7

‎.env

כותב את .env ומפנה כל קריאה ל-Vertex AI בפרויקט שלכם – Gemini בנקודת הקצה (endpoint) של global, ‏Veo, ‏Memory Bank ו-RAG Engine ב-us-central1 – ובלי מפתח API בשום מקום. קובץ .env קודם נשמר בתור .env.bak וכל מה שהוספתם באופן ידני נשמר

veo · room

מאשרת ש-veo-3.1-fast-generate-001 מוצג ב-us-central1 (קריאת מטא-נתונים, ללא עיבוד), ושואלת את VibeTube אם החדר שלך פתוח

מודל

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

starter

הפקודה הזו מציבה את תשעת הקבצים שתערכו במקום מ-starter/, ושומרת על שורות ה-TODO שלהם – רק אלה שחסרות. העריכות שלכם אף פעם לא יוחלפו.

דף

מתקין ויוצר את web/, דף סביבת העבודה, ואז מפעיל את השרת ברקע ביציאה 4600

cloud

מתחיל ליצור את הקורפוס של RAG Engine ואת ה-Memory Bank ברקע, כדי שהם יהיו מוכנים לסשנים 4 ו-6

preflight

מפעילה את scripts/preflight.py ומדפיסה את הקישור לפתיחה

‫6 · קוראים את ההנחיות לפני הטיסה (כדקה)

הדבר האחרון שהודפס בשלב 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 · פתיחת סביבת העבודה (כדקה)

סביבת העבודה כבר פועלת. בשלב 5 התהליך התחיל ברקע, תהליך uvicorn אחד ביציאה 4600 שמציג את הדף, כל נתיב /api/* ו-adk web שנטען ב-/workbench. אין מה להפעיל, ואין שום דבר בשיעור ה-Lab הזה שמבקש מכם להפעיל אותו מחדש: עריכה מופיעה בהפעלה הבאה.

👉🖥 לוחצים על הקישור שהודפס בבדיקה המקדימה, או ב-Cloud Shell: תצוגה מקדימה באינטרנט ← שינוי יציאה ← 4600. נקרא לה כרטיסייה 2. הוא נפתח במפגש 1 · הסיפור.

שלוש הפקודות שאולי תצטרכו בהמשך, כולן מ-~/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 נפתחת בקטע 1 · הסיפור. הוא קרא את זה: שישה שלבים מסודרים במעגל סביב אדם אחד – לחקור את הטרנד, לעבור על החומרים, לבחור כיוון, לבדוק את המדיניות, לכתוב את התסריט, לערוך את הקליפ – והמעגל חוזר על עצמו כל לילה. ‫Gemini יכול לבצע את המחקר, להכין את התסריט ולטפל בצבר המשימות בעצמו. Veo יכול לעבד את הסרטון בעצמו. הבחירה והבדיקה הן של האדם.

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

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

מילים

מה המשמעות של זה כאן

agent

‫ADK Agent: מודל, הוראה, כלים. סשן 2

node

שלב אחד בתרשים – פונקציית Python, נציג, איחוד, נתב

edge

קבוצה של צמתים שפועלים לפי הסדר. התרשים הוא רשימה של הצמתים האלה. סשן 3

סשן

רשומה של הפעלה אחת ב-runs/sessions.db: האירועים שלה, המצב שלה וכל שיחה שנותרה פתוחה. מפגש 5

state

מילון שהצמתים משתפים לפי שם המפתח; צומת אחד כותב Event(state=...), צומת אחר מצהיר על פרמטר באותו שם. מפגש 5

זיכרון

מה נשמר אחרי סשן – Memory Bank, באמצעות קריאות חוזרות (callbacks). מפגש 6

4. ‫2 · יצירת תמונות – נציג אחד, ומודל התמונות ככלי

סשן 2 · יצירת תמונות

‫👉🖥 ב-Workbench, פותחים את Session 2 · יצירת תמונות. לפני הגרף, הדבר הכי פשוט שיכול לעבוד: Agent אחד שההוראה שלו היא כל הלילה כפרוזה, שלוש פונקציות Python כידיים שלו, והרצה שמסתיימת בתמונה.

סוכן הוא מודל, הוראה וכלים

‫stage0_prompt/agent.py מגדיר את root_agent באמצעות שלושה מהשדות של סוכן ADK. ‫model הוא מודל Gemini של agent/platform/config.py. ‫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, קובץ PNG אחד מתחת ל-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 מחזיר מילון שאומר שהפוסטר דולג. 👉🖥 בעורך של Workbench, מגדירים קריאה למודל התמונות במקום השורה TODO:

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

שמירה. שורות האימות קוראות את הקובץ: שלושה כלים בנציג, הקריאה למודל התמונה במקום. ‫👉🖥 לוחצים על Check the workflow loads (בדיקת העומסים בתהליך העבודה), ואז על Open adk web (פתיחת 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 נשמר. בחלונית האימות מוצג הפוסטר לצד השורות: סצנת ליל כל הקדושים בסגנון של הערוץ, שצוירה תוך כשמונה שניות.
  • למה זה לא ערוץ. המחקר משולב בטקסט שאי אפשר לצטט. המדיניות אושרה על ידי המודל שכתב את ההנחיה, והכרזה צוירה לפני שמישהו בדק אותה. ההסכם הוזכר בהודעה אחת. טקסט ההנחיה הוא לא שער. בשאר שיעור ה-Lab נפרק את שש המשימות האלה לתרשים מצויר.

הדף מסתיים בקטע ממה מורכב סוכן ADK, חומר עזר שאפשר לחזור אליו: המודל, ההקשר שבו הוא מסיק מסקנות, הכלים ושותפי העריכה שדרכם הוא פועל, הקריאות החוזרות (callbacks) שעוטפות כל קריאה, והסשן והזיכרון שקיימים מחוץ לו.

5. ‫3 · תהליכי עבודה – פיצול ואיחוד, הנתב והלולאה

מפגש 3 · תהליכי עבודה

👉🖥 בסביבת העבודה, פותחים את Session 3 · Workflows (סשן 3 – תהליכי עבודה), חלקים 3a ו-3b. כל חלק נבנה באפליקציית ארגז חול (sandbox) כדי שתוכלו להריץ אותו ב-adk web בפני עצמו: ה-fan-out ב-stage1_fanout, הנתב והלולאה שלו ב-stage3_router.

פיצול ואיחוד (3א)

‫ADK Workflow הוא רשימת קצוות. כל רשומה היא שרשרת, קבוצה של צמתים שפועלים לפי הסדר. שני מסלולים שיוצאים מ-START פועלים במקביל. שרשרת שמסתיימת ב-JoinNode מדווחת לו, והאיחוד ממתין לכל שרשרת שמסתיימת שם לפני שהוא ממשיך, ומעביר מילון אחד עם מפתח לפי שם הצומת. יעד שכתוב כמילון ממפה שמות של מסלולים בנתב לצמתים. בדף 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 ל-JOIN

התרשים הראשון הוא המחקר של סשן 2 כצמתים. ‫scan_trends ו-read_backlog ב-agent/graph.py הם שני הקוראים. read_backlog גם כותב את הרעיון של הערב למצב המשותף. ‫stage1_fanout/agent.py מגדיר את הצירוף ויוצר את ה-Workflow שלו מרשימה בשם EDGES, שנשלחת ריקה (ה-0 edges של ה-preflight). 👉🖥 מלאו אותה בשתי השרשראות:

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

👉🖥 מריצים אותו ב-adk web. שני צמתים נדלקים יחד, ואז מצטרפים; פותחים את האירוע של join_research וקוראים מילון אחד עם מפתח scan_trends ומפתח read_backlog. לא בוצעה קריאה למודל. שום דבר לא משנה את הפורמט של המילון של הצומת המאחד: הצומת שאחריו, בשלב 3b, הוא סוכן, וצומת סוכן מקבל את ה-node_input שלו כהודעה, כך שהמילון מגיע כ-JSON שהמודל קורא ישירות.

הנתב והלולאה (3b)

‫Agent בתוך Workflow פועל במצב 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 הוא ה-fan-out פלוס 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)

כשהמכשיר נשלח, השרשרת פועלת מהמפרט ישירות ל-scripter. 👉🖥 שמים את הנתב ביניהם, עם שתי היציאות שלו והדרך חזרה מההסגר. שלוש שורות מחליפות אחת:

           (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 מכיל את הכול, וקורא שלישי ב-fan-out מחפש אותו לפי משמעות ומחזיר את המדיה.

מה שהקהל שלח (4א)

‫agent/comments.md מכיל 12 תגובות שנכתבו מתחת ל-6 סרטוני ליל כל הקדושים של הערוץ. agent/audience/ מכיל את השאר: שמונה פתקים קוליים (voice/*.mp3), ארבע עשרה תמונות של החדרים של הצופים (photo/*.jpg) ושישה קליפים (clip/*.mp4), תנועת פנורמה איטית מעל תמונה עם הקול של הצופה מעליה. הדף של 4a נפתח עם כל התוכן: כל ההודעות הקוליות מושמעות, כל התמונות מוצגות וכל הקליפים מופעלים.

רשת המשתתפים אחרי העיבוד: כל כרטיס מראה מה Gemini שמע או ראה

מנוע RAG מאחזר טקסט. הודעה קולית, תמונה או קליפ יכולים להצטרף לקורפוס רק כמו שהפוסטים הקודמים הצטרפו ב-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 ...

קורא אחד לכל סוג, צורת קו אחת, קורפוס אחד

הקורפוס. מנוע ה-RAG של Vertex AI מאחזר מידע ממסמכים: קורפוס מחולק לפסקאות של כ-120 אסימונים, כל פסקה מוטמעת באמצעות text-embedding-005, ושאילתה מוטמעת באותו אופן ומקבלת תשובה עם הפסקאות הקרובות ביותר לפי המשמעות, ולא לפי מילים משותפות. "רוחות רפאים קטנות ונעימות בבית" מוצא את התגובה שמבקשת קסם קטן בחדרים קטנים, את ההודעה הקולית על העטלף שמחבק את הנורה ואת הקליפ שבו הפסקול אומר שרוח הרפאים היא נייר.

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,

👉🖥 שומרים ומפעילים שוב את ההטמעה. הקוראים מסיימים את 40 הפריטים תוך שלוש דקות בערך. במסוף מופיעות השורות heard, saw ו-watched, אחת אחרי השנייה, וכל כרטיס ברשת מתמלא בשורה שלו. שורות האימות קוראות את הקובץ, את הפלט של הקוראים ואת הקורפוס: ארבעה קוראים מחוברים, שמונה הערות קוליות נשמעו, ארבע עשרה תמונות נראו, שישה קליפים נצפו, חמישה קבצים בקורפוס.

הקורא השלישי (4b)

משוב מהקהל הוא מחקר שמשותף בכל הרצה, ולכן הוא נכנס לתרשים במקום שבו המחקר נכנס: כקורא שלישי ב-join. ‫read_feedback ב-agent/graph.py לוקח את הרעיון של הלילה, מבקש מהמאגר את הקטעים הכי קרובים ומדווח ל-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) קורא את השמות האלה מהקטעים והופך אותם לכתובות URL שהדף יכול להציג ולהפעיל. 👉🖥 החזירו את המדיה עם הקטעים, כדי שהדברים שהקהל שלח יועברו יחד עם מה שהם אמרו:

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

👉🖥 מריצים את stage5_rag עם רעיון. פותחים את read_feedback: השאילתה היא הרעיון שלכם, הפלט כולל את חמשת הקטעים הקרובים ביותר ורשימת media עם כתובת URL לכל קובץ. פתיחת join_research: שלושה מקשים. פותחים את propose_directions: מועמדים 1 עד 3 מצטטים את feedback בראיות שלהם. בחלונית האימות מוצג מה שהקורא הסתכל עליו מתחת לשורות: קליפ, שלוש הערות קוליות ופוסטר של הרוח באור של המקרר.

מה המודל השלישי בודק: המדיה שמצורפת לפסקאות שנשלפו

7. ‫5 · יצירת סרטונים – שני סוגים של השעיה, מנגנון אחד

אימון 5 · יצירת סרטונים

👉🖥 בסביבת העבודה, פותחים את Session 5 · יצירת סרטונים, חלקים 5a ו-5b, שנוצרו ב-stage6_video, כל התרשים. הקליפ נוצר תוך כמה דקות והפוסטר תוך כמה שניות, והתהליך נעצר פעמיים בדרך: פעם אחת בשבילכם ופעם אחת בשביל Veo. שתי התחנות הן מנגנון אחד, ושני הסניפים של המשרד חולקים את מה שהם יצרו לפי שם.

Two kinds of suspension (5a)

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

לאדם · direction_gate

לכלי איטי · render_desk

מי מפסיק את ההרצה

‫direction_gate מניב RequestInput

‫render_submit מחזיר את {status: pending}; LongRunningFunctionTool הופך את זה לעצירה

מה הוא ממתין לו

אדם: אחד מתוך ארבעה קלפים

משימה איטית: הפעולה של Veo, דקות

מה נכלל בסשן

קריאה פתוחה ל-adk_request_input: ההודעה, הטופס, המטען הייעודי (payload)

קריאה פתוחה ל-render_submit: הקבלה עם שם הפעולה

איך ממשיכים את ההפעלה

‫function_response עם המזהה של השיחה, שכולל את הבחירה שלכם

‫function_response עם המזהה של השיחה הזו, שכולל את כתובת ה-URL של הקליפ

מי שולח את האימייל

‫adk web, כששולחים את הטופס

‫python -m agent.deliver, מתהליך אחר, מאוחר יותר

שם הוא נמצא בינתיים

runs/sessions.db · מזהה סשן אחד · שום דבר לא פועל

אותה שורה, אותו מזהה · שום דבר לא פועל

שני סוגים של השעיה, טבלה אחת

לאדם. בהנחיה אחת, "בקש מהיוצר לאשר" היא המלצה, ובסשן 2 היא לא בוצעה בהודעה אחת. בתהליך עבודה, ההשהיה היא של המנוע. צומת שמניב RequestInput משהה את ההרצה: ADK כותב קריאה פתוחה בשם adk_request_input בסשן עם message, response_schema ו-payload, נותן לה interrupt_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})

התשובה שלכם מגיעה אל persist_direction בתור node_input, מילון עם מפתח אחד, pick. ארבעת המועמדים מגיעים בדרך אחרת: השער כתב אותם למצב משותף ב-candidates, ו-ADK מקשר מפתח מצב לפרמטר עם אותו שם. הצומת פותר את הבחירה למועמד אחד וכותב בחזרה את 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: התור של הסוכן מסתיים, תהליך העבודה מושהה ב-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 עד שהוא נעצר פעמיים. שליחת רעיון: ההפעלה נעצרת בטופס שנוצר מהסכימה, וארבעת המועמדים נמצאים במטען הייעודי (payload) שלו. מקלידים 2 ב-pick ושולחים. התרשים ימשיך ב-persist_direction. נסו את בדיקת ההשבתה כאן: השעו את הטופס, עצרו את השרת (scripts/stop.sh בכרטיסייה 1), הפעילו אותו שוב (scripts/restart.sh), פתחו מחדש את הסשן. הטופס עדיין קיים, כי השהיה היא שורה, לא שרשור. אחרי script_ready, render_desk קורא ל-render_submit, מקבל את הקבלה, משיב WAITING, וההרצה מסתיימת. מזהה הקריאה נשמר בסשן. מפעילים את הצגת המודעות מהמסוף או מהכרטיסייה הראשונה של הדף:

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 (רענון adk web): הפונקציה function_response עוקבת אחרי הקריאה הממתינה, הפונקציה join_media משתחררת והפונקציה store_release פועלת. שמונה שורות אימות קוראות את שתי העצירות מהסשן: הקריאה של השער והתשובה שלכם, הקבלה של הרינדור והמסירה שלו.

המדינה והשולחן (5ב')

הדסק של המדיה הוא שוב המבנה של 3a, פיצול ואיחוד, עם ענף אחד איטי. ‫script_ready שולח את התסריט בשתי דרכים. ‫render_desk שולח את הקליפ וההרצה מושהית. ‫make_poster הוא הכלי של סשן 2 בתור צומת: poster_idea של התסריט, הסגנון של הערוץ ומה ש-Memory Bank יודע על הטעם של היוצר בכרזות (מקושר מהמצב בתור 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: בהודעה מופיע השם של מי שפרסם את הפוסט. בבלוק This run's session בדף מוצגת אותה שורה שמוצגת בכרטיסייה State של adk web: מזהה הסשן שנדרש להמשך, השיחה הפתוחה והמפתחות שהצמתים כתבו, כולל poster_url. בחלונית האימות מוצגים הקליפ והפוסטר זה לצד זה.

הקליפ והפוסטר שלו, זה לצד זה אחרי המסירה

8. ‫6 · זיכרון – מה שהערוץ זוכר בין הלילות

סשן 6 · זיכרון

👉🖥 בסביבת העבודה, פותחים את Session 6 · Memory, חלקים 6a ו-6b, שנוצרו ב-stage4_memory. סשן 5 עסק במצב של הרצה אחת. הסשן הזה עוסק במה שהערוץ זוכר בין הפעלות: מה שהתרשים שומע מבחוץ, באמצעות שני קריאות חוזרות (callbacks).

‫Memory Bank‏ (6א)

‫Vertex AI Memory Bank הוא זיכרון מנוהל לטווח ארוך על אדם מסוים. העובדות נמצאות בהיקף מסוים, בשם האפליקציה ובמזהה המשתמש, ובנושאים שאתם מגדירים. כשמעבירים לו שיחה עם 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)

הזיכרון הוא לא צומת. קריאה חוזרת (callback) היא פונקציה שמעבירים ל-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, חלקים 7a ו-7b.

פריסת VibeStudio‏ (7א)

בארגזי החול, adk web הניע את התרשים. ‫VibeStudio מפעיל את זה עם Runner של ADK, ב-Thread עובד עם לולאת אירוע משלו, ומקפל כל אירוע למצב הפעלה אחד שהדף קורא דרך אירועים שנשלחו מהשרת: הקוראים, הבחירה שלכם כארבעה קלפים, הפוסטר כשהוא נוחת, העיבוד כשהוא נמסר. התשובה של השער והעיבוד של התוצאה הם אותה קריאה, חלק מ-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 היא כל מבנה של ה-Codelab הזה במקום אחד, שלושה עשר צמתים. היא נשלחת מלאה, כדי שתוכלו לקרוא אותה, ולא להקליד:

        (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 בסביבת העבודה, או 👉💻 מהכרטיסייה הראשונה:

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 ופורסת אותו בשלב אחד. ה-build הראשון נמשך ארבע או חמש דקות. --session-affinity עם מכונה חמה אחת שומרת על הרצה מושהית במכונה שמכילה אותה: מאגר הסשנים של סשן 5, פעם נוספת. בפריסה גדולה יותר, מאגר הסשנים עובר אל VertexAiSessionService והשיוך נעלם. ה-build והשירות פועלים כחשבון השירות שמוגדר כברירת מחדל ב-Compute Engine של הפרויקט, שהוקצו לו התפקידים הנדרשים במהלך ההגדרה.

👉🖥 פותחים את כתובת ה-URL של השירות ומריצים לילה באפליקציה: רעיון, בחירה, ממתינים לקליפ ולפוסטר. בכרטיסייה "היסטוריה" נשמרות שתי הגרסאות.

פרסום (7ב)

הפרסום הוא בקשת 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 נשמר סרטון אחד לכל projectId וחדר, והפרויקט שהאפליקציה שולחת הוא הפרויקט שלכם ב-Google Cloud. לכן, פרסום מחדש מחליף את הסרטון במקום להוסיף סרטון. התמונה הממוזערת היא תמונה אמיתית, ולא תמונה חלופית.

העריכה האחרונה היא הגדרה, לא שורת קוד. 👉🖥 בדף 7b: כתובת ה-URL של VibeStudio (שמולאה מהפריסה של 7a), קוד האירוע של החדר, בדיוק כמו שהוא מאוית ב-VibeTube (הקוד שנתתם בהגדרה, אלא אם המדריך פתח קוד אחר), והשם שלכם. לוחצים על פרסום. סביבת העבודה מעבירה את הקליק לאפליקציה, האפליקציה מפרסמת את הלילה, והשורות שמתחת קוראות את התוצאה מהאפליקציה ומ-VibeTube: האפליקציה עונה, הלילה מסתיים עם פוסטר אמיתי, שם הקוד נותן שם לחדר פתוח, VibeTube החזירה מזהה סרטון, הסרטון נמצא על הקיר, והתמונה הממוזערת שלו היא הפוסטר שלכם. בכרטיס התוצאה מוצגת התמונה הממוזערת ב-VibeTube לצד הכרזה באפליקציה.

פורסם: התמונה הממוזערת ב-VibeTube היא הפוסטר שמודל התמונה יצר

10. מה יצרתם

👉🖥 בסוף הדף 7b מוצג התרשים המלא, 13 צמתים מרעיון ועד לקליפ ולפוסטר שלו. אפשר להעביר את העכבר מעל צומת כדי לראות מה הוא עושה ואת הסשן שבו הוא נוצר.

סשן

מה הוא לימד

נכתב בתור

‫2 · יצירת תמונות

סוכן: מודל, הוראות, כלים; מודל התמונות ככלי; מה שהנחיה לא יכולה להבטיח

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

‫3 · תהליכי עבודה

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

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. המשך הטיפול בכל הפניות באותו אופן – לפי המזהה של הפנייה הפתוחה, בין אם אדם או תהליך עונים.
  3. משתפים נתונים לפי שם במצב, ולא על ידי העברה שלהם דרך כל מטען ייעודי (payload).
  4. החליטו באמצעות קוד לפני שאתם משלמים על מודל. המדיניות היא קובץ טקסט ונתב, והיא פועלת לפני העיבוד.
  5. קוראים את כל מה שהוא לא טקסט לתוך טקסט לפני שהוא מגיע לקורפוס, עם שם הקובץ בתוך השורה כדי שהקובץ יוכל לחזור.

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

מה אפשר לעשות עם זה

  • VertexAiSessionService לסשנים שמתקיימים מעבר למכונה, ומוחקים את --session-affinity.
  • תגובת Webhook שעונה לקריאה של הרינדור במקום פולר: אותו FunctionResponse(id=...), שולח שונה.
  • עוד RequestInput לפני הפרסום – הסכימה היא הטופס, אז היא צומת אחד ואין קוד דף.
  • התגובות, ההודעות הקוליות והקליפים החדשים של הקהל מצורפים לקורפוס אחרי כל פרסום, יחד עם הפוסטר שמתחתיו הם נשלחו.

הסרת המשאבים

כל מה שנוצר ב-Lab הזה נמצא בפרויקט שנוצר בשלב 4, ולכן הדרך הזולה ביותר לניקוי היא למחוק את הפרויקט. ב-Cloud Shell:

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

כדי להשאיר את הפרויקט ולהסיר רק את החלקים בתשלום: שירות Cloud Run (gcloud run services delete vibestudio --region us-central1), מאגר ה-RAG ו-Memory Bank (שמות המשאבים שלהם נמצאים ב-runs/ragcorpus.json וב-runs/memorybank.json, ואפשר למחוק אותם בדפים Agent Engine ו-RAG Engine במסוף), וקובץ האימג' של הקונטיינר ב-Artifact Registry.