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.
- 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.
- 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
2. Łączenie konta rozliczeniowego
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 |
| Vertex AI – |
| Cloud Storage – przechowuje obrazy katalogu produktów i wygenerowane wyniki testowania. |
| Cloud Run – hostuje backend jako bezserwerowy kontener. |
| Cloud Build – tworzy obrazy Dockera na podstawie kodu źródłowego. |
| 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 |
| 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 |
| 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 |
| 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 |
| 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.NewMultiLoaderrejestruje wszystkie 4 agenty, dzięki czemu interfejs API REST może kierować do dowolnego z nich według nazwy.adkrest.NewServerautomatycznie 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.NewServiceprzechowuje 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:
- Otrzymywanie odpowiedzi na pytanie „Zaproponuj mi stylizację na wakacje na plaży”
- Wywołaj narzędzie
catalog_agent, aby uzyskać listę produktów - Wybierz 3 kombinacje strojów
- Kliknij
fitting_toolprzy każdym stroju, aby wygenerować obrazy. - 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:
listProducts– zwraca pełny katalog produktów z pliku YAML.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 zwrotneSaveIncomingBlobs. - 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 jakTryOnState - ViewModel (
ChangeNotifier): przechowuje bieżący stan i przesyła zmiany do interfejsu za pomocąnotifyListeners(). - View (Widget): subskrybuje ViewModel za pomocą
context.watchi 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 InjectPreviousProducts i ExtractAndInjectUserImage 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
- W Cloud Shell kliknij ikonę Podgląd w przeglądarce (w prawym górnym rogu) → Podejrzyj na porcie 8080.
- Aplikacja zakupowa we Flutterze wczytuje się w nowej karcie.
- Przeglądanie katalogu produktów i wybieranie pozycji
- Kliknij ikonę osoby (👤), aby rozpocząć proces wirtualnego testowania.
- Prześlij zdjęcie i obserwuj, jak AI generuje obraz stylizacji.
- Kliknij „Style Me”, aby uzyskać rekomendacje dotyczące stroju
- 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:
- Przeglądaj → Wybierz produkt
- Wirtualna przymierzalnia → prześlij zdjęcie → zobacz obraz wygenerowany przez AI
- Zaproponuj stylizację → podaj lokalizację lub okazję → zobacz wybrane zestawy ubrań
- Opinie → wpisz „make it more casual” (zmień na bardziej swobodny) → zobacz zaktualizowane stroje.
- 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 |
|
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 |
|
MVVM + Provider w Flutterze |
|
Przekazanie do agenta |
|
Następne kroki
- 🎨 Dostosowywanie promptów agenta – edytuj
instructions.md, aby zmienić osobowość stylisty. - 🛍️ Dodaj więcej produktów – zaktualizuj
catalog.yamlo nowe produkty. - 📱 Uwzględnianie urządzeń mobilnych – uruchom
flutter build ioslubflutter build apk - 🔄 Dodaj stałe sesje – zastąp
InMemoryServiceimplementacją opartą na bazie danych. - 🔒 Dodaj uwierzytelnianie – zabezpiecz punkt końcowy Cloud Run za pomocą IAM.
Zasoby
- Dokumentacja pakietu ADK – oficjalna dokumentacja pakietu Agent Development Kit
- Kod źródłowy ADK Go – repozytorium GitHub
- Dokumentacja pakietu ADK Go – dokumentacja API
- Dokumentacja interfejsu Gemini API – funkcje modeli i przewodniki
- Pakiet Flutter Provider – dokumentacja zarządzania stanem
- Dokumentacja Cloud Run – przewodniki po wdrażaniu i skalowaniu