👗 Tworzenie wirtualnej przymierzalni i stylisty AI za pomocą Fluttera, pakietu ADK Go i Gemini

1. Wprowadzenie

Co utworzysz

W tym ćwiczeniu wcielisz się w rolę programisty tworzącego aplikację Fashion App, czyli aplikację zakupową we Flutterze dla fikcyjnej marki detalicznej. Twoje zadanie: dodaj 2 funkcje oparte na AI, które odmienią zakupy online.

  1. Wirtualna przymierzalnia – użytkownik przesyła swoje zdjęcie, wybiera element garderoby i widzi wygenerowany przez AI obraz, na którym ma na sobie ten element.
  2. Stylista AI – na podstawie lokalizacji użytkownika, okazji i preferencji dotyczących stylu agent AI przygotowuje rekomendacje dotyczące kompletnych strojów, które użytkownik może doprecyzować w rozmowie.

Założenie jest proste: gdy klienci przymierzają ubrania w przebieralni, są znacznie bardziej skłonni do ich zakupu. Ale w internecie? Po prostu zgadujesz. Ten projekt wypełnia tę lukę dzięki AI.

Architektura: Szybki podgląd

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

Technologie podstawowe

Komponent

Technologia

Cel

Platforma agenta

Pakiet ADK (Agent Development Kit) dla Go

Orkiestracja wielu agentów, sesje, artefakty

Rozumowanie agenta (Pro)

Gemini 3.1 Pro (wersja testowa)

Umożliwia korzystanie z wirtualnej przymierzalni i usług stylistów

Rozumowanie agenta (Flash)

Gemini 3 Flash (wersja testowa)

Obsługuje agenty główne i agenty katalogu (uproszczone routing i wyszukiwanie).

Generowanie obrazów

Gemini 2.5 Flash Image

Generowanie obrazów z wirtualnym przymierzaniem i zestawami ubrań

Frontend

Flutter (Dart)

Aplikacja na wielu platformach (internet, iOS, Android)

Miejsce na dane

Google Cloud Storage

przechowuje zdjęcia produktów i wygenerowane artefakty,

Hosting

Cloud Run

Wdrażanie kontenerów bezserwerowych

2. 📦 Wymagania wstępne i konfiguracja Cloud Shell

1. Otwórz edytor Cloud Shell

👉 Otwórz edytor Cloud Shell w przeglądarce.

Jeśli terminal nie pojawia się u dołu ekranu:

  • Kliknij Widok → Terminal.

2. Konfigurowanie pakietu Flutter SDK

Cloud Shell ma wstępnie zainstalowany pakiet Flutter w lokalizacji /google/flutter. Ponieważ ten katalog jest własnością innego użytkownika systemu, przy pierwszym uruchomieniu polecenia flutter pojawi się błąd fatal: detected dubious ownership. Dodaj go do listy bezpiecznych katalogów Git:

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

Sprawdź, czy Flutter jest zainstalowany na urządzeniu PATH i działa:

flutter --version

Podczas pierwszego uruchomienia pobierany jest pakiet SDK Dart i budowane jest narzędzie Flutter. Może to potrwać minutę. Powinien pojawić się ekran podobny do tego: Flutter 3.x • channel stable.

3. Klonowanie repozytorium

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

4. Poznaj strukturę projektu

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. ☁️ Konfiguracja projektu Google Cloud

1. Tworzenie nowego projektu

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

Wyświetl listę kont rozliczeniowych:

gcloud billing accounts list

Spójrz na

OPEN

kolumnie. Musi zawierać tekst True. Jeśli wyświetla się komunikat False (często pojawia się w przypadku wygasłego bezpłatnego okresu próbnego), konto jest zamknięte i nie będzie można z niego nic opłacić. Zanim przejdziesz dalej, zapoznaj się z blokiem rozwiązywania problemów poniżej.

Skopiuj ACCOUNT_ID konta OPEN: True (wygląda jak 0X0X0X-0X0X0X-0X0X0X) i połącz je z projektem:

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

Sprawdź link:

gcloud billing projects describe fashion-app-demo

Powinien wyświetlić się tekst billingEnabled: true. Jeśli po połączeniu konta nadal widzisz symbol billingEnabled: false, oznacza to, że konto jest zamknięte OPEN: False – zapoznaj się z blokiem rozwiązywania problemów poniżej.

3. Włącz wymagane interfejsy API

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

Interfejs API

Cel

aiplatform.googleapis.com

Vertex AI – fitting_tool wywołuje generowanie obrazów przez Gemini za pomocą Vertex AI.

storage.googleapis.com

Cloud Storage – przechowuje obrazy katalogu produktów i wygenerowane wyniki testowania.

run.googleapis.com

Cloud Run – hostuje backend jako bezserwerowy kontener.

cloudbuild.googleapis.com

Cloud Build – tworzy obrazy Dockera na podstawie kodu źródłowego.

artifactregistry.googleapis.com

Artifact Registry – przechowuje utworzone obrazy Dockera.

4. Tworzenie zasobnika GCS

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. Przesyłanie obrazów katalogu produktów

Narzędzie getProductImage backendu odczytuje dane z gs://$GCS_BUCKET/catalog-assets/images/. Prześlij obrazy katalogu do tej ścieżki:

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

Sprawdź, czy plik został przesłany (powinna się wyświetlić lista plików .png):

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

6. Skonfiguruj .env plik.

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

7. Uwierzytelnianie za pomocą domyślnego uwierzytelniania aplikacji

Musisz uruchomić to polecenie przed rozpoczęciem lokalnego działania backendu. Backend w Go używa ADC do uwierzytelniania każdego wywołania Vertex AI (Gemini) i Cloud Storage. Bez ADC backend uruchomi się, ale każde żądanie wypróbowania zakończy się niepowodzeniem z kodem 401 CREDENTIALS_MISSING.

Jedne dane logowania obejmują obie usługi. Uruchom te 2 polecenia w podanej kolejności:

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

Sprawdź, czy ADC działa prawidłowo:

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

Powinno się wyświetlić około 20 znaków tokena, a po nich znak .... Jeśli wystąpi błąd, logowanie się nie powiodło – ponownie wykonaj krok 1.

4. 🏗️ Omówienie architektury

Środowisko jest już gotowe, więc zanim przyjrzymy się kodowi, zobaczmy, jak działa system.

System czterech agentów

Backend jest zbudowany jako system wieloagentowy przy użyciu pakietu Agent Development Kit (ADK) dla Go. Współpracują ze sobą 4 agenty, z których każdy ma określone zadanie:

                   ┌──────────────┐
                    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

Model

Rola

Agent główny

gemini-3-flash-preview

Policjant drogowy. Odczytuje wiadomość użytkownika i przekazuje ją do odpowiedniego specjalisty. Korzysta z szybkiego i lekkiego modelu, ponieważ musi podejmować tylko decyzje dotyczące routingu.

Agent katalogu

gemini-3-flash-preview

Ekspert Produktowy. Wczytuje katalog produktów z pliku YAML i odpowiada na zapytania dotyczące produktów. Jest też lekki – po prostu wyszukuje dane.

Fitting Room Agent

gemini-3.1-pro-preview

Specjalista ds. wirtualnego testowania. Łączy zdjęcie użytkownika ze zdjęciem produktu i generuje obraz kompozytowy przedstawiający osobę noszącą ten produkt. Używa bardziej zaawansowanego modelu, ponieważ musi analizować obrazy.

Stylist Agent

gemini-3.1-pro-preview

Doradca ds. mody. Na podstawie lokalizacji, okazji i preferencji użytkownika wybiera 3 kombinacje strojów z katalogu. Może generować obrazy z wirtualnym przymierzaniem każdego stroju. Wykorzystuje też zaawansowany model do rozumowania na podstawie kreacji.

Punkt wejścia: main.go

Wszystko zaczyna się w pliku main.go, który łączy ze sobą agentów i uruchamia serwer HTTP:

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

Kilka ważnych uwag:

  • Agenci są tworzeni od podstaw: najpierw tworzony jest agent katalogu, ponieważ zależy od niego zarówno agent przymierzalni, jak i agent stylisty (przekazują mu oni zadanie wyszukiwania produktów).
  • agent.NewMultiLoader rejestruje wszystkie 4 agenty, dzięki czemu interfejs API REST może kierować do dowolnego z nich według nazwy.
  • adkrest.NewServer automatycznie udostępnia interfejs API REST – nie musisz samodzielnie pisać modułów obsługi punktów końcowych. Pakiet ADK zapewnia gotowe funkcje zarządzania sesjami, przechowywania artefaktów i wykonywania agentów.
  • session.InMemoryService() przechowuje sesje w pamięci. Oznacza to, że sesje są tracone po ponownym uruchomieniu serwera, co jest w porządku w przypadku wersji demonstracyjnej. W środowisku produkcyjnym używasz trwałego magazynu.
  • gcsartifact.NewService przechowuje artefakty (wygenerowane obrazy) w Google Cloud Storage, dzięki czemu są one dostępne w różnych żądaniach i można je udostępniać za pomocą identyfikatorów URI GCS.

5. 🤖 Szczegółowe omówienie pakietu ADK (Agent Development Kit)

Co to jest ADK?

Pakiet Agent Development Kit (ADK) to platforma open source od Google do tworzenia agentów AI w języku Go (oraz Python i Java). Jest to warstwa między aplikacją a interfejsem Gemini API.

Możesz bezpośrednio wywołać interfejs Gemini API. Gdy jednak aplikacja musi:

  • Wyszukiwanie produktów z katalogu
  • Generowanie obrazów na podstawie zdjęć użytkownika
  • Pamiętaj, jakie stroje były wcześniej sugerowane
  • Koordynowanie pracy wielu agentów AI

Potrzebujesz struktury. ADK zapewnia tę strukturę.

Pętla agenta

Każdy agent ADK działa w pętli:

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

Ta pętla może się powtarzać wielokrotnie w ramach jednego żądania. Na przykład agent stylisty może:

  1. Otrzymywanie odpowiedzi na pytanie „Zaproponuj mi stylizację na wakacje na plaży”
  2. Wywołaj narzędzie catalog_agent, aby uzyskać listę produktów
  3. Wybierz 3 kombinacje strojów
  4. Kliknij fitting_tool przy każdym stroju, aby wygenerować obrazy.
  5. Zwróć uporządkowaną odpowiedź JSON

Podstawowe pojęcia (z kodem z tego repozytorium)

Agenty LLM

Podstawowy element składowy. Utworzono za pomocą modelu 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
})

Pole Instruction to persona agenta – informuje model LLM, kim jest agent i jak powinien się zachowywać. W tym repozytorium instrukcje są zapisywane jako pliki Markdown i osadzane w czasie kompilacji za pomocą dyrektywy //go:embed w Go:

//go:embed instructions.md
var instructions string

Dzięki temu prompty są przechowywane jako oddzielne dokumenty z możliwością tworzenia wersji, a nie jako ciągi tekstowe w kodzie.

Narzędzia

Narzędzia to funkcje Go, które mogą być wywoływane przez LLM. ADK obsługuje tłumaczenie między formatem wywoływania narzędzi LLM a wpisaną funkcją Go:

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

ADK automatycznie generuje schemat JSON z Twoich struktur Go i wysyła go do LLM. Gdy LLM zdecyduje się wywołać funkcję listProducts, ADK deserializuje argumenty, wywołuje Twoją funkcję i odsyła wynik.

Parametr tool.Context umożliwia narzędziom dostęp do usług środowiska wykonawczego ADK, a przede wszystkim do artefaktów:

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


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

Przekazywanie uprawnień sub-agentowi

Agent może używać innego agenta jako narzędzia za pomocą funkcji agenttool.New():

// 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
},

Gdy pracownik przymierzalni potrzebuje informacji o produkcie, może wywołać agenta katalogu tak jak zwykłe narzędzie. Model LLM widzi go na liście narzędzi i może zdecydować się na jego wywołanie.

Sesje

Sesje śledzą historię rozmów. Interfejs API REST pakietu ADK zarządza nimi automatycznie:

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

Kluczowa decyzja projektowa w tej aplikacji: wirtualna przymierzalnia tworzy nową sesję dla każdego żądania (każde przymierzenie jest niezależne), a stylista ponownie wykorzystuje tę samą sesję (dzięki temu pamięta poprzednie sugestie i może je ulepszać na podstawie opinii).

Stan

Stan to magazyn par klucz-wartość dołączony do sesji. Agenci odczytują i zapisują stan, aby koordynować:

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


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

Agent stylisty używa stanu, aby zapamiętać, które produkty już zasugerował, więc następnym razem wybierze inne.

Artefakty

Artefakty to nazwane obiekty binarne (zwykle obrazy) przechowywane w ramach poszczególnych sesji. W przeciwieństwie do odpowiedzi tekstowych są one przechowywane oddzielnie i pobierane według nazwy:

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

Dzięki temu odpowiedzi są lekkie – agent zwraca tylko nazwę artefaktu, a interfejs pobiera dane obrazu binarnego osobno.

Wywołania zwrotne

Funkcje zwrotne to punkty zaczepienia, które są uruchamiane w określonych momentach pętli agenta. Mogą oni sprawdzać, modyfikować lub skracać wykonanie:

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

Jeśli funkcja zwrotna zwróci odpowiedź inną niż nil, domyślne działanie zostanie pominięte. Na przykład BeforeModelCallback, która zwraca odpowiedź z pamięci podręcznej, całkowicie pomija rzeczywiste wywołanie modelu LLM.

Wymuszanie schematu JSON

Zarówno wirtualna przymierzalnia, jak i stylista wymuszają na LLM odpowiedź w postaci strukturalnego kodu JSON:

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

Dzięki temu interfejs Flutter zawsze otrzymuje dane, które można przeanalizować, a nie tekst w dowolnej formie.

Agent katalogu: najprostszy przykład

Agent katalogu (catalog/agent.go) to najprostszy agent w systemie – dobry punkt wyjścia do zrozumienia wzorców pakietu ADK.

Zawiera 2 narzędzia:

  1. listProducts – zwraca pełny katalog produktów z pliku YAML.
  2. getProductImage – wczytuje zdjęcie produktu z GCS (lub lokalną wersję zapasową) i zapisuje go jako artefakt.

Narzędzie getProductImage pokazuje ważny wzorzec: wczytywanie z wielu źródeł z pamięcią podręczną artefaktów:

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

Narzędzie najpierw próbuje znaleźć artefakty, potem GCS, a na końcu pliki lokalne. Po załadowaniu obraz jest buforowany jako artefakt, więc kolejne wywołania są natychmiastowe.

6. 🧪 Potok AI: agenci w akcji

Przyjrzyjmy się teraz 2 najbardziej zaawansowanym agentom, którzy generują obrazy i dobierają stroje.

6.1 Agent przymierzalni

Plik:

adk_backend/fittingroom/agent.go

Agent przymierzalni to mechanizm, który umożliwia korzystanie z funkcji „Wirtualne testowanie”. Gdy użytkownik prześle swoje zdjęcie i wybierze produkt, ten agent wygeneruje złożony obraz przedstawiający osobę noszącą ten produkt.

fitting_tool – Krok po kroku

Podstawowa logika znajduje się w funkcji doFitting. Gdy agent wywoła to narzędzie:

Krok 1. Rozwiąż problem z obrazem użytkownika

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
   }

Obraz użytkownika może pochodzić z 2 źródeł:

  • Nazwa artefaktu (np. upload_abc123_1) – to początkowe przesłanie zapisane przez wywołanie zwrotne SaveIncomingBlobs.
  • URI – to wcześniej wygenerowany wynik dopasowania przechowywany w GCS w celu ponownego wykorzystania w różnych sesjach.gs://

Ten dwutorowy projekt jest celowy: gdy agent stylisty wygeneruje później przymiarki, ponownie użyje adresu URL GCS z początkowego wyniku przymierzalni, aby tożsamość użytkownika była spójna we wszystkich strojach.

Krok 2. Utwórz prompt multimodalny

   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
   }

Symbol toolInstructions (osadzony z tool_instructions.md) jest kluczowy – informuje Gemini, że należy zachować tożsamość użytkownika (twarz, typ sylwetki, odcień skóry, włosy), a zastosować tylko element odzieży. Bez tego inżynieria promptów model może zmienić wygląd osoby.

Krok 3. Wywołaj Gemini, aby wygenerować obraz

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

Wszystkie 4 agenty i narzędzie do generowania obrazów korzystają z jednej ścieżki uwierzytelniania: Backend: genai.BackendVertexAI z identyfikatorem projektu, uwierzytelnianym za pomocą domyślnego uwierzytelniania aplikacji. Modele orkiestracji (gemini-3.1-pro-preview, gemini-3-flash-preview) i model obrazu (gemini-2.5-flash-image) znajdują się za tym samym punktem końcowym Vertex AI, a ten sam ADC autoryzuje też dostęp do Cloud Storage – jeden zestaw danych logowania, każde wywołanie.

Krok 4. Zapisz wynik

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

Podwójne zapisywanie (artefakt + GCS) to klucz do przekazywania klienta między przymierzalnią a stylistą. Artefakt zapewnia natychmiastowy dostęp w bieżącej sesji, a identyfikator URI GCS umożliwia styliście (który działa w innej sesji) późniejsze odwoływanie się do tego samego obrazu.

SaveIncomingBlobs Oddzwanianie

Zanim agent zacznie rozumowanie, ten kod BeforeAgentCallback zapisuje wszystkie obrazy przesłane przez użytkownika:

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
}

Zwracając wartość (nil, nil), wywołanie zwrotne sygnalizuje: „Przetwarzanie wstępne zostało zakończone – teraz uruchom agenta w normalny sposób”. Jeśli zwróci niepustą treść, całkowicie pominie agenta.

6.2 Agent Stylista

Plik:

adk_backend/stylist/agent.go

Agent stylisty jest najbardziej zaawansowany w systemie. Zawiera spersonalizowane rekomendacje dotyczące strojów i umożliwia ich dopracowywanie w trakcie rozmowy.

Trzy powtórzenia – pamięć stylistki

Stylista używa 3 wywołań zwrotnych, aby zachować kontekst w rozmowach wieloetapowych:

Callback 1:

InjectPreviousProducts (BeforeModel)

Problem: jeśli użytkownik powie „pokaż mi inne opcje”, LLM może ponownie zaproponować te same produkty, ponieważ nie śledzi, co już polecił.

Rozwiązanie: po każdej odpowiedzi identyfikatory produktów są zapisywane w stanie sesji. Przed kolejnym wywołaniem LLM ta funkcja zwrotna odczytuje je i wstawia wskazówkę:

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
}

Callback 2:

ExtractAndInjectUserImage (BeforeModel)

Problem: gdy użytkownik przekaże opinię („zmień styl na bardziej swobodny”), w kolejnej wiadomości nie pojawi się ponownie jego zdjęcie. Ale narzędzie do dopasowywania go potrzebuje.

Rozwiązanie: przy pierwszym żądaniu ta funkcja zwrotna wyodrębnia odniesienie do obrazu użytkownika i zapisuje je w stanie. W przypadku kolejnych żądań ponownie wstrzykuje ten kod:

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
}

Callback 3:

SaveSelectedProducts (AfterModel)

Gdy LLM odpowie sugestiami dotyczącymi stroju, ta funkcja zwrotna przeanalizuje JSON, aby wyodrębnić identyfikatory produktów i zapisać je na potrzeby następnego wywołania funkcji zwrotnej InjectPreviousProducts:

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
}

Te 3 wywołania zwrotne tworzą pętlę informacji zwrotnych:

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 Agent główny

Plik:

adk_backend/rootagent/agent.go

Najprostszy agent – tylko 31 wierszy:

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

Używa gemini-3-flash-preview (najszybszego modelu), ponieważ decyzje dotyczące przekierowania są proste – model LLM musi tylko odczytać intencje użytkownika i wybrać odpowiedniego subagenta. Nie musisz używać żadnych narzędzi. SubAgents automatycznie obsługuje delegowanie.

7. 📱 Architektura frontendu Flutter

Interfejs Flutter to w pełni funkcjonalna aplikacja do zakupów detalicznych. Funkcje AI znajdują się w flutter_frontend/lib/workshop_tasks/ i są oddzielone od gotowego środowiska zakupowego w core_app/.

Wzorzec MVVM

Aplikacja korzysta z architektury Model-View-ViewModel z pakietem Provider:

┌──────────────────┐    ┌────────────────────┐    ┌──────────────────┐
  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                                                 
└──────────────────┘    └────────────────────┘    └──────────────────┘

Każda warstwa ma określoną rolę:

  • Model: klasy danych, takie jak Product, Outfit, StyleRequest, i wyliczenia, takie jak TryOnState
  • ViewModel (ChangeNotifier): przechowuje bieżący stan i przesyła zmiany do interfejsu za pomocą notifyListeners().
  • View (Widget): subskrybuje ViewModel za pomocą context.watch() i ponownie tworzy widżet, gdy zmienia się stan.
  • Usługa: wykonuje wywołania HTTP do backendu ADK i zwraca dane z określonym typem.

Warstwa usług

Usługi są definiowane jako abstrakcyjne interfejsy z implementacjami specyficznymi dla ADK:

// 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 { ... }

Dzięki temu możesz zastąpić backend ADK usługą Firebase AI, usługą testową lub dowolną inną implementacją bez zmiany pozostałej części aplikacji.

3-etapowy wzorzec interfejsu API

Zarówno AdkFittingRoomService, jak i AdkStylingService korzystają z tego samego wzorca komunikacji z backendem ADK:

Krok 1. Utwórz sesję

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
}

Krok 2. Uruchom agenta

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

Krok 3. Pobierz artefakt

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
}

Kluczowa różnica w projektowaniu: usługa przymierzalni tworzy nową sesję dla każdego żądania (_createSession() jest wywoływana za każdym razem), a usługa stylizacji ponownie wykorzystuje tę samą sesję (_sessionId ??= await _createSession()), aby umożliwić wieloetapową rozmowę.

Zarządzanie stanem: TryItOnProvider

Plik:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

TryItOnProvider zarządza całym procesem testowania. Używa wyliczenia TryOnState jako automatu stanów:

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


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

Prywatne przejścia stanu zapewniają spójność – nigdy nie aktualizujesz stanu bez wyczyszczenia nieaktualnych danych i powiadomienia interfejsu:

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

Główna metoda generowania łączy wszystkie te elementy:

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

Interfejs: ekrany jako router stanu

Plik:

workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart

Ekran wirtualnej przymierzalni wykorzystuje dopasowywanie wzorców w Dart 3 z AnimatedSwitcher, aby przełączać się między podrzędnymi ekranami na podstawie stanu dostawcy:

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() subskrybuje usługę dostawcy. Za każdym razem, gdy wywoływana jest funkcja notifyListeners(), ten widżet jest przebudowywany, a AnimatedSwitcher płynnie przechodzi między ekranami. Nie ma Navigator.push – zawartość ekranu zmienia się w miejscu na podstawie wyliczenia stanu.

Przekazanie agentowi: przymierzalnia → stylista

Najciekawszym wzorcem UX jest sposób, w jaki aplikacja przekazuje kontekst od agenta w przymierzalni do agenta stylisty.

5_fitting_room.dart Po wygenerowaniu obrazu z wirtualną przymiarką przycisk „Zaproponuj stylizację” otwiera formularz. Gdy użytkownik prześle:

// 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,
));

StyleRequest zawiera wszystko, czego potrzebuje stylista:

  • Lokalizacja i okazja – kontekst tekstowy do stylizacji
  • Adres URL obrazu użytkownika w GCS – aby stylista mógł ponownie wykorzystać dokładnie tę samą reprezentację użytkownika.
  • Wybrany produkt – stylista uwzględnia go w każdym zestawie.

Jest to przekazywanie agentowe – płynne przekazywanie kontekstu multimodalnego z jednego agenta AI do drugiego, przy czym użytkownik widzi tylko prosty formularz.

Proces stylizacji: StylingProvider

Plik:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

StylingProvider jest prostszy niż TryItOnProvider, ponieważ większość złożoności przekazuje do backendu:

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

Metoda refineWithFeedback wysyła wiadomość w formacie zwykłego tekstu do tej samej sesji – wywołania zwrotne InjectPreviousProductsExtractAndInjectUserImage na backendzie automatycznie obsługują zarządzanie kontekstem.

8. 🚀 Lokalne uruchamianie aplikacji

Aby zapewnić płynne działanie Cloud Shell, backend Go obsługuje skompilowaną aplikację internetową Flutter z tego samego portu (8080). Jeden proces, jeden adres URL podglądu, brak problemów z różnymi domenami i brak konieczności edytowania plików konfiguracyjnych.

Zanim zaczniesz – sprawdź, czy ADC działa prawidłowo

Backend potrzebuje domyślnego uwierzytelniania aplikacji, aby wywoływać Vertex AI. Jeśli krok 7 konfiguracji projektu został wykonany w tej sesji Cloud Shell i na tym koncie Google, możesz przejść dalej. Jeśli wracasz po przerwie, zmieniasz konto lub nie masz pewności, poświęć 5 sekund na sprawdzenie:

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

Jeśli wydrukuje to około 20 znaków tokena, wszystko jest w porządku. Jeśli wystąpi błąd, ponownie wykonaj krok 7 konfiguracji projektu:

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

Będziesz używać 2 terminali Cloud Shell:

  • Terminal A – działa w tle w sposób ciągły (./run.sh). Pozostaw go otwartego.
  • Terminal B – uruchamia kompilację internetową Fluttera raz (flutter build web). Kończy działanie po zakończeniu.

Kolejność nie ma znaczenia – możesz zacząć od dowolnego z nich. Aby jednak zapewnić jak najlepsze wrażenia podczas pierwszego uruchomienia, najpierw skompiluj aplikację Flutter, aby backend miał interfejs użytkownika do obsługi od momentu uruchomienia.

1. Terminal B – tworzenie pakietu Flutter Web (jednorazowo)

Otwórz nową kartę Cloud Shell (kliknij + u góry panelu terminala), a następnie:

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

Spowoduje to utworzenie katalogu flutter_frontend/build/web/ zawierającego pliki statyczne (HTML, JS, zasoby) i zakończenie działania po zakończeniu procesu. Usługa backendu będzie je udostępniać, gdy tylko wykryje, że katalog istnieje.

2. Terminal A – uruchom backend (długotrwały)

W pierwotnym terminalu Cloud Shell:

cd ~/fashion_app_demo/adk_backend
./run.sh

Powinien pojawić się ekran podobny do tego:

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

Pozostaw to okno terminala otwarte – backend pozostanie aktywny, dopóki run.sh będzie działać. Aby zatrzymać, kliknij Ctrl+C.

Serwer udostępnia wszystko na porcie 8080:

  • / – aplikacja internetowa Fluttera (interfejs zakupowy);
  • /api/ – punkty końcowe REST pakietu ADK (wywoływane przez aplikację Flutter);
  • Interfejs programistyczny ADK – również na /, gdy nie ma kompilacji Fluttera; przydatny do bezpośredniego debugowania agenta.

3. Otwórz podgląd w przeglądarce

  1. W Cloud Shell kliknij ikonę Podgląd w przeglądarce (w prawym górnym rogu) → Podejrzyj na porcie 8080.
  2. Aplikacja zakupowa we Flutterze wczytuje się w nowej karcie.
  3. Przeglądanie katalogu produktów i wybieranie pozycji
  4. Kliknij ikonę osoby (👤), aby rozpocząć proces wirtualnego testowania.
  5. Prześlij zdjęcie i obserwuj, jak AI generuje obraz stylizacji.
  6. Kliknij „Style Me”, aby uzyskać rekomendacje dotyczące stroju
  7. Wpisz dodatkowe uwagi, np. „sformułuj to w bardziej przystępny sposób” – ulepszanie w ramach tej samej sesji

9. ☁️ Wdróż w Cloud Run

Pakowanie kompilacji Fluttera w backendzie

Kontener Cloud Run zawiera zarówno interfejs API, jak i interfejs użytkownika w jednym obrazie. Skopiuj kompilację internetową Fluttera do katalogu adk_backend/flutter_web/. Jest to pierwsza ścieżka, którą serwer Go sprawdza podczas wybierania interfejsu użytkownika do wyświetlenia:

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

(Jeśli pracujesz lokalnie, możesz już mieć build/web z kroku Uruchom lokalnie. Ponowne uruchomienie flutter build web jest w porządku).

Wdrażanie backendu (obsługującego interfejs API i interfejs)

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

Po zakończeniu wdrażania otrzymasz adres URL usługi, np. https://fashion-app-backend-xyz-uc.a.run.app. Otwórz ją w przeglądarce – aplikacja zakupowa we Flutterze wczytuje się z /, a wywołania interfejsu API są kierowane do /api/ na tym samym hoście. Nie trzeba wprowadzać zmian w konfiguracji frontendu, nie przekazano klucza interfejsu API.

Sprawdzanie wdrożenia

Otwórz adres URL Cloud Run w przeglądarce i wykonaj cały proces:

  1. Przeglądaj → Wybierz produkt
  2. Wirtualna przymierzalnia → prześlij zdjęcie → zobacz obraz wygenerowany przez AI
  3. Zaproponuj stylizację → podaj lokalizację lub okazję → zobacz wybrane zestawy ubrań
  4. Opinie → wpisz „make it more casual” (zmień na bardziej swobodny) → zobacz zaktualizowane stroje.
  5. Dodaj do koszyka → dokończ proces zakupowy

10. 🎉 Podsumowanie

Co utworzysz

Poznałeś(-aś) pełne możliwości AI w branży handlowej, w tym:

  • ✅ Wieloagentowe zaplecze z 4 wyspecjalizowanymi agentami, którzy współpracują ze sobą.
  • Wirtualna przymierzalnia, która generuje spersonalizowane obrazy przymierzania.
  • Stylista AI, który dobiera stroje i dopracowuje je w trakcie rozmowy.
  • ✅ Aplikacja Flutter na wielu platformach, która łączy się z backendem agenta.
  • ✅ Wdrożenie Cloud Run na potrzeby skalowalnego hostingu bezserwerowego

Kluczowe pojęcia

Pomysł

Gdzie widzisz tę informację

Orkiestracja wielu agentów ADK

Kierowanie do agentów w przymierzalni, katalogu i stylistów

Generowanie obrazów multimodalnych przez Gemini

fitting_tool łączenie zdjęć użytkowników ze zdjęciami produktów,

Stan sesji w przypadku konwersacyjnej AI

Stylista ponownie wykorzystuje sesje do przekazywania iteracyjnych opinii

Przechowywanie artefaktów z danymi binarnymi

Oddzielenie przechowywania obrazów od odpowiedzi tekstowych

Wywołania zwrotne dla logiki oprogramowania pośredniczącego

SaveIncomingBlobs, InjectPreviousProducts, SaveSelectedProducts

MVVM + Provider w Flutterze

TryItOnProvider i StylingProvider z ChangeNotifier

Przekazanie do agenta

StyleRequest przekazywanie kontekstu multimodalnego między agentami,

Następne kroki

  • 🎨 Dostosowywanie promptów agenta – edytuj instructions.md, aby zmienić osobowość stylisty.
  • 🛍️ Dodaj więcej produktów – zaktualizuj catalog.yaml o nowe produkty.
  • 📱 Uwzględnianie urządzeń mobilnych – uruchom flutter build ios lub flutter build apk
  • 🔄 Dodaj stałe sesje – zastąp InMemoryService implementacją opartą na bazie danych.
  • 🔒 Dodaj uwierzytelnianie – zabezpiecz punkt końcowy Cloud Run za pomocą IAM.

Zasoby