👗 CrĂ©er un essayage virtuel et un styliste IA avec Flutter, ADK Go et Gemini

1. Introduction

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez vous mettre dans la peau d'un développeur qui crée Fashion App, une application d'achat Flutter pour une marque de vente au détail fictive. Votre mission : ajouter deux fonctionnalités optimisées par l'IA qui transforment l'expérience d'achat en ligne.

  1. Cabine d'essayage virtuelle : un utilisateur importe une photo de lui, sĂ©lectionne un vĂȘtement et voit une image gĂ©nĂ©rĂ©e par IA de lui portant ce vĂȘtement.
  2. Styliste IA : en fonction de l'emplacement, de l'occasion et des préférences de style de l'utilisateur, un agent IA propose des tenues complÚtes. L'utilisateur peut les affiner par le biais d'une conversation.

L'idĂ©e est simple : lorsque les clients essayent des vĂȘtements dans une cabine, ils sont beaucoup plus susceptibles de les acheter. Mais en ligne ? Vous ne faites que deviner. Ce projet comble cette lacune grĂące Ă  l'IA.

Architecture en un coup d'Ɠil

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

Technologie principale

Composant

Technologie

Objectif

Framework de l'agent

ADK (Agent Development Kit) pour Go

Orchestration multi-agents, sessions, artefacts

Raisonnement de l'agent (Pro)

Gemini 3.1 Pro (preview)

Alimente les agents de cabine d'essayage et de styliste

Raisonnement de l'agent (Flash)

Preview Gemini 3 Flash

Alimente les agents racine et de catalogue (routage/recherche légers)

Génération d'images

Gemini 2.5 Flash Image

Générer des images d'essayages et de tenues

Frontend

Flutter (Dart)

Application multiplate-forme (Web, iOS, Android)

Stockage

Google Cloud Storage

Stocke les images de produits et les artefacts générés

Hébergement

Cloud Run

Déploiement de conteneurs sans serveur

2. 📩 PrĂ©requis et configuration de Cloud Shell

1. Ouvrir l'éditeur Cloud Shell

👉 Ouvrez l'Ă©diteur Cloud Shell dans votre navigateur.

Si le terminal n'apparaßt pas en bas de l'écran :

  • Cliquez sur Afficher → Terminal.

2. Configurer le SDK Flutter

Flutter est prĂ©installĂ© dans Cloud Shell Ă  l'emplacement /google/flutter. Étant donnĂ© que ce rĂ©pertoire appartient Ă  un autre utilisateur du systĂšme, vous rencontrerez une erreur fatal: detected dubious ownership la premiĂšre fois que vous exĂ©cuterez flutter. Ajoutez-le une fois Ă  la liste des rĂ©pertoires sĂ©curisĂ©s de Git :

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

Vérifiez que Flutter est installé et fonctionne sur votre PATH :

flutter --version

La premiĂšre exĂ©cution tĂ©lĂ©charge le SDK Dart et crĂ©e l'outil Flutter. Patientez une minute. Vous devriez voir quelque chose semblable Ă  Flutter 3.x ‱ channel stable.

3. Cloner le dépÎt

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

4. Explorer la structure du projet

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. ☁ Configurer un projet Google Cloud

1. Créer un projet

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

Répertoriez vos comptes de facturation :

gcloud billing accounts list

Regardez le

OPEN

colonne. Il doit indiquer True. Si le message False s'affiche (ce qui est courant avec un essai sans frais expiré), le compte est clÎturé et ne paiera rien. Passez directement au bloc de dépannage ci-dessous avant de continuer.

Copiez le ACCOUNT_ID d'un compte OPEN: True (qui ressemble à 0X0X0X-0X0X0X-0X0X0X) et associez-le à votre projet :

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

Vérifiez le lien :

gcloud billing projects describe fashion-app-demo

Vous devriez voir billingEnabled: true. Si vous voyez billingEnabled: false mĂȘme aprĂšs l'association, cela signifie que le compte est clĂŽturĂ© (OPEN: False). Consultez la section de dĂ©pannage ci-dessous.

3. Activer les API requises

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

API

Objectif

aiplatform.googleapis.com

Vertex AI : les appels fitting_tool à la génération d'images de Gemini via Vertex AI

storage.googleapis.com

Cloud Storage : stocke les images du catalogue de produits et les résultats d'essayage générés

run.googleapis.com

Cloud Run : héberge le backend en tant que conteneur sans serveur.

cloudbuild.googleapis.com

Cloud Build : crée des images Docker à partir de la source.

artifactregistry.googleapis.com

Artifact Registry : stocke les images Docker créées

4. Créer un 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. Importer des images de catalogue de produits

L'outil getProductImage du backend lit les données à partir de gs://$GCS_BUCKET/catalog-assets/images/. Importez les images du catalogue dans ce chemin d'accÚs exact :

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

Vérifiez l'importation (une liste de fichiers .png devrait s'afficher) :

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

6. Configurer le fichier .env

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

7. S'authentifier avec les identifiants par défaut de l'application

Vous devez exĂ©cuter cette commande avant de dĂ©marrer le backend en local. Le backend Go utilise ADC pour authentifier chaque appel Ă  Vertex AI (Gemini) et Cloud Storage. Sans ADC, le backend dĂ©marrera, mais chaque requĂȘte d'essayage Ă©chouera avec un code d'erreur 401 CREDENTIALS_MISSING.

Un seul identifiant couvre les deux services. Exécutez les deux commandes suivantes dans l'ordre :

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

Vérifiez que l'ADC est opérationnel :

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

Vous devriez voir environ 20 caractÚres d'un jeton suivis de .... Si une erreur se produit, cela signifie que la connexion n'a pas fonctionné. Répétez l'étape 1.

4. đŸ—ïžÂ PrĂ©sentation de l'architecture

Maintenant que l'environnement est prĂȘt, comprenons comment fonctionne le systĂšme avant d'examiner le code.

Le systĂšme Ă  quatre agents

Le backend est conçu comme un systÚme multi-agent à l'aide d'ADK (Agent Development Kit) pour Go. Quatre agents travaillent ensemble, chacun ayant une responsabilité spécifique :

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

ModĂšle

RĂŽle

Agent racine

gemini-3-flash-preview

Un agent de la circulation. Lit le message de l'utilisateur et le transmet à l'agent spécialiste approprié. Utilise un modÚle rapide et léger, car il n'a besoin que de prendre des décisions de routage.

Agent de catalogue

gemini-3-flash-preview

Expert Produit Charge le catalogue de produits à partir d'un fichier YAML et répond aux questions sur les produits. Il est également léger, car il ne fait que rechercher des données.

Agent pour les cabines d'essayage

gemini-3.1-pro-preview

Spécialiste de l'essai virtuel. Prend une photo de l'utilisateur et une image du produit, puis génÚre une image composite de la personne portant l'article. Utilise un modÚle plus performant, car il doit raisonner sur des images.

Agent Stylist

gemini-3.1-pro-preview

Conseiller en mode. En fonction du lieu, de l'occasion et des préférences, il sélectionne trois combinaisons de tenues dans le catalogue. peut générer des images d'essayage pour chaque tenue. Utilise également le modÚle performant pour le raisonnement créatif.

Point d'entrée : main.go

Tout commence dans main.go, qui relie les agents et démarre le serveur 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()
}

Voici quelques points importants à retenir :

  • Les agents sont créés de bas en haut : l'agent de catalogue est créé en premier, car les agents de cabine d'essayage et de styliste en dĂ©pendent (ils lui dĂ©lĂšguent les recherches de produits).
  • agent.NewMultiLoader enregistre les quatre agents afin que l'API REST puisse les acheminer vers l'un d'eux par nom.
  • adkrest.NewServer fournit automatiquement l'API REST. Vous n'avez pas besoin d'Ă©crire vous-mĂȘme les gestionnaires de points de terminaison. L'ADK vous offre une gestion des sessions, un stockage des artefacts et une exĂ©cution des agents prĂȘts Ă  l'emploi.
  • session.InMemoryService() stocke les sessions en mĂ©moire. Cela signifie que les sessions sont perdues si le serveur redĂ©marre, ce qui convient pour une dĂ©monstration. En production, vous utiliserez un magasin persistant.
  • gcsartifact.NewService stocke les artefacts (images gĂ©nĂ©rĂ©es) dans Google Cloud Storage. Ils sont donc conservĂ©s d'une requĂȘte Ă  l'autre et peuvent ĂȘtre partagĂ©s via des URI GCS.

5. đŸ€–Â PrĂ©sentation dĂ©taillĂ©e de l'ADK (Agent Development Kit)

Qu'est-ce que ADK ?

L'Agent Development Kit (ADK) est un framework Open Source de Google permettant de créer des agents IA en Go (et en Python/Java). Il s'agit de la couche entre votre application et l'API Gemini.

Vous pourriez appeler directement l'API Gemini. Mais une fois que votre application doit :

  • Rechercher des produits dans un catalogue
  • GĂ©nĂ©rer des images Ă  partir de photos d'utilisateurs
  • Se souvenir des tenues suggĂ©rĂ©es prĂ©cĂ©demment
  • Coordonner plusieurs agents IA

Vous avez besoin de structure. L'ADK fournit cette structure.

Boucle de l'agent

Chaque agent ADK suit une boucle :

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

Cette boucle peut se rĂ©pĂ©ter plusieurs fois dans une mĂȘme requĂȘte. Par exemple, l'agent de coiffure peut :

  1. Recevoir "Trouve-moi une tenue pour des vacances Ă  la plage"
  2. Appeler l'outil catalog_agent pour obtenir la liste des produits
  3. Sélectionnez trois combinaisons de tenues
  4. Appelez fitting_tool pour chaque tenue afin de générer des images.
  5. Renvoyer la réponse JSON structurée

Concepts fondamentaux (avec le code de ce dépÎt)

Agents LLM

Composant de base principal. Créé avec 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
})

Le champ Instruction correspond au persona de l'agent. Il indique au LLM qui il est et comment il doit se comporter. Dans ce dépÎt, les instructions sont écrites sous forme de fichiers Markdown et intégrées au temps de compilation à l'aide de la directive //go:embed de Go :

//go:embed instructions.md
var instructions string

Les requĂȘtes sont ainsi conservĂ©es sous forme de documents distincts et versionnables, plutĂŽt que de chaĂźnes intĂ©grĂ©es.

Outils

Les outils sont des fonctions Go que le LLM peut appeler. L'ADK gÚre la traduction entre le format d'appel d'outil du LLM et votre fonction Go typée :

// 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 génÚre automatiquement un schéma JSON à partir de vos structs Go et l'envoie au LLM. Lorsque le LLM décide d'appeler listProducts, ADK désérialise les arguments, appelle votre fonction et renvoie le résultat.

Le paramÚtre tool.Context permet aux outils d'accéder aux services d'exécution d'ADK, et plus particuliÚrement aux artefacts :

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


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

Délégation de sous-agent

Un agent peut utiliser un autre agent comme outil via 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
},

Lorsque l'agent de la cabine d'essayage a besoin d'informations produit, il peut appeler l'agent de catalogue comme s'il s'agissait d'un outil normal. Le LLM le voit dans la liste des outils et peut décider de l'appeler.

Sessions

Les sessions permettent de suivre l'historique des conversations. L'API REST de l'ADK les gÚre automatiquement :

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

Une dĂ©cision de conception essentielle dans cette application : la cabine d'essayage crĂ©e une session par requĂȘte (chaque essayage est indĂ©pendant), tandis que le styliste rĂ©utilise la mĂȘme session (il se souvient donc des suggestions prĂ©cĂ©dentes et peut les affiner en fonction des commentaires).

État

L'état est un magasin clé-valeur associé à une session. Les agents lisent et écrivent l'état pour se coordonner :

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


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

L'agent styliste utilise l'état pour se souvenir des produits qu'il a déjà suggérés et en choisir d'autres la prochaine fois.

Artefacts

Les artefacts sont des objets binaires nommés (généralement des images) stockés par session. Contrairement aux réponses textuelles, elles sont stockées séparément et récupérées par leur nom :

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

Cela permet de garder les réponses légÚres : l'agent ne renvoie que le nom de l'artefact, et le frontend récupÚre les données binaires de l'image séparément.

Rappels

Les rappels sont des hooks qui s'exécutent à des moments précis de la boucle de l'agent. Ils peuvent inspecter, modifier ou court-circuiter l'exécution :

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

Si un rappel renvoie une réponse non nulle, le comportement par défaut est ignoré. Par exemple, un BeforeModelCallback qui renvoie une réponse mise en cache ignorerait complÚtement l'appel LLM réel.

Application du schéma JSON

Les agents de cabine d'essayage et de styliste forcent le LLM à répondre au format JSON structuré :

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

Cela garantit que le frontend Flutter reçoit toujours des données analysables, et non du texte libre.

Agent de catalogue : exemple le plus simple

L'agent de catalogue (catalog/agent.go) est l'agent le plus simple du systÚme. Il constitue un bon point de départ pour comprendre les schémas ADK.

Il comporte deux outils :

  1. listProducts : renvoie le catalogue de produits complet à partir d'un fichier YAML.
  2. getProductImage : charge une image de produit à partir de GCS (ou d'une solution de secours locale) et l'enregistre en tant qu'artefact.

L'outil getProductImage affiche un modÚle important : le chargement multisource avec mise en cache des artefacts :

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

L'outil essaie d'abord les artefacts, puis GCS, puis les fichiers locaux. Une fois l'image chargée, elle est mise en cache en tant qu'artefact. Les appels suivants sont donc instantanés.

6. đŸ§Ș Le pipeline d'IA : les agents en action

Passons maintenant en revue les deux agents les plus sophistiqués, ceux qui génÚrent des images et sélectionnent des tenues.

6.1 L'agent de la cabine d'essayage

Fichier :

adk_backend/fittingroom/agent.go

L'agent de cabine d'essayage est le moteur de la fonctionnalité "Essayer virtuellement". Lorsqu'un utilisateur importe sa photo et choisit un produit, cet agent génÚre une image composite de la personne portant cet article.

fitting_tool : étape par étape

La logique principale se trouve dans la fonction doFitting. Voici ce qui se passe lorsque l'agent l'appelle :

Étape 1 : RĂ©solvez le problĂšme liĂ© Ă  l'image de l'utilisateur

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
   }

L'image de l'utilisateur peut provenir de deux sources :

  • Nom de l'artefact (comme upload_abc123_1) : il s'agit de l'importation initiale, enregistrĂ©e par le rappel SaveIncomingBlobs.
  • Un gs:// URI : il s'agit d'un rĂ©sultat d'ajustement gĂ©nĂ©rĂ© prĂ©cĂ©demment et stockĂ© dans GCS pour une rĂ©utilisation entre les sessions.

Cette conception à double chemin est intentionnelle : lorsque l'agent styliste génÚre des essayages de tenues, il réutilise l'URL GCS du résultat initial de l'essayage afin que l'identité de l'utilisateur reste cohérente pour toutes les tenues.

Étape 2 : CrĂ©ez le prompt 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
   }

Le toolInstructions (intĂ©grĂ© Ă  partir de tool_instructions.md) est essentiel : il indique Ă  Gemini de prĂ©server l'identitĂ© de l'utilisateur (visage, morphologie, teint, cheveux) tout en appliquant uniquement le vĂȘtement. Sans cette ingĂ©nierie des prompts, le modĂšle pourrait modifier l'apparence de la personne.

Étape 3 : Appeler Gemini pour gĂ©nĂ©rer des images

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

Les quatre agents et l'outil de gĂ©nĂ©ration d'images partagent un mĂȘme chemin d'authentification : Backend: genai.BackendVertexAI avec l'ID du projet, authentifiĂ© via les identifiants par dĂ©faut de l'application. Les modĂšles d'orchestration (gemini-3.1-pro-preview, gemini-3-flash-preview) et le modĂšle d'image (gemini-2.5-flash-image) se trouvent tous derriĂšre le mĂȘme point de terminaison Vertex AI, et les mĂȘmes identifiants ADC autorisent Ă©galement l'accĂšs Ă  Cloud Storage.

Étape 4 : Enregistrez le rĂ©sultat

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

La double sauvegarde (artefact + GCS) est essentielle pour le transfert de l'agent entre la cabine d'essayage et le styliste. L'artefact permet d'accĂ©der immĂ©diatement Ă  l'image dans la session en cours, tandis que l'URI GCS permet au styliste (qui s'exĂ©cute dans une autre session) de faire rĂ©fĂ©rence Ă  la mĂȘme image ultĂ©rieurement.

Rappel SaveIncomingBlobs

Avant mĂȘme que l'agent ne commence Ă  raisonner, ce BeforeAgentCallback s'exĂ©cute pour enregistrer les images que l'utilisateur a importĂ©es :

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
}

En renvoyant (nil, nil), le rappel indique "J'ai terminé le prétraitement. Exécute maintenant l'agent normalement." Si elle renvoyait du contenu non nul, elle court-circuiterait complÚtement l'agent.

6.2 L'Agent Stylist

Fichier :

adk_backend/stylist/agent.go

L'agent styliste est le plus sophistiqué du systÚme. Il propose des recommandations de tenues personnalisées et permet d'affiner les résultats de maniÚre itérative grùce à la conversation.

Trois rappels : la mémoire du styliste

Le styliste utilise trois rappels pour conserver le contexte dans les conversations multitours :

Rappel 1 :

InjectPreviousProducts (BeforeModel)

ProblĂšme : si l'utilisateur demande "montre-moi d'autres options", le LLM peut suggĂ©rer Ă  nouveau les mĂȘmes produits, car il ne suit pas intrinsĂšquement ce qu'il a dĂ©jĂ  recommandĂ©.

Solution : aprÚs chaque réponse, les ID de produit sont enregistrés dans l'état de la session. Avant le prochain appel LLM, ce rappel les lit et insÚre un indice :

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
}

Rappel 2 :

ExtractAndInjectUserImage (BeforeModel)

ProblÚme : lorsque l'utilisateur fournit des commentaires ("Rends-le plus décontracté"), le message de suivi n'inclut plus la photo de l'utilisateur. Mais l'outil d'ajustement en a besoin.

Solution : Lors de la premiĂšre requĂȘte, ce rappel extrait la rĂ©fĂ©rence de l'image de l'utilisateur et l'enregistre dans l'Ă©tat. Lors des requĂȘtes suivantes, il le rĂ©injecte :

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
}

Rappel 3 :

SaveSelectedProducts (AfterModel)

Une fois que le LLM a répondu avec des suggestions de tenues, ce rappel analyse le JSON pour extraire les ID de produit et les enregistre pour que le rappel InjectPreviousProducts les utilise la prochaine fois :

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
}

Ensemble, ces trois rappels créent une boucle de rétroaction :

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 L'agent racine

Fichier :

adk_backend/rootagent/agent.go

L'agent le plus simple (seulement 31 lignes) :

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

Il utilise gemini-3-flash-preview (le modÚle le plus rapide) parce que les décisions de routage sont simples : le LLM n'a qu'à lire l'intention de l'utilisateur et à choisir le bon sous-agent. Aucun outil n'est nécessaire : SubAgents gÚre la délégation automatiquement.

7. đŸ“±Â Architecture du frontend Flutter

Le frontend Flutter est une application d'achat entiÚrement fonctionnelle. Les fonctionnalités d'IA se trouvent dans flutter_frontend/lib/workshop_tasks/, séparément de l'expérience d'achat prédéfinie dans core_app/.

Le modĂšle MVVM

L'application suit l'architecture Model-View-ViewModel avec le package 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  │    │                     │    │                  │
└──────────────────┘    └────────────────────┘    └──────────────────┘

Chaque couche a un rÎle clair :

  • ModĂšle : classes de donnĂ©es telles que Product, Outfit, StyleRequest et Ă©numĂ©rations telles que TryOnState
  • ViewModel (ChangeNotifier) : contient l'Ă©tat actuel et diffuse les modifications apportĂ©es Ă  l'UI via notifyListeners()
  • View (Widget) : s'abonne au ViewModel avec context.watch() et se reconstruit lorsque l'Ă©tat change
  • Service : effectue des appels HTTP au backend de l'ADK et renvoie des donnĂ©es typĂ©es.

La couche de service

Les services sont définis comme des interfaces abstraites, avec des implémentations spécifiques à l'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 { ... }

Cette séparation signifie que vous pouvez remplacer le backend de l'ADK par Firebase AI, un service fictif ou toute autre implémentation sans modifier le reste de l'application.

Le modÚle d'API en trois étapes

AdkFittingRoomService et AdkStylingService suivent le mĂȘme modĂšle pour communiquer avec le backend ADK :

Étape 1 : CrĂ©ez une session

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
}

Étape 2 : ExĂ©cutez l'agent

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

Étape 3 : RĂ©cupĂ©rez l'artefact

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
}

Une diffĂ©rence de conception essentielle : le service d'essayage crĂ©e une session pour chaque requĂȘte (_createSession() est appelĂ© Ă  chaque fois), tandis que le service de stylisme rĂ©utilise la mĂȘme session (_sessionId ??= await _createSession()) pour permettre une conversation en plusieurs tours.

Gestion de l'état : TryItOnProvider

Fichier :

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

Le TryItOnProvider gÚre l'ensemble du parcours d'essayage. Il utilise une énumération TryOnState comme machine à états :

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


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

Les transitions d'état privées garantissent la cohérence : vous ne mettez jamais à jour l'état sans effacer également les données obsolÚtes et notifier l'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();
 }

La méthode de génération principale relie le tout :

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

L'UI : écrans en tant que routeur d'état

Fichier :

workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart

L'écran d'essayage utilise la correspondance de modÚles de Dart 3 avec AnimatedSwitcher pour effectuer le routage entre les sous-écrans en fonction de l'état du fournisseur :

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() s'abonne au fournisseur. Chaque fois que notifyListeners() est appelé, ce widget est reconstruit et AnimatedSwitcher effectue une transition fluide entre les écrans. Il n'y a pas de Navigator.push : le contenu de l'écran change sur place en fonction de l'énumération d'état.

Transfert agentique : cabine d'essayage → styliste

Le modÚle d'UX le plus intéressant est la façon dont l'application transmet le contexte de l'agent de cabine d'essayage à l'agent styliste.

Dans 5_fitting_room.dart, une fois l'image d'essayage générée, le bouton "Style Me" (Trouve mon style) ouvre un formulaire. Lorsque l'utilisateur envoie :

// 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 regroupe tout ce dont le styliste a besoin :

  • Lieu et occasion : contexte textuel pour la mise en forme
  • URL de l'image utilisateur GCS : le styliste peut ainsi rĂ©utiliser exactement la mĂȘme reprĂ©sentation de l'utilisateur.
  • Produit sĂ©lectionné : le styliste l'inclut dans chaque tenue.

Il s'agit d'un transfert agentique, qui consiste à transférer de maniÚre fluide le contexte multimodal d'un agent d'IA à un autre, l'utilisateur ne voyant qu'un simple formulaire.

Processus de stylisation : StylingProvider

Fichier :

workshop_tasks/step_2_style_me/providers/styling_provider.dart

StylingProvider est plus simple que TryItOnProvider, car il délÚgue la plupart de la complexité au 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;
 }
}

La mĂ©thode refineWithFeedback envoie un message en texte brut Ă  la mĂȘme session. Les rappels InjectPreviousProducts et ExtractAndInjectUserImage du backend gĂšrent automatiquement toute la gestion du contexte.

8. 🚀 ExĂ©cuter l'application en local

Pour une expĂ©rience Cloud Shell fluide, le backend Go sert l'application Web Flutter compilĂ©e Ă  partir du mĂȘme port (8080). Un processus, une URL de prĂ©visualisation, aucun problĂšme d'origine croisĂ©e, aucune modification des fichiers de configuration.

Avant de commencer : vérifiez l'intégrité de l'ADC

Le backend a besoin des identifiants par dĂ©faut de l'application pour appeler Vertex AI. Si vous avez terminĂ© l'Ă©tape 7 de la configuration du projet dans cette session Cloud Shell et avec ce compte Google, tout est en ordre. Si vous revenez aprĂšs une pause, si vous avez changĂ© de compte ou si vous n'ĂȘtes pas sĂ»r, prenez cinq secondes pour vĂ©rifier :

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

Si environ 20 caractÚres d'un jeton s'affichent, tout est en ordre. Si une erreur se produit, réexécutez l'étape 7 de la configuration du projet :

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

Vous allez utiliser deux terminaux Cloud Shell :

  • Terminal A : exĂ©cute le backend en continu (./run.sh). Laissez-le ouvert.
  • Terminal B : exĂ©cute la compilation Web Flutter une seule fois (flutter build web). Quitte une fois terminĂ©.

L'ordre n'a pas d'importance. Vous pouvez commencer par l'un ou l'autre. Toutefois, pour une expérience de premiÚre exécution la plus propre possible, commencez par compiler Flutter afin que le backend dispose d'une UI à partir de laquelle servir le contenu dÚs son démarrage.

1. Terminal B : compiler le bundle Flutter Web (ponctuel)

Ouvrez un nouvel onglet Cloud Shell (+ en haut du panneau du terminal), puis :

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

Cela génÚre flutter_frontend/build/web/, un répertoire de fichiers statiques (HTML, JS, éléments), et se ferme une fois terminé. Le backend les diffusera dÚs qu'il verra que le répertoire existe.

2. Terminal A : démarrer le backend (opération de longue durée)

Dans votre terminal Cloud Shell d'origine :

cd ~/fashion_app_demo/adk_backend
./run.sh

Vous devez obtenir un résultat semblable au suivant :

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

Laissez ce terminal en cours d'exĂ©cution : le backend reste actif tant que run.sh est actif. Pour l'arrĂȘter, appuyez sur Ctrl+C.

Le serveur expose tout sur le port 8080 :

  • / : application Web Flutter (interface utilisateur d'achat)
  • /api/ : points de terminaison REST ADK (appelĂ©s par l'application Flutter)
  • UI de dĂ©veloppement ADK : Ă©galement disponible sur / en l'absence de build Flutter. Utile pour le dĂ©bogage direct des agents.

3. Ouvrir l'aperçu sur le Web

  1. Dans Cloud Shell, cliquez sur l'icîne Aperçu sur le Web (en haut à droite) → Aperçu sur le port 8080.
  2. L'application d'achat Flutter se charge dans un nouvel onglet.
  3. Parcourez le catalogue de produits et sélectionnez un article.
  4. Appuyez sur l'icĂŽne reprĂ©sentant une personne (đŸ‘€) pour lancer le flux d'essayage.
  5. Importez une photo et regardez l'IA générer une image avec l'article porté.
  6. Appuyez sur "Style Me" pour obtenir des recommandations de tenues.
  7. Saisissez des commentaires de suivi tels que "rends-le plus dĂ©contractĂ©" : affinement au cours de la mĂȘme session

9. ☁ DĂ©ployer sur Cloud Run

Regrouper la compilation Flutter dans le backend

Le conteneur Cloud Run fournit à la fois l'API et l'UI à partir d'une seule image. Copiez la compilation Web Flutter dans adk_backend/flutter_web/. Il s'agit du premier chemin d'accÚs que le serveur Go vérifie lorsqu'il choisit l'UI à diffuser :

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

(Si vous avez effectuĂ© des itĂ©rations en local, vous avez peut-ĂȘtre dĂ©jĂ  build/web Ă  l'Ă©tape "ExĂ©cuter en local". Vous pouvez toujours rĂ©exĂ©cuter flutter build web.)

Déployer le backend (qui sert l'API et l'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

Une fois le dĂ©ploiement terminĂ©, une URL de service s'affiche, par exemple https://fashion-app-backend-xyz-uc.a.run.app. Ouvrez-la dans un navigateur : l'application d'achat Flutter se charge Ă  partir de /, et ses appels d'API sont envoyĂ©s Ă  /api/ sur le mĂȘme hĂŽte. Aucune modification de la configuration du frontend n'est nĂ©cessaire, aucune clĂ© API n'est transmise.

Vérifier le déploiement

Ouvrez l'URL Cloud Run dans votre navigateur et parcourez l'intégralité du flux :

  1. Parcourir → SĂ©lectionnez un produit
  2. Essayage → Importez votre photo → DĂ©couvrez l'image gĂ©nĂ©rĂ©e par l'IA
  3. Style-moi → Indiquez le lieu/l'occasion → DĂ©couvrez des tenues sĂ©lectionnĂ©es
  4. Commentaires → Saisissez "Rends-le plus dĂ©contractĂ©" → DĂ©couvrez les tenues modifiĂ©es
  5. Ajouter au panier → Finaliser le parcours d'achat

10. 🎉 Conclusion

Ce que vous avez créé

Vous avez exploré une expérience retail complÚte optimisée par l'IA avec :

  • ✅ Un backend multi-agent avec quatre agents spĂ©cialisĂ©s qui travaillent ensemble
  • ✅ Une cabine d'essayage virtuelle qui gĂ©nĂšre des images d'essayage personnalisĂ©es
  • ✅ Un styliste IA qui sĂ©lectionne des tenues et les affine grĂące Ă  la conversation
  • ✅ Une application Flutter multiplate-forme qui se connecte au backend de l'agent
  • ✅ DĂ©ploiement Cloud Run pour un hĂ©bergement Ă©volutif et sans serveur

Concepts clés

Concept

OĂč l'avez-vous vu ?

Orchestration multi-agents ADK

Routage de l'agent racine vers les agents de cabine d'essayage, de catalogue et de styliste

Génération d'images multimodales Gemini

fitting_tool combiner des photos d'utilisateurs avec des images de produits

État de la session pour l'IA conversationnelle

Styliste réutilisant des sessions pour obtenir des commentaires itératifs

Stockage des artefacts pour les données binaires

Séparer le stockage des images des réponses textuelles

Rappels pour la logique du middleware

SaveIncomingBlobs, InjectPreviousProducts, SaveSelectedProducts

MVVM + Provider dans Flutter

TryItOnProvider et StylingProvider avec ChangeNotifier

Transfert agentif

StyleRequest transmettre le contexte multimodal entre les agents

Étapes suivantes

  • 🎹 Personnaliser les requĂȘtes de l'agent : modifiez instructions.md pour changer la personnalitĂ© du styliste.
  • đŸ›ïžÂ Ajouter des produits : mettez Ă  jour catalog.yaml avec de nouveaux articles.
  • đŸ“±Â CrĂ©ez une vidĂ©o adaptĂ©e aux mobiles : exĂ©cutez flutter build ios ou flutter build apk.
  • 🔄 Ajouter des sessions persistantes : remplacez InMemoryService par une implĂ©mentation basĂ©e sur une base de donnĂ©es.
  • 🔒 Ajouter l'authentification : sĂ©curiser le point de terminaison Cloud Run avec IAM

Ressources