👗 Crea una cabina di prova virtuale e uno stilista AI con Flutter, ADK Go e Gemini

1. Introduzione

Cosa creerai

In questo codelab, vestirai i panni di uno sviluppatore che crea Fashion App, un'app di shopping Flutter per un brand di vendita al dettaglio fittizio. La tua missione: aggiungere due funzionalità basate sull'AI che trasformino l'esperienza di shopping online.

  1. Cabina di prova virtuale: un utente carica una propria foto, seleziona un capo di abbigliamento e vede un'immagine generata dall'AI che lo ritrae mentre lo indossa.
  2. AI Stylist: in base alla posizione, all'occasione e alle preferenze di stile dell'utente, un agente AI seleziona consigli per outfit completi e l'utente può perfezionarli tramite conversazione.

L'idea è semplice: quando le persone provano i vestiti in un camerino, è molto più probabile che li acquistino. Ma online? Stai solo tirando a indovinare. Questo progetto colma questa lacuna con l'AI.

Architettura in sintesi

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

Tecnologie di base

Componente

Tecnologia

Finalità

Framework dell'agente

ADK (Agent Development Kit) per Go

Orchestrazione multi-agente, sessioni, artefatti

Motivazione dell'agente (Pro)

Gemini 3.1 Pro (anteprima)

Alimenta gli agenti di camerino e stilista

Agent Reasoning (Flash)

Gemini 3 Flash (anteprima)

Alimenta gli agenti root e catalogo (routing/ricerca leggeri)

Generazione di immagini

Gemini 2.5 Flash Image

Genera immagini di prova e outfit

Frontend

Flutter (Dart)

App multipiattaforma (web, iOS, Android)

Spazio di archiviazione

Google Cloud Storage

Memorizza le immagini prodotto e gli artefatti generati

Hosting

Cloud Run

Deployment di container serverless

2. 📦 Prerequisiti e configurazione di Cloud Shell

1. Apri editor di Cloud Shell

👉 Apri Cloud Shell Editor nel browser.

Se il terminale non viene visualizzato nella parte inferiore dello schermo:

  • Fai clic su VisualizzaTerminale.

2. Configura l'SDK Flutter

Cloud Shell viene fornito con Flutter preinstallato in /google/flutter. Poiché la directory è di proprietà di un altro utente di sistema, la prima volta che esegui flutter si verifica un errore fatal: detected dubious ownership. Aggiungilo all'elenco safe-directory di Git una sola volta:

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

Verifica che Flutter sia presente nel tuo PATH e funzioni:

flutter --version

La prima esecuzione scarica l'SDK Dart e crea lo strumento Flutter. Attendi un minuto. Dovresti vedere qualcosa di simile: Flutter 3.x • channel stable.

3. Clona il repository

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

4. Esplora la struttura del progetto

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. ☁️ Configurazione del progetto Google Cloud

1. Crea un nuovo progetto

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

Elenca i tuoi account di fatturazione:

gcloud billing accounts list

Guarda il

OPEN

. Deve essere indicato True. Se viene visualizzato il messaggio False (comune con una prova senza costi scaduta), l'account è chiuso e non pagherà nulla. Prima di continuare, vai al blocco di risoluzione dei problemi riportato di seguito.

Copia l'ACCOUNT_ID di un account OPEN: True (ha l'aspetto di 0X0X0X-0X0X0X-0X0X0X) e collegalo al tuo progetto:

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

Verifica il link:

gcloud billing projects describe fashion-app-demo

Dovresti visualizzare billingEnabled: true. Se visualizzi billingEnabled: false anche dopo il collegamento, l'account è chiuso (OPEN: False). Consulta il blocco per la risoluzione dei problemi riportato di seguito.

3. Abilita le API richieste

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

API

Finalità

aiplatform.googleapis.com

Vertex AI: le chiamate fitting_tool alla generazione di immagini di Gemini tramite Vertex AI

storage.googleapis.com

Cloud Storage: memorizza le immagini del catalogo prodotti e i risultati della prova generati

run.googleapis.com

Cloud Run: ospita il backend come container serverless

cloudbuild.googleapis.com

Cloud Build: crea immagini Docker dall'origine

artifactregistry.googleapis.com

Artifact Registry: archivia le immagini Docker create

4. Crea 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. Caricare le immagini del catalogo dei prodotti

Lo strumento getProductImage del backend legge da gs://$GCS_BUCKET/catalog-assets/images/. Carica le immagini del catalogo in questo percorso esatto:

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

Verifica il caricamento (dovresti visualizzare un elenco di file .png):

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

6. Configurare il file .env

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

7. Eseguire l'autenticazione con le credenziali predefinite dell'applicazione

Devi eseguire questo comando prima di avviare il backend localmente. Il backend Go utilizza ADC per autenticare ogni chiamata a Vertex AI (Gemini) e Cloud Storage. Senza ADC, il backend si avvierà, ma ogni richiesta di prova virtuale non andrà a buon fine e verrà restituito l'errore 401 CREDENTIALS_MISSING.

Una credenziale copre entrambi i servizi. Esegui questi due comandi in ordine:

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

Verifica che ADC sia integro:

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

Dovresti vedere circa 20 caratteri di un token seguiti da .... Se si verifica un errore, l'accesso non è andato a buon fine. Esegui di nuovo il passaggio 1.

4. 🏗️ Panoramica dell'architettura

Ora che l'ambiente è pronto, vediamo come funziona il sistema prima di esaminare il codice.

Il sistema a quattro agenti

Il backend è creato come un sistema multi-agente utilizzando ADK (Agent Development Kit) per Go. Quattro agenti lavorano insieme, ognuno con una responsabilità specifica:

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

Agente

Modello

Ruolo

Root Agent

gemini-3-flash-preview

Vigile. Legge il messaggio dell'utente e lo delega all'agente specializzato giusto. Utilizza un modello veloce e leggero perché deve solo prendere decisioni di routing.

Catalog Agent

gemini-3-flash-preview

Esperto di prodotto. Carica il catalogo dei prodotti da un file YAML e risponde alle query sui prodotti. Inoltre, è leggero, in quanto si limita a cercare i dati.

Fitting Room Agent

gemini-3.1-pro-preview

Specialista della prova virtuale. Prende una foto dell'utente e un'immagine prodotto e genera un'immagine composita della persona che indossa l'articolo. Utilizza un modello più potente perché deve ragionare sulle immagini.

Stylist Agent

gemini-3.1-pro-preview

Consulente di moda. In base a posizione, occasione e preferenze, seleziona 3 combinazioni di outfit dal catalogo. Può generare immagini di prova per ogni outfit. Utilizza anche il modello avanzato per il ragionamento creativo.

Il punto di ingresso: main.go

Tutto inizia in main.go, che collega gli agenti e avvia il 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()
}

Alcuni aspetti importanti da notare:

  • Gli agenti vengono creati dal basso verso l'alto: l'agente catalogo viene creato per primo perché sia l'agente camerino che l'agente stilista dipendono da esso (gli delegano le ricerche dei prodotti).
  • agent.NewMultiLoader registra tutti e quattro gli agenti in modo che l'API REST possa indirizzare le richieste a uno qualsiasi di essi in base al nome.
  • adkrest.NewServer fornisce automaticamente l'API REST, quindi non devi scrivere autonomamente i gestori degli endpoint. ADK offre gestione delle sessioni, archiviazione degli artefatti ed esecuzione degli agenti preconfigurati.
  • session.InMemoryService() memorizza le sessioni in memoria. Ciò significa che le sessioni vengono perse se il server viene riavviato, il che va bene per una demo. In produzione, utilizzerai un archivio permanente.
  • gcsartifact.NewService archivia gli artefatti (immagini generate) in Google Cloud Storage, in modo che vengano mantenuti nelle varie richieste e possano essere condivisi tramite gli URI GCS.

5. 🤖 Approfondimento di ADK (Agent Development Kit)

Che cos'è ADK?

L'Agent Development Kit (ADK) è un framework open source di Google per la creazione di agenti AI in Go (e Python/Java). È il livello tra l'applicazione e l'API Gemini.

Potresti chiamare direttamente l'API Gemini. Ma una volta che la tua app deve:

  • Cercare prodotti da un catalogo
  • Generare immagini basate sulle foto degli utenti
  • Ricordare gli outfit suggeriti in precedenza
  • Coordinare più agenti AI

Hai bisogno di una struttura. L'ADK fornisce questa struttura.

The Agent Loop

Ogni agente ADK segue un ciclo:

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

Questo ciclo può ripetersi più volte all'interno di una singola richiesta. Ad esempio, l'agente stilista potrebbe:

  1. Ricevere "Style me for a beach vacation" (Crea un outfit per una vacanza al mare)
  2. Chiama lo strumento catalog_agent per ottenere l'elenco dei prodotti
  3. Seleziona 3 combinazioni di outfit
  4. Chiama fitting_tool per ogni outfit per generare immagini
  5. Restituisci la risposta JSON strutturata

Concetti principali (con il codice di questo repository)

Agenti LLM

Il componente di base principale. Creato con 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
})

Il campo Instruction è il ruolo dell'agente: indica all'LLM chi è e come deve comportarsi. In questo repository, le istruzioni sono scritte come file Markdown e incorporate in fase di tempo di compilazione utilizzando l'istruzione //go:embed di Go:

//go:embed instructions.md
var instructions string

In questo modo, i prompt vengono mantenuti come documenti separati e versionabili anziché come stringhe inline.

Strumenti

Gli strumenti sono funzioni Go che l'LLM può chiamare. ADK gestisce la traduzione tra il formato di chiamata degli strumenti del modello linguistico di grandi dimensioni e la funzione Go che hai digitato:

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

L'SDK genera automaticamente uno schema JSON dalle tue struct Go e lo invia al LLM. Quando l'LLM decide di chiamare listProducts, ADK deserializza gli argomenti, chiama la funzione e restituisce il risultato.

Il parametro tool.Context consente agli strumenti di accedere ai servizi di runtime dell'ADK, in particolare agli artefatti:

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


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

Delega del sub-agente

Un agente può utilizzare un altro agente come strumento tramite 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
},

Quando l'agente del camerino ha bisogno di informazioni sul prodotto, può chiamare l'agente del catalogo come se fosse uno strumento normale. Il modello LLM lo vede nell'elenco degli strumenti e può decidere di richiamarlo.

Sessioni

Le sessioni tengono traccia della cronologia delle conversazioni. L'API REST dell'ADK le gestisce automaticamente:

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

Una decisione di progettazione fondamentale in questa app: il camerino crea una nuova sessione per richiesta (ogni prova è indipendente), mentre lo stylist riutilizza la stessa sessione (quindi ricorda i suggerimenti precedenti e può perfezionarli in base al feedback).

Stato

Lo stato è un archivio coppia chiave-valore collegato a una sessione. Gli agenti leggono e scrivono lo stato per coordinare:

// 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'agente stilista utilizza lo stato per ricordare i prodotti che ha già suggerito, in modo da sceglierne di diversi la volta successiva.

Artefatti

Gli artefatti sono oggetti binari denominati (di solito immagini) archiviati per sessione. A differenza delle risposte di testo, vengono archiviate separatamente e recuperate per nome:

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

In questo modo le risposte rimangono leggere: l'agente restituisce solo il nome dell'artefatto e il frontend recupera separatamente i dati binari dell'immagine.

Callback

I callback sono hook che vengono eseguiti in punti specifici del ciclo dell'agente. Possono ispezionare, modificare o interrompere l'esecuzione:

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

Se un callback restituisce una risposta non nulla, il comportamento predefinito viene ignorato. Ad esempio, un BeforeModelCallback che restituisce una risposta memorizzata nella cache salterebbe completamente la chiamata LLM effettiva.

Applicazione dello schema JSON

Sia l'agente del camerino che quello dello stilista forzano l'LLM a rispondere in JSON strutturato:

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

In questo modo, il frontend Flutter riceve sempre dati analizzabili, non testo in formato libero.

L'agente di catalogo: l'esempio più semplice

L'agente catalogo (catalog/agent.go) è l'agente più semplice del sistema e un buon punto di partenza per comprendere i pattern ADK.

Dispone di due strumenti:

  1. listProducts: restituisce l'intero catalogo prodotti da un file YAML
  2. getProductImage: carica un'immagine prodotto da GCS (o dal fallback locale) e la salva come artefatto

Lo strumento getProductImage mostra un pattern importante: il caricamento da più origini con memorizzazione nella cache degli artefatti:

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

Lo strumento prova prima gli artefatti, poi GCS e infine i file locali. Una volta caricata, l'immagine viene memorizzata nella cache come artefatto, quindi le chiamate successive sono immediate.

6. 🧪 La pipeline AI: gli agenti in azione

Ora esaminiamo i due agenti più sofisticati, quelli che generano effettivamente immagini e selezionano outfit.

6.1 L'agente del camerino

File:

adk_backend/fittingroom/agent.go

L'agente del camerino è il motore alla base della funzionalità "Prova virtuale". Quando un utente carica la propria foto e sceglie un prodotto, questo agente genera un'immagine composita della persona che indossa l'articolo.

fitting_tool passo dopo passo

La logica di base si trova nella funzione doFitting. Ecco cosa succede quando l'agente lo chiama:

Passaggio 1: risolvi il problema relativo all'immagine dell'utente

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'immagine dell'utente può provenire da due fonti:

  • Un nome di artefatto (ad esempio upload_abc123_1): questo è il caricamento iniziale, salvato dal callback SaveIncomingBlobs
  • Un gs:// URI: si tratta di un risultato di adattamento generato in precedenza, archiviato in GCS per il riutilizzo tra sessioni

Questo design a doppio percorso è intenzionale: quando l'agente stilista genera in un secondo momento le prove di abiti, riutilizza l'URL GCS del risultato iniziale del camerino, in modo che l'identità dell'utente rimanga coerente in tutti gli outfit.

Passaggio 2: crea il prompt multimodale

   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
   }

Il prompt toolInstructions (incorporato da tool_instructions.md) è fondamentale: indica a Gemini di preservare l'identità dell'utente (viso, corporatura, tono della pelle, capelli) applicando solo il capo di abbigliamento. Senza questa ingegneria dei prompt, il modello potrebbe modificare l'aspetto della persona.

Passaggio 3: chiama Gemini per la generazione di immagini

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

Tutti e quattro gli agenti e lo strumento di generazione delle immagini condividono un unico percorso di autenticazione: Backend: genai.BackendVertexAI con l'ID progetto, autenticato tramite le credenziali predefinite dell'applicazione. I modelli di orchestrazione (gemini-3.1-pro-preview, gemini-3-flash-preview) e il modello di immagine (gemini-2.5-flash-image) si trovano tutti dietro lo stesso endpoint Vertex AI e le stesse credenziali ADC autorizzano anche l'accesso a Cloud Storage: una credenziale, ogni chiamata.

Passaggio 4: salva il risultato

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

Il doppio salvataggio (artefatto + GCS) è la chiave per il trasferimento dell'agente tra il camerino e lo stilista. L'artefatto fornisce l'accesso immediato all'interno della sessione corrente, mentre l'URI GCS consente allo strumento di stile (che viene eseguito in una sessione diversa) di fare riferimento alla stessa immagine in un secondo momento.

Il callback SaveIncomingBlobs

Prima ancora che l'agente inizi a ragionare, viene eseguito questo BeforeAgentCallback per salvare le immagini caricate dall'utente:

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
}

Restituendo (nil, nil), i segnali di callback indicano "Ho terminato la pre-elaborazione. Ora esegui l'agente normalmente". Se restituisse contenuti non nulli, l'agente verrebbe interrotto completamente.

6.2 L'agente stilista

File:

adk_backend/stylist/agent.go

L'agente di stile è il più sofisticato del sistema. Seleziona consigli personalizzati per gli outfit e supporta il perfezionamento iterativo tramite conversazione.

Tre richiami: il ricordo dello stilista

Lo stilista utilizza tre callback per mantenere il contesto nelle conversazioni multi-turno:

Callback 1:

InjectPreviousProducts (BeforeModel)

Il problema: se l'utente dice "mostrami altre opzioni", il LLM potrebbe suggerire di nuovo gli stessi prodotti perché non tiene traccia in modo intrinseco di ciò che ha già consigliato.

La soluzione: dopo ogni risposta, gli ID prodotto vengono salvati nello stato della sessione. Prima della successiva chiamata LLM, questo callback li legge e inserisce un suggerimento:

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)

Il problema: quando l'utente fornisce un feedback ("rendilo più informale"), il messaggio di follow-up non include di nuovo la foto dell'utente. Ma lo strumento di prova virtuale ne ha bisogno.

La soluzione: alla prima richiesta, questo callback estrae il riferimento all'immagine dell'utente e lo salva nello stato. Nelle richieste successive, lo reinserisce:

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)

Dopo che l'LLM risponde con suggerimenti per l'outfit, questo callback analizza il JSON per estrarre gli ID prodotto e li salva per il callback InjectPreviousProducts da utilizzare la volta successiva:

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
}

Insieme, queste tre callback creano un ciclo di feedback:

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'agente principale

File:

adk_backend/rootagent/agent.go

L'agente più semplice, solo 31 righe:

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

Utilizza gemini-3-flash-preview (il modello più veloce) perché le decisioni di routing sono semplici: l'LLM deve solo leggere l'intent dell'utente e scegliere il sub-agente giusto. Non sono necessari strumenti; SubAgents gestisce automaticamente la delega.

7. 📱 Architettura frontend di Flutter

Il frontend Flutter è un'app di acquisto al dettaglio completamente funzionale. Le funzionalità di AI si trovano in flutter_frontend/lib/workshop_tasks/, separatamente dall'esperienza di acquisto predefinita in core_app/.

Il pattern MVVM

L'app segue l'architettura Model-View-ViewModel con il pacchetto 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                                                 
└──────────────────┘    └────────────────────┘    └──────────────────┘

Ogni livello ha un ruolo chiaro:

  • Modello: classi di dati come Product, Outfit, StyleRequest ed enumerazioni come TryOnState
  • ViewModel (ChangeNotifier): contiene lo stato attuale e trasmette le modifiche all'interfaccia utente tramite notifyListeners()
  • View (widget): si iscrive al ViewModel con context.watch() e viene ricompilata quando lo stato cambia
  • Service: effettua chiamate HTTP al backend dell'ADK e restituisce dati digitati

Il livello di servizio

I servizi sono definiti come interfacce astratte, con implementazioni specifiche dell'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 { ... }

Questa separazione significa che puoi sostituire il backend dell'ADK con Firebase AI, un servizio simulato o qualsiasi altra implementazione senza modificare il resto dell'app.

Il pattern API in tre passaggi

AdkFittingRoomService e AdkStylingService seguono lo stesso pattern per comunicare con il backend dell'ADK:

Passaggio 1: crea una sessione

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
}

Passaggio 2: esegui l'agente

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

Passaggio 3: recupera l'artefatto

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
}

Una differenza di progettazione fondamentale: il servizio di camerino virtuale crea una nuova sessione per ogni richiesta (_createSession() viene chiamato ogni volta), mentre il servizio di consulenza di stile riutilizza la stessa sessione (_sessionId ??= await _createSession()) per consentire conversazioni a più turni.

Gestione dello stato: TryItOnProvider

File:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

TryItOnProvider gestisce l'intero flusso di prova. Utilizza un'enumerazione TryOnState come macchina a stati:

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


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

Le transizioni di stato private garantiscono la coerenza: non aggiorni mai lo stato senza cancellare anche i dati obsoleti e senza inviare una notifica alla 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();
 }

Il metodo di generazione principale lega tutto insieme:

 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'interfaccia utente: le schermate come router di stato

File:

workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart

La schermata di prova utilizza la corrispondenza di pattern di Dart 3 con AnimatedSwitcher per passare da una schermata secondaria all'altra in base allo stato del fornitore:

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() si iscrive al fornitore. Ogni volta che viene chiamato notifyListeners(), questo widget viene ricompilato e AnimatedSwitcher esegue la transizione senza problemi tra le schermate. Non è presente alcun Navigator.push: i contenuti dello schermo cambiano in base all'enumerazione dello stato.

Il trasferimento agentico: camerino → stilista

Il modello UX più interessante è il modo in cui l'app passa il contesto dall'agente del camerino all'agente stilista.

In 5_fitting_room.dart, dopo la generazione dell'immagine di prova, il pulsante "Prova stile" apre un modulo. Quando l'utente invia:

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

Il StyleRequest include tutto ciò di cui ha bisogno lo stilista:

  • Posizione e occasione: contesto del testo per lo stile
  • URL dell'immagine dell'utente GCS, in modo che lo stilista possa riutilizzare la stessa rappresentazione dell'utente
  • Prodotto selezionato, in modo che lo stilista lo includa in ogni outfit

Si tratta del trasferimento agentico, che consente di trasferire senza problemi il contesto multimodale da un agente AI all'altro, con l'utente che vede solo un semplice modulo.

Il flusso di applicazione dello stile: StylingProvider

File:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

StylingProvider è più semplice di TryItOnProvider perché delega la maggior parte della complessità al 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;
 }
}

Il metodo refineWithFeedback invia un messaggio di testo normale alla stessa sessione. I callback InjectPreviousProducts e ExtractAndInjectUserImage del backend gestiscono automaticamente tutta la gestione del contesto.

8. 🚀 Esegui l'app localmente

Per un'esperienza fluida di Cloud Shell, il backend Go pubblica l'app web Flutter compilata dalla stessa porta (8080). Un processo, un URL di anteprima, nessun problema di origine diversa, nessuna modifica dei file di configurazione.

Prima di iniziare: verifica dell'ADC

Il backend richiede le credenziali predefinite dell'applicazione per chiamare Vertex AI. Se hai completato il passaggio 7 della configurazione del progetto in questa sessione di Cloud Shell e in questo Account Google, puoi procedere. Se stai tornando dopo una pausa, hai cambiato account o non ne sei sicuro, verifica in 5 secondi:

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

Se vengono stampati circa 20 caratteri di un token, è tutto pronto. Se si verifica un errore, esegui di nuovo il passaggio 7 della configurazione del progetto:

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

Utilizzerai due terminali Cloud Shell:

  • Terminale A: esegue il backend in modo continuo (./run.sh). Lascialo aperto.
  • Terminal B: esegue la build web di Flutter una volta (flutter build web). Termina al termine.

L'ordine non è importante, puoi iniziare da uno qualsiasi dei due. Tuttavia, per un'esperienza di prima esecuzione più pulita, crea prima Flutter in modo che il backend abbia una UI da cui eseguire il servizio dal momento in cui viene avviato.

1. Terminale B: crea il pacchetto web Flutter (una tantum)

Apri una nuova scheda Cloud Shell (il pulsante + nella parte superiore del riquadro del terminale), quindi:

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

Viene generato flutter_frontend/build/web/, una directory di file statici (HTML, JS, asset) e il processo termina al termine. Il backend li mostrerà non appena rileverà l'esistenza della directory.

2. Terminale A: avvia il backend (a esecuzione prolungata)

Nel terminale Cloud Shell originale:

cd ~/fashion_app_demo/adk_backend
./run.sh

Dovresti vedere qualcosa di simile a questo:

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

Lascia in esecuzione questo terminale: il backend rimane attivo finché run.sh è attivo. Per interromperla, premi Ctrl+C.

Il server espone tutto sulla porta 8080:

  • /: app web Flutter (l'interfaccia utente dello shopping)
  • /api/: endpoint REST ADK (chiamati dall'app Flutter)
  • Interfaccia utente di ADK Dev, disponibile anche all'indirizzo / quando non è presente una build Flutter, utile per il debug diretto dell'agente

3. Apri anteprima web

  1. In Cloud Shell, fai clic sull'icona Anteprima web (in alto a destra) → Anteprima sulla porta 8080.
  2. L'app di shopping Flutter viene caricata in una nuova scheda
  3. Sfoglia il catalogo prodotti e seleziona un articolo.
  4. Tocca l'icona della persona (👤) per iniziare il flusso Prova
  5. Carica una foto e guarda l'AI generare un'immagine di prova
  6. Tocca "Suggerimenti di stile" per ricevere consigli sugli outfit.
  7. Digita un feedback di follow-up come "rendilo più informale" per perfezionare la risposta nella stessa sessione

9. ☁️ Esegui il deployment in Cloud Run

Raggruppa la build Flutter nel backend

Il container Cloud Run include sia l'API che la UI da un'unica immagine. Copia la build web di Flutter in adk_backend/flutter_web/. Questo è il primo percorso che il server Go controlla quando sceglie quale UI pubblicare:

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

Se hai eseguito l'iterazione in locale, potresti già avere build/web dal passaggio Esegui in locale. L'esecuzione di nuovo di flutter build web va ancora bene.)

Esegui il deployment del backend (che gestisce l'API e la 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

Al termine del deployment, riceverai un URL del servizio simile a https://fashion-app-backend-xyz-uc.a.run.app. Aprirla in un browser: l'app di shopping Flutter viene caricata da / e le chiamate API vengono inviate a /api/ sullo stesso host. Non sono necessarie modifiche alla configurazione del frontend, nessuna chiave API è stata trasmessa.

Verifica il deployment

Apri l'URL di Cloud Run nel browser e segui l'intero flusso:

  1. Sfoglia → Seleziona un prodotto
  2. Prova → Carica la tua foto → Visualizza l'immagine creata con l'AI
  3. Style Me → Compila la posizione/l'occasione → Visualizza gli outfit curati
  4. Feedback → Digita "rendi più casual" → Visualizza gli outfit aggiornati
  5. Aggiungi al carrello → Completa il flusso di acquisto

10. 🎉 Conclusione

Cosa hai creato

Hai esplorato un'esperienza di vendita al dettaglio completa basata sull'AI con:

  • ✅ Un backend multi-agente con 4 agenti specializzati che lavorano insieme
  • ✅ Una cabina di prova virtuale che genera immagini personalizzate di prova
  • ✅ Un consulente di stile AI che seleziona gli outfit e li perfeziona attraverso la conversazione
  • ✅ Un'app Flutter multipiattaforma che si connette al backend dell'agente
  • Deployment di Cloud Run per un hosting serverless scalabile

Concetti principali

Concetto

Dove l'hai visto

Orchestrazione multi-agente ADK

Routing dell'agente principale agli agenti di camerino, catalogo e stilista

Generazione di immagini multimodali di Gemini

fitting_tool combinando le foto degli utenti con le immagini dei prodotti

Stato della sessione per l'AI conversazionale

Lo stilista riutilizza le sessioni per un feedback iterativo

Archiviazione degli artefatti per i dati binari

Separazione dell'archiviazione delle immagini dalle risposte di testo

Callback per la logica del middleware

SaveIncomingBlobs, InjectPreviousProducts, SaveSelectedProducts

MVVM + Provider in Flutter

TryItOnProvider e StylingProvider con ChangeNotifier

Agentic handoff

StyleRequest Trasferimento del contesto multimodale tra agenti

Passaggi successivi

  • 🎨 Personalizza i prompt dell'agente: modifica instructions.md per cambiare la personalità dello stilista
  • 🛍️ Aggiungi altri prodotti: aggiorna catalog.yaml con nuovi articoli
  • 📱 Ottimizza gli annunci per i dispositivi mobili: esegui flutter build ios o flutter build apk
  • 🔄 Aggiungi sessioni persistenti: sostituisci InMemoryService con un'implementazione basata su database
  • 🔒 Aggiungi autenticazione: proteggi l'endpoint Cloud Run con IAM

Risorse