👗 Membangun Ruang Pas Virtual & Penata Gaya AI dengan Flutter, ADK Go & Gemini

1. Pengantar

Yang Akan Anda Buat

Dalam codelab ini, Anda akan berperan sebagai developer yang membangun Fashion App, aplikasi belanja Flutter untuk merek retail fiktif. Misi Anda: menambahkan dua fitur berteknologi AI yang mengubah pengalaman belanja online.

  1. Ruang Pas Virtual — Pengguna mengupload foto dirinya, memilih item pakaian, dan melihat gambar dirinya yang dihasilkan AI saat mengenakan item tersebut.
  2. Penata Gaya AI — Berdasarkan lokasi, acara, dan preferensi gaya pengguna, agen AI akan menyusun rekomendasi pakaian lengkap — dan pengguna dapat menyempurnakannya melalui percakapan.

Idenya sederhana: saat orang mencoba pakaian di ruang ganti, mereka cenderung akan membelinya. Tapi secara online? Anda hanya menebak. Project ini menjembatani kesenjangan tersebut dengan AI.

Sekilas Arsitektur

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

Teknologi Inti

Komponen

Teknologi

Tujuan

Framework Agen

ADK (Agent Development Kit) untuk Go

Orkestrasi multi-agen, sesi, artefak

Penalaran Agen (Pro)

Pratinjau Gemini 3.1 Pro

Mendukung agen ruang pas dan penata gaya

Penalaran Agen (Flash)

Pratinjau Gemini 3 Flash

Mendukung agen root dan katalog (perutean/pencarian ringan)

Pembuatan Gambar

Gambar Gemini 2.5 Flash

Membuat gambar coba pakaian dan pakaian

Frontend

Flutter (Dart)

Aplikasi lintas platform (Web, iOS, Android)

Penyimpanan

Google Cloud Storage

Menyimpan gambar produk dan artefak yang dibuat

Hosting

Cloud Run

Deployment container serverless

2. 📦 Prasyarat & Penyiapan Cloud Shell

1. Buka Cloud Shell Editor

👉 Buka Cloud Shell Editor di browser Anda.

Jika terminal tidak muncul di bagian bawah layar:

  • Klik LihatTerminal

2. Menyiapkan Flutter SDK

Cloud Shell dilengkapi dengan Flutter yang sudah diinstal sebelumnya di /google/flutter. Karena direktori tersebut dimiliki oleh pengguna sistem lain, Anda akan mengalami error fatal: detected dubious ownership saat pertama kali menjalankan flutter. Tambahkan ke daftar safe-directory git satu kali:

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

Pastikan Flutter ada di PATH Anda dan berfungsi:

flutter --version

Saat dijalankan pertama kali, Dart SDK akan didownload dan alat Flutter akan dibangun — tunggu sebentar. Anda akan melihat yang seperti Flutter 3.x • channel stable.

3. Membuat Clone Repositori

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

4. Mempelajari Struktur Project

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. ☁️ Penyiapan Project Google Cloud

1. Membuat Project Baru

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

Mencantumkan akun penagihan Anda:

gcloud billing accounts list

Lihat

OPEN

kolom. Harus bertuliskan True. Jika muncul pesan False (umum terjadi pada uji coba gratis yang telah berakhir), akun tersebut ditutup dan tidak akan membayar apa pun — lanjutkan ke blok pemecahan masalah di bawah sebelum melanjutkan.

Salin ACCOUNT_ID akun OPEN: True (terlihat seperti 0X0X0X-0X0X0X-0X0X0X) dan tautkan ke project Anda:

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

Verifikasi link:

gcloud billing projects describe fashion-app-demo

Anda akan melihat billingEnabled: true. Jika Anda melihat billingEnabled: false meskipun setelah menautkan, akun tersebut ditutup (OPEN: False) — lihat blok pemecahan masalah di bawah.

3. Mengaktifkan API yang Diperlukan

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

API

Tujuan

aiplatform.googleapis.com

Vertex AI — panggilan fitting_tool memanggil pembuatan gambar Gemini melalui Vertex AI

storage.googleapis.com

Cloud Storage — menyimpan gambar katalog produk dan hasil coba yang dihasilkan

run.googleapis.com

Cloud Run — menghosting backend sebagai container serverless

cloudbuild.googleapis.com

Cloud Build — membangun image Docker dari sumber

artifactregistry.googleapis.com

Artifact Registry — menyimpan image Docker yang dibuat

4. Membuat Bucket 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. Mengupload Gambar Katalog Produk

Alat getProductImage backend membaca dari gs://$GCS_BUCKET/catalog-assets/images/. Upload gambar katalog ke jalur yang tepat tersebut:

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

Verifikasi upload (Anda akan melihat daftar file .png):

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

6. Mengonfigurasi File .env

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

7. Melakukan autentikasi dengan Kredensial Default Aplikasi

Anda harus menjalankan perintah ini sebelum memulai backend secara lokal. Backend Go menggunakan ADC untuk mengautentikasi setiap panggilan ke Vertex AI (Gemini) dan Cloud Storage. Tanpa ADC, backend akan dimulai, tetapi setiap permintaan coba akan gagal dengan 401 CREDENTIALS_MISSING.

Satu kredensial mencakup kedua layanan. Jalankan kedua perintah ini secara berurutan:

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

Verifikasi ADC berfungsi dengan baik:

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

Anda akan melihat sekitar 20 karakter token yang diikuti dengan .... Jika terjadi error, berarti login tidak berhasil — jalankan kembali langkah 1.

4. 🏗️ Ringkasan Arsitektur

Setelah lingkungan siap, mari kita pahami cara kerja sistem sebelum melihat kodenya.

Sistem Empat Agen

Backend dibangun sebagai sistem multi-agen menggunakan ADK (Agent Development Kit) untuk Go. Empat agen bekerja sama, masing-masing dengan tanggung jawab tertentu:

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

Agen

Model

Peran

Agen Root

gemini-3-flash-preview

Polisi lalu lintas. Membaca pesan pengguna dan mendelegasikan ke agen spesialis yang tepat. Menggunakan model yang cepat dan ringan karena hanya perlu membuat keputusan perutean.

Agen Katalog

gemini-3-flash-preview

Pakar produk. Memuat katalog produk dari file YAML dan menjawab kueri produk. Juga ringan — hanya mencari data.

Agen Ruang Pas

gemini-3.1-pro-preview

Spesialis coba virtual. Mengambil foto pengguna + gambar produk dan membuat gambar komposit orang yang mengenakan item tersebut. Menggunakan model yang lebih mumpuni karena perlu bernalar tentang gambar.

Agen Penata Gaya (Stylist Agent)

gemini-3.1-pro-preview

Penasihat mode. Berdasarkan lokasi, acara, dan preferensi, aplikasi ini memilih 3 kombinasi pakaian dari katalog. Dapat membuat gambar coba pakaian untuk setiap pakaian. Juga menggunakan model yang mumpuni untuk penalaran kreatif.

Titik Entri: main.go

Semuanya dimulai di main.go, yang menghubungkan agen dan memulai server 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()
}

Beberapa hal penting yang perlu diperhatikan:

  • Agen dibuat dari bawah ke atas: Agen katalog dibuat terlebih dahulu karena agen ruang pas dan agen penata gaya bergantung padanya (mereka mendelegasikan pencarian produk ke agen katalog).
  • agent.NewMultiLoader mendaftarkan keempat agen sehingga REST API dapat merutekan ke salah satu agen berdasarkan nama.
  • adkrest.NewServer menyediakan REST API secara otomatis — Anda tidak perlu menulis sendiri handler endpoint. ADK menyediakan pengelolaan sesi, penyimpanan artefak, dan eksekusi agen secara langsung.
  • session.InMemoryService() menyimpan sesi dalam memori. Artinya, sesi akan hilang jika server dimulai ulang, yang tidak masalah untuk demo. Dalam produksi, Anda akan menggunakan penyimpanan persisten.
  • gcsartifact.NewService menyimpan artefak (gambar yang dihasilkan) di Google Cloud Storage, sehingga artefak tersebut tetap ada di seluruh permintaan dan dapat dibagikan melalui URI GCS.

5. 🤖 Pembahasan Mendalam ADK (Agent Development Kit)

Apa itu ADK?

Agent Development Kit (ADK) adalah framework open source dari Google untuk membangun agen AI di Go (dan Python/Java). Lapisan ini berada di antara aplikasi Anda dan Gemini API.

Anda dapat memanggil Gemini API secara langsung. Namun, setelah aplikasi Anda perlu:

  • Mencari produk dari katalog
  • Membuat gambar berdasarkan foto pengguna
  • Ingat pakaian yang disarankan sebelumnya
  • Mengkoordinasikan beberapa agen AI

Anda memerlukan struktur. ADK menyediakan struktur tersebut.

Loop Agen

Setiap agen ADK mengikuti loop:

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

Loop ini dapat diulang beberapa kali dalam satu permintaan. Misalnya, agen penata gaya dapat:

  1. Menerima "Pilihkan gaya untuk liburan di pantai"
  2. Panggil alat catalog_agent untuk mendapatkan daftar produk
  3. Pilih 3 kombinasi pakaian
  4. Panggil fitting_tool untuk setiap pakaian guna membuat gambar
  5. Menampilkan respons JSON terstruktur

Konsep Inti (Dengan Kode Dari Repo Ini)

Agen LLM

Elemen penyusun utama. Dibuat dengan 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
})

Kolom Instruction adalah persona agen — kolom ini memberi tahu LLM siapa agen tersebut dan cara berperilakunya. Dalam repo ini, petunjuk ditulis sebagai file markdown dan disematkan pada waktu kompilasi menggunakan direktif //go:embed Go:

//go:embed instructions.md
var instructions string

Hal ini membuat perintah tetap sebagai dokumen terpisah yang dapat di-versi, bukan string inline.

Alat

Alat adalah fungsi Go yang dapat dipanggil oleh LLM. ADK menangani terjemahan antara format panggilan alat LLM dan fungsi Go yang diketik:

// 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 otomatis menghasilkan skema JSON dari struct Go Anda dan mengirimkannya ke LLM. Saat LLM memutuskan untuk memanggil listProducts, ADK akan mendeserialisasi argumen, memanggil fungsi Anda, dan mengirimkan hasilnya kembali.

Parameter tool.Context memberikan akses alat ke layanan runtime ADK — yang paling penting adalah artefak:

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


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

Delegasi Sub-Agen

Agen dapat menggunakan agen lain sebagai alat melalui 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
},

Saat memerlukan informasi produk, agen ruang pas dapat memanggil agen katalog seolah-olah itu adalah alat biasa. LLM melihatnya di daftar alat dan dapat memutuskan untuk memanggilnya.

Sesi

Sesi melacak histori percakapan. REST API ADK mengelolanya secara otomatis:

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

Keputusan desain penting dalam aplikasi ini: ruang pas membuat sesi baru per permintaan (setiap coba pakaian bersifat independen), sedangkan penata gaya menggunakan kembali sesi yang sama (sehingga mengingat saran sebelumnya dan dapat menyempurnakan berdasarkan masukan).

Negara bagian/Provinsi

Status adalah penyimpanan nilai kunci yang dilampirkan ke sesi. Agen membaca dan menulis status untuk berkoordinasi:

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


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

Agen penata gaya menggunakan status untuk mengingat produk yang telah disarankan, sehingga agen akan memilih produk yang berbeda pada waktu berikutnya.

Artefak

Artefak adalah objek biner bernama (biasanya gambar) yang disimpan per sesi. Tidak seperti respons teks, respons ini disimpan secara terpisah dan diambil berdasarkan nama:

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

Hal ini membuat respons tetap ringan — agen hanya menampilkan nama artefak, dan frontend mengambil data gambar biner secara terpisah.

Callback

Callback adalah hook yang berjalan pada titik tertentu dalam loop agen. Mereka dapat memeriksa, mengubah, atau menghentikan eksekusi:

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

Jika callback menampilkan respons non-null, perilaku default akan dilewati. Misalnya, BeforeModelCallback yang menampilkan respons yang di-cache akan melewati panggilan LLM yang sebenarnya.

Penerapan Skema JSON

Agen ruang pas dan stylist memaksa LLM untuk merespons dalam JSON terstruktur:

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

Hal ini memastikan frontend Flutter selalu menerima data yang dapat diuraikan, bukan teks bebas.

Agen Katalog: Contoh Paling Sederhana

Agen katalog (catalog/agent.go) adalah agen paling sederhana dalam sistem — titik awal yang baik untuk memahami pola ADK.

Alat ini memiliki dua fungsi:

  1. listProducts — Menampilkan katalog produk lengkap dari file YAML
  2. getProductImage — Memuat gambar produk dari GCS (atau penggantian lokal) dan menyimpannya sebagai artefak

Alat getProductImage menunjukkan pola penting — pemuatan multi-sumber dengan penyiapan cache artefak:

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

Alat ini mencoba artefak terlebih dahulu, lalu GCS, lalu file lokal. Setelah dimuat, gambar di-cache sebagai artefak sehingga panggilan berikutnya akan langsung dilakukan.

6. 🧪 Pipeline AI: Agen Beraksi

Sekarang, mari kita bahas dua agen yang paling canggih — yang benar-benar menghasilkan gambar dan memilih pakaian.

6.1 Agen Ruang Pas

File:

adk_backend/fittingroom/agent.go

Agen ruang pas adalah mesin di balik "Coba Virtual". Saat pengguna mengupload fotonya dan memilih produk, agen ini akan membuat gambar komposit orang yang mengenakan item tersebut.

fitting_tool — Langkah demi Langkah

Logika inti berada di fungsi doFitting. Berikut yang terjadi saat agen memanggilnya:

Langkah 1: Atasi masalah gambar pengguna

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
   }

Gambar pengguna dapat berasal dari dua sumber:

  • Nama artefak (seperti upload_abc123_1) — ini adalah upload awal, yang disimpan oleh callback SaveIncomingBlobs
  • URI gs:// — ini adalah hasil penyesuaian yang dihasilkan sebelumnya, yang disimpan di GCS untuk penggunaan ulang lintas sesi

Desain jalur ganda ini disengaja: saat agen penata gaya membuat coba pakaian nanti, agen tersebut akan menggunakan kembali URL GCS dari hasil ruang pas awal sehingga identitas pengguna tetap konsisten di semua pakaian.

Langkah 2: Buat perintah multimodal

   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
   }

toolInstructions (disematkan dari tool_instructions.md) sangat penting — perintah ini memberi tahu Gemini untuk mempertahankan identitas pengguna (wajah, tipe tubuh, warna kulit, rambut) sekaligus hanya menerapkan item pakaian. Tanpa teknik pembuatan perintah ini, model dapat mengubah penampilan orang tersebut.

Langkah 3: Panggil Gemini untuk pembuatan gambar

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

Keempat agen dan alat pembuatan gambar berbagi jalur autentikasi tunggal: Backend: genai.BackendVertexAI dengan ID project, yang diautentikasi melalui Kredensial Default Aplikasi. Model orkestrasi (gemini-3.1-pro-preview, gemini-3-flash-preview) dan model gambar (gemini-2.5-flash-image) semuanya berada di belakang endpoint Vertex AI yang sama, dan ADC yang sama juga mengizinkan akses Cloud Storage — satu kredensial, setiap panggilan.

Langkah 4: Simpan hasil

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

Penyimpanan ganda (artefak + GCS) adalah kunci untuk pengalihan agen antara ruang pas dan penata gaya. Artefak memberikan akses langsung dalam sesi saat ini, sementara URI GCS memungkinkan penata gaya (yang berjalan dalam sesi yang berbeda) mereferensikan gambar yang sama nanti.

Callback SaveIncomingBlobs

Sebelum agen mulai melakukan penalaran, BeforeAgentCallback ini berjalan untuk menyimpan gambar yang diupload pengguna:

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
}

Dengan menampilkan (nil, nil), callback memberi sinyal "Saya sudah selesai melakukan pra-pemrosesan — sekarang jalankan agen seperti biasa." Jika menampilkan konten non-null, agen akan sepenuhnya dihentikan.

6.2 Agen Penata Gaya

File:

adk_backend/stylist/agent.go

Agen penata gaya adalah yang paling canggih dalam sistem. Gemini mengumpulkan rekomendasi pakaian yang dipersonalisasi dan mendukung penyempurnaan iteratif melalui percakapan.

Tiga Panggilan Balik — Ingatan Stylist

Penata gaya menggunakan tiga callback untuk mempertahankan konteks di seluruh percakapan multi-turn:

Callback 1:

InjectPreviousProducts (BeforeModel)

Masalahnya: Jika pengguna mengatakan "tunjukkan opsi lain", LLM mungkin menyarankan produk yang sama lagi karena secara inheren tidak melacak apa yang sudah direkomendasikannya.

Solusi: Setelah setiap respons, ID produk disimpan ke status sesi. Sebelum panggilan LLM berikutnya, callback ini akan membacanya dan menyisipkan petunjuk:

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)

Masalah: Saat pengguna memberikan masukan ("buat lebih santai"), pesan lanjutan tidak menyertakan foto pengguna lagi. Namun, alat pemasangan memerlukannya.

Solusinya: Pada permintaan pertama, callback ini mengekstrak referensi gambar pengguna dan menyimpannya ke status. Pada permintaan berikutnya, kode tersebut akan disisipkan kembali:

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)

Setelah LLM merespons dengan saran pakaian, callback ini akan mengurai JSON untuk mengekstrak ID produk dan menyimpannya agar dapat digunakan oleh callback InjectPreviousProducts pada waktu berikutnya:

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
}

Ketiga callback ini bersama-sama membuat feedback loop:

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 Agen Root

File:

adk_backend/rootagent/agent.go

Agen paling sederhana — hanya 31 baris:

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

Model ini menggunakan gemini-3-flash-preview (model tercepat) karena keputusan perutean sederhana — LLM hanya perlu membaca maksud pengguna dan memilih sub-agen yang tepat. Tidak memerlukan alat; SubAgents menangani delegasi secara otomatis.

7. 📱 Arsitektur Frontend Flutter

Frontend Flutter adalah aplikasi belanja retail yang berfungsi penuh. Fitur AI tersedia di flutter_frontend/lib/workshop_tasks/, terpisah dari pengalaman belanja bawaan di core_app/.

Pola MVVM

Aplikasi mengikuti arsitektur Model-View-ViewModel dengan paket 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                                                 
└──────────────────┘    └────────────────────┘    └──────────────────┘

Setiap lapisan memiliki peran yang jelas:

  • Model: Class data seperti Product, Outfit, StyleRequest, dan enum seperti TryOnState
  • ViewModel (ChangeNotifier): Menyimpan status saat ini dan menyiarkan perubahan ke UI melalui notifyListeners()
  • View (Widget): Berlangganan ke ViewModel dengan context.watch() dan membangun ulang saat status berubah
  • Layanan: Melakukan panggilan HTTP ke backend ADK dan menampilkan data yang diketik

Lapisan Layanan

Layanan ditentukan sebagai antarmuka abstrak, dengan implementasi khusus 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 { ... }

Pemisahan ini berarti Anda dapat menukar backend ADK dengan Firebase AI, layanan tiruan, atau implementasi lainnya tanpa mengubah aplikasi lainnya.

Pola API 3 Langkah

AdkFittingRoomService dan AdkStylingService mengikuti pola yang sama untuk berkomunikasi dengan backend ADK:

Langkah 1: Buat sesi

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
}

Langkah 2: Jalankan agen

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

Langkah 3: Ambil artefak

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
}

Perbedaan desain yang penting: layanan ruang pas membuat sesi baru untuk setiap permintaan (_createSession() dipanggil setiap kali), sedangkan layanan penataan ulang menggunakan kembali sesi yang sama (_sessionId ??= await _createSession()) untuk memungkinkan percakapan multi-turn.

Pengelolaan Status: TryItOnProvider

File:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

TryItOnProvider mengelola seluruh alur coba. Menggunakan enum TryOnState sebagai mesin status:

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


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

Transisi status pribadi memastikan konsistensi — Anda tidak pernah mengupdate status tanpa juga menghapus data yang sudah tidak berlaku dan memberi tahu UI:

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

Metode pembuatan utama menyatukan semuanya:

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

UI: Layar sebagai Router Status

File:

workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart

Layar coba menggunakan pencocokan pola Dart 3 dengan AnimatedSwitcher untuk merutekan antar-sub-layar berdasarkan status penyedia:

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() berlangganan ke penyedia. Setiap kali notifyListeners() dipanggil, widget ini akan dibangun ulang, dan AnimatedSwitcher akan bertransisi antar-layar dengan lancar. Tidak ada Navigator.push — konten layar berubah di tempat berdasarkan enum status.

Pengalihan Agentic: Ruang Pas → Penata Gaya

Pola UX yang paling menarik adalah cara aplikasi meneruskan konteks dari agen ruang pas ke agen penata gaya.

Di 5_fitting_room.dart, setelah gambar coba-coba dibuat, tombol "Style Me" akan membuka formulir. Saat pengguna mengirimkan:

// 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 menggabungkan semua yang dibutuhkan penata gaya:

  • Lokasi dan acara — konteks teks untuk gaya
  • URL gambar pengguna GCS — sehingga penata gaya dapat menggunakan kembali representasi pengguna yang sama persis
  • Produk yang dipilih — sehingga penata gaya menyertakannya dalam setiap pakaian

Inilah yang disebut pengalihan agentik — mentransfer konteks multimodal secara lancar dari satu agen AI ke agen AI lainnya, dengan pengguna hanya melihat formulir sederhana.

Alur Gaya: StylingProvider

File:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

StylingProvider lebih sederhana daripada TryItOnProvider karena mendelegasikan sebagian besar kompleksitas ke backend:

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

Metode refineWithFeedback mengirim pesan teks biasa ke sesi yang sama — callback InjectPreviousProducts dan ExtractAndInjectUserImage backend menangani semua pengelolaan konteks secara otomatis.

8. 🚀 Menjalankan Aplikasi Secara Lokal

Untuk pengalaman Cloud Shell yang lancar, backend Go menyajikan aplikasi web Flutter yang dikompilasi dari port yang sama (8080). Satu proses, satu URL pratinjau, tidak ada masalah lintas origin, tidak perlu mengedit file konfigurasi.

Sebelum Anda memulai — periksa kewarasan ADC

Backend memerlukan Kredensial Default Aplikasi untuk memanggil Vertex AI. Jika Anda telah menyelesaikan langkah 7 penyiapan project di sesi Cloud Shell ini dan akun Google ini, Anda sudah siap. Jika Anda kembali setelah istirahat, beralih akun, atau tidak yakin, luangkan waktu 5 detik untuk memverifikasi:

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

Jika mencetak ~20 karakter token, Anda sudah siap. Jika terjadi error, jalankan kembali langkah 7 penyiapan project:

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

Anda akan menggunakan dua terminal Cloud Shell:

  • Terminal A — menjalankan backend secara terus-menerus (./run.sh). Biarkan terbuka.
  • Terminal B — menjalankan build web Flutter satu kali (flutter build web). Keluar setelah selesai.

Urutannya tidak menjadi masalah — Anda dapat memulai salah satunya terlebih dahulu. Namun, untuk pengalaman pertama yang paling bersih, bangun Flutter terlebih dahulu sehingga backend memiliki UI yang dapat ditayangkan sejak dimulai.

1. Terminal B — Buat Paket Web Flutter (sekali jalan)

Buka tab Cloud Shell baru (+ di bagian atas panel terminal), lalu:

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

Tindakan ini akan menghasilkan flutter_frontend/build/web/ — direktori file statis (HTML, JS, aset) — dan keluar setelah selesai. Backend akan menayangkannya segera setelah melihat direktori ada.

2. Terminal A — Mulai Backend (berjalan lama)

Di terminal Cloud Shell asli Anda:

cd ~/fashion_app_demo/adk_backend
./run.sh

Anda akan melihat yang seperti:

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

Biarkan terminal ini tetap berjalan — backend akan tetap aktif selama run.sh aktif. Untuk menghentikannya, tekan Ctrl+C.

Server mengekspos semuanya di port 8080:

  • / — Aplikasi web Flutter (UI belanja)
  • /api/ — Endpoint REST ADK (dipanggil oleh aplikasi Flutter)
  • UI Dev ADK — juga di / jika tidak ada build Flutter; berguna untuk men-debug agen secara langsung

3. Membuka Pratinjau Web

  1. Di Cloud Shell, klik ikon Web Preview (kanan atas) → Preview on port 8080
  2. Aplikasi belanja Flutter dimuat di tab baru
  3. Jelajahi katalog produk dan pilih item
  4. Ketuk ikon orang (👤) untuk memulai alur Coba
  5. Upload foto dan saksikan AI membuat gambar coba
  6. Ketuk "Gaya Saya" untuk mendapatkan rekomendasi pakaian
  7. Ketik masukan lanjutan seperti "buat lebih santai" — penyempurnaan dalam sesi yang sama

9. ☁️ Deploy ke Cloud Run

Menggabungkan Build Flutter ke Backend

Container Cloud Run mengirimkan API dan UI dari satu image. Salin build web Flutter ke adk_backend/flutter_web/ — itulah jalur pertama yang diperiksa server Go saat memilih UI yang akan ditayangkan:

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

(Jika Anda telah melakukan iterasi secara lokal, Anda mungkin sudah memiliki build/web dari langkah Run-Locally. Menjalankan ulang flutter build web masih tidak masalah.)

Men-deploy Backend (Menyediakan API + UI)

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

Setelah deployment selesai, Anda akan mendapatkan Service URL seperti https://fashion-app-backend-xyz-uc.a.run.app. Buka di browser — aplikasi belanja Flutter dimuat dari /, dan panggilan API-nya ditujukan ke /api/ di host yang sama. Tidak perlu mengedit konfigurasi frontend, tidak ada kunci API yang diteruskan.

Memverifikasi Deployment

Buka URL Cloud Run di browser Anda dan jalankan seluruh alurnya:

  1. Telusuri → Pilih produk
  2. Coba → Upload foto Anda → Lihat gambar buatan AI
  3. Style Me → Isi lokasi/acara → Lihat pakaian pilihan
  4. Masukan → Ketik "buat lebih kasual" → Lihat pakaian yang diperbarui
  5. Tambahkan ke Tas → Selesaikan alur belanja

10. 🎉 Kesimpulan

Yang Anda Bangun

Anda telah menjelajahi pengalaman retail lengkap yang didukung AI dengan:

  • Backend multi-agen dengan 4 agen khusus yang bekerja sama
  • Ruang pas virtual yang membuat gambar coba pakaian yang dipersonalisasi
  • Penata gaya AI yang memilih pakaian dan menyempurnakannya melalui percakapan
  • Aplikasi Flutter lintas platform yang terhubung ke backend agen
  • Deployment Cloud Run untuk hosting serverless yang skalabel

Konsep Utama

Konsep

Tempat Anda Melihatnya

Orkestrasi multi-agen ADK

Agen root merutekan ke agen ruang pas, katalog, dan penata gaya

Pembuatan gambar multimodal Gemini

fitting_tool menggabungkan foto pengguna dengan gambar produk

Status sesi untuk AI percakapan

Sesi penggunaan ulang stylist untuk masukan berulang

Penyimpanan artefak untuk data biner

Memisahkan penyimpanan gambar dari respons teks

Callback untuk logika middleware

SaveIncomingBlobs, InjectPreviousProducts, SaveSelectedProducts

MVVM + Provider di Flutter

TryItOnProvider dan StylingProvider dengan ChangeNotifier

Pengalihan agentic

StyleRequest meneruskan konteks multimodal antar-agen

Langkah Berikutnya

  • 🎨 Menyesuaikan perintah agen — edit instructions.md untuk mengubah kepribadian stylist
  • 🛍️ Menambahkan lebih banyak produk — perbarui catalog.yaml dengan item baru
  • 📱 Dibuat untuk perangkat seluler — jalankan flutter build ios atau flutter build apk
  • 🔄 Menambahkan sesi persisten — ganti InMemoryService dengan penerapan yang didukung database
  • 🔒 Menambahkan autentikasi — mengamankan endpoint Cloud Run dengan IAM

Resource