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.
- Ruang Pas Virtual — Pengguna mengupload foto dirinya, memilih item pakaian, dan melihat gambar dirinya yang dihasilkan AI saat mengenakan item tersebut.
- 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 Lihat → Terminal
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
2. Menautkan Akun Penagihan
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 |
| Vertex AI — panggilan |
| Cloud Storage — menyimpan gambar katalog produk dan hasil coba yang dihasilkan |
| Cloud Run — menghosting backend sebagai container serverless |
| Cloud Build — membangun image Docker dari sumber |
| 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 |
| 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 |
| Pakar produk. Memuat katalog produk dari file YAML dan menjawab kueri produk. Juga ringan — hanya mencari data. |
Agen Ruang Pas |
| 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) |
| 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.NewMultiLoadermendaftarkan keempat agen sehingga REST API dapat merutekan ke salah satu agen berdasarkan nama.adkrest.NewServermenyediakan 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.NewServicemenyimpan 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:
- Menerima "Pilihkan gaya untuk liburan di pantai"
- Panggil alat
catalog_agentuntuk mendapatkan daftar produk - Pilih 3 kombinasi pakaian
- Panggil
fitting_tooluntuk setiap pakaian guna membuat gambar - 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:
listProducts— Menampilkan katalog produk lengkap dari file YAMLgetProductImage— 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 callbackSaveIncomingBlobs - 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 sepertiTryOnState - ViewModel (
ChangeNotifier): Menyimpan status saat ini dan menyiarkan perubahan ke UI melaluinotifyListeners() - View (Widget): Berlangganan ke ViewModel dengan
context.watchdan 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
- Di Cloud Shell, klik ikon Web Preview (kanan atas) → Preview on port 8080
- Aplikasi belanja Flutter dimuat di tab baru
- Jelajahi katalog produk dan pilih item
- Ketuk ikon orang (👤) untuk memulai alur Coba
- Upload foto dan saksikan AI membuat gambar coba
- Ketuk "Gaya Saya" untuk mendapatkan rekomendasi pakaian
- 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:
- Telusuri → Pilih produk
- Coba → Upload foto Anda → Lihat gambar buatan AI
- Style Me → Isi lokasi/acara → Lihat pakaian pilihan
- Masukan → Ketik "buat lebih kasual" → Lihat pakaian yang diperbarui
- 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 |
|
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 |
|
MVVM + Provider di Flutter |
|
Pengalihan agentic |
|
Langkah Berikutnya
- 🎨 Menyesuaikan perintah agen — edit
instructions.mduntuk mengubah kepribadian stylist - 🛍️ Menambahkan lebih banyak produk — perbarui
catalog.yamldengan item baru - 📱 Dibuat untuk perangkat seluler — jalankan
flutter build iosatauflutter build apk - 🔄 Menambahkan sesi persisten — ganti
InMemoryServicedengan penerapan yang didukung database - 🔒 Menambahkan autentikasi — mengamankan endpoint Cloud Run dengan IAM
Resource
- Dokumentasi ADK — Dokumentasi resmi Agent Development Kit
- Kode Sumber ADK Go — Repositori GitHub
- Referensi Paket Go ADK — Referensi API
- Dokumentasi Gemini API — Kemampuan dan panduan model
- Paket Penyedia Flutter — Dokumen pengelolaan status
- Dokumentasi Cloud Run — Panduan deployment dan penskalaan