VibeStudio – Agentic Workflows z użyciem pakietu ADK, edycja Haunted

1. Wprowadzenie

VibeStudio, Haunted Edition

Prowadzisz na VibeTube kanał o przytulno-upiornej tematyce: co wieczór publikujesz jeden film Short aż do Halloween. Odbiorcy chcą małej magii w małych pokojach i wysyłają ją do Ciebie na wszystkie możliwe sposoby – jako komentarze, notatki głosowe, zdjęcia własnych nawiedzonych szuflad i klipy z ganku o zmierzchu. Zaległości są pełne pomysłów. Każdy wieczór poświęca na te same zadania: sprawdza, co jest popularne na karcie, przegląda zaległe materiały, czyta wiadomości od widzów, wybiera kierunek, sprawdza, czy jest on zgodny z zasadami kanału, pisze scenariusz, rysuje plakat, renderuje klip i publikuje.

Modele generatywne mogą wykonywać każde zadanie samodzielnie. Gemini wyszukuje, słucha, patrzy i pisze, model obrazu rysuje, a Veo renderuje. Nie potrafią jednak samodzielnie przeprowadzić nocy w odpowiedniej kolejności, zatrzymać się, aby podjąć decyzję, która należy do Ciebie, odrzucić złego kierunku, zanim będzie kosztował pieniądze, poczekać kilka minut na renderowanie bez pozostawiania niczego otwartego i pozostawić zapisu.

W tym ćwiczeniu z programowania ten wieczór zamieni się w ADK Workflow: narysowany wykres węzłów i krawędzi, który działa sam, pyta Cię tylko o to, gdzie potrzebna jest Twoja ocena, i kończy każde uruchomienie klipem i jego plakatem na VibeTube.

Prompt nie może obiecywać zamówienia, zatrzymania ani czeku. Wykres może.

Czego się nauczysz

  • 1 agent i model obrazu jako narzędzie. Agent to model, instrukcje i narzędzia. Trzecie narzędzie wywołuje model obrazu, a pierwsze uruchomienie kończy się plakatem. Następnie zdanie, które jest podstawą reszty modułu: prompt nie może obiecywać zamówienia, zatrzymania ani sprawdzenia.
  • Struktury, z których składa się graf. Workflow to lista krawędzi. Tworzysz rozgałęzienie i połączenie (równoległe czytniki spotykające się w JoinNode) oraz router, który nazywa własne wyjście, z agentem w trybie zadania za odrzuconym wyjściem, który czyści kierunek w pętli wywołań narzędzi i ponownie dołącza do głównej linii.
  • Potok przetwarzania multimodalnego. 4 rodzaje treści wysłanych przez odbiorców, po jednym czytniku na każdy rodzaj: Gemini transkrybuje notatki głosowe, dodaje napisy do zdjęć, ogląda klipy i słucha ich ścieżek dźwiękowych, a każdy czytnik zapisuje ten sam kształt linii w jednym korpusie silnika RAG. Trzeci czytnik w procesie fan-out pobiera informacje według znaczenia i przekazuje multimedia z fragmentami.
  • Dwa rodzaje zawieszenia, jeden mechanizm Bieg zatrzymuje się, gdy pojawia się osoba (RequestInput: 4 karty, odpowiadasz) i gdy narzędzie działa wolno (Veo za LongRunningFunctionTool: paragon). Oba te elementy są tym samym: otwartym wywołaniem w magazynie sesji, wznowionym przez jego identyfikator, z formularza lub z innego procesu. Dwa oddziały biura medialnego udostępniają plakat według nazwy w stanie.
  • Pamięć w ciągu nocy. Dwa wywołania zwrotne cyklu życia w Banku zapamiętanych informacji: before_model_callback przywołuje preferencje twórcy, zanim Proponujący je odczyta, a after_agent_callback zapamiętuje dzisiejszy wybór po Scenarzystce.
  • Wersja. Wykres w aplikacji z ADK Runner, zdarzenia wysyłane przez serwer na stronę, Cloud Run z koligacją sesji i jedno wieloczęściowe żądanie POST, które umieszcza klip na VibeTube z plakatem jako miniaturą.

Co utworzysz

aplikację działającą w przeglądarce: VibeStudio Workbench, stronę towarzyszącą z edytorem kodu na stronie, weryfikatory, które odczytują pliki na dysku i sesje zapisane przez uruchomienia, oraz wbudowany adk web. Siedem sesji odpowiada siedmiu stronom tego ćwiczenia, z tymi samymi numerami i literami części, dzięki czemu oba te elementy są ze sobą zsynchronizowane. Każda strona otwiera się na pasku, który zawiera jedno zdanie o sesji i 3 czynności, które można w niej wykonać: edytować, uruchomić i zweryfikować. Przeczytaj sekcję, a następnie wprowadź zmiany w obszarze roboczym. Każda zmiana kończy się panelem weryfikacji, który odczytuje, co się stało.

Co gdzie działa: VibeStudio Workbench, Twój backend i usługi Google Cloud

Uruchamiane są 3 elementy: Workbench (strona na porcie 4600, edytor, weryfikatory), Twój backend (Workflow w agent/ i 1 aplikacja piaskownicy na sesję w stage0_prompt/ do stage6_video/, którą obsługuje adk web) oraz Google Cloud (Gemini, model obrazu, RAG Engine, Veo, bank pamięci – wszystkie dostępne przez Vertex AI z Twoimi danymi logowania).

7 sesji,

Sesja

Co utworzysz

Twoje zmiany

1 · Historia

Jeszcze nic – noc i graf, który z niej powstaje

–

2 · Generowanie obrazów

Jeden agent rysuje pierwszy plakat

POSTER_TOOL

3 · Przepływy pracy

Noc jako wykres: rozgałęzienie i złączenie, router i jego pętla

FANOUT, ROUTER

4 · Przetwarzanie multimodalne

To, co wysłali odbiorcy, odczytane w jednym korpusie; trzeci czytelnik

INGEST_READERS, RAG_MEDIA

5 · Generowanie filmów

Dwa rodzaje zawieszenia; plakat udostępniony według nazwy w stanie

GATE, VIDEO_TOOL, DELIVER_RESPONSE, STATE_SHARE

6 · Pamięć

Co kanał zapamiętuje między nocami

MEMORY_RECALL, MEMORY_REMEMBER

7. Wdrażanie i publikowanie

VibeStudio w Cloud Run; noc na VibeTube

kod wydarzenia w pokoju i opublikuj go.

Jak korzystać z tego ćwiczenia. Najpierw przedstawiamy koncepcję: każda sesja wyjaśnia ideę, a następnie pokazuje kod po wprowadzeniu zmiany i omawia go. Nigdy nie prosi o edytowanie pliku tutaj – edytowanie odbywa się w obszarze roboczym, który wyświetla po jednej zmianie naraz, ze wskazówką obok i panelem weryfikacji poniżej. Przeczytaj sesję, a następnie postępuj zgodnie z instrukcjami na stronie.

Gdzie znajduje się ta funkcja

Wykres, motyw i pokój w VibeTube pochodzą z edycji Lin's Haunted Edition w VibeStudio. To multimodalna wersja, która skupia się na tym, co wysłali widzowie, i na 2 rodzajach zawieszenia, w jednostronicowym interaktywnym formacie laboratoriów Doliny Agentów. Studio to tylko scenografia. Każdy mechanizm pod nim jest zwykłym mechanizmem, którego używasz w potoku pomocy, procesie sprawdzania treści lub w jakimkolwiek innym procesie, który musi zostać wstrzymany, aby człowiek mógł wykonać powolne zadanie.

2. 🧰 Konfiguracja

  • Co – konto, które może płacić, projekt, zainstalowane laboratorium, model, który może odpowiadać, i środowisko robocze działające w tle na jednym porcie.
  • Dlaczego – jeden proces na jednym porcie: strona, edytor, weryfikatory, /api/* i adk web mają wspólne źródło, więc nic w tym module nie wymaga drugiego terminala ani drugiego portu.
  • How – klonowanie, 2 skrypty. Drugi wykonuje rzeczywiste wywołanie modelu, uruchamia środowisko robocze i rozpoczyna tworzenie korpusu RAG i banku zapamiętanych informacji, podczas gdy Ty czytasz, dzięki czemu sesje 4 i 6 są gotowe.

Wszystko w tym module jest uruchamiane w Cloud Shell – gcloud, Python, Node, ffmpeg i git są już tam zainstalowane, a Gemini, model obrazu, Veo, mechanizm RAG i bank pamięci są dostępne przez Vertex AI przy użyciu Twoich danych logowania. W tym module nie ma żadnego klucza interfejsu API i nie musisz niczego instalować na laptopie. Laptop z (pakietem) SDK Google Cloud, Pythonem 3.12 i Node 20+ działa tak samo.

Jest jednak jedna rzecz, której nie możemy Ci zapewnić w tym module: projekt Google Cloud z włączonymi płatnościami. Krok 1 pozwala uzyskać konto, które może za nie zapłacić, a krok 4 przekształca to konto w projekt. Wykonaj je w tej kolejności – każdy krok po kroku 4 odczytuje projekt, który został ustalony w kroku 4.

1. Uzyskanie konta z rozliczeniami (~1 min)

Zaloguj się na konto Google, na którym włączone są płatności. Możesz użyć dowolnego z nich. Jeśli nie, rozpocznij bezpłatny okres próbny, który zapewnia konto rozliczeniowe, czyli wszystko, czego potrzebujesz w kroku 4. Jeśli uczestniczysz w module prowadzonym przez instruktora, najpierw wykorzystaj środki na tę sesję – w ten sposób utworzysz konto rozliczeniowe.

2. Otwórz Cloud Shell (~2 min)

👉🖥 Kliknij Aktywuj Cloud Shell u góry konsoli Google Cloud – to ikona w kształcie terminala w nagłówku.

Aktywowanie Cloud Shell z poziomu nagłówka konsoli Google Cloud

👉🖥 Kliknij Otwórz edytor (ikona otwartego folderu z ołówkiem). Spowoduje to wyświetlenie edytora Cloud Shell z eksploratorem plików po lewej stronie – tam możesz przejść, gdy chcesz odczytać cały plik.

Otwieranie edytora Cloud Shell

👉🖥 Jeśli w którymś momencie pojawi się prośba o autoryzację, kliknij Autoryzuj, aby przejść dalej.

👉🖥 Otwórz terminal w edytorze – to karta 1, na której będą wykonywane wszystkie kroki oznaczone ikoną 👉💻. Karta 2 to sam obszar roboczy, który otworzysz w kroku 7. Jeśli terminal nie jest jeszcze widoczny u dołu ekranu, otwórz go, wybierając Widok → Terminal:

Otwieranie nowego terminala w edytorze Cloud Shell

👉💻 Sprawdź, na które konto jesteś zalogowany(-a) – powinno to być konto z kroku 1:

gcloud auth list

3. Pobierz moduł (~1 min)

👉💻 Sklonuj repozytorium:

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

4. Utwórz projekt (~2 min)

👉💻 Jeden skrypt znajduje Twoje konto rozliczeniowe, tworzy w nim projekt, łączy rozliczenia i wskazuje ten projekt w Cloud Shell:

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

Powinien pojawić się komunikat ✓ dotyczący wybranego konta rozliczeniowego, komunikat ✓ dotyczący utworzonego projektu oraz zakończenie podobne do tego:

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

vibe-studio-4271 to identyfikator projektu – Twój będzie miał inne cyfry. W tym miejscu dzieją się 4 rzeczy:

billing

wybiera najnowsze konto [YYYY-MM-DD] GDP Credit:, jeśli je masz, lub inne otwarte konto rozliczeniowe.

project

tworzy vibe-studio-XXXX i łączy z nim to konto rozliczeniowe

selects

zapisuje identyfikator w ~/project_id.txt i uruchamia gcloud config set project, więc nie musisz go już wpisywać.

waits

odpytuje, dopóki projekt nie będzie mógł być wyświetlany (maksymalnie przez 2 minuty);

👉🖥 Ten sam projekt jest teraz w konsoli – jego identyfikator znajduje się na karcie Informacje o projekcie w panelu:

Identyfikator projektu w panelu konsoli

5. Zainstaluj i skonfiguruj moduł (~4 min)

👉💻

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

Jeden skrypt, który można bezpiecznie uruchomić ponownie – drugie uruchomienie zachowuje wszystkie odpowiedzi udzielone za pierwszym razem. Na początku pojawią się 2 pytania. W obu przypadkach naciśnij Enter, aby wybrać domyślną odpowiedź:

kod zdarzenia

pokój VibeTube, w którym opublikowano Twój film w sesji 7. Wpisz kod sali otrzymany od nauczyciela lub naciśnij Enter, aby wejść do pokoju sandbox, który jest otwarty dla wszystkich przez cały czas. W kodach wielkość liter ma znaczenie, a pokój akceptuje przesyłanie tylko w określonym oknie. Skrypt informuje, czy nazwany przez Ciebie pokój jest otwarty.

display name

nazwę, pod którą pokój Cię wymienia, lub naciśnij Enter, aby wybrać nazwę utworzoną na Twoim koncie.

Oba te elementy zostaną umieszczone w .env i będzie można je później zmienić w .env lub na stronie publikowania. Następnie wykonuje pozostałe czynności w tej kolejności:

uv env

instaluje uv, jeśli go brakuje, a następnie uv venv i uv sync instalują dokładnie to, co przypina uv.lock – google-adk, google-genai, vertexai, fastapi, uvicorn – w .venv

apis

włącza 6 interfejsów API w projekcie wybranym w kroku 4 i potwierdza, że każdy z nich odpowiada, zanim przejdzie dalej: aiplatform (Gemini, model obrazu, Veo, bank pamięci, RAG Engine), vectorsearch (magazyn za korpusem RAG), run, cloudbuild, artifactregistry (sesja 7), cloudtrace (ślady aplikacji);

account

sprawdza, czy domyślne konto usługi Compute projektu istnieje i ma role wymagane przez kompilację i usługę z sesji 7;

.env

zapisuje plik .env, kierując każde wywołanie do Vertex AI w projekcie – Gemini w punkcie końcowym global, Veo, banku pamięci i silnika RAG w us-central1 – i nie ma żadnego klucza interfejsu API; poprzedni plik .env jest zachowywany jako .env.bak, a wszystko, co zostało dodane ręcznie, pozostaje bez zmian;

veo · room

potwierdza, że reklama veo-3.1-fast-generate-001 jest wyświetlana w us-central1 (odczyt metadanych, brak renderowania), i pyta VibeTube, czy Twój pokój jest otwarty.

model

wykonuje jedno prawdziwe wywołanie Gemini, dzięki czemu nieprawidłowy projekt nie powiedzie się podczas konfiguracji, a nie w trakcie sesji;

starter

umieszcza 9 plików, które będziesz edytować, w miejscu z starter/, zachowując ich wiersze TODO – tylko te, których brakuje; Twoje zmiany nigdy nie zostaną zastąpione;

page

instaluje i kompiluje web/, stronę obszaru roboczego, a następnie uruchamia serwer w tle na porcie 4600.

cloud

rozpoczyna tworzenie korpusu RAG Engine i banku zapamiętanych informacji w tle, dzięki czemu sesje 4 i 6 będą mogły z nich korzystać.

preflight

uruchamia scripts/preflight.py i wyświetla link do otwarcia,

6. Przeczytaj informacje o kontroli wstępnej (~1 min)

Ostatnia rzecz, która została wydrukowana w kroku 5, to sprawdzenie wstępne – każda linia to prawdziwa sonda, a nie domysł:

  ✓ 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) ma rację: ten wykres jest pusty, a sesja 3 to miejsce, w którym go wypełniasz. Dwa wiersze - też są prawidłowe – korpus pojawia się po minucie lub dwóch, a bank po około 20 sekundach. Żaden z nich nie będzie potrzebny przed sesją 4.

👉💻 Możesz uruchomić go ponownie w dowolnym momencie, aby sprawdzić, na jakim etapie jesteś. Odczytuje on Twoje pliki, projekt i uruchomiony serwer, a nie listę kontrolną:

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

7. Otwórz środowisko robocze (~1 min)

Środowisko pracy jest już uruchomione. W kroku 5 uruchomiono go w tle – 1 proces uvicorn na porcie 4600, który obsługuje stronę, każdą trasę /api/* i adk web zamontowane w /workbench. Nie ma nic do uruchomienia i nie musisz niczego restartować – zmiana zostanie wprowadzona przy następnym uruchomieniu.

👉🖥 Kliknij link wyświetlony przez proces wstępny lub w Cloud Shell: Podgląd w przeglądarce → Zmień port → 4600. Nazwij ją tab 2. Otworzy się Sesja 1 – Historia.

Trzy polecenia, które mogą Ci się później przydać, wszystkie z ~/vibe-studio-multimodal:

scripts/stop.sh

zatrzymuje workbench,

scripts/start.sh

uruchamia go na pierwszym planie; Ctrl+C zatrzymuje go i każdy uruchomiony przez niego proces

scripts/restart.sh --pull

git pull, a następnie uruchom ponownie w tle, synchronizując venv i ponownie tworząc stronę tylko wtedy, gdy uległy zmianie

Układ repozytorium

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 · Historia – noc i wykres, który z niej powstaje

👉🖥 Karta 2 otworzy się na stronie Sesja 1 · Historia. Przeczytaj: 6 zadań ułożonych w pierścień wokół jednej osoby – zbadaj trend, przejrzyj zaległości, wybierz kierunek, sprawdź zasady, napisz skrypt, wyrenderuj klip – i pierścień powtarza się co noc. Gemini może samodzielnie przeprowadzić analizę, przygotować zaległe zadania i napisać skrypt. Veo może samodzielnie wyrenderować film. Wybór i sprawdzenie należą do człowieka.

Pod pierścieniem znajduje się wykres, który jest tworzony w pozostałej części modułu. Składa się on z 13 węzłów – od pomysłu do klipu i plakatu. Najedź kursorem na węzeł, aby zobaczyć, co robi i w której sesji jest tworzony. Nie musisz niczego edytować. Przycisk Wygeneruj pierwszy plakat na stronie to sesja 2.

Słownictwo, więc następne 6 stron czyta się bez zatrzymywania:

słowo

co to oznacza w tym przypadku.

agent

ADK Agent: model, instrukcja, narzędzia. Sesja 2

node

one step of the graph – a Python function, an agent, a join, a router

edge

krotka węzłów, które działają w określonej kolejności; graf jest ich listą. Sesja 3

session

rekord jednego uruchomienia w runs/sessions.db: jego zdarzenia, stan i wszystkie otwarte wywołania. Sesja 5

state

słownik, który węzły udostępniają według nazwy klucza; węzeł zapisuje Event(state=...), a inny deklaruje parametr o tej samej nazwie. Sesja 5

pamięć

co jest zachowywane po zakończeniu sesji – Bank zapamiętanych informacji, za pomocą wywołań zwrotnych. Sesja 6

4. 2. Generowanie obrazów – 1 agent i model obrazu jako narzędzie

Sesja 2. Generowanie obrazów

👉🖥 W środowisku roboczym otwórz Sesja 2 · Generowanie obrazów. Zanim powstał wykres, najprostsze rozwiązanie, które mogło zadziałać: jeden Agent, którego instrukcja to cała noc w formie prozy, 3 funkcje Pythona jako jego ręce i uruchomienie, które kończy się obrazem.

Agent to model, instrukcja i narzędzia.

stage0_prompt/agent.py definiuje root_agent za pomocą 3 pól agenta ADK. model to model Gemini od agent/platform/config.py. instruction to prompt systemowy: 6 zadań w jednym, z czego ostatnie to obraz. tools ma 3 funkcje.

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

Model to zamknięty świat: wnioskuje na podstawie swoich wag i tokenów w kontekście. Narzędzie to funkcja Pythona, o którą model może poprosić. ADK odczytuje sygnaturę i docstring każdej funkcji i deklaruje ją w modelu. Gdy model potrzebuje funkcji, emituje zdarzenie function_call, środowisko wykonawcze uruchamia Pythona i zapisuje zdarzenie function_response z powrotem w kontekście, a model kontynuuje działanie z wynikiem przed sobą.

Trzy funkcje, z których jedna rysuje

Dwie z nich przeczytały wyniki badań. Trzeci prosi model generowania obrazów o plakat:

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 wywołuje agent/platform/images.py, czyli model obrazu opakowany jako zwykła funkcja, tak jak videogen.py opakowuje Veo: prefiks w stylu domu (przytulny, low-poly halloweenowy wygląd kanału, dyniowy pomarańcz i kremowy kolor świecy na tle śliwki o północy), temat, pionowa ramka 9:16, jeden plik PNG poniżej runs/media/posters, wyświetlany w /static/posters. W przypadku STUDIO_REAL_IMAGES=0 kopiuje plakat zastępczy i oznacza wynik jako prebaked, dzięki czemu moduł działa w dowolnym miejscu.

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

To samo wywołanie jest węzłem na wykresie w sesji 5.

Praktyczne: jedna edycja

W obecnej wersji make_poster zwraca słownik z informacją, że plakat został pominięty. 👉🖥 W edytorze Workbench wywołaj model obrazu, zastępując wiersz TODO:

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

Zapisz. Wiersze weryfikacji odczytują plik: 3 narzędzia w agencie, wywołanie modelu obrazu na miejscu. 👉🖥 Kliknij Sprawdź obciążenia przepływu pracy, a następnie Otwórz ADK Web i wyślij:

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

Czego możesz się spodziewać i dlaczego

  • Dwa zdarzenia narzędzia przed odpowiedzią, a potem pytanie. check_trends i read_backlog, ponieważ instrukcja mówiła o sprawdzeniu i zajrzeniu, a model nie miał żadnego z tych słów w swoich wagach. Odpowiedź proponuje kierunek i prosi o potwierdzenie, ponieważ instrukcja wymagała najpierw zgody.
  • Druga wiadomość zawiera wszystkie informacje. Wyślij: skip the questions, describe the video and draw the poster. Model opisuje film, wywołuje funkcję make_poster raz z tematem, który sam napisał, i informuje, gdzie został zapisany plik PNG. W panelu weryfikacji obok wierszy pojawia się plakat: scena Halloween w stylu kanału, narysowana w około 8 sekund.
  • Dlaczego nie jest to kanał. Badania są przedstawione w formie tekstu, którego nie można zacytować. Zasady zostały zatwierdzone przez model, który napisał instrukcje, a plakat został narysowany, zanim ktokolwiek go sprawdził. W jednej wiadomości wspomniano o umowie. Tekst prompta nie jest bramą. W dalszej części modułu te 6 zadań zostanie przedstawionych w formie grafu.

Strona kończy się sekcją Z czego składa się agent ADK, do której możesz wrócić: model, kontekst, na podstawie którego wnioskuje, narzędzia i współpracownicy, za pomocą których działa, wywołania zwrotne, które opakowują każde wywołanie, oraz sesja i pamięć, które istnieją poza nim.

5. 3 · Przepływy pracy – rozgałęzianie i łączenie, router i pętla

Sesja 3. Przepływy pracy

👉🖥 W obszarze roboczym otwórz Sesja 3 · Przepływy pracy, części 3a i 3b. Każda część jest tworzona w aplikacji piaskownicy, dzięki czemu można ją uruchomić w adk web samodzielnie: fan-out w stage1_fanout, router i jego pętlę w stage3_router.

Rozgałęzianie i scalanie (3a)

ADK Workflow to lista krawędzi. Każdy wpis to łańcuch, czyli krotka węzłów, które są uruchamiane w określonej kolejności. Dwa łańcuchy opuszczające START działają równolegle. Łańcuch, który kończy się w węźle JoinNode, raportuje do niego, a połączenie czeka na każdy łańcuch, który kończy się w tym miejscu, zanim przejdzie dalej, przekazując jeden słownik z kluczem według nazwy węzła. Cel zapisany jako słownik mapuje nazwy tras routera na węzły. Na stronie 3a w zeszycie ćwiczeń znajdują się kształty i ćwiczenie polegające na rysowaniu krawędzi.

Rodzaj węzła

Zapisane jako

Działanie

Węzeł funkcji

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

Zwykły Python. Może też zapisywać stan udostępniony za pomocą Event(state=...).

Węzeł złączenia

JoinNode(name="join_research")

Czeka, aż wszystkie przychodzące łańcuchy zostaną zgłoszone, a następnie przekazuje wszystkie ich dane wyjściowe jako jeden słownik.

Węzeł agenta

Agent(...)

Jedno wywołanie modelu w trybie single_turn, ustrukturyzowana odpowiedź, brak rozmowy.

Router

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

Węzeł funkcji, którego zwracana wartość określa nazwę krawędzi wychodzącej.

Węzeł danych wejściowych od człowieka

yield RequestInput(...)

Wstrzymuje wykres, dopóki ktoś nie odpowie na formularz. Sesja 5.

Rozgałęzienie badań: 2 czytniki od START do połączenia

Pierwszy wykres przedstawia badania z sesji 2 jako węzły. scan_trends i read_backlog w agent/graph.py to 2 czytniki. read_backlog zapisuje też dzisiejszy pomysł w stanie udostępnionym. stage1_fanout/agent.py definiuje połączenie i tworzy Workflow z listy o nazwie EDGES, która jest pusta (0 edges kontroli wstępnej). 👉🖥 Wypełnij ją 2 łańcuchami:

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

👉🖥 Uruchom go w adk web. Dwa węzły zapalają się razem, a następnie łączą. Otwórz zdarzenie join_research i odczytaj jeden słownik z kluczem scan_trends i kluczem read_backlog. Nie nastąpiło wywołanie modelu. Nic nie zmienia formatu słownika węzła łączenia: węzeł po nim, w kroku 3b, jest agentem, a węzeł agenta otrzymuje node_input jako wiadomość, więc słownik dociera jako JSON, który model odczytuje bezpośrednio.

Router i pętla (3b)

Węzeł Agent wewnątrz węzła Workflow działa w trybie single_turn: otrzymuje dane wyjściowe poprzedniego węzła jako wiadomość, wykonuje jedno wywołanie modelu i odpowiada raz. W przypadku output_schema odpowiedź jest weryfikowana pod kątem modelu Pydantic, zanim zobaczy ją następny węzeł, więc graf przekazuje obiekty typowane, a nie tekst. propose_directions odczytuje słownik wyszukiwania i proponuje 4 kandydatów, z których każdy jest sceną halloweenową. Kandydat 4 to przynęta: jego tytuł zawiera słowo, którego kanał nie akceptuje, więc router ma coś do wychwycenia. Każdy kandydat ma poster_idea, czyli jeden wiersz z nazwą pojedynczego obrazu, który sprzedaje film. Scenarzysta go zachowuje, a zespół ds. multimediów rysuje go podczas sesji 5.

Router to węzeł funkcji, którego zwracana wartość określa krawędź, którą przyjmuje uruchomienie. Brak wywołania modelu: policy_check odczytuje agent/policy_words.txt w momencie podejmowania decyzji, dopasowuje całe słowa do tytułu i kąta kierunku oraz kieruje je, zanim zostanie napisany skrypt lub wydane pieniądze. Zasada to dane: edytuj plik, a następne uruchomienie go odczyta.

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

Bramka zasad: router z 2 oznaczonymi wyjściami

stage3_router/agent.py to fan-out plus propose_directions i choose_direction, zastępujący ludzkie drzwi, które tworzysz w sesji 5: wybiera kandydata, którego numer kończy dzisiejszy pomysł (..., pick 4), w przeciwnym razie wybiera pierwszego i zapisuje wybór w stanie. scripter to agent single_turn, który przekształca zatwierdzony kierunek w Script: tytuł, opis, tagi, zdanie otwierające, 3 ujęcia i pomysł na plakat. Każde ujęcie to obraz Halloween w stylu kanału.

Pętla za odmową wyjścia. quarantine jest dostarczany jako agent w trybie zadania. Agent ma mode: chat to pętla konwersacyjna, którą agent główny uruchamia z użytkownikiem, single_turn to jedno wywołanie z ustrukturyzowaną odpowiedzią, a task to pętla wywołań narzędzi, którą agent uruchamia samodzielnie, dopóki nie wywoła wbudowanego finish_task, którego argumenty ADK pobiera z output_schema. Kwarantanna wywołuje funkcję find_policy_hits w odrzuconym kierunku, prosi suggest_replacement o zatwierdzony zamiennik kanału dla każdego słowa (agent/policy_replacements.txt, ponownie dane), przepisuje, sprawdza ponownie i kończy wpisanym CleanedDirection. Przeczytaj go na stronie, nie wpisuj go.

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

W ustawieniach domyślnych łańcuch prowadzi od wyboru bezpośrednio do skryptu. 👉🖥 Umieść między nimi router z 2 wyjściami i ścieżką powrotną z kwarantanny. 3 linie zastępują 1:

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

👉🖥 Uruchom obie trasy. Wyślij pomysł w obecnej postaci: choose_direction pobiera kandydata 1, policy_check generuje OK, a scenarzysta pisze ustrukturyzowany skrypt. Kliknij Nowa sesja i wyślij pomysł ponownie z , pick 4 na końcu: kandydat 4 to przynęta, policy_check emituje BLOCK, kwarantanna wywołuje swoje narzędzia, przepisuje odrzucone słowa, wywołuje finish_task, a oczyszczone polecenie wraca do skryptu. Wiersze weryfikacji odczytują oba uruchomienia z magazynu sesji.

Każde zdanie prompta z sesji 2 ma teraz węzeł: badania to 2 czytniki i połączenie, odmowa to router i agent zadania, a skrypt to węzeł agenta. Na całym wykresie zastępczy wybór to 2 węzły funkcji, direction_gate i persist_direction, które będą działać, dopóki sesja 5 nie zatrzyma drzwi. Umowa z twórcą jest nadal tylko wstępna, podobnie jak badania, które nie uwzględniają jeszcze opinii odbiorców.

6. 4 · Przetwarzanie multimodalne – co wysłali odbiorcy, odczytane w jednym korpusie

Sesja 4. Przetwarzanie multimodalne

👉🖥 W środowisku roboczym otwórz Sesja 4 · Pozyskiwanie danych multimodalnych, części 4a i 4b. Odbiorcy nie tylko pisali. Po jednym czytniku na każdy rodzaj danych przekształca wszystko, co zostało wysłane, w tekst o tym samym kształcie. Jeden korpus silnika RAG przechowuje wszystkie te dane, a trzeci czytnik w procesie rozsyłania przeszukuje je według znaczenia i zwraca multimedia.

Co wysłali odbiorcy (4a)

agent/comments.md zawiera 12 komentarzy napisanych pod 6 filmami o Halloween na kanale. W agent/audience/ znajdują się pozostałe elementy: 8 notatek głosowych (voice/*.mp3), 14 zdjęć pokoi widzów (photo/*.jpg) i 6 klipów (clip/*.mp4) – powolne przesuwanie po zdjęciu z głosem widza w tle. Na stronie 4a otwierają się wszystkie te elementy: odtwarzają się wszystkie notatki głosowe, wyświetlają się wszystkie zdjęcia i uruchamiają się wszystkie klipy.

Siatka odbiorców po przetworzeniu: każda karta pokazuje, co Gemini usłyszał lub zobaczył

RAG Engine pobiera tekst. Notatka głosowa, zdjęcie lub klip mogą dołączyć do korpusu tylko w taki sposób, jak wcześniejsze posty w rag.caption_posters: model przekształca je w wiersz tekstu, który nazywa plik, wiersz staje się fragmentem, a gdy fragment wraca, wraca z nim plik. agent/platform/audience.py ma po jednym czytniku każdego rodzaju. Każdy z nich wysyła do Gemini 1 plik i 1 prompta oraz zapisuje 1 wiersz na plik:

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 robi to samo z image/jpeg i PHOTO_PROMPT, watch_clips z video/mp4 i CLIP_PROMPT, a Gemini ogląda obraz i słucha ścieżki dźwiękowej w jednym wywołaniu. Każdy czytnik zapisuje ten sam kształt linii, więc fragmentowanie, umieszczanie i pobieranie nigdy nie wie, z jakiego rodzaju pochodzi fragment:

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

One reader per kind, one line shape, one corpus

Korpus. Mechanizm RAG w Vertex AI to wyszukiwanie w dokumentach: korpus jest dzielony na fragmenty o długości około 120 tokenów, każdy fragment jest osadzany za pomocą text-embedding-005, a zapytanie jest osadzane w ten sam sposób i odpowiada na nie najbliższymi fragmentami według znaczenia, a nie według wspólnych słów. „przytulne małe nawiedzenia w domu” znajduje komentarz z prośbą o małą magię w małych pokojach, notatkę głosową o nietoperzu przytulającym żarówkę i klip, którego ścieżka dźwiękowa mówi, że duch to papierowy kubek.

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

Konfiguracja rozpoczęła już tworzenie korpusu. Wiersz połączenia na stronie 4a informuje, czy korpus jest aktywny, i tworzy go, jeśli konfiguracja nie mogła tego zrobić. Z przycisków na stronie lub terminala: python -m agent.platform.rag tworzy korpus i zapisuje go w runs/ragcorpus.json; python -m agent.platform.rag load --audience uruchamia każdy czytnik w audience.READERS, a następnie przesyła to, co zapisał, obok komentarzy i podpisów autorów, każdy plik pod własną nazwą, i czeka na odpowiedź indeksu; python -m agent.platform.rag query "cozy little hauntings at home" pokazuje pobieranie, znajdując fragmenty, które nie mają żadnego wspólnego słowa z zapytaniem.

Zmiana. W konfiguracji fabrycznej READERS zawiera tylko read_comments. Trzy czytniki multimediów znajdują się za jednym #. 👉🖥 Uruchom przetwarzanie raz, tak jak jest: dane wyjściowe to not read (no reader wired in audience.READERS): voice, photo, clip, a przetwarzane są tylko komentarze i napisy. Następnie usuń # :

    transcribe_voice, caption_photos, watch_clips,

👉🖥 Zapisz i ponownie uruchom przetwarzanie. Odczytanie 40 elementów zajmuje czytnikom około 3 minut. Konsola wyświetla po kolei heard, saw i watched, a każda karta w siatce wypełnia się odpowiednim wierszem. Wiersze weryfikacji odczytują plik, dane wyjściowe czytników i korpus: 4 czytniki podłączone, 8 odsłuchanych notatek głosowych, 14 obejrzanych zdjęć, 6 obejrzanych klipów, 5 plików w korpusie.

Trzeci czytnik (4b)

Opinie odbiorców to badania, które są udostępniane przez każde uruchomienie, więc trafiają do wykresu tam, gdzie badania: jako trzeci czytnik w połączeniu. read_feedback w agent/graph.py pobiera pomysł na dzisiejszy wieczór, prosi korpus o najbliższe fragmenty i zgłasza je do join_research, który czeka na 3 fragmenty. Krawędź (START, read_feedback, join_research) jest dostarczana w stage5_rag/agent.py, kształt sesji 3; Twoja zmiana znajduje się wewnątrz węzła. Pobieranie, a nie wywołanie modelu, i deterministyczne: ten sam pomysł znajduje te same fragmenty, podczas gdy propozycja się zmienia.

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

Każdy wiersz w korpusie, który pochodzi z notatki głosowej, zdjęcia, klipu lub plakatu, zawiera nazwę pliku. rag.media_in(hits) odczytuje te nazwy z fragmentów i zamienia je na adresy URL, które strona może wyświetlać i odtwarzać. 👉🖥 Przekaż multimedia z powrotem wraz z fragmentami, aby treści wysłane przez odbiorców były powiązane z ich wypowiedziami:

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

👉🖥 Uruchom stage5_rag, podając pomysł. Otwórz read_feedback: zapytanie to Twój pomysł, a wynik zawiera 5 najbliższych fragmentów i listę media z adresem URL każdego pliku. Otwórz join_research: 3 klawisze. Otwórz propose_directions: kandydaci od 1 do 3 cytują feedback w swoich dowodach. W panelu weryfikacji widać, co czytelnik oglądał pod wierszami: klip, 3 notatki głosowe i plakat przedstawiający ducha w świetle lodówki.

Co sprawdził trzeci czytelnik: media, z których pochodziły pobrane fragmenty.

7. 5 · Generowanie filmów – 2 rodzaje zawieszenia, 1 mechanizm

Sesja 5. Generowanie filmów

👉🖥 W obszarze roboczym otwórz Sesję 5 – Generowanie filmów, części 5a i 5b, utworzone w stage6_video, cały wykres. Klipy zajmują minuty, a plakaty sekundy, a proces zatrzymuje się 2 razy: raz dla Ciebie, raz dla Veo. Te 2 przystanki to jeden mechanizm, a 2 gałęzie biurka mają wspólną nazwę.

Dwa rodzaje zawieszenia (5a)

Nie ma żadnych oczekujących elementów. Uruchomienie, które musi się zatrzymać, zapisuje otwarte wywołanie w sesji i kończy się. Każdy, kto odpowie na to wywołanie, osoba lub proces, kontynuuje je. Strona otworzy się na jednej tabeli:

dla osoby · direction_gate

powolne narzędzie · render_desk

kto zatrzymał uruchomienie,

direction_gate daje RequestInput

render_submit zwraca {status: pending}; LongRunningFunctionTool sprawia, że jest to przystanek

na co czeka,

osoba: jedna z 4 kart,

powolne zadanie: działanie Veo, minuty

co znajduje się w sesji,

otwarte wywołanie adk_request_input: wiadomość, formularz, ładunek

otwarte wywołanie render_submit: potwierdzenie z nazwą operacji,

jak jest wznawiana

function_response z identyfikatorem tego połączenia, zawierający Twój wybór.

obiekt function_response z identyfikatorem tego wywołania, zawierający adres URL klipu.

kto ją wysyła,

adk web, gdy prześlesz formularz

python -m agent.deliver, z innego procesu, później

gdzie mieszka w międzyczasie,

runs/sessions.db · 1 identyfikator sesji · nic nie jest uruchamiane

ten sam wiersz, ten sam identyfikator – nic się nie uruchamia

2 rodzaje zawieszenia, 1 tabela

Osoba W jednym prompcie „poproś twórcę o potwierdzenie” jest poradą, a w sesji 2 została ona pominięta w jednej wiadomości. W przepływie pracy pauza jest wykonywana przez wyszukiwarkę. Węzeł, który zwraca RequestInput, zawiesza działanie: ADK zapisuje otwarte wywołanie o nazwie adk_request_input w sesji z message, response_schema i payload, nadaje mu interrupt_id i wraca do elementu wywołującego. Brak oczekiwania na wątek. Jedyną rzeczą, która może wznowić działanie, jest function_response z tym identyfikatorem, zweryfikowany pod kątem schematu. Wiadomość na czacie wysłana do przepływu pracy to nowa tura, a nie odpowiedź. Schemat jest też formularzem: adk web renderuje go, a aplikacja VibeStudio renderuje ten sam schemat jako 4 karty. direction_gate już zapisuje kandydatów do stanu. 👉🖥 Jak to zatrzymać:

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

Twoja odpowiedź dociera do persist_direction jako node_input, słownik z jednym kluczem, pick. Cztery kandydatury docierają w inny sposób: brama zapisała je w stanie udostępnionym w candidates, a ADK wiąże klucz stanu z parametrem o tej samej nazwie. Węzeł wybiera jednego kandydata i zapisuje z powrotem direction, angle, hook i user:prefs z Event(state=...). Klucze z prefiksem user: są zachowywane między sesjami danego użytkownika.

W przypadku wolnego narzędzia. Renderowanie Veo trwa kilka minut, a wykres nie powinien czekać tak długo. render_submit w agent/desk.py uruchamia zadanie i natychmiast zwraca potwierdzenie, którego stan to 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"]}

Potwierdzenie opakowane w LongRunningFunctionTool ma znaczenie dla ADK: tura agenta się kończy, przepływ pracy zostaje zawieszony w render_desk, a otwarte wywołanie z identyfikatorem i potwierdzeniem jest wierszem w runs/sessions.db. 👉🖥 W stage6_video/agent.py opakuj narzędzie:

    tools=[LongRunningFunctionTool(render_submit)])

Wznów według identyfikatora, z innego procesu. agent/deliver.py znajduje oczekujące wywołanie w magazynie sesji, odpytuje Veo, dopóki klip nie zostanie utworzony, zapisuje wynik w runs/state.json i wysyła do tej samej sesji function_response z identyfikatorem tego wywołania. 👉🖥 Utwórz część odpowiedzi:

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

👉🖥 Uruchom stage6_video, aż zatrzyma się 2 razy. Wyślij pomysł: uruchomienie zatrzymuje się na formularzu utworzonym na podstawie schematu, a w ładunku znajdują się 4 kandydaci. Wpisz 2 w pick i prześlij. Wykres zostanie wznowiony w persist_direction. Wypróbuj test zatrzymania: zawieś na formularzu, zatrzymaj serwer (scripts/stop.sh na karcie 1), uruchom go ponownie (scripts/restart.sh), otwórz ponownie sesję. Formularz nadal stoi, ponieważ pauza to rząd, a nie wątek. Po script_ready render_desk wywołuje render_submit, otrzymuje potwierdzenie, odpowiada WAITING i uruchomienie się kończy. Sesja zachowuje identyfikator wywołania. Uruchom wyświetlanie z konsoli lub karty 1 strony:

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

👉🖥 Kliknij Odśwież adk web: function_response następuje po oczekującym wywołaniu, join_media zostaje zwolniony, a store_release zostaje uruchomiony. 8 wierszy weryfikacji odczytuje 2 przystanki z sesji: wywołanie bramy i odpowiedź, potwierdzenie renderowania i jego dostarczenie.

Stan i biurko (5b)

Biuro medialne ma strukturę podobną do tej z przykładu 3a, czyli rozgałęzienie i połączenie z jedną wolną gałęzią. script_ready wysyła skrypt na 2 sposoby. render_desk przesyła klip, a bieg zostaje zawieszony. make_poster to narzędzie z sesji 2 jako węzeł: skrypt poster_idea, styl kanału i to, co bank pamięci wie o guście twórcy w kwestii plakatów (powiązane ze stanem jako memory_facts, sesja 6), trafiają do modelu obrazu, a po kilku sekundach otrzymujemy plik PNG w formacie 9:16. join_media czeka na wolniejszą, a store_release zapisuje znalezione dane w stanie.

Udostępnianie według nazwy. store_release nigdy nie otrzymuje plakatu. Deklaruje parametr o nazwie poster_url, a ADK wiąże go ze stanem udostępnionym, ponieważ węzeł w drugiej gałęzi zapisał ten klucz. Żadne ładunki nie przechodzą między gałęziami:

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

W obecnej postaci make_poster zwraca plakat i wiadomość, a store_release wyświetla komunikat poster: not in state. 👉🖥 Dodaj zapis stanu do ostatecznego wyniku:

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

👉🖥 Uruchom jeszcze raz i dostarcz. Otwórz zdarzenie make_poster: adres URL plakatu w danych wyjściowych i delta stanu z poster_url. Otwórz store_release: w wiadomości podana jest nazwa plakatu. Blok Sesja tego uruchomienia na stronie pokazuje te same informacje co karta Stan wiersza adk web: identyfikator sesji, którego potrzebuje wznowienie, otwarte wywołanie i klucze zapisane przez węzły, w tym poster_url. W panelu weryfikacji klip i plakat są wyświetlane obok siebie.

Klip i jego plakat obok siebie po dostarczeniu

8. 6 · Pamięć – co kanał zapamiętuje między nocami

Sesja 6 · Pamięć

👉🖥 W obszarze roboczym otwórz Sesję 6 · Pamięć, części 6a i 6b, utworzone w stage4_memory. Sesja 5 dotyczyła stanu jednego uruchomienia. Ta sesja dotyczy tego, co kanał zapamiętuje między uruchomieniami: co wykres słyszy z zewnątrz, za pomocą 2 wywołań zwrotnych.

Bank zapamiętanych informacji (6a)

Bank zapamiętanych informacji Vertex AI to zarządzana pamięć długoterminowa dotycząca danej osoby. Fakty są przechowywane w zakresie, nazwie aplikacji i identyfikatorze użytkownika oraz w zdefiniowanych przez Ciebie tematach. Gdy przekazujesz mu wymianę zdań z użytkownikiem memories.generate, wyodrębnia on to, co pasuje do tematu, osadza to i łączy z tym, co już ma: tworzony jest nowy fakt, zmieniony jest aktualizowany, a powtórzenie jest scalane. memories.retrieve zwraca fakty dotyczące zakresu. Oba wywołania znajdują się w pliku 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.",
}

Trzeci temat to gust kanału w zakresie zdjęć. W sesji 5 model zapisał w niej informacje: wysłany plakat został opisany w jednym zdaniu i zapamiętany, a następny plakat został narysowany z użyciem tego zdania w prompcie.

Konfiguracja rozpoczęła już tworzenie banku. Wiersz połączenia na stronie 6a informuje, czy jest on aktywny. Za pomocą przycisków na stronie lub terminala: python -m agent.platform.bank tworzy bank i jego tematy oraz zapisuje nazwę zasobu w pamięci podręcznej w runs/memorybank.json; python -m agent.platform.bank load generuje 4 poprzednie sesje (2 epoki zwierząt z regułą stylu, epokę gadżetów, niedawną epokę fantasy); python -m agent.platform.bank list pokazuje, czym stały się transkrypcje: skonsolidowanymi stwierdzeniami faktów, a nie transkrypcjami.

Wywołania zwrotne (6b)

Pamięć nie jest węzłem. Wywołanie zwrotne to funkcja przekazywana do Agent. Pakiet ADK wywołuje ją w ustalonym momencie z kontekstem na żywo, a zwrócenie None umożliwia normalne działanie. 3 pary: wokół tury agenta (before_agent_callback, after_agent_callback, z CallbackContext), wokół każdego wywołania modelu (before_model_callback z LlmRequest, after_model_callback z LlmResponse), wokół każdego wywołania narzędzia. W tym miejscu znajdują się zabezpieczenia, dane telemetryczne, pamięci podręczne i wstrzykiwanie kontekstu, bez węzła na wykresie.

W stage4_memory/agent.py propose_directions odczytuje bank przed wywołaniem modelu: recall_taste pobiera fakty, dołącza je do żądania jako blok MEMORY (najstarsze najpierw) i nakazuje modelowi, aby kandydaci od 1 do 3 byli zgodni z bieżącymi preferencjami, a reguły były traktowane jako ograniczenia. Zapisuje też fakty w stanie jako memory_facts, gdzie make_poster może je znaleźć. 👉🖥 Połączenie przewodowe:

    output_schema=Directions,
    before_model_callback=recall_taste)

Scenarzysta zapisuje to, co twórca wybrał po swojej turze: remember_pick odczytuje direction ze stanu, pisze jedno zdanie o wyborze i przekazuje je do memories.generate. 👉🖥 Połączenie przewodowe:

    output_schema=Script,
    after_agent_callback=remember_pick)

👉🖥 Uruchom z pustą wiadomością i wybierz kartę. Otwórz wydarzenie propose_directions: prośba zawiera 3 epoki, a kandydaci od 1 do 3 skłaniają się ku obecnemu gustowi, podczas gdy trendy sugerują coś innego. Po skrypcie kliknij Pokaż bank: pamięć została zmieniona lub dodana do dzisiejszego wyboru. Uruchom drugą noc: pamięta pierwszą.

9. 7 · Wdrażanie i publikowanie – graf w aplikacji, noc na VibeTube

Sesja 7. Wdrażanie i publikowanie

👉🖥 W obszarze roboczym otwórz Sesję 7. Wdrażanie i publikowanie, części 7a i 7b.

Wdrażanie VibeStudio (7a)

W środowiskach testowych adk web napędzało wykres. VibeStudio obsługuje go za pomocą Runner ADK w wątku instancji roboczej z własną pętlą zdarzeń i łączy każde zdarzenie w jeden stan uruchomienia, który strona odczytuje za pomocą zdarzeń wysyłanych przez serwer: czytniki, Twój wybór jako 4 karty, plakat po wylądowaniu, renderowanie po dostarczeniu. Odpowiedź bramki i dostarczenie renderowania to to samo wywołanie, część function_response, a aplikacja sama odpytuje Veo, zamiast czekać na konsolę.

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/ to kopia bajt po bajcie pliku agent/ laboratorium, sprawdzona przez checks/verify_app.py. Pełna lista krawędzi w pliku agent/graph.py zawiera wszystkie struktury z tego ćwiczenia w jednym miejscu, czyli 13 węzłów. Jest ona wypełniona, więc nie musisz jej wpisywać:

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

👉🖥 Przeprowadź wdrożenie za pomocą przycisku Wdróż w obszarze roboczym lub 👉💻 z karty 1:

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

który działa z wartościami z Twojego .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 tworzy kontener za pomocą Cloud Build i wdraża go w jednym kroku. Pierwsza kompilacja trwa 4–5 minut. --session-affinity z jedną instancją ciepłą utrzymuje zawieszony przebieg w instancji, która go przechowuje: ponownie w magazynie sesji 5. W przypadku większego wdrożenia magazyn sesji jest przenoszony do VertexAiSessionService, a powiązanie znika. Kompilacja i usługa działają jako domyślne konto usługi Compute Engine projektu, któremu konfiguracja przyznała wymagane role.

👉🖥 Otwórz adres URL usługi i wygeneruj wieczór w aplikacji: pomysł, wybór, poczekaj na klip i plakat. Na karcie Historia zachowywane są obie te informacje.

Publikowanie (7b)

Publikowanie to jedno wieloczęściowe żądanie POST z aplikacji do punktu końcowego pokoju w VibeTube, w formacie vibestudio/server/platform/publish.py. Klip to film, plakat to miniatura, a profil zawiera nazwę pokoju i Twoje imię i nazwisko:

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 przechowuje 1 film na projectId i pokój, a projekt, który wysyła aplikacja, to Twój projekt Google Cloud, więc ponowne opublikowanie zastępuje film, zamiast dodawać nowy. Plakat zastępczy nigdy nie jest przesyłany. Miniatura jest identyczna z prawdziwym plakatem.

Ostatnia zmiana to konfiguracja, a nie linijka kodu. 👉🖥 Na stronie 7b: adres URL VibeStudio (wypełniony z wdrożenia 7a), kod wydarzenia pokoju dokładnie taki, jak w VibeTube (ten, który został podany podczas konfiguracji, chyba że instruktor otworzył inny), oraz Twoje imię i nazwisko. Kliknij Opublikuj. Środowisko robocze przekazuje kliknięcie do aplikacji, aplikacja publikuje wieczór, a wiersze poniżej odczytują wynik z aplikacji i VibeTube: aplikacja odpowiada, wieczór kończy się prawdziwym plakatem, kryptonim nazywa otwarty pokój, VibeTube zwraca identyfikator filmu, film jest na ścianie, a jego miniaturą jest Twój plakat. Karta wyników pokazuje miniaturę w VibeTube obok plakatu w aplikacji.

Opublikowano: miniatura na VibeTube to plakat narysowany przez model generowania obrazów

10. Co zostało utworzone

👉🖥 Na końcu strony 7b znajduje się cały wykres – 13 węzłów od pomysłu do klipu i plakatu. Najedź kursorem na węzeł, aby zobaczyć, co robi i w której sesji został utworzony.

Sesja

Czego się nauczyliśmy

Zapisane jako

2 · Generowanie obrazów

Agent: model, instrukcje, narzędzia; model obrazu jako narzędzie; czego prompt nie może obiecać

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

3 · Przepływy pracy

Lista krawędzi, rozgałęzienie i połączenie, router z pętlą trybu zadania za nim

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

4 · Przetwarzanie multimodalne

One reader per kind, one line shape, one corpus; a third reader by meaning, with the media

generate_content z częściami audio, obrazem i filmem, rag.upload_file, rag.retrieve, media_in

5 · Generowanie filmów

Dwa rodzaje zawieszenia, jeden mechanizm; magazyn sesji; plakat udostępniony według nazwy

RequestInput, LongRunningFunctionTool, FunctionResponse(id=...), Event(state=...), powiązanie parametru

6 · Pamięć

Pamięć z kilku nocy, 2 wywołania zwrotne

memories.generate / retrieve, before_model_callback, after_agent_callback

7. Wdrażanie i publikowanie

wykres w aplikacji w Cloud Run; noc w VibeTube

Runner(agent=wf), run_async, zdarzenia wysyłane przez serwer, jedno wieloczęściowe żądanie POST

5 zasad, których przestrzega wykres:

  1. Zawieś zamiast czekać. Wstrzymanie to wiersz w magazynie sesji, a nie wątek.
  2. Wznów wszystko w ten sam sposób – za pomocą identyfikatora otwartego połączenia, niezależnie od tego, czy odpowiada osoba czy proces.
  3. Udostępniaj dane według nazwy w stanie, a nie przez przekazywanie ich w każdym ładunku.
  4. Zanim zapłacisz za model, przetestuj go za pomocą kodu. Zasada to plik tekstowy i router, który działa przed renderowaniem.
  5. Odczytaj wszystko, co nie jest tekstem, jako tekst, zanim trafi do korpusu, z nazwą pliku w wierszu, aby plik mógł wrócić.

Prompt nie może obiecywać zamówienia, zatrzymania ani czeku. Wykres może.

Co dalej

  • VertexAiSessionService w przypadku sesji, które trwają dłużej niż instancja, i usuń --session-affinity.
  • Webhook, który odpowiada na wywołanie renderowania zamiast mechanizmu odpytywania: ten sam FunctionResponse(id=...), inny nadawca.
  • A second RequestInput before publishing – the schema is the form, so it is one node and no page code.
  • Nowe komentarze, notatki głosowe i klipy od odbiorców są dodawane do korpusu po każdej premierze wraz z plakatem, pod którym zostały wysłane.

Czyszczenie danych

Wszystkie zasoby utworzone w tym module znajdują się w projekcie utworzonym w kroku 4, więc najtańszym sposobem na ich usunięcie jest usunięcie projektu. W Cloud Shell:

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

Aby zachować projekt i usunąć tylko płatne elementy: usługę Cloud Run (gcloud run services delete vibestudio --region us-central1), korpus RAG i bank pamięci (ich nazwy zasobów znajdują się w runs/ragcorpus.json i runs/memorybank.json, a strony Agent Engine i RAG Engine w konsoli je usuwają) oraz obraz kontenera w Artifact Registry.