👗 Virtuelles Anprobezimmer und KI‑Stylist mit Flutter, ADK Go und Gemini erstellen

1. EinfĂŒhrung

Umfang

In diesem Codelab schlĂŒpfen Sie in die Rolle eines Entwicklers, der die Fashion App erstellt, eine Flutter-Shopping-App fĂŒr eine fiktive Einzelhandelsmarke. Ihre Aufgabe: FĂŒgen Sie zwei KI-basierte Funktionen hinzu, die das Online-Shopping-Erlebnis verĂ€ndern.

  1. Virtueller Anproberaum: Ein Nutzer lĂ€dt ein Foto von sich selbst hoch, wĂ€hlt ein KleidungsstĂŒck aus und sieht ein KI-generiertes Bild von sich selbst mit diesem KleidungsstĂŒck.
  2. AI Stylist: Basierend auf dem Standort, dem Anlass und den Stilvorlieben des Nutzers stellt ein KI-Agent Empfehlungen fĂŒr komplette Outfits zusammen, die der Nutzer im GesprĂ€ch verfeinern kann.

Die Idee ist einfach: Wenn Nutzer Kleidung in einer Umkleidekabine anprobieren, ist die Wahrscheinlichkeit, dass sie sie kaufen, viel höher. Aber online? Du rĂ€tst doch nur. Dieses Projekt schließt diese LĂŒcke mit KI.

Architektur auf einen Blick

Flutter App  ──── HTTP/REST ────▶  ADK Go Backend
                                       │
                            ┌──────────┌──────────┐
                       Fitting Room  Stylist    Catalog
                         Agent       Agent      Agent
                                       │
                            Gemini API + Cloud Storage

Kerntechnologien

Komponente

Technologie

Zweck

Agenten-Framework

ADK (Agent Development Kit) fĂŒr Go

Orchestrierung mehrerer Agenten, Sitzungen, Artefakte

Agent Reasoning (Pro)

Gemini 3.1 Pro (Vorabversion)

UnterstĂŒtzt die Kundenservicemitarbeiter in der Umkleidekabine und die Stylisten

Agent Reasoning (Flash)

Gemini 3 Flash (Vorabversion)

UnterstĂŒtzt die Root- und Katalog-Agents (einfaches Routing/Lookup)

Bildgenerierung

Gemini 2.5 Flash Image

Generiert Bilder zum Anprobieren und Outfit-Bilder

Frontend

Flutter (Dart)

PlattformĂŒbergreifende App (Web, iOS, Android)

Speicher

Google Cloud Storage

Produktbilder und generierte Artefakte speichern

Hosting

Cloud Run

Bereitstellung serverloser Container

2. 📩 Voraussetzungen und Cloud Shell-Einrichtung

1. Cloud Shell-Editor öffnen

👉 Öffnen Sie den Cloud Shell-Editor in Ihrem Browser.

Wenn das Terminal nicht unten auf dem Bildschirm angezeigt wird:

  • Klicken Sie auf Ansicht → Terminal.

2. Flutter SDK einrichten

In Cloud Shell ist Flutter unter /google/flutter vorinstalliert. Da das Verzeichnis einem anderen Systemnutzer gehört, wird beim ersten AusfĂŒhren von flutter ein fatal: detected dubious ownership-Fehler angezeigt. FĂŒgen Sie es einmalig der Liste der sicheren Verzeichnisse von Git hinzu:

git config --global --add safe.directory /google/flutter

PrĂŒfen Sie, ob Flutter auf Ihrem PATH installiert ist und funktioniert:

flutter --version

Beim ersten AusfĂŒhren wird das Dart SDK heruntergeladen und das Flutter-Tool erstellt. Das kann eine Weile dauern. Sie sollten etwa Flutter 3.x ‱ channel stable sehen.

3. Repository klonen

cd ~
git clone https://github.com/gca-americas/fashion-app-demo
cd fashion_app_demo

4. Projektstruktur ansehen

fashion_app_demo/
├── adk_backend/                 # Go backend with ADK agents
│   ├── main.go                  # Entry point — wires all agents + REST server
│   ├── catalog/                 # Catalog Agent — product lookup
│   │   ├── agent.go
│   │   ├── catalog.yaml         # Product database (YAML)
│   │   └── instructions.md      # Agent persona prompt
│   ├── fittingroom/             # Fitting Room Agent — virtual try-on
│   │   ├── agent.go
│   │   ├── instructions.md      # Agent persona prompt
│   │   └── tool_instructions.md # Image generation prompt
│   ├── stylist/                 # Stylist Agent — outfit curation
│   │   ├── agent.go
│   │   └── instructions.md      # Agent persona + output format
│   ├── rootagent/               # Root Agent — routes to the right agent
│   │   └── agent.go
│   └── tools/                   # Shared tools
│       ├── imagetool.go         # getProductImage — loads product images
│       ├── outfit_gen_tool.go   # generate_outfit_image — creates outfit images
│       └── cors_helper.go      # CORS middleware + request logging
│
├── flutter_frontend/            # Flutter cross-platform app
│   ├── lib/
│   │   ├── main.dart            # App entry point + Provider setup
│   │   ├── app_config.dart      # Backend URL configuration
│   │   ├── core_app/            # Pre-built shopping app (browse, cart, etc.)
│   │   └── workshop_tasks/      # AI feature code
│   │       ├── step_1_try_it_on/  # Virtual Try-On flow
│   │       │   ├── providers/     # TryItOnProvider (state management)
│   │       │   ├── services/      # AdkFittingRoomService (HTTP calls)
│   │       │   └── ui/            # Screens (product detail → try on → fitting room)
│   │       └── step_2_style_me/   # Style Me flow
│   │           ├── models/        # Outfit, StyleRequest data classes
│   │           ├── providers/     # StylingProvider (state management)
│   │           ├── services/      # AdkStylingService (HTTP calls)
│   │           └── ui/            # Screens (form sheet → outfit carousel)
│   └── assets/images/           # Product catalog images

3. ☁ Google Cloud-Projekt einrichten

1. Neues Projekt erstellen

gcloud projects create fashion-app-demo --name="Fashion App Demo"
gcloud config set project fashion-app-demo

Rechnungskonten auflisten:

gcloud billing accounts list

Schau dir die an.

OPEN

-Spalte Dort muss True stehen. Wenn dort False steht (hĂ€ufig bei einem abgelaufenen kostenlosen Testzeitraum), ist das Konto geschlossen und es werden keine Zahlungen ausgefĂŒhrt. Fahren Sie mit dem Block zur Fehlerbehebung unten fort, bevor Sie fortfahren.

Kopieren Sie die ACCOUNT_ID eines OPEN: True-Kontos (sieht so aus: 0X0X0X-0X0X0X-0X0X0X) und verknĂŒpfen Sie sie mit Ihrem Projekt:

gcloud billing projects link fashion-app-demo \
 --billing-account=YOUR_BILLING_ACCOUNT_ID

Link prĂŒfen:

gcloud billing projects describe fashion-app-demo

Sie sollten billingEnabled: true sehen. Wenn billingEnabled: false auch nach der VerknĂŒpfung angezeigt wird, ist das Konto geschlossen (OPEN: False). Weitere Informationen finden Sie im Block zur Fehlerbehebung unten.

3. Erforderliche APIs aktivieren

gcloud services enable \
 aiplatform.googleapis.com \
 storage.googleapis.com \
 run.googleapis.com \
 cloudbuild.googleapis.com \
 artifactregistry.googleapis.com

API

Zweck

aiplatform.googleapis.com

Vertex AI: Der fitting_tool ruft die Bildgenerierung von Gemini ĂŒber Vertex AI auf.

storage.googleapis.com

Cloud Storage: Hier werden Produktkatalogbilder und generierte Anprobeergebnisse gespeichert.

run.googleapis.com

Cloud Run: Das Backend wird als serverloser Container gehostet.

cloudbuild.googleapis.com

Cloud Build: Erstellt Docker-Images aus dem Quellcode.

artifactregistry.googleapis.com

Artifact Registry: Hier werden erstellte Docker-Images gespeichert.

4. GCS-Bucket erstellen

export PROJECT_ID=$(gcloud config get-value project)
gcloud storage buckets create gs://fashion-app-$PROJECT_ID \
 --location=us-central1 \
 --uniform-bucket-level-access

5. Produktkatalogbilder hochladen

Das getProductImage-Tool des Backends liest aus gs://$GCS_BUCKET/catalog-assets/images/. Laden Sie die Katalogbilder in diesen Pfad hoch:

cd ~/fashion_app_demo
gcloud storage cp flutter_frontend/assets/images/*.png \
 gs://fashion-app-$PROJECT_ID/catalog-assets/images/

PrĂŒfen Sie den Upload. Sie sollten eine Liste mit .png-Dateien sehen:

gcloud storage ls gs://fashion-app-$PROJECT_ID/catalog-assets/images/

6. .env-Datei konfigurieren

cd ~/fashion_app_demo/adk_backend
cat > .env << EOF
GOOGLE_CLOUD_PROJECT=$PROJECT_ID
GCS_BUCKET=fashion-app-$PROJECT_ID
EOF

7. Mit Standardanmeldedaten fĂŒr Anwendungen authentifizieren

Sie mĂŒssen diesen Befehl ausfĂŒhren, bevor Sie das Back-End lokal starten. Das Go-Back-End verwendet ADC, um jeden Aufruf von Vertex AI (Gemini) und Cloud Storage zu authentifizieren. Ohne ADC wird das Backend gestartet, aber jeder Try-on-Anfrage schlĂ€gt mit dem Fehler 401 CREDENTIALS_MISSING fehl.

Ein Anmeldedatenpaar deckt beide Dienste ab. FĂŒhren Sie diese beiden Befehle in der angegebenen Reihenfolge aus:

# 1. Log in (opens a browser; in Cloud Shell, paste the verification code back)
gcloud auth application-default login

# 2. Attach your project as the quota / billing project for ADC
gcloud auth application-default set-quota-project $(gcloud config get-value project)

PrĂŒfen Sie, ob ADC fehlerfrei ist:

gcloud auth application-default print-access-token | head -c 20 && echo "..."

Sie sollten etwa 20 Zeichen eines Tokens gefolgt von ... sehen. Wenn ein Fehler auftritt, hat die Anmeldung nicht funktioniert. FĂŒhren Sie Schritt 1 noch einmal aus.

4. đŸ—ïžî€€î€€ArchitekturĂŒbersicht

Nachdem die Umgebung eingerichtet ist, sehen wir uns an, wie das System funktioniert, bevor wir uns den Code ansehen.

Das Four-Agent-System

Das Backend ist als Multi-Agenten-System mit dem ADK (Agent Development Kit) fĂŒr Go aufgebaut. Vier Agents arbeiten zusammen, jeder mit einer bestimmten Aufgabe:

                   ┌──────────────┐
                   │ Root Agent   │  ← Routes requests to the right agent
                   │ (gemini-3-   │
                   │  flash-      │
                   │  preview)    │
                   └──────┬───────┘
                          │
           ┌──────────────┌──────────────┐
           ▌              ▌              ▌
  ┌────────────────┐ ┌──────────┐ ┌────────────────┐
  │ Fitting Room   │ │ Catalog  │ │ Stylist        │
  │ Agent          │ │ Agent    │ │ Agent          │
  │ (gemini-3.1-   │ │ (gemini- │ │ (gemini-3.1-   │
  │  pro-preview)  │ │  3-flash-│ │  pro-preview)  │
  │                │ │  preview)│ │                │
  │ Tools:         │ │          │ │ Tools:         │
  │ ‱ fitting_tool │ │ Tools:   │ │ ‱ fitting_tool │
  │ ‱ getProduct   │ │ ‱ list   │ │ ‱ getProduct   │
  │   Image        │ │ Products │ │   Image        │
  │ ‱ catalog_agent│ │ ‱ get    │ │ ‱ catalog_agent│
  │   (delegation) │ │ Product  │ │   (delegation) │
  │                │ │  Image   │ │ ‱ generate_    │
  └────────────────┘ └──────────┘ │   outfit_image │
                                  └────────────────┘

Agent

Modell

Rolle

Root-Agent

gemini-3-flash-preview

Verkehrspolizist. Liest die Nachricht des Nutzers und leitet sie an den richtigen Kundenservicemitarbeiter weiter. Es wird ein schnelles, schlankes Modell verwendet, da es nur Routing-Entscheidungen treffen muss.

Catalog Agent

gemini-3-flash-preview

Produktexperte LĂ€dt den Produktkatalog aus einer YAML-Datei und beantwortet Produktanfragen. Außerdem ist es ressourcenschonend, da nur Daten abgerufen werden.

Fitting Room Agent

gemini-3.1-pro-preview

Spezialist fĂŒr virtuelle Anproben. Nimmt ein Nutzerfoto und ein Produktbild und generiert ein zusammengesetztes Bild der Person, die den Artikel trĂ€gt. Es wird ein leistungsfĂ€higeres Modell verwendet, da Bilder analysiert werden mĂŒssen.

Stylist Agent

gemini-3.1-pro-preview

Modeberater Basierend auf Standort, Anlass und Vorlieben werden drei Outfit-Kombinationen aus dem Katalog zusammengestellt. Kann Anprobierbilder fĂŒr jedes Outfit generieren. Das leistungsstarke Modell wird auch fĂŒr kreative Schlussfolgerungen verwendet.

Der Einstiegspunkt: main.go

Alles beginnt in main.go, wo die Agents miteinander verbunden und der HTTP-Server gestartet wird:

// main.go — simplified for clarity


func main() {
   godotenv.Load()  // Load .env file


   // 1. Create the artifact storage (GCS-backed)
   artifacts, _ := gcsartifact.NewService(ctx, bucket)


   // 2. Build agents bottom-up (dependencies first)
   catagent, _    := catalog.NewCatalogAgent(apikey, "catalog/catalog.yaml")
   fitagent, _    := fittingroom.NewFittingRoomAgent(apikey, catagent)
   stylistAgent, _ := stylist.NewStylistAgent(apikey, catagent)
   ragent, _      := rootagent.NewRootAgent(apikey, fitagent, catagent, stylistAgent)


   // 3. Register all agents with a multi-loader
   loader, _ := agent.NewMultiLoader(ragent, fitagent, catagent, stylistAgent)


   // 4. Create the ADK REST server
   restHandler, _ := adkrest.NewServer(adkrest.ServerConfig{
       SessionService:  session.InMemoryService(),
       MemoryService:   memory.InMemoryService(),
       AgentLoader:     loader,
       ArtifactService: artifacts,
   })


   // 5. Mount behind /api/ with CORS support
   r := mux.NewRouter()
   r.Use(tools.LocalhostCORS)
   r.PathPrefix("/api/").Handler(
       http.StripPrefix("/api", tools.LogHandler(restHandler)))


   http.Server{Addr: ":8080", Handler: r}.ListenAndServe()
}

Wichtige Hinweise:

  • Agenten werden von unten nach oben erstellt: Der Katalog-Agent wird zuerst erstellt, da sowohl der Anproberaum- als auch der Stylist-Agent von ihm abhĂ€ngen (sie delegieren Produktsuchen an ihn).
  • Mit agent.NewMultiLoader werden alle vier Agents registriert, sodass die REST API Anfragen anhand des Namens an jeden von ihnen weiterleiten kann.
  • adkrest.NewServer stellt die REST API automatisch bereit. Sie mĂŒssen keine Endpunkthandler selbst schreiben. ADK bietet Ihnen standardmĂ€ĂŸig Sitzungsverwaltung, Artefaktspeicher und AgentenausfĂŒhrung.
  • session.InMemoryService() speichert Sitzungen im Arbeitsspeicher. Das bedeutet, dass Sitzungen verloren gehen, wenn der Server neu gestartet wird. FĂŒr eine Demo ist das in Ordnung. In der Produktion wĂŒrden Sie einen persistenten Speicher verwenden.
  • gcsartifact.NewService speichert Artefakte (generierte Bilder) in Google Cloud Storage, sodass sie ĂŒber Anfragen hinweg erhalten bleiben und ĂŒber GCS-URIs freigegeben werden können.

5. đŸ€–Â ADK (Agent Development Kit) – Detaillierte Informationen

Was ist das ADK?

Das Agent Development Kit (ADK) ist ein Open-Source-Framework von Google zum Erstellen von KI-Agenten in Go (und Python/Java). Sie bildet die Schicht zwischen Ihrer Anwendung und der Gemini API.

Sie könnten die Gemini API direkt aufrufen. Wenn Ihre App jedoch

  • Produkte aus einem Katalog suchen
  • Bilder auf Grundlage von Nutzerfotos generieren
  • Sich merken, welche Outfits zuvor vorgeschlagen wurden
  • Mehrere KI-Agenten koordinieren

Sie brauchen Struktur. Das ADK bietet diese Struktur.

Die Agentenschleife

Jeder ADK-Agent folgt einem Loop:

1. Receive a message (from user or another agent)
2. Think — the LLM reasons about what to do
3. Act — call a tool, delegate to a sub-agent, or respond
4. Return — send the result back

Diese Schleife kann innerhalb einer einzelnen Anfrage mehrmals wiederholt werden. Der Stylist-Agent könnte beispielsweise:

  1. „Stelle ein Outfit fĂŒr einen Strandurlaub fĂŒr mich zusammen“
  2. Rufen Sie das Tool catalog_agent auf, um die Produktliste abzurufen.
  3. WĂ€hle drei Outfitkombinationen aus.
  4. Rufen Sie fitting_tool fĂŒr jedes Outfit auf, um Bilder zu generieren.
  5. Strukturierte JSON-Antwort zurĂŒckgeben

Wichtige Konzepte (mit Code aus diesem Repository)

LLM-Agents

Der primÀre Baustein. Erstellt mit llmagent.New():

// From catalog/agent.go
agent, err := llmagent.New(llmagent.Config{
   Name:        "catalog_agent",                    // Unique identifier
   Model:       m,                                  // Which LLM to use
   Description: "An agent that can search and list products from the catalog",
   Instruction: instructions,                       // Persona prompt (embedded from .md file)
   Tools:       []tool.Tool{listTool, imageTool},   // What the agent can do
})

Das Feld Instruction ist die Rolle des KI-Agenten. Es gibt dem LLM vor, wer er ist und wie er sich verhalten soll. In diesem Repository werden Anleitungen als Markdown-Dateien geschrieben und zur Kompilierzeit mit der //go:embed-Anweisung von Go eingebettet:

//go:embed instructions.md
var instructions string

So werden Prompts als separate, versionierbare Dokumente und nicht als Inline-Strings gespeichert.

Tools

Tools sind Go-Funktionen, die vom LLM aufgerufen werden können. Das ADK ĂŒbernimmt die Übersetzung zwischen dem Tool-Aufrufformat des LLM und Ihrer typisierten Go-Funktion:

// From catalog/agent.go
type ListProductsArgs struct{}                    // Input (can be empty)
type ListProductsResult struct {
   Products []Product `json:"products"`          // Output
}


func ListProducts(ctx tool.Context, args ListProductsArgs) (ListProductsResult, error) {
   return ListProductsResult{Products: catalogProducts}, nil
}


// Register it:
listTool, _ := functiontool.New(functiontool.Config{
   Name:        "listProducts",
   Description: "list all products in the catalog",
}, ListProducts)

Das ADK generiert automatisch ein JSON-Schema aus Ihren Go-Structs und sendet es an das LLM. Wenn das LLM beschließt, listProducts aufzurufen, deserialisiert das ADK die Argumente, ruft Ihre Funktion auf und sendet das Ergebnis zurĂŒck.

Über den Parameter tool.Context können Tools auf die Laufzeitdienste des ADK zugreifen, insbesondere auf Artefakte:

// Save an image as an artifact
ctx.Artifacts().Save(ctx, "my_image", imagePart)


// Load an artifact
resp, _ := ctx.Artifacts().Load(ctx, "my_image")

Delegierung von Sub-Agents

Ein Agent kann einen anderen Agenten als Tool ĂŒber agenttool.New() verwenden:

// From fittingroom/agent.go
Tools: []tool.Tool{
   loadartifactstool.New(),              // List available artifacts
   imgtool,                              // Get product images
   agenttool.New(catalogAgent, nil),     // Delegate to catalog agent
   fittingTool,                          // Generate try-on image
},

Wenn der Ankleide-Agent Produktinformationen benötigt, kann er den Katalog-Agenten wie ein regulÀres Tool aufrufen. Das LLM sieht es in der Tool-Liste und kann es aufrufen.

Sitzungen

In Sitzungen wird der Unterhaltungsverlauf aufgezeichnet. Die REST API des ADK verwaltet sie automatisch:

POST /api/apps/{appName}/users/{userId}/sessions  →  Creates a new session
POST /api/run  (with sessionId)                   →  Runs agent within that session

Eine wichtige Designentscheidung in dieser App: Der Anproberaum erstellt fĂŒr jede Anfrage eine neue Sitzung (jede Anprobe ist unabhĂ€ngig), wĂ€hrend der Stylist dieselbe Sitzung wiederverwendet (er merkt sich also frĂŒhere VorschlĂ€ge und kann sie anhand von Feedback verfeinern).

Bundesland

Der Status ist ein SchlĂŒssel/Wert-Speicher, der an eine Sitzung angehĂ€ngt ist. Agenten lesen und schreiben den Status, um sich abzustimmen:

// Write to state
ctx.State().Set("previously_used_products", "[\"id_bomber\",\"id_hat\"]")


// Read from state
val, err := ctx.State().Get("previously_used_products")

Der Stylist-KI-Agent verwendet den Status, um sich zu merken, welche Produkte er bereits vorgeschlagen hat, damit er beim nÀchsten Mal andere auswÀhlt.

Artefakte

Artefakte sind benannte binÀre Objekte (in der Regel Bilder), die pro Sitzung gespeichert werden. Im Gegensatz zu Textantworten werden sie separat gespeichert und nach Namen abgerufen:

// Save a generated image as an artifact
artName := fmt.Sprintf("generated_fitting_%s_%s", ctx.InvocationID(), uuid.NewString()[:8])
ctx.Artifacts().Save(ctx, artName, imagePart)


// The frontend fetches it via:
// GET /api/apps/{app}/users/{user}/sessions/{session}/artifacts/{artName}

So bleiben die Antworten schlank – der Agent gibt nur den Artefaktnamen zurĂŒck und das Frontend ruft die binĂ€ren Bilddaten separat ab.

Callbacks

Callbacks sind Hooks, die an bestimmten Punkten im Agenten-Loop ausgefĂŒhrt werden. Sie können die AusfĂŒhrung prĂŒfen, Ă€ndern oder abkĂŒrzen:

llmagent.Config{
   // Runs before the agent starts — used to save uploaded images
   BeforeAgentCallbacks: []agent.BeforeAgentCallback{SaveIncomingBlobs},


   // Runs before each LLM call — used to inject context
   BeforeModelCallbacks: []llmagent.BeforeModelCallback{
       ExtractAndInjectUserImage,
       InjectPreviousProducts,
   },


   // Runs after each LLM response — used to extract data
   AfterModelCallbacks: []llmagent.AfterModelCallback{SaveSelectedProducts},
}

Wenn ein Callback eine Antwort zurĂŒckgibt, die nicht „nil“ ist, wird das Standardverhalten ĂŒbersprungen. Wenn beispielsweise ein BeforeModelCallback eine Antwort aus dem Cache zurĂŒckgibt, wird der eigentliche LLM-Aufruf vollstĂ€ndig ĂŒbersprungen.

JSON-Schema-Durchsetzung

Sowohl der Anprobekabinen- als auch der Stylisten-Agent zwingen das LLM, in strukturiertem JSON zu antworten:

GenerateContentConfig: &genai.GenerateContentConfig{
   ResponseMIMEType:   "application/json",
   ResponseJsonSchema: fittingSchemaMap(),  // Defines the expected structure
}

So erhÀlt das Flutter-Frontend immer analysierbare Daten und keinen Freitext.

Der Katalog-Agent: Einfachstes Beispiel

Der Katalog-Agent (catalog/agent.go) ist der einfachste Agent im System und ein guter Ausgangspunkt, um ADK-Muster zu verstehen.

Es enthÀlt zwei Tools:

  1. listProducts: Gibt den vollstĂ€ndigen Produktkatalog aus einer YAML-Datei zurĂŒck.
  2. getProductImage: LĂ€dt ein Produktbild aus GCS (oder lokaler Fallback) und speichert es als Artefakt.

Das getProductImage-Tool zeigt ein wichtiges Muster: Laden aus mehreren Quellen mit Artefakt-Caching:

// From tools/imagetool.go — simplified
func GetProductImage(ctx tool.Context, args GetProductImageArgs) (GetProductImageResult, error) {
   // First: check if it's already an artifact
   _, err := ctx.Artifacts().Load(ctx, args.ImageName)
   if err == nil {
       return GetProductImageResult{Image: args.ImageName}, nil  // Cache hit!
   }


   // Second: try loading from GCS bucket
   rc, err := client.Bucket(bucket).Object("catalog-assets/images/" + args.ImageName).NewReader(ctx)
   if err != nil {
       // Third: fall back to local filesystem
       localData, _ := os.ReadFile("../flutter_frontend/assets/images/" + args.ImageName)
       ctx.Artifacts().Save(ctx, args.ImageName, &genai.Part{InlineData: ...})
       return GetProductImageResult{Image: args.ImageName}, nil
   }


   // Save to artifact cache and return
   ctx.Artifacts().Save(ctx, args.ImageName, &genai.Part{InlineData: ...})
   return GetProductImageResult{Image: args.ImageName}, nil
}

Das Tool versucht es zuerst mit Artefakten, dann mit GCS und dann mit lokalen Dateien. Nach dem Laden wird das Bild als Artefakt im Cache gespeichert, sodass nachfolgende Aufrufe sofort erfolgen.

6. đŸ§Ș Die KI-Pipeline: Agents in Aktion

Sehen wir uns nun die beiden anspruchsvollsten Agents an, die Bilder generieren und Outfits zusammenstellen.

6.1 Der Fitting Room-Agent

Datei:

adk_backend/fittingroom/agent.go

Der Anprobe-Agent ist die Engine hinter „Virtuelle Anprobe“. Wenn ein Nutzer sein Foto hochlĂ€dt und ein Produkt auswĂ€hlt, generiert dieser Agent ein zusammengesetztes Bild der Person, die den Artikel trĂ€gt.

Die fitting_tool – Schritt fĂŒr Schritt

Die Kernlogik befindet sich in der Funktion doFitting. Wenn der Agent die Funktion aufruft, geschieht Folgendes:

Schritt 1: Nutzerbild auflösen

func doFitting(ctx tool.Context, args FittingToolArgs) (FittingToolResult, error) {
   if len(args.Accessories) > 2 {
       args.Accessories = args.Accessories[:2]  // Safety limit: max 2 items
   }


   var userPart *genai.Part
   if strings.HasPrefix(args.UserImage, "gs://") {
       // If we have a GCS URI from a previous fitting, use it directly
       userPart = &genai.Part{FileData: &genai.FileData{
           FileURI:  args.UserImage,
           MIMEType: gcsURIMimeType(args.UserImage),
       }}
   } else {
       // Otherwise, load the image from artifact storage
       userImgResp, err := ctx.Artifacts().Load(ctx, args.UserImage)
       userPart = userImgResp.Part
   }

Das Nutzerbild kann aus zwei Quellen stammen:

  • Ein Artefaktnamen (z. B. upload_abc123_1): Dies ist der erste Upload, der vom SaveIncomingBlobs-Callback gespeichert wird.
  • Ein gs://-URI: Dies ist ein zuvor generiertes Anpassungsergebnis, das in GCS zur sitzungsĂŒbergreifenden Wiederverwendung gespeichert ist.

Dieses Dual-Path-Design ist beabsichtigt: Wenn der Stylist-Agent spĂ€ter Outfit-Anproben generiert, wird die GCS-URL aus dem ursprĂŒnglichen Ergebnis des virtuellen Anproberaums wiederverwendet, damit die IdentitĂ€t des Nutzers bei allen Outfits gleich bleibt.

Schritt 2: Multimodalen Prompt erstellen

   parts := []*genai.Part{
       genai.NewPartFromText(toolInstructions),           // Identity preservation prompt
       genai.NewPartFromText("Reference Person Photo:"),
       userPart,                                          // The user's photo
   }


   for _, acc := range args.Accessories {
       accResp, _ := ctx.Artifacts().Load(ctx, acc)       // Load product image artifact
       parts = append(parts, genai.NewPartFromText("Product Image to Apply:"))
       parts = append(parts, accResp.Part)                // The product photo
   }

Der toolInstructions-Prompt (eingebettet aus tool_instructions.md) ist entscheidend, da er Gemini anweist, die IdentitĂ€t des Nutzers (Gesicht, Körpertyp, Hautton, Haare) beizubehalten und nur das KleidungsstĂŒck anzuwenden. Ohne diese Prompt-Technik könnte das Modell das Aussehen der Person Ă€ndern.

Schritt 3: Gemini zur Bildgenerierung aufrufen

   client, _ := genai.NewClient(ctx, &genai.ClientConfig{
       Backend:  genai.BackendVertexAI,                 // Vertex AI endpoint
       Project:  os.Getenv("GOOGLE_CLOUD_PROJECT"),     // From your .env
       Location: "global",                              // Multi-region endpoint
   })


   resp, _ := client.Models.GenerateContent(ctx, "gemini-2.5-flash-image",
       []*genai.Content{genai.NewContentFromParts(parts, "user")},
       &genai.GenerateContentConfig{
           ResponseModalities: []string{"TEXT", "IMAGE"},  // Request both text and image output
           Temperature:        genai.Ptr(float32(0.2)),    // Low temperature for consistency
       })

Alle vier Agents und das Tool zur Bildgenerierung verwenden denselben Authentifizierungspfad: Backend: genai.BackendVertexAI mit der Projekt-ID, authentifiziert ĂŒber Standardanmeldedaten fĂŒr Anwendungen. Die Orchestrierungsmodelle (gemini-3.1-pro-preview, gemini-3-flash-preview) und das Bildmodell (gemini-2.5-flash-image) befinden sich alle hinter demselben Vertex AI-Endpunkt und mit denselben ADC-Anmeldedaten wird auch der Cloud Storage-Zugriff autorisiert – eine Anmeldedaten fĂŒr jeden Aufruf.

Schritt 4: Ergebnis speichern

   // Find the image in the response (may contain both text + image parts)
   var genPart *genai.Part
   for _, p := range resp.Candidates[0].Content.Parts {
       if p.InlineData != nil {
           genPart = p
           break
       }
   }


   // Save as an ADK artifact
   artName := fmt.Sprintf("generated_fitting_%s_%s", ctx.InvocationID(), uuid.NewString()[:8])
   ctx.Artifacts().Save(ctx, artName, genPart)


   // Also upload to GCS for cross-session reuse
   objectName := fmt.Sprintf("generated-fittings/%s.jpg", ctx.InvocationID())
   w := storageClient.Bucket(bucket).Object(objectName).NewWriter(ctx)
   w.Write(genPart.InlineData.Data)
   gcsURI := fmt.Sprintf("gs://%s/%s", bucket, objectName)


   return FittingToolResult{ArtifactName: artName, GCSUrl: gcsURI}, nil

Das doppelte Speichern (Artefakt + GCS) ist der SchlĂŒssel fĂŒr die Übergabe des Kundenservice-Agents zwischen Anproberaum und Stylist. Das Artefakt ermöglicht den sofortigen Zugriff in der aktuellen Sitzung, wĂ€hrend der GCS-URI es dem Stylist (der in einer anderen Sitzung ausgefĂŒhrt wird) ermöglicht, spĂ€ter auf dasselbe Bild zu verweisen.

Der SaveIncomingBlobs-Callback

Bevor der Kundenservicemitarbeiter mit der BegrĂŒndung beginnt, wird dieses BeforeAgentCallback ausgefĂŒhrt, um alle vom Nutzer hochgeladenen Bilder zu speichern:

func SaveIncomingBlobs(ctx agent.CallbackContext) (*genai.Content, error) {
   for pindex, p := range ctx.UserContent().Parts {
       if p.InlineData != nil {
           aname := fmt.Sprintf("upload_%s_%d", ctx.InvocationID(), pindex)
           ctx.Artifacts().Save(ctx, aname, p)
       }
   }
   return nil, nil  // Return nil to proceed with normal agent execution
}

Durch die RĂŒckgabe von (nil, nil) signalisiert der Callback: „Ich bin mit der Vorverarbeitung fertig – fĂŒhre den Agenten jetzt wie gewohnt aus.“ Wenn nicht leere Inhalte zurĂŒckgegeben wĂŒrden, wĂŒrde der Agent vollstĂ€ndig umgangen.

6.2 Der Stylist-Agent

Datei:

adk_backend/stylist/agent.go

Der Stylist-Agent ist der anspruchsvollste im System. Es werden personalisierte Outfit-Empfehlungen zusammengestellt und durch Konversationen können diese iterativ verfeinert werden.

Drei Callbacks – Die Erinnerung des Stylisten

Der Stylist verwendet drei Callbacks, um den Kontext ĂŒber mehrere Konversationsrunden hinweg beizubehalten:

Callback 1:

InjectPreviousProducts (BeforeModel)

Das Problem: Wenn der Nutzer sagt: „Zeig mir verschiedene Optionen“, schlĂ€gt das LLM möglicherweise dieselben Produkte noch einmal vor, da es nicht automatisch nachverfolgt, was es bereits empfohlen hat.

Die Lösung: Nach jeder Antwort werden Produkt-IDs im Sitzungsstatus gespeichert. Vor dem nĂ€chsten LLM-Aufruf werden diese gelesen und ein Hinweis wird eingefĂŒgt:

func InjectPreviousProducts(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
   prev, err := ctx.State().Get(stateKeyPreviousProducts)  // Read from session state
   if err != nil {
       return nil, nil  // No previous state — first run
   }


   // Append hint to the user's message
   for i := len(req.Contents) - 1; i >= 0; i-- {
       if req.Contents[i].Role == "user" {
           req.Contents[i].Parts = append(req.Contents[i].Parts,
               genai.NewPartFromText(fmt.Sprintf(
                   "IMPORTANT: You previously suggested these products: %s. "+
                   "You MUST pick DIFFERENT complementary products this time.", prev)))
           break
       }
   }
   return nil, nil  // Continue to LLM call
}

RĂŒckruf 2:

ExtractAndInjectUserImage (BeforeModel)

Das Problem: Wenn der Nutzer Feedback gibt („make it more casual“), wird das Foto des Nutzers in der Folgeantwort nicht noch einmal eingefĂŒgt. Das Anpassungstool benötigt sie jedoch.

Die Lösung: Bei der ersten Anfrage wird in diesem Callback die Nutzerbildreferenz extrahiert und im Status gespeichert. Bei nachfolgenden Anfragen wird sie wieder eingefĂŒgt:

func ExtractAndInjectUserImage(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) {
   var foundImgStr string


   // Search for user image in the latest message
   for i := len(req.Contents) - 1; i >= 0; i-- {
       if req.Contents[i].Role == "user" {
           for _, part := range req.Contents[i].Parts {
               if strings.Contains(part.Text, "User try-on base image") {
                   foundImgStr = part.Text  // Found the GCS URI reference
               }
           }
           break
       }
   }


   if foundImgStr != "" {
       ctx.State().Set(stateKeyUserImageStr, foundImgStr)  // Save for later
   } else {
       // Not in current message — retrieve from state and inject
       val, _ := ctx.State().Get(stateKeyUserImageStr)
       if savedImgStr, ok := val.(string); ok {
           // Inject into the latest user message
           req.Contents[last].Parts = append(req.Contents[last].Parts,
               genai.NewPartFromText("REMINDER: Use this image: " + savedImgStr))
       }
   }
   return nil, nil
}

RĂŒckruf 3:

SaveSelectedProducts (AfterModel)

Nachdem das LLM mit OutfitvorschlĂ€gen geantwortet hat, wird das JSON in diesem Callback geparst, um Produkt-IDs zu extrahieren und fĂŒr den nĂ€chsten Aufruf des InjectPreviousProducts-Callbacks zu speichern:

func SaveSelectedProducts(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) {
   for _, part := range resp.Content.Parts {
       ids := extractProductIDs(part.Text)  // Parse JSON → extract product IDs
       if len(ids) > 0 {
           data, _ := json.Marshal(ids)
           ctx.State().Set(stateKeyPreviousProducts, string(data))  // Save to state
       }
   }
   return nil, nil  // Don't modify the response
}

Zusammen bilden diese drei RĂŒckrufe eine Feedbackschleife:

Request 1:  User sends styling request + user image
           → ExtractAndInjectUserImage SAVES image to state
           → LLM generates 3 outfits
           → SaveSelectedProducts SAVES product IDs to state


Request 2:  User says "make it more casual"
           → ExtractAndInjectUserImage INJECTS saved image into prompt
           → InjectPreviousProducts INJECTS "don't reuse these IDs"
           → LLM generates 3 NEW outfits
           → SaveSelectedProducts UPDATES product IDs in state

6.3 Der Root-Agent

Datei:

adk_backend/rootagent/agent.go

Der einfachste Agent – nur 31 Zeilen:

func NewRootAgent(project string, fittingAgent, catalogAgent, stylistAgent agent.Agent) (agent.Agent, error) {
   m, _ := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{
       Backend:  genai.BackendVertexAI,
       Project:  project,
       Location: "global",
   })


   return llmagent.New(llmagent.Config{
       Name:        "root_agent",
       Model:       m,
       Description: "A root agent that delegates to other agents",
       Instruction: "You are a helpful shopping assistant. If the user asks about fitting " +
           "items or generating images, delegate to the fitting room agent. If the user " +
           "asks about products, delegate to the catalog agent. If the user asks for " +
           "styling advice, delegate to the stylist agent.",
       SubAgents: []agent.Agent{fittingAgent, catalogAgent, stylistAgent},
   })
}

Es wird gemini-3-flash-preview (das schnellste Modell) verwendet, da die Routing-Entscheidungen einfach sind – das LLM muss nur die Absicht des Nutzers lesen und den richtigen Sub-Agenten auswĂ€hlen. Es sind keine Tools erforderlich. SubAgents ĂŒbernimmt die Delegierung automatisch.

7. đŸ“±Â Flutter-Frontend-Architektur

Das Flutter-Frontend ist eine voll funktionsfÀhige Einzelhandels-Shopping-App. Die KI-Funktionen befinden sich in flutter_frontend/lib/workshop_tasks/, getrennt vom vorgefertigten Einkaufserlebnis in core_app/.

Das MVVM-Muster

Die App folgt der Model-View-ViewModel-Architektur mit dem Provider-Paket:

┌──────────────────┐    ┌────────────────────┐    ┌──────────────────┐
│  View (Widget)   │◀───│ ViewModel (Provider)│◀───│  Service (HTTP)  │
│                  │    │                     │    │                  │
│ ‱ Renders UI     │    │ ‱ Holds state       │    │ ‱ Makes API calls│
│ ‱ User gestures  │───▶│ ‱ Business logic    │───▶│ ‱ Parses response│
│ ‱ Listens for    │    │ ‱ notifyListeners() │    │ ‱ Returns data   │
│   state changes  │    │                     │    │                  │
└──────────────────┘    └────────────────────┘    └──────────────────┘

Jede Ebene hat eine klare Rolle:

  • Modell: Datenklassen wie Product, Outfit, StyleRequest und Enums wie TryOnState
  • ViewModel (ChangeNotifier): EnthĂ€lt den aktuellen Zustand und ĂŒbertrĂ€gt Änderungen ĂŒber notifyListeners() an die BenutzeroberflĂ€che.
  • View (Widget): Abonniert das ViewModel mit context.watch() und wird bei StatusĂ€nderungen neu erstellt.
  • Dienst: FĂŒhrt HTTP-Aufrufe an das ADK-Backend aus und gibt typisierte Daten zurĂŒck.

Die Serviceebene

Dienste werden als abstrakte Schnittstellen mit ADK-spezifischen Implementierungen definiert:

// Abstract interface — defines WHAT the service does
abstract class TryItOnService {
 Future<(Uint8List?, String?)> generateTryOnImage(
   Uint8List userImageBytes,
   Uint8List productImageBytes,
 );
}


// Concrete implementation — defines HOW (via ADK REST API)
class AdkFittingRoomService implements TryItOnService { ... }

Durch diese Trennung können Sie das ADK-Backend durch Firebase AI, einen Mock-Dienst oder eine andere Implementierung ersetzen, ohne den Rest der App Ă€ndern zu mĂŒssen.

Das 3-Schritt-API-Muster

Sowohl AdkFittingRoomService als auch AdkStylingService folgen demselben Muster fĂŒr die Kommunikation mit dem ADK-Backend:

Schritt 1: Sitzung erstellen

Future<String> _createSession() async {
 final url = Uri.parse(
   '$_baseUrl/apps/${Uri.encodeComponent(_appName)}/users/$_userId/sessions');
 final response = await _client.post(url,
   headers: {'Content-Type': 'application/json'},
   body: jsonEncode({}));
 final body = jsonDecode(response.body) as Map<String, dynamic>;
 return body['id'] as String;  // Returns the session ID
}

Schritt 2: Agent ausfĂŒhren

Future<(String?, String?)> _runAgent({required String sessionId, ...}) async {
 final requestBody = jsonEncode({
   'appName': _appName,
   'userId': _userId,
   'sessionId': sessionId,
   'newMessage': {
     'role': 'user',
     'parts': [
       {'text': 'Generate a virtual try-on...'},
       {'inlineData': {'mimeType': 'image/jpeg', 'data': base64Encode(userImageBytes)}},
       {'inlineData': {'mimeType': 'image/png', 'data': base64Encode(productImageBytes)}},
     ],
   },
 });
 final response = await _client.post(Uri.parse('$_baseUrl/run'), body: requestBody);
 // Parse response events for artifact name and GCS URL...
}

Schritt 3: Artefakt abrufen

Future<Uint8List?> _loadArtifact({required String sessionId, required String artifactName}) async {
 final url = Uri.parse(
   '$_baseUrl/apps/$_appName/users/$_userId/sessions/$sessionId/artifacts/$artifactName');
 final response = await _client.get(url);
 final part = jsonDecode(response.body) as Map<String, dynamic>;
 final data = part['inlineData']['data'] as String;
 return base64Decode(data);  // Returns raw image bytes
}

Ein wichtiger Unterschied im Design: Der Anprobeservice erstellt fĂŒr jede Anfrage eine neue Sitzung (_createSession() wird jedes Mal aufgerufen), wĂ€hrend der Styling-Service dieselbe Sitzung wiederverwendet (_sessionId ??= await _createSession()), um eine Konversation mit mehreren ZĂŒgen zu ermöglichen.

Zustandsverwaltung: TryItOnProvider

Datei:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

Der TryItOnProvider verwaltet den gesamten Anprobefluss. Dabei wird ein TryOnState-Enum als Zustandsautomat verwendet:

enum TryOnState { initial, imagePicked, generating, success, error }


class TryItOnProvider with ChangeNotifier {
 TryOnState _state = TryOnState.initial;
 Uint8List? _userImageBytes;
 Uint8List? _generatedImage;
 String? _errorMessage;

Private StatusĂŒbergĂ€nge sorgen fĂŒr Konsistenz. Sie aktualisieren den Status nie, ohne auch veraltete Daten zu löschen und die BenutzeroberflĂ€che zu benachrichtigen:

 void _setGenerating() {
   _state = TryOnState.generating;
   _errorMessage = null;            // Clear any previous error
   _wasLastGenerationCached = false;
   notifyListeners();               // Tell the UI to rebuild
 }


 void _setSuccess(Uint8List image, {bool isCached = false}) {
   _generatedImage = image;
   _errorMessage = null;
   _wasLastGenerationCached = isCached;
   _state = TryOnState.success;
   notifyListeners();
 }

Die Hauptgenerierungsmethode fasst alles zusammen:

 Future<String?> generateTryOnImage(String productImagePath) async {
   final userImageBytes = _userImageBytes;
   if (userImageBytes == null) {
     _setError('No image selected.');
     return _errorMessage;
   }


   // Check local cache first
   final cachedImage = ImageUtils.getCachedImage(_sessionCache, userImageBytes, productUint8List);
   if (cachedImage != null) {
     _setSuccess(cachedImage, isCached: true);
     return null;
   }


   _setGenerating();  // Triggers loading state in UI


   try {
     final (generatedBytes, gcsUrl) = await _aiService.generateTryOnImage(
       userImageBytes, productUint8List);
     if (generatedBytes != null) {
       _fittingGcsUrl = gcsUrl;     // Save for the stylist agent later
       _setSuccess(generatedBytes);
     }
   } catch (e) {
     _setError(e.toString());
   }
   return _state == TryOnState.success ? null : _errorMessage;
 }

Die BenutzeroberflÀche: Bildschirme als State Router

Datei:

workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart

Auf dem Try-on-Bildschirm wird das Pattern Matching von Dart 3 mit AnimatedSwitcher verwendet, um basierend auf dem Status des Anbieters zwischen Unterbildschirmen zu wechseln:

class TryItOnScreen extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   final tryOnProvider = context.watch<TryItOnProvider>();


   return ScaffoldWithBackgroundNoise(
     appBar: const TryOnAppBar(),
     body: AnimatedSwitcher(
       duration: AppDurations.medium,
       child: switch (tryOnProvider.state) {
         TryOnState.initial || TryOnState.error => const ChooseImageScreen(),
         TryOnState.imagePicked || TryOnState.generating => LoadingScreen(
           userImage: tryOnProvider.userImageBytes),
         TryOnState.success => const FittingRoomScreen(),
       },
     ),
   );
 }
}

context.watch() abonniert den Anbieter. Immer wenn notifyListeners() aufgerufen wird, wird dieses Widget neu erstellt und AnimatedSwitcher wechselt nahtlos zwischen den Bildschirmen. Es gibt kein Navigator.push. Der Bildschirminhalt wird basierend auf dem Status-Enum direkt geÀndert.

Die agentische Übergabe: Umkleidekabine → Stylist

Das interessanteste UX-Muster ist, wie die App den Kontext vom Anproberaum-Agent an den Stylist-Agent weitergibt.

In 5_fitting_room.dart wird nach dem Generieren des Anprobebilds ĂŒber den Button „Style Me“ (Style mich) ein Formular geöffnet. Wenn der Nutzer Folgendes einreicht:

// From 1_style_me_form_sheet.dart
Navigator.pop(context, StyleRequest(
 location: _locationController.text.trim(),
 occasion: _occasionController.text.trim(),
 notes: _notesController.text.trim(),
 gcsUserImageUrl: provider.fittingGcsUrl,          // GCS URI from fitting result
 userImageData: provider.fittingGcsUrl == null
     ? provider.userImageBytes : null,              // Fallback to raw bytes
 selectedProductId: provider.selectedProduct?.id,   // Product they already tried on
 selectedProductTitle: provider.selectedProduct?.title,
));

Das StyleRequest bietet alles, was der Stylist braucht:

  • Ort und Anlass: Textkontext fĂŒr die Gestaltung
  • GCS-Nutzerbild-URL – damit der Stylist genau dieselbe Nutzerdarstellung wiederverwenden kann
  • AusgewĂ€hltes Produkt: Der Stylist nimmt es in jedes Outfit auf.

Das ist die Agent-Übergabe – der multimodale Kontext wird nahtlos von einem KI-Agent an einen anderen ĂŒbertragen und der Nutzer sieht nur ein einfaches Formular.

Der Styling-Ablauf: StylingProvider

Datei:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

Die StylingProvider ist einfacher als die TryItOnProvider, da die meiste KomplexitÀt an das Backend delegiert wird:

class StylingProvider with ChangeNotifier {
 StylingState _state = StylingState.initial;
 List<Outfit> _outfits = [];


 Future<bool> getStyleSuggestions(StyleRequest request) async {
   if (_state == StylingState.loading) return false;  // Prevent spam
   _currentRequest = request;
   _setLoading();
   try {
     final suggestions = await _stylingService.getStyleSuggestions(request);
     _setSuccess(suggestions);
     return true;
   } catch (e) {
     _setError('Something went wrong.');
   }
   return false;
 }


 // Multi-turn refinement — uses the same session
 Future<bool> refineWithFeedback(String feedback) async {
   if (_state == StylingState.loading) return false;
   _setLoading();
   try {
     final suggestions = await _stylingService.refineWithFeedback(feedback);
     _setSuccess(suggestions);
     return true;
   } catch (e) {
     _setError('Something went wrong.');
   }
   return false;
 }
}

Mit der Methode refineWithFeedback wird eine Nur-Text-Nachricht an dieselbe Sitzung gesendet. Die Callbacks InjectPreviousProducts und ExtractAndInjectUserImage des Back-Ends ĂŒbernehmen die gesamte Kontextverwaltung automatisch.

8. 🚀 App lokal ausfĂŒhren

Damit Cloud Shell reibungslos funktioniert, stellt das Go-Backend die kompilierte Flutter-Webanwendung ĂŒber denselben Port (8080) bereit. Ein Prozess, eine Vorschau-URL, keine Probleme mit ursprungsĂŒbergreifenden Anfragen und keine Bearbeitung von Konfigurationsdateien.

Vorab: ADC-Test

FĂŒr den Aufruf von Vertex AI sind Standardanmeldedaten fĂŒr Anwendungen erforderlich. Wenn Sie Schritt 7 der Projekteinrichtung in dieser Cloud Shell-Sitzung und mit diesem Google-Konto abgeschlossen haben, können Sie fortfahren. Wenn Sie nach einer Pause zurĂŒckkehren, das Konto gewechselt haben oder sich nicht sicher sind, können Sie in 5 Sekunden Folgendes prĂŒfen:

gcloud auth application-default print-access-token | head -c 20 && echo "..."

Wenn etwa 20 Zeichen eines Tokens ausgegeben werden, ist alles in Ordnung. Wenn ein Fehler auftritt, fĂŒhren Sie Schritt 7 der Projekteinrichtung noch einmal aus:

gcloud auth application-default login
gcloud auth application-default set-quota-project $(gcloud config get-value project)

Sie verwenden zwei Cloud Shell-Terminals:

  • Terminal A: Hier wird das Backend kontinuierlich ausgefĂŒhrt (./run.sh). Lassen Sie es geöffnet.
  • Terminal B: FĂŒhrt den Flutter-Web-Build einmal aus (flutter build web). Wird nach Abschluss beendet.

Die Reihenfolge spielt keine Rolle. FĂŒr eine möglichst reibungslose Erstnutzung sollten Sie Flutter jedoch zuerst erstellen, damit das Backend von Anfang an eine BenutzeroberflĂ€che hat.

1. Terminal B: Flutter-Web-Bundle erstellen (einmalig)

Öffnen Sie einen neuen Cloud Shell-Tab (+ oben im Terminalbereich) und gehen Sie dann so vor:

cd ~/fashion_app_demo/flutter_frontend
flutter pub get
flutter build web

Dadurch wird flutter_frontend/build/web/ erstellt, ein Verzeichnis mit statischen Dateien (HTML, JS, Assets). Das Programm wird beendet, wenn der Vorgang abgeschlossen ist. Der Backend-Dienst stellt diese bereit, sobald er feststellt, dass das Verzeichnis vorhanden ist.

2. Terminal A – Backend starten (lang andauernd)

FĂŒhren Sie im ursprĂŒnglichen Cloud Shell-Terminal folgende Schritte aus:

cd ~/fashion_app_demo/adk_backend
./run.sh

Sie sollte etwa so aussehen:

Serving Flutter web build from ../flutter_frontend/build/web

Lassen Sie dieses Terminal geöffnet: Das Backend bleibt aktiv, solange run.sh ausgefĂŒhrt wird. DrĂŒcken Sie die Ctrl+C, um die Aufnahme zu beenden.

Der Server stellt alles auf Port 8080 bereit:

  • / – Flutter-Web-App (die Shopping-BenutzeroberflĂ€che)
  • /api/: ADK-REST-Endpunkte (werden von der Flutter-App aufgerufen)
  • ADK-EntwicklungsoberflĂ€che – auch unter / verfĂŒgbar, wenn kein Flutter-Build vorhanden ist; nĂŒtzlich fĂŒr das direkte Debugging von Agenten

3. Webvorschau öffnen

  1. Klicken Sie in Cloud Shell rechts oben auf das Symbol fĂŒr die Webvorschau → Vorschau auf Port 8080.
  2. Die Flutter-Shopping-App wird in einem neuen Tab geladen.
  3. Im Produktkatalog nach einem Artikel suchen und ihn auswÀhlen
  4. Tippen Sie auf das Personensymbol (đŸ‘€), um den Try-On-Vorgang zu starten.
  5. Laden Sie ein Foto hoch und lassen Sie von der KI ein Bild zur Anprobe generieren.
  6. Auf „Style Me“ tippen, um Outfit-Empfehlungen zu erhalten
  7. Geben Sie Folge-Feedback wie „Formuliere es lockerer“ ein – Verfeinerung in derselben Sitzung

9. ☁ In Cloud Run bereitstellen

Flutter-Build in das Backend einbinden

Der Cloud Run-Container enthĂ€lt sowohl die API als auch die BenutzeroberflĂ€che in einem Image. Kopieren Sie den Flutter-Web-Build in adk_backend/flutter_web/. Das ist der erste Pfad, den der Go-Server prĂŒft, wenn er die auszuliefernde BenutzeroberflĂ€che auswĂ€hlt:

cd ~/fashion_app_demo/flutter_frontend
flutter build web
rm -rf ../adk_backend/flutter_web
cp -r build/web ../adk_backend/flutter_web

Wenn Sie lokal iteriert haben, haben Sie möglicherweise bereits build/web aus dem Schritt „Lokal ausfĂŒhren“. flutter build web kann weiterhin ausgefĂŒhrt werden.)

Backend bereitstellen (stellt API und BenutzeroberflÀche bereit)

cd ~/fashion_app_demo/adk_backend

gcloud run deploy fashion-app-backend \
 --source . \
 --region us-central1 \
 --allow-unauthenticated \
 --set-env-vars "GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GCS_BUCKET=fashion-app-$PROJECT_ID" \
 --memory 1Gi \
 --cpu 2 \
 --timeout 300s \
 --min-instances 0 \
 --max-instances 3

Wenn die Bereitstellung abgeschlossen ist, erhalten Sie eine Service-URL wie https://fashion-app-backend-xyz-uc.a.run.app. Öffnen Sie sie in einem Browser. Die Flutter-Shopping-App wird von / geladen und ihre API-Aufrufe werden an /api/ auf demselben Host gesendet. Keine Änderungen an der Frontend-Konfiguration erforderlich, kein API-SchlĂŒssel ĂŒbergeben.

Deployment prĂŒfen

Öffnen Sie die Cloud Run-URL in Ihrem Browser und fĂŒhren Sie den gesamten Ablauf durch:

  1. Durchsuchen → Produkt auswĂ€hlen
  2. Anprobieren → Foto hochladen → KI-generiertes Bild ansehen
  3. Style Me → Standort/Anlass eingeben → AusgewĂ€hlte Outfits ansehen
  4. Feedback → „Mache es lĂ€ssiger“ eingeben → aktualisierte Outfits ansehen
  5. In den Warenkorb → Kaufvorgang abschließen

10. 🎉 Fazit

Was Sie erstellt haben

Sie haben ein vollstĂ€ndiges KI-gestĂŒtztes Einzelhandelserlebnis kennengelernt, das Folgendes umfasst:

  • ✅ Ein Multi-Agenten-Backend mit vier spezialisierten Agenten, die zusammenarbeiten
  • ✅ Eine virtuelle Umkleidekabine, in der personalisierte Anprobefotos generiert werden
  • ✅ Ein KI-Stylist, der Outfits zusammenstellt und sie durch Unterhaltungen verfeinert
  • ✅ Eine plattformĂŒbergreifende Flutter-App, die eine Verbindung zum Agent-Backend herstellt
  • ✅ Cloud Run-Bereitstellung fĂŒr skalierbares, serverloses Hosting

SchlĂŒsselkonzepte

Konzept

Wo Sie es gesehen haben

ADK-Multiagenten-Orchestrierung

Root-Agent-Routing zu Anproberaum-, Katalog- und Stylist-Agents

Gemini Multimodal Bildgenerierung

fitting_tool Kombination von Nutzerfotos mit Produktbildern

Sitzungsstatus fĂŒr konversationelle KI

Stylist verwendet Sitzungen fĂŒr iteratives Feedback wieder

Artefaktspeicher fĂŒr BinĂ€rdaten

Bildspeicher von Textantworten trennen

Callbacks fĂŒr Middleware-Logik

SaveIncomingBlobs, InjectPreviousProducts, SaveSelectedProducts

MVVM + Provider in Flutter

TryItOnProvider und StylingProvider mit ChangeNotifier

Agentic Handoff

StyleRequest multimodaler Kontext wird zwischen Agents ĂŒbergeben

NĂ€chste Schritte

  • 🎹 Agent-Prompts anpassen: Bearbeiten Sie instructions.md, um die Persönlichkeit des Stylisten zu Ă€ndern.
  • đŸ›ïžÂ Weitere Produkte hinzufĂŒgen: Aktualisieren Sie catalog.yaml mit neuen Artikeln.
  • đŸ“±Â Anzeigen fĂŒr MobilgerĂ€te optimieren – fĂŒhren Sie flutter build ios oder flutter build apk aus.
  • 🔄 Persistente Sitzungen hinzufĂŒgen: Ersetzen Sie InMemoryService durch eine datenbankbasierte Implementierung.
  • 🔒 Authentifizierung hinzufĂŒgen: Cloud Run-Endpunkt mit IAM sichern

Ressourcen