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.
- 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.
- 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 Visualizza → Terminale.
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
2. Collega un account di fatturazione
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à |
| Vertex AI: le chiamate |
| Cloud Storage: memorizza le immagini del catalogo prodotti e i risultati della prova generati |
| Cloud Run: ospita il backend come container serverless |
| Cloud Build: crea immagini Docker dall'origine |
| 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 |
| 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 |
| 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 |
| 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 |
| 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.NewMultiLoaderregistra 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.NewServerfornisce 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.NewServicearchivia 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:
- Ricevere "Style me for a beach vacation" (Crea un outfit per una vacanza al mare)
- Chiama lo strumento
catalog_agentper ottenere l'elenco dei prodotti - Seleziona 3 combinazioni di outfit
- Chiama
fitting_toolper ogni outfit per generare immagini - 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:
listProducts: restituisce l'intero catalogo prodotti da un file YAMLgetProductImage: 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 callbackSaveIncomingBlobs - 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,StyleRequested enumerazioni comeTryOnState - ViewModel (
ChangeNotifier): contiene lo stato attuale e trasmette le modifiche all'interfaccia utente tramitenotifyListeners() - View (widget): si iscrive al ViewModel con
context.watche 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
- In Cloud Shell, fai clic sull'icona Anteprima web (in alto a destra) → Anteprima sulla porta 8080.
- L'app di shopping Flutter viene caricata in una nuova scheda
- Sfoglia il catalogo prodotti e seleziona un articolo.
- Tocca l'icona della persona (👤) per iniziare il flusso Prova
- Carica una foto e guarda l'AI generare un'immagine di prova
- Tocca "Suggerimenti di stile" per ricevere consigli sugli outfit.
- 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:
- Sfoglia → Seleziona un prodotto
- Prova → Carica la tua foto → Visualizza l'immagine creata con l'AI
- Style Me → Compila la posizione/l'occasione → Visualizza gli outfit curati
- Feedback → Digita "rendi più casual" → Visualizza gli outfit aggiornati
- 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 |
|
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 |
|
MVVM + Provider in Flutter |
|
Agentic handoff |
|
Passaggi successivi
- 🎨 Personalizza i prompt dell'agente: modifica
instructions.mdper cambiare la personalità dello stilista - 🛍️ Aggiungi altri prodotti: aggiorna
catalog.yamlcon nuovi articoli - 📱 Ottimizza gli annunci per i dispositivi mobili: esegui
flutter build iosoflutter build apk - 🔄 Aggiungi sessioni persistenti: sostituisci
InMemoryServicecon un'implementazione basata su database - 🔒 Aggiungi autenticazione: proteggi l'endpoint Cloud Run con IAM
Risorse
- Documentazione ADK: documentazione ufficiale dell'Agent Development Kit
- Codice sorgente di ADK Go: repository GitHub
- Riferimento al pacchetto ADK Go: riferimento API
- Documentazione dell'API Gemini: funzionalità e guide del modello
- Pacchetto Flutter Provider: documentazione sulla gestione dello stato
- Documentazione di Cloud Run: guide al deployment e allo scaling