VibeStudio — Multimodal Agentic Workflow with ADK, Haunted Edition

1. Introduction

VibeStudio, Haunted Edition

Vous gérez une chaîne cosy et effrayante sur VibeTube : un Short chaque soir jusqu'à Halloween. L'audience veut de la magie à petite échelle dans de petites pièces et vous le fait savoir de toutes les manières possibles : commentaires, messages vocaux, photos de leurs propres tiroirs hantés et extraits de leur porche au crépuscule. Le backlog est plein d'idées. Chaque soir, il effectue les mêmes tâches : rechercher ce qui hante le flux, passer en revue les tâches en attente, lire ce que l'audience a envoyé, choisir une direction, vérifier qu'elle est conforme aux règles de la chaîne, écrire le script, dessiner l'affiche, générer le clip et le publier.

Les modèles génératifs peuvent effectuer chaque tâche individuellement. Gemini recherche, écoute, regarde et écrit, le modèle d'image dessine et Veo génère. Ce qu'ils ne peuvent pas faire seuls, c'est exécuter la nuit dans l'ordre, s'arrêter pour la seule décision qui vous incombe, refuser une mauvaise direction avant qu'elle ne coûte de l'argent, attendre des minutes pour un rendu sans rien laisser ouvert et laisser un enregistrement.

Cet atelier de programmation transforme cette soirée en un ADK Workflow : un graphique dessiné de nœuds et d'arêtes qui s'exécute de lui-même, ne vous demande que votre avis et se termine par un extrait et son affiche sur VibeTube.

Une requête ne peut pas promettre un ordre, un arrêt ou une vérification. Un graphique, oui.

Objectifs de l'atelier

  • Un agent et le modèle d'image en tant qu'outil. Un Agent est un modèle, une instruction et des outils. Le troisième outil appelle le modèle d'image, et la première exécution se termine par une affiche. Ensuite, la phrase qui guide le reste de l'atelier : une requête ne peut pas promettre d'ordre, d'arrêt ou de vérification.
  • Les structures qui composent un graphique. Un Workflow est une liste d'arêtes. Vous créez la distribution et la jointure (lecteurs parallèles se rencontrant au niveau d'un JoinNode) et un routeur qui nomme sa propre sortie, avec un agent en mode tâche derrière la sortie refusée qui nettoie une direction dans une boucle d'appels d'outils et rejoint la ligne principale.
  • Un pipeline d'ingestion multimodal Quatre types de contenus envoyés par l'audience, un lecteur par type : Gemini transcrit les notes vocales, légende les photos, regarde les clips et écoute leurs bandes-son, et chaque lecteur écrit la même forme de ligne dans un corpus du moteur RAG. Un troisième lecteur dans la distribution récupère par signification et renvoie le média avec les passages.
  • Deux types de suspension, un seul mécanisme Une course s'arrête pour une personne (RequestInput : quatre cartes, vous répondez) et pour un outil lent (Veo derrière LongRunningFunctionTool : un reçu). Il s'agit de la même chose : un appel ouvert dans le magasin de sessions, repris par son ID, à partir du formulaire ou d'un autre processus. Les deux branches du bureau des médias partagent l'affiche par nom, dans l'état.
  • Mémoire d'une nuit à l'autre. Deux rappels de cycle de vie sur Memory Bank : before_model_callback rappelle les goûts du créateur avant que le proposeur ne lise, after_agent_callback mémorise le choix de ce soir après le scripteur.
  • La version Le graphique d'une application avec un ADK Runner, des événements envoyés par le serveur à la page, Cloud Run avec affinité de session et une requête POST multipart qui place l'extrait sur VibeTube avec l'affiche comme miniature.

Objectifs de l'atelier

Une application en cours d'exécution dans votre navigateur : VibeStudio Workbench, une page associée avec un éditeur de code dans la page, des vérificateurs qui lisent les fichiers sur le disque et les sessions écrites par vos exécutions, et un adk web intégré. Ses sept sessions correspondent aux sept pages de cet atelier de programmation, avec les mêmes numéros et les mêmes lettres de partie, de sorte que les deux restent synchronisés. Chaque page s'ouvre sur une bande qui résume la session en une phrase et les trois actions que vous y effectuez : modifier, exécuter, vérifier. Lisez une section ici, puis effectuez la modification dans l'atelier. Chaque modification se termine par un panneau de vérification qui indique ce qui s'est réellement passé.

Ce qui s'exécute et où : VibeStudio Workbench, votre backend et les services Google Cloud

Trois éléments sont exécutés : le Workbench (la page sur le port 4600, l'éditeur, les vérificateurs), votre backend (le Workflow dans agent/ et une application de bac à sable par session dans stage0_prompt/ via stage6_video/, que adk web diffuse) et Google Cloud (Gemini, le modèle d'image, le moteur RAG, Veo, Memory Bank, tous accessibles via Vertex AI avec vos propres identifiants).

Les sept sessions

Session

Objectif de l'atelier

Vos modifications

1 · L'histoire

Rien pour l'instant : la nuit et le graphique qu'elle devient

—

2 · Génération d'images

Un agent dessine la première affiche

POSTER_TOOL

3 · Workflows

La nuit comme un graphique : la distribution et la jointure, le routeur et sa boucle

FANOUT, ROUTER

4 · Ingestion multimodale

Ce que l'audience a envoyé, lu dans un corpus ; le troisième lecteur

INGEST_READERS, RAG_MEDIA

5 · Génération de vidéos

Deux types de suspension : l'affiche partagée par nom dans l'état

GATE, VIDEO_TOOL, DELIVER_RESPONSE, STATE_SHARE

6 · Mémoire

Ce que la chaîne mémorise d'une nuit à l'autre

MEMORY_RECALL, MEMORY_REMEMBER

7 · Déployer et publier

VibeStudio sur Cloud Run ; la nuit sur VibeTube

le code d'événement de la salle, puis publiez

Comment utiliser cet atelier de programmation Il est axé sur les concepts : chaque session explique l'idée, puis montre le code tel qu'il se présente une fois la modification effectuée et le parcourt. Il ne vous demande jamais de modifier un fichier ici. Les modifications sont effectuées dans l'atelier, qui vous propose une modification à la fois, avec un indice à côté et un panneau de vérification en dessous. Lisez la session, puis laissez-vous guider par la page.

Où se situe-t-il ?

Le graphique, le thème et la pièce sur VibeTube proviennent de l'édition hantée de VibeStudio de Lin. Il s'agit de la version multimodale, recentrée sur ce que l'audience a envoyé et sur les deux types de suspension, au format interactif d'une page des ateliers Agent Valley. Le studio est un décor ; chaque mécanisme sous-jacent est celui que vous utiliseriez habituellement pour un pipeline d'assistance, un flux d'examen de contenu ou tout autre élément qui doit s'interrompre pour une personne et attendre un job lent.

2. 🧰 Configuration

  • Quoi : un compte qui peut payer, un projet, l'atelier installé, le modèle dont la capacité à répondre a été prouvée et le workbench qui s'exécute en arrière-plan sur un port.
  • Pourquoi : un processus sur un port : la page, l'éditeur, les vérificateurs, /api/* et adk web partagent une origine. Par conséquent, rien dans cet atelier ne nécessite un deuxième terminal ou un deuxième port.
  • Comment : clonez deux scripts. Le second effectue un véritable appel de modèle, démarre le workbench et commence à créer le corpus RAG et la banque de mémoire pendant que vous lisez, afin que les sessions 4 et 6 les trouvent prêts.

Tout ce qui est utilisé dans cet atelier s'exécute dans Cloud Shell : gcloud, Python, Node, ffmpeg et git y sont déjà installés, et Gemini, le modèle d'image, Veo, le moteur RAG et Memory Bank sont tous accessibles via Vertex AI à l'aide de vos propres identifiants. Il n'y a aucune clé API dans cet atelier, et rien à installer sur votre ordinateur portable. Un ordinateur portable équipé du SDK Google Cloud, de Python 3.12 et de Node 20 ou version ultérieure fonctionne de la même manière.

Il y a une chose que cet atelier ne peut pas vous fournir : un projet Google Cloud avec facturation activée. L'étape 1 vous permet d'obtenir un compte pour payer, et l'étape 4 transforme ce compte en projet. Suivez cet ordre, car chaque étape après l'étape 4 lit le projet défini à l'étape 4.

1 · Obtenir un compte avec facturation (~1 min)

Connectez-vous avec un compte Google pour lequel la facturation est activée. Tous les comptes de ce type fonctionnent. Si ce n'est pas le cas, démarrez un essai sans frais. Vous obtiendrez ainsi un compte de facturation, ce qui est suffisant pour l'étape 4. Si vous participez à un atelier dirigé par un formateur, utilisez d'abord les crédits de cette session. Cela créera le compte de facturation pour vous.

2 · Ouvrir Cloud Shell (~2 min)

👉🖥 Cliquez sur Activer Cloud Shell en haut de la console Google Cloud (icône en forme de terminal dans l'en-tête).

Activer Cloud Shell depuis l'en-tête de la console Google Cloud

👉🖥 Cliquez sur Ouvrir l'éditeur (icône représentant un dossier ouvert avec un crayon). L'éditeur Cloud Shell s'affiche avec un explorateur de fichiers sur la gauche. C'est là que vous devez vous rendre chaque fois que vous souhaitez lire un fichier entier.

Ouvrir l'éditeur Cloud Shell

👉🖥 Si vous êtes invité à autoriser l'accès à un moment ou à un autre aujourd'hui, cliquez sur Autoriser pour continuer.

👉🖥 Ouvrez un terminal dans l'éditeur (onglet 1). Toutes les étapes 👉💻 s'exécutent dans ce terminal. L'onglet 2 est le workbench lui-même, que vous ouvrez à l'étape 7. Si le terminal n'est pas déjà affiché en bas de l'écran, ouvrez-le en sélectionnant View → Terminal (Affichage → Terminal) :

Ouvrez un nouveau terminal dans l'éditeur Cloud Shell.

👉💻 Vérifiez le compte auquel vous êtes connecté. Il doit s'agir de celui de l'étape 1 :

gcloud auth list

3 · Obtenir l'atelier (~1 min)

👉💻 Clonez le dépôt :

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

4 · Créez votre projet (~2 min)

👉💻 Un script recherche votre compte de facturation, crée un projet, associe la facturation et pointe Cloud Shell vers ce projet :

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

Vous devriez obtenir un ✓ pour le compte de facturation choisi, un ✓ pour le projet créé et une fin de ce type :

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

vibe-studio-4271 est l'ID de votre projet (le vôtre aura des chiffres différents). Quatre choses s'y produisent :

facturation

prend le compte [YYYY-MM-DD] GDP Credit: le plus récent si vous en avez un, et tout autre compte de facturation ouvert sinon

project

crée vibe-studio-XXXX et lui associe ce compte de facturation.

selects

écrit l'ID dans ~/project_id.txt et exécute gcloud config set project, de sorte que rien après cela ne vous demande de le saisir

waits

interroge jusqu'à ce que le projet soit réellement autorisé à diffuser, pendant deux minutes maximum

👉🖥 Le même projet est maintenant dans la console. Son ID se trouve dans la fiche Informations sur le projet du tableau de bord :

L'ID de votre projet sur le tableau de bord de la console

5 · Installer et configurer l'atelier (~4 min)

👉💻

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

Un seul script, que vous pouvez exécuter à nouveau en toute sécurité : une deuxième exécution conserve toutes les réponses que vous avez fournies la première fois. Deux questions sont posées au début, et la touche Entrée permet de sélectionner la valeur par défaut pour chacune d'elles :

code d'événement

la salle VibeTube dans laquelle votre vidéo est publiée lors de la session 7. Saisissez le code de la salle fourni par votre formateur ou appuyez sur Entrée pour sandbox, qui est accessible à tous à tout moment. Les codes sont sensibles à la casse, et une salle n'accepte les importations que pendant sa fenêtre d'ouverture. Le script vous indique si la salle que vous avez nommée est ouverte.

nom à afficher

le nom de la salle qui vous crédite, ou Entrée pour une salle créée à partir de votre compte.

Les deux sont enregistrés dans .env et peuvent être modifiés ultérieurement, dans .env ou sur la page de publication. Ensuite, il effectue le reste de manière autonome, dans l'ordre suivant :

uv env

installe uv s'il est manquant, puis uv venv et uv sync installent exactement ce que uv.lock épingle (google-adk, google-genai, vertexai, fastapi, uvicorn) dans .venv

apis

active six API sur le projet sélectionné à l'étape 4 et confirme que chacune d'elles répond avant de continuer : aiplatform (Gemini, le modèle d'image, Veo, Memory Bank, moteur RAG), vectorsearch (le magasin derrière un corpus RAG), run, cloudbuild, artifactregistry (session 7), cloudtrace (les traces de l'application)

compte

vérifie que le compte de service Compute par défaut du projet existe et dispose des rôles nécessaires pour la compilation et le service de la session 7.

.env

écrit .env en pointant chaque appel vers Vertex AI sur votre projet : Gemini sur le point de terminaison global, Veo, Memory Bank et le moteur RAG dans us-central1, et aucune clé API nulle part ; un .env précédent est conservé en tant que .env.bak et tout ce que vous avez ajouté manuellement est conservé

veo · room

confirme que veo-3.1-fast-generate-001 est diffusé dans us-central1 (lecture des métadonnées, pas de rendu) et demande à VibeTube si votre salon est ouvert

model

effectue un véritable appel à Gemini, de sorte qu'un projet incorrect échoue au moment de la configuration et non au cours d'une session

starter

Place les neuf fichiers que vous allez modifier à partir de starter/, en conservant leurs lignes TODO (uniquement celles qui sont manquantes). Vos modifications ne sont jamais écrasées.

page

installe et compile web/, la page de l'atelier, puis démarre le serveur en arrière-plan sur le port 4600.

cloud

commence à créer le corpus du moteur RAG et la banque de mémoire en arrière-plan, de sorte que les sessions 4 et 6 les trouvent prêts.

preflight

Exécute scripts/preflight.py et affiche le lien à ouvrir

6 · Lire la vérification préliminaire (~1 min)

La dernière chose imprimée à l'étape 5 est le contrôle en amont. Chaque ligne est une véritable sonde, et non une supposition :

  ✓ 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) a raison : ce graphique est vide, et vous le remplissez lors de la session 3. Les deux lignes - sont également correctes : un corpus met une minute ou deux à s'afficher, et la banque environ 20 secondes. Vous n'en aurez pas besoin avant la session 4.

👉💻 Exécutez-la à tout moment pour voir où vous en êtes. Elle lit vos fichiers, votre projet et le serveur en cours d'exécution, et non une checklist :

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

7 · Ouvrir Workbench (~1 min)

Le workbench est déjà en cours d'exécution. L'étape 5 l'a démarré en arrière-plan, un processus uvicorn sur le port 4600 qui diffuse la page, chaque route /api/* et adk web installé sur /workbench. Il n'y a rien à démarrer, et rien dans cet atelier ne vous demande de le redémarrer : une modification est appliquée lors de votre prochaine exécution.

👉🖥 Cliquez sur le lien affiché par le preflight ou, dans Cloud Shell, sur Aperçu sur le Web → Modifier le port → 4600. Appelez-le onglet 2. La page Session 1 · L'histoire s'ouvre.

Voici les trois commandes dont vous pourriez avoir besoin plus tard, toutes issues de ~/vibe-studio-multimodal :

scripts/stop.sh

arrête le workbench

scripts/start.sh

le démarre au premier plan ; Ctrl+C l'arrête, ainsi que tous les processus qu'il a démarrés

scripts/restart.sh --pull

git pull, puis redémarrez en arrière-plan, en resynchronisant le venv et en recréant la page uniquement lorsqu'ils ont changé.

Mise en page du dépôt

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 · L'histoire : la nuit et le graphique qu'elle devient

👉🖥 L'onglet 2 s'ouvre sur Session 1 · L'histoire. Lisez-le : six tâches disposées en cercle autour d'une personne (rechercher la tendance, passer en revue les tâches en attente, choisir une direction, vérifier les règles, écrire le script, afficher le clip), et le cercle se répète chaque nuit. Gemini peut effectuer les recherches, le backlog et le script seul. Veo peut effectuer le rendu seul. Le choix et la vérification sont effectués par la personne.

Sous l'anneau se trouve le graphique que le reste de l'atelier crée, soit 13 nœuds allant d'une idée à un extrait et à son affiche. Pointez sur un nœud pour voir ce qu'il fait et la session qui le crée. Rien à modifier ici. Le bouton Générer la première affiche de la page correspond à la session 2.

Le vocabulaire, de sorte que les six pages suivantes se lisent sans s'arrêter :

word

ce que cela signifie ici

agent

un Agent ADK : un modèle, une instruction, des outils. Session 2

nœud

une étape du graphe : une fonction Python, un agent, une jointure, un routeur

edge

un tuple de nœuds qui s'exécutent dans l'ordre ; le graphe est une liste de ces nœuds. Session 3

session

l'enregistrement d'une exécution dans runs/sessions.db : ses événements, son état et tout appel laissé ouvert. Session 5

state

un dictionnaire que les nœuds partagent par nom de clé ; un nœud écrit Event(state=...), un autre déclare un paramètre du même nom. Session 5

mémoire

ce qui survit à une session : Memory Bank, via des rappels. Session 6

4. 2 · Génération d'images : un agent et le modèle d'image comme outil

Session 2 · Génération d'images

👉🖥 Dans le workbench, ouvrez Session 2 · Génération d'images. Avant le graphique, la chose la plus simple qui pourrait fonctionner : un Agent dont l'instruction est la nuit entière sous forme de texte, trois fonctions Python comme mains et une exécution qui se termine par une image.

Un agent est un modèle, une instruction et des outils

stage0_prompt/agent.py définit root_agent avec trois des champs d'un agent ADK. model est le modèle Gemini de agent/platform/config.py. instruction est le prompt système : six tâches en une seule fois, la dernière étant une image. tools contient trois fonctions.

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

Un modèle est un monde fermé : il raisonne sur ses pondérations et les jetons de son contexte. Un outil est une fonction Python que le modèle peut demander. L'ADK lit la signature et la docstring de chaque fonction, puis les déclare au modèle. Lorsque le modèle en veut une, il émet un événement function_call, l'environnement d'exécution exécute le code Python et réécrit un événement function_response dans le contexte, et le modèle continue avec le résultat sous les yeux.

Trois fonctions, et l'une d'elles dessine

Deux d'entre eux ont lu l'étude. La troisième demande au modèle d'image de créer une affiche :

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 appelle agent/platform/images.py, le modèle d'image encapsulé en tant que fonction simple, de la même manière que videogen.py encapsule Veo : un préfixe de style maison (le look Halloween low-poly chaleureux de la chaîne, orange citrouille et crème à la lueur des bougies sur fond prune nuit), le sujet, un cadre vertical 9:16, un PNG de moins de runs/media/posters, diffusé à /static/posters. Avec STUDIO_REAL_IMAGES=0, il copie un poster de remplacement et signale le résultat prebaked, de sorte que l'atelier s'exécute n'importe où.

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

Le même appel est un nœud du graphique de la session 5.

Exercice pratique : une seule modification

En l'état, make_poster renvoie un dictionnaire indiquant que l'affiche a été ignorée. 👉🖥 Dans l'éditeur Workbench, faites en sorte qu'il appelle le modèle d'image, à la place de la ligne TODO :

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

Économisez. Les lignes de vérification lisent le fichier : trois outils sur l'agent, l'appel du modèle d'image en place. 👉🖥 Cliquez sur Check the workflow loads (Vérifier les charges du workflow), puis sur Open adk web (Ouvrir adk web) et envoyez :

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

À quoi s'attendre et pourquoi

  • Deux événements d'outil avant la réponse, puis une question. check_trends et read_backlog sont exécutés, car l'instruction demandait de vérifier et de rechercher, et le modèle n'avait aucune de ces informations dans ses pondérations. La réponse propose une direction et vous demande de confirmer, car l'instruction demandait d'abord un accord.
  • Le deuxième message reçoit tout. Envoyez skip the questions, describe the video and draw the poster. Le modèle décrit la vidéo, appelle make_poster une fois avec une ligne d'objet qu'il a lui-même rédigée et vous indique où le fichier PNG a été enregistré. Le panneau de validation affiche l'affiche à côté des lignes : une scène d'Halloween dans le style de la chaîne, dessinée en huit secondes environ.
  • Pourquoi ce n'est pas un canal. La recherche est intégrée à une prose que rien ne peut citer. La règle a été certifiée par le modèle qui a rédigé l'instruction, et l'affiche a été dessinée avant d'être vérifiée. L'accord a été évoqué dans un message. Le texte de la requête n'est pas une porte. Dans la suite de l'atelier, nous allons décomposer ces six jobs dans un graphique.

La page se termine par De quoi est composé un agent ADK, une référence à laquelle vous pouvez revenir : le modèle, le contexte avec lequel il raisonne, les outils et les collaborateurs par lesquels il agit, les rappels qui encapsulent chaque appel, ainsi que la session et la mémoire qui existent en dehors de lui.

5. 3 · Workflows : fan-out et jointure, routeur et boucle

Session 3 · Workflows

👉🖥 Dans Workbench, ouvrez Session 3 · Workflows, parties 3a et 3b. Chaque partie est créée dans une application bac à sable afin que vous puissiez l'exécuter seule dans adk web : la distribution ramifiée dans stage1_fanout, le routeur et sa boucle dans stage3_router.

Distribution ramifiée et jointure (3a)

Un Workflow ADK est une liste d'arêtes. Chaque entrée est une chaîne, un tuple de nœuds qui s'exécutent dans l'ordre. Deux chaînes quittant START s'exécutent en parallèle. Une chaîne qui se termine par un JoinNode lui transmet des informations, et la jointure attend que chaque chaîne qui se termine à cet endroit se termine avant de continuer, en transmettant un dictionnaire indexé par nom de nœud. Une cible écrite sous forme de dictionnaire mappe les noms de routes d'un routeur à des nœuds. La page 3a du cahier d'exercices dessine les formes et propose un exercice d'écriture des arêtes.

Type de nœud

Écrit comme

Description

Nœud de fonction

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

Python standard. Peut également écrire l'état partagé avec Event(state=...).

Nœud de jointure

JoinNode(name="join_research")

Attend que toutes les chaînes entrantes aient renvoyé un résultat, puis transmet toutes leurs sorties sous la forme d'un seul dictionnaire.

Nœud de l'agent

Agent(...)

Un appel de modèle en mode single_turn, une réponse structurée, aucune conversation.

Routeur

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

Un nœud de fonction dont le retour nomme l'arête sortante.

Nœud d'action humaine

yield RequestInput(...)

Suspend le graphique jusqu'à ce qu'une personne réponde à un formulaire. Session 5.

La distribution de la recherche : deux lecteurs de START dans une jointure

Le premier graphique représente la recherche de la session 2 sous forme de nœuds. scan_trends et read_backlog dans agent/graph.py sont les deux lecteurs. read_backlog écrit également l'idée de ce soir dans l'état partagé. stage1_fanout/agent.py définit la jointure et crée son Workflow à partir d'une liste nommée EDGES, qui est vide (le 0 edges de la vérification préliminaire). 👉🖥 Remplissez-la avec les deux chaînes :

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

👉🖥 Exécutez-le dans adk web. Deux nœuds s'allument ensemble, puis la jointure. Ouvrez l'événement de join_research et lisez un dictionnaire avec une clé scan_trends et une clé read_backlog. Aucun appel de modèle n'a eu lieu. Rien ne reformate le dictionnaire de la jointure : le nœud qui le suit, en 3b, est un agent, et un nœud d'agent reçoit son node_input en tant que message. Un dictionnaire arrive donc au format JSON que le modèle lit directement.

Le routeur et la boucle (3b)

Un Agent à l'intérieur d'un Workflow s'exécute en mode single_turn : il reçoit la sortie du nœud précédent en tant que message, effectue un appel de modèle et répond une fois. Avec output_schema, la réponse est validée par rapport à un modèle Pydantic avant que le nœud suivant ne la voie. Le graphe transmet donc des objets typés, et non du texte. propose_directions lit le dictionnaire de recherche de la jointure et propose quatre candidats, chacun représentant une scène d'Halloween. Le candidat 4 est l'appât : son titre contient un mot que le canal refuse, de sorte que le routeur a quelque chose à intercepter. Chaque candidat porte un poster_idea, une ligne nommant l'image unique qui vend la vidéo. Le scripteur la conserve et le bureau des médias la dessine lors de la session 5.

Un routeur est un nœud de fonction dont le retour nomme l'arête empruntée par l'exécution. Aucun appel de modèle : policy_check lit agent/policy_words.txt au moment de la décision, compare les mots entiers au titre et à l'angle de la direction, et l'achemine avant qu'un script ne soit écrit ou que de l'argent ne soit dépensé. La stratégie est une donnée : modifiez le fichier, et la prochaine exécution le lira.

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

La porte de la règle : un routeur avec deux sorties étiquetées

stage3_router/agent.py est le fan-out plus propose_directions et choose_direction, un substitut de la porte humaine que vous créez dans la session 5 : il sélectionne le candidat dont le numéro termine l'idée de ce soir (..., pick 4), sinon le premier, et écrit la sélection dans l'état. scripter est un agent single_turn qui transforme une direction approuvée en Script : un titre, une description, des tags, une phrase d'accroche, trois plans et une idée d'affiche, chaque plan étant une image d'Halloween dans le style de la chaîne.

La boucle derrière la sortie refusée. quarantine est fourni en tant qu'agent en mode tâche. Un Agent possède un mode : chat est la boucle de conversation qu'un agent racine exécute avec une personne, single_turn est un appel avec une réponse structurée, et task est une boucle d'appels d'outils que l'agent exécute seul jusqu'à ce qu'il appelle l'outil finish_task intégré, dont les arguments ADK sont dérivés de output_schema. La mise en quarantaine appelle find_policy_hits dans la direction refusée, demande à suggest_replacement le remplaçant approuvé du canal pour chaque mot (agent/policy_replacements.txt, données à nouveau), réécrit, vérifie à nouveau et termine par un CleanedDirection typé. Lisez-le sur la page, ne le tapez pas.

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

Par défaut, la chaîne va du sélecteur directement au scripteur. 👉🖥 Placez le routeur entre les deux, avec ses deux sorties et le chemin de retour de la quarantaine. Trois lignes remplacent une seule :

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

👉🖥 Exécutez les deux routes. Envoyer l'idée telle quelle : choose_direction prend le candidat 1, policy_check émet OK et le scripteur écrit un script structuré. Cliquez sur Nouvelle session et envoyez à nouveau l'idée avec , pick 4 à la fin : le candidat 4 est l'appât, policy_check émet BLOCK, la quarantaine appelle ses outils, réécrit les mots refusés, appelle finish_task, et la direction nettoyée est renvoyée au scénariste. Les lignes de vérification lisent les deux exécutions à partir du magasin de sessions.

Chaque phrase de la requête de la session 2 dispose désormais d'un nœud : la recherche correspond à deux lecteurs et à une jointure, le refus correspond à un routeur et à un agent de tâche, et le script correspond à un nœud d'agent. Dans l'ensemble du graphique, le choix de remplacement est constitué de deux nœuds de fonction, direction_gate et persist_direction, qui s'exécutent directement jusqu'à ce que la session 5 arrête la porte. L'accord avec la créatrice n'est pas encore finalisé, tout comme l'étude, qui n'a pas encore interrogé l'audience.

6. 4 · Ingestion multimodale : ce que l'audience a envoyé, lu dans un seul corpus

Session 4 · Ingestion multimodale

👉🖥 Dans l'atelier, ouvrez Session 4 · Multimodal ingestion (Session 4 · Ingestion multimodale), parties 4a et 4b. Le public n'a pas seulement écrit. Un lecteur par type convertit tout ce qu'il a envoyé en texte de même forme, un corpus RAG Engine contient tout, et un troisième lecteur dans la distribution recherche par signification et renvoie le média.

Ce que l'audience a envoyé (4a)

agent/comments.md contient 12 commentaires publiés sous les six vidéos d'Halloween de la chaîne. agent/audience/ contient le reste : huit notes vocales (voice/*.mp3), 14 photos des chambres des spectateurs (photo/*.jpg) et six clips (clip/*.mp4), un panoramique lent sur une photo avec la voix du spectateur en voix off. La page du 4a s'ouvre sur l'ensemble de ces éléments : chaque note vocale est lue, chaque photo s'affiche, chaque extrait est diffusé.

La grille de l'audience après l'ingestion : chaque fiche montre ce que Gemini a entendu ou vu

Le moteur RAG récupère du texte. Une note vocale, une photo ou un extrait ne peut rejoindre le corpus que de la même manière que les affiches précédentes dans rag.caption_posters : un modèle le transforme en une ligne de texte qui nomme son fichier, la ligne devient un passage, et lorsque le passage revient, le fichier revient avec lui. agent/platform/audience.py comporte un lecteur par genre. Chacun envoie un fichier et un prompt à Gemini, et écrit une ligne par fichier :

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 fait de même avec image/jpeg et PHOTO_PROMPT, watch_clips avec video/mp4 et CLIP_PROMPT, et Gemini regarde l'image et écoute la bande-son en un seul appel. Chaque lecteur écrit la même forme de ligne. Par conséquent, le chunking, l'embedding et la récupération ne savent jamais de quel type provient un passage :

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

Un lecteur par genre, une forme de ligne, un corpus

Le corpus. Le moteur RAG Vertex AI permet de récupérer des documents : un corpus est divisé en passages d'environ 120 jetons, chaque passage est intégré à text-embedding-005, et une requête est intégrée de la même manière et répondue avec les passages les plus proches par signification, et non par mots partagés. "petites hantises douillettes à la maison" trouve le commentaire qui demande un peu de magie dans les petites pièces, le message vocal sur la chauve-souris qui enlace l'ampoule et l'extrait dont la bande-son dit que le fantôme est un gobelet en carton.

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

La configuration a déjà commencé à créer le corpus. La ligne de connexion de la page 4a indique s'il est actif et le crée si la configuration n'a pas pu le faire. À partir des boutons de la page ou du terminal : python -m agent.platform.rag crée le corpus et l'enregistre dans runs/ragcorpus.json ; python -m agent.platform.rag load --audience exécute chaque lecteur dans audience.READERS, puis importe ce qu'ils ont écrit à côté des commentaires et des légendes des affiches, chaque fichier sous son propre nom, et attend que l'index réponde ; python -m agent.platform.rag query "cozy little hauntings at home" affiche les passages trouvés qui ne partagent aucun mot avec la requête.

La modification À la livraison, READERS ne contient que read_comments. Les trois lecteurs multimédias se trouvent derrière un #. 👉🖥 Exécutez l'ingestion une fois telle quelle : le résultat indique not read (no reader wired in audience.READERS): voice, photo, clip, et seuls les commentaires et les sous-titres sont inclus. Supprimez ensuite le # :

    transcribe_voice, caption_photos, watch_clips,

👉🖥 Enregistrez et exécutez à nouveau l'ingestion. Les lecteurs mettent environ trois minutes pour les 40 éléments. La console indique heard, saw et watched une ligne à la fois, et chaque fiche de la grille se remplit avec sa ligne. Les lignes de vérification lisent le fichier, la sortie des lecteurs et le corpus : quatre lecteurs connectés, huit notes vocales écoutées, 14 photos vues, six clips regardés, cinq fichiers dans le corpus.

Le troisième lecteur (4b)

Les commentaires de l'audience sont des recherches, partagées par chaque exécution. Ils entrent donc dans le graphique là où se trouvent les recherches : en tant que troisième lecteur dans la jointure. read_feedback dans agent/graph.py prend l'idée de ce soir, demande au corpus les passages les plus proches et les transmet à join_research, qui en attend trois. L'arête (START, read_feedback, join_research) est envoyée dans stage5_rag/agent.py, la forme de la session 3. Votre modification se trouve à l'intérieur du nœud. Récupération, et non appel de modèle, et déterministe : la même idée trouve les mêmes passages, tandis que le proposant varie.

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

Chaque ligne du corpus provenant d'un mémo vocal, d'une photo, d'un extrait ou d'une affiche indique le nom de son fichier. rag.media_in(hits) lit ces noms dans les passages et les transforme en URL que la page peut afficher et lire. 👉🖥 Renvoyez les contenus multimédias avec les passages, afin que les éléments envoyés par l'audience soient associés à ce qu'elle a dit :

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

👉🖥 Exécutez stage5_rag avec une idée. Ouvrez read_feedback : la requête est votre idée, le résultat contient les cinq passages les plus proches et une liste media avec une URL par fichier. Ouvrez join_research : trois clés. Ouvrez propose_directions : les candidats 1 à 3 citent feedback dans leurs preuves. Le panneau de vérification affiche ce que le lecteur a consulté sous les lignes : un extrait, trois notes vocales et une affiche pour le fantôme dans la lumière du réfrigérateur.

Ce que le troisième lecteur a examiné : les médias accompagnant les passages récupérés

7. 5 · Génération de vidéos : deux types de suspension, un seul mécanisme

Session 5 · Génération de vidéos

👉🖥 Dans le workbench, ouvrez Session 5 · Génération de vidéo, parties 5a et 5b, créées dans stage6_video, l'intégralité du graphique. Le clip dure quelques minutes et l'affiche quelques secondes, et l'exécution s'arrête deux fois en cours de route : une fois pour vous, une fois pour Veo. Les deux arrêts sont un seul mécanisme, et les deux branches du bureau partagent ce qu'elles ont créé par nom.

Deux types de suspension (5a)

Rien n'attend. Une exécution qui doit s'arrêter écrit un appel ouvert dans sa session et se termine. La personne ou le processus qui répond à cet appel poursuit l'exécution. La page s'ouvre sur une table :

pour une personne · direction_gate

pour un outil lent · render_desk

qui arrête l'exécution

direction_gate génère un RequestInput

render_submit renvoie {status: pending} ; LongRunningFunctionTool en fait un arrêt

ce qu'il attend

une personne : une des quatre cartes

une tâche lente : l'opération de Veo, en minutes

ce qui se trouve dans la session

un appel adk_request_input ouvert : le message, le formulaire, la charge utile

un appel render_submit ouvert : le reçu avec le nom de l'opération

comment elle reprend

un function_response avec l'ID de cet appel, contenant votre choix

un function_response avec l'ID de cet appel, contenant l'URL de l'extrait

qui l'envoie

adk web, lorsque vous envoyez le formulaire

python -m agent.deliver, à partir d'un autre processus, plus tard

où il vit entre-temps

runs/sessions.db · un ID de session · rien ne s'exécute

la même ligne, le même ID : rien ne s'exécute

Deux types de suspension, un seul tableau

Pour une personne Dans un seul prompt, "demander au créateur de confirmer" est un conseil, et la session 2 l'a ignoré dans un message. Dans un workflow, la pause est celle du moteur. Un nœud qui génère RequestInput suspend l'exécution : ADK écrit un appel ouvert nommé adk_request_input dans la session avec vos message, response_schema et payload, lui attribue un interrupt_id et revient à l'appelant. Aucun thread en attente. La seule chose qui la reprend est un function_response portant cet ID, validé par rapport à votre schéma. Un message de chat envoyé au workflow est un nouveau tour, pas une réponse. Le schéma est également le formulaire : adk web l'affiche, et l'application VibeStudio affiche le même schéma sous la forme de quatre fiches. direction_gate écrit déjà les candidats dans l'état. 👉🖥 Arrêtez-le :

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

Votre réponse arrive à persist_direction en tant que node_input, un dictionnaire avec une clé, pick. Les quatre candidats arrivent d'une autre manière : la porte les a écrits dans l'état partagé sous candidates, et ADK lie une clé d'état à un paramètre du même nom. Le nœud résout la sélection en un candidat et réécrit direction, angle, hook et user:prefs avec Event(state=...). Les clés précédées de user: sont conservées d'une session à l'autre pour cet utilisateur.

Pour un outil lent. Le rendu Veo prend plusieurs minutes, et un graphique ne devrait pas attendre aussi longtemps. render_submit dans agent/desk.py lance le job et renvoie immédiatement un reçu dont l'état est 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"]}

Encapsulé dans LongRunningFunctionTool, ce reçu signifie quelque chose pour l'ADK : le tour de l'agent se termine, le workflow est suspendu à render_desk, et l'appel ouvert avec son ID et son reçu est une ligne dans runs/sessions.db. 👉🖥 Dans stage6_video/agent.py, encapsulez l'outil :

    tools=[LongRunningFunctionTool(render_submit)])

Reprendre par ID, à partir d'un autre processus. agent/deliver.py recherche l'appel en attente dans le magasin de session, interroge Veo jusqu'à ce que l'extrait existe, écrit le résultat dans runs/state.json et envoie à la même session un function_response avec l'ID de cet appel. 👉🖥 Créez la partie de la réponse :

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

👉🖥 Exécutez stage6_video jusqu'à ce qu'il s'arrête deux fois. Envoyez une idée : l'exécution s'arrête sur un formulaire créé à partir de votre schéma, avec les quatre candidats dans sa charge utile. Saisissez 2 dans pick et envoyez la requête. Le graphique reprend à persist_direction. Essayez le test d'arrêt ici : suspendez le formulaire, arrêtez le serveur (scripts/stop.sh dans l'onglet 1), redémarrez-le (scripts/restart.sh), puis rouvrez la session. Le formulaire est toujours là, car une pause est une ligne, pas un thread. Après script_ready, render_desk appelle render_submit, obtient le reçu, répond WAITING et l'exécution se termine. La session conserve l'ID d'appel. Exécutez la livraison à partir de la console de la page ou de l'onglet 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

👉🖥 Cliquez sur Refresh adk web (Actualiser adk web) : function_response suit l'appel en attente, join_media est libéré et store_release s'exécute. Huit lignes de vérification lisent les deux arrêts de la session : l'appel de la porte et votre réponse, la réception du rendu et sa livraison.

État et bureau (5b)

Le bureau multimédia est à nouveau la structure de 3a, un fan-out et une jointure, avec une branche lente. script_ready envoie le script de deux manières. render_desk envoie l'extrait et l'exécution est suspendue. make_poster est l'outil de la session 2 en tant que nœud : le poster_idea du script, le style de la chaîne et ce que Memory Bank sait des goûts du créateur en matière d'affiches (lié à l'état en tant que memory_facts, session 6) sont envoyés au modèle d'image, et un PNG 9:16 est renvoyé en quelques secondes. join_media attend la lente, et store_release écrit ce qu'elle a trouvé dans l'état.

Partager par nom. store_release ne reçoit jamais l'affiche. Il déclare un paramètre nommé poster_url, et l'ADK le lie à partir de l'état partagé, car un nœud de l'autre branche a écrit cette clé. Aucune charge utile ne passe d'une branche à l'autre :

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'}")

Par défaut, make_poster génère l'affiche et un message, et store_release indique poster: not in state. 👉🖥 Ajoutez l'écriture d'état à son rendement final :

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

👉🖥 Exécutez une dernière fois et livrez. Événement d'ouverture de make_poster : l'URL de l'affiche dans sa sortie et un delta d'état avec poster_url. Ouvrez store_release : son message indique le nom de l'auteur de l'affiche. Le bloc This run's session (Session de cette exécution) de la page affiche les mêmes informations que l'onglet "State" (État) de la ligne adk web : l'ID de session dont une reprise a besoin, l'appel ouvert et les clés écrites par les nœuds, dont poster_url. Le panneau de vérification affiche l'extrait et l'affiche côte à côte.

L'extrait et son affiche, côte à côte après la livraison

8. 6 · Mémoire : ce que le canal mémorise entre les nuits

Session 6 · Mémoire

👉🖥 Dans l'atelier, ouvrez Session 6 · Memory, parties 6a et 6b, créées dans stage4_memory. La session 5 portait sur l'état d'une exécution. Cette session porte sur ce que le canal mémorise entre les exécutions : ce que le graphe entend de l'extérieur, via deux rappels.

Memory Bank (6a)

Vertex AI Memory Bank est une mémoire à long terme gérée concernant une personne. Les faits sont associés à un champ d'application, au nom de l'application, à l'ID utilisateur et aux thèmes que vous définissez. Lorsque vous lui transmettez un échange avec memories.generate, il extrait ce qui correspond à un sujet, l'intègre et le consolide avec ce qu'il contient déjà : un nouveau fait est créé, un fait modifié est mis à jour, un fait répété est fusionné. memories.retrieve renvoie les faits pour un champ d'application. Les deux appels se trouvent dans 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.",
}

Le troisième sujet est le goût de la chaîne en matière d'images. La version a écrit dans ce fichier lors de la session 5 : une affiche expédiée est décrite en une phrase et mémorisée, et l'affiche suivante est dessinée avec cette phrase dans son prompt.

La configuration a déjà commencé à créer la banque. La ligne de connexion de la page 6a indique si elle est opérationnelle. À partir des boutons de la page ou du terminal : python -m agent.platform.bank crée la banque et ses thèmes, et met en cache le nom de la ressource dans runs/memorybank.json ; python -m agent.platform.bank load amorce quatre sessions passées (deux époques d'animaux avec une règle de style, une époque de gadgets, une époque fantastique récente) ; python -m agent.platform.bank list montre ce que sont devenues les transcriptions : des énoncés de faits consolidés, et non des transcriptions.

Rappels (6b)

"Memory" n'est pas un nœud. Un rappel est une fonction que vous transmettez à un Agent. ADK l'appelle à un moment précis avec le contexte en direct, et le renvoi de None permet de poursuivre le processus normal. Trois paires : autour du tour de l'agent (before_agent_callback, after_agent_callback, avec un CallbackContext), autour de chaque appel de modèle (before_model_callback avec LlmRequest, after_model_callback avec LlmResponse), autour de chaque appel d'outil. Les garde-fous, la télémétrie, les caches et l'injection de contexte sont placés ici, sans nœud dans le graphe.

Dans stage4_memory/agent.py, propose_directions lit la banque avant l'appel du modèle : recall_taste récupère les faits, les ajoute à la requête en tant que bloc MEMORY (les plus anciens en premier) et indique au modèle d'orienter les candidats 1 à 3 vers le goût actuel et de traiter les règles comme des contraintes. Il écrit également les faits dans l'état sous la forme memory_facts, où make_poster les trouve. 👉🖥 Câblez-le :

    output_schema=Directions,
    before_model_callback=recall_taste)

Le scripteur écrit ce que le créateur a choisi après son tour : remember_pick lit direction à partir de l'état, écrit une phrase sur le choix et la transmet à memories.generate. 👉🖥 Câblez-le :

    output_schema=Script,
    after_agent_callback=remember_pick)

👉🖥 Exécutez-le avec un message vide et choisissez une carte. Ouvrez l'événement propose_directions : la requête comporte les trois époques, et les candidats 1 à 3 penchent vers le goût actuel, tandis que les tendances suggèrent autre chose. Après le scripteur, cliquez sur Afficher la banque : un souvenir a été modifié ou ajouté pour la sélection de ce soir. Exécutez une deuxième nuit : elle se souvient de la première.

9. 7 · Déployer et publier : le graphique dans une application, la nuit sur VibeTube

Session 7 · Déployer et publier

👉🖥 Dans l'atelier, ouvrez Session 7 · Déployer et publier, parties 7a et 7b.

Déployer VibeStudio (7a)

Dans les bacs à sable, adk web a piloté le graphique. VibeStudio l'exécute avec Runner d'ADK, sur un thread de travail avec sa propre boucle d'événements, et regroupe chaque événement dans un état d'exécution que la page lit via des événements envoyés par le serveur : les lecteurs, votre sélection de quatre cartes, l'affiche à son arrivée, le rendu à sa livraison. La réponse de la porte et la livraison du rendu sont le même appel, une partie function_response, et l'application interroge Veo elle-même au lieu d'attendre la console.

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/ est une copie octet par octet du agent/ de l'atelier, vérifiée par checks/verify_app.py. La liste complète des arêtes dans agent/graph.py regroupe toutes les structures de cet atelier de programmation en un seul endroit, soit 13 nœuds. Elle est déjà remplie, vous n'avez pas besoin de la saisir :

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

👉🖥 Déployez avec le bouton Déployer du workbench ou 👉💻 depuis l'onglet 1 :

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

qui s'exécute, avec les valeurs de votre .env renseignées :

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 crée le conteneur avec Cloud Build et le déploie en une seule étape. La première compilation prend quatre ou cinq minutes. --session-affinity avec une instance chaude conserve une exécution suspendue sur l'instance qui la contient : le magasin de sessions de la session 5, une fois de plus. Dans un déploiement plus important, le magasin de sessions passe à VertexAiSessionService et l'affinité disparaît. La compilation et le service s'exécutent en tant que compte de service Compute par défaut du projet, auquel la configuration a accordé les rôles nécessaires.

👉🖥 Ouvrez l'URL du service et lancez une soirée dans l'application : une idée, un choix, puis attendez l'extrait et l'affiche. L'onglet "Historique" conserve les deux.

Publier (7b)

La publication est une requête POST multipartite de l'application vers le point de terminaison du salon sur VibeTube, dans vibestudio/server/platform/publish.py. Le clip est la vidéo, l'affiche est la miniature, et le profil vous nomme, vous et la salle :

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 conserve une vidéo par projectId et par salle, et le projet que l'application envoie est votre projet Google Cloud. Par conséquent, si vous publiez à nouveau, votre vidéo est remplacée au lieu d'en ajouter une. Aucune affiche de substitution n'est importée. Une vraie affiche est la miniature, octet par octet.

La dernière modification est une configuration, pas une ligne de code. 👉🖥 Sur la page 7b : l'URL de VibeStudio (remplie à partir du déploiement de la page 7a), le code d'événement de la salle, exactement comme VibeTube l'écrit (celui que vous avez indiqué lors de la configuration, sauf si votre formateur en a ouvert un autre) et votre nom. Appuyez sur Publier. Le workbench transmet le clic à l'application, l'application publie la nuit, et les lignes ci-dessous lisent le résultat de l'application et de VibeTube : l'application répond, la nuit est terminée avec une vraie affiche, le nom de code nomme une salle ouverte, VibeTube a renvoyé un ID vidéo, la vidéo est sur le mur et sa miniature est votre affiche. La fiche de résultat affiche la miniature sur VibeTube à côté de l'affiche dans l'application.

Publié : la miniature sur VibeTube est l'affiche dessinée par le modèle d'image

10. Ce que vous avez créé

👉🖥 La fin de la page 7b dessine l'intégralité du graphique, soit 13 nœuds, de l'idée au clip et à son affiche. Pointez sur un nœud pour voir ce qu'il fait et la session qui l'a créé.

Session

Ce qu'il a enseigné

Écrit comme

2 · Génération d'images

Un agent : modèle, instruction, outils ; le modèle d'image comme outil ; ce qu'un prompt ne peut pas promettre

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

3 · Workflows

La liste des périphéries, la distribution et la jointure, un routeur avec une boucle en mode tâche derrière lui

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

4 · Ingestion multimodale

Un lecteur par genre, une forme de ligne, un corpus ; un troisième lecteur par sens, avec le média

generate_content avec des parties audio, image et vidéo, rag.upload_file, rag.retrieve, media_in

5 · Génération de vidéos

Deux types de suspension, un mécanisme ; le magasin de sessions ; l'affiche partagée par nom

RequestInput, LongRunningFunctionTool, FunctionResponse(id=...), Event(state=...), liaison de paramètres

6 · Mémoire

Mémoire sur plusieurs nuits, deux rappels

memories.generate / retrieve, before_model_callback, after_agent_callback

7 · Déployer et publier

Le graphique dans une application, sur Cloud Run ; la nuit sur VibeTube

Runner(agent=wf), run_async, événements envoyés par le serveur, une requête POST multipart

Cinq règles suivies par le graphique :

  1. Suspendez au lieu d'attendre. Une pause est une ligne dans le magasin de sessions, et non un thread.
  2. Reprenez tout de la même manière, en utilisant l'ID de l'appel ouvert, qu'une personne ou un processus réponde.
  3. Partagez les données par nom dans l'état, et non en les transmettant à chaque charge utile.
  4. Décidez avec du code avant de payer pour un modèle. La règle est un fichier texte et un routeur, et elle s'exécute avant l'affichage.
  5. Lisez tout ce qui n'est pas du texte dans le texte avant qu'il n'atteigne le corpus, avec le nom du fichier dans la ligne pour que le fichier puisse revenir.

Une requête ne peut pas promettre un ordre, un arrêt ou une vérification. Un graphique, oui.

Où l'emmener ensuite

  • VertexAiSessionService pour les sessions qui survivent à une instance, et supprimez --session-affinity.
  • Un webhook qui répond à l'appel du moteur de rendu au lieu d'un poller : même FunctionResponse(id=...), expéditeur différent.
  • Un deuxième RequestInput avant la publication : le schéma est le formulaire, il s'agit donc d'un nœud et non d'un code de page.
  • Les nouveaux commentaires, notes vocales et clips de l'audience ajoutés au corpus après chaque sortie, avec le poster sous lequel ils ont été envoyés.

Effectuer un nettoyage

Tout ce qui a été créé dans cet atelier se trouve dans le projet créé à l'étape 4. Le moyen le plus économique de nettoyer les ressources est donc de supprimer le projet. Dans Cloud Shell :

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

Pour conserver le projet et ne supprimer que les éléments payants : le service Cloud Run (gcloud run services delete vibestudio --region us-central1), le corpus RAG et la banque de mémoire (leurs noms de ressources se trouvent dans runs/ragcorpus.json et runs/memorybank.json, et les pages Agent Engine et RAG Engine de la console les suppriment), ainsi que l'image de conteneur dans Artifact Registry.