1. Introdução
O que você criará
Neste codelab, você vai assumir o papel de um desenvolvedor criando o Fashion App, um app de compras do Flutter para uma marca de varejo fictícia. Sua missão: adicionar dois recursos com tecnologia de IA que transformam a experiência de compras on-line.
- Provador virtual: o usuário envia uma foto dele, seleciona uma peça de roupa e vê uma imagem gerada por IA dele usando essa peça.
- Consultor de estilo de IA: com base na localização, na ocasião e nas preferências de estilo do usuário, um agente de IA seleciona recomendações de roupas completas, que podem ser refinadas por conversa.
A ideia é simples: quando as pessoas provam roupas em um provador, é muito mais provável que elas comprem. Mas on-line? Você está apenas chutando. Este projeto preenche essa lacuna com a IA.
Visão geral da arquitetura
Flutter App ──── HTTP/REST ────▶ ADK Go Backend
│
┌──────────┼──────────┐
Fitting Room Stylist Catalog
Agent Agent Agent
│
Gemini API + Cloud Storage
Principais tecnologias
Componente | Tecnologia | Finalidade |
Framework do agente | ADK (Kit de Desenvolvimento de Agente) para Go | Orquestração multiagente, sessões, artefatos |
Raciocínio do agente (Pro) | Pré-lançamento do Gemini 3.1 Pro | Alimenta o provador e os agentes de estilista |
Raciocínio do agente (Flash) | Pré-lançamento do Gemini 3 Flash | Alimenta os agentes raiz e de catálogo (roteamento/pesquisa leves). |
Geração de imagens | Imagem do Gemini 2.5 Flash | Gera imagens de provar e de roupas |
Front-end | Flutter (Dart) | App multiplataforma (Web, iOS, Android) |
Armazenamento | Google Cloud Storage | Armazena imagens de produtos e artefatos gerados. |
Hosting | Cloud Run | Implantação de contêineres sem servidor |
2. 📦 Pré-requisitos e configuração do Cloud Shell
1. Abrir editor do Cloud Shell
👉 Abra o editor do Cloud Shell no navegador.
Se o terminal não aparecer na parte de baixo da tela:
- Clique em Visualizar → Terminal.
2. Configurar o SDK do Flutter
O Cloud Shell vem com o Flutter pré-instalado em /google/flutter. Como esse diretório pertence a um usuário do sistema diferente, você vai receber um erro fatal: detected dubious ownership na primeira vez que executar flutter. Adicione-o à lista de diretórios seguros do Git uma vez:
git config --global --add safe.directory /google/flutter
Verifique se o Flutter está no seu PATH e funcionando:
flutter --version
A primeira execução faz o download do SDK do Dart e cria a ferramenta do Flutter. Aguarde um minuto. Você verá algo como Flutter 3.x • channel stable.
3. Clone o repositório
cd ~
git clone https://github.com/gca-americas/fashion-app-demo
cd fashion_app_demo
4. Conheça a estrutura do projeto
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. ☁️ Configuração do projeto do Google Cloud
1. Criar um novo projeto
gcloud projects create fashion-app-demo --name="Fashion App Demo"
gcloud config set project fashion-app-demo
2. Vincular uma conta de faturamento
Liste suas contas de faturamento:
gcloud billing accounts list
Confira o
OPEN
. Ele precisa dizer True. Se aparecer False (comum em um teste sem custo financeiro expirado), a conta será encerrada e não vai pagar nada. Vá para o bloco de solução de problemas abaixo antes de continuar.
Copie o ACCOUNT_ID de uma conta do OPEN: True (parece 0X0X0X-0X0X0X-0X0X0X) e vincule ao seu projeto:
gcloud billing projects link fashion-app-demo \
--billing-account=YOUR_BILLING_ACCOUNT_ID
Verifique o link:
gcloud billing projects describe fashion-app-demo
Você vai ver billingEnabled: true. Se você vir billingEnabled: false mesmo depois de vincular, a conta estará encerrada (OPEN: False). Consulte o bloco de solução de problemas abaixo.
3. Ativar APIs obrigatórias
gcloud services enable \
aiplatform.googleapis.com \
storage.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
API | Finalidade |
| Vertex AI: as |
| Cloud Storage: armazena imagens do catálogo de produtos e resultados gerados de simulação. |
| Cloud Run: hospeda o back-end como um contêiner sem servidor. |
| Cloud Build: cria imagens Docker da origem |
| Artifact Registry: armazena imagens Docker criadas. |
4. Criar um bucket do 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. Fazer upload de imagens do catálogo de produtos
A ferramenta getProductImage do back-end lê de gs://$GCS_BUCKET/catalog-assets/images/. Faça upload das imagens do catálogo para esse caminho exato:
cd ~/fashion_app_demo
gcloud storage cp flutter_frontend/assets/images/*.png \
gs://fashion-app-$PROJECT_ID/catalog-assets/images/
Verifique o upload. Uma lista de arquivos .png vai aparecer.
gcloud storage ls gs://fashion-app-$PROJECT_ID/catalog-assets/images/
6. Configurar o arquivo .env
cd ~/fashion_app_demo/adk_backend
cat > .env << EOF
GOOGLE_CLOUD_PROJECT=$PROJECT_ID
GCS_BUCKET=fashion-app-$PROJECT_ID
EOF
7. Autenticar com o Application Default Credentials
Execute este comando antes de iniciar o back-end localmente. O back-end Go usa o ADC para autenticar todas as chamadas para a Vertex AI (Gemini) e o Cloud Storage. Sem o ADC, o back-end será iniciado, mas todas as solicitações de simulação vão falhar com um 401 CREDENTIALS_MISSING.
Uma credencial abrange os dois serviços. Execute estes dois comandos em ordem:
# 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)
Verifique se o ADC está íntegro:
gcloud auth application-default print-access-token | head -c 20 && echo "..."
Você vai ver cerca de 20 caracteres de um token seguidos por .... Se houver um erro, o login não foi concluído. Repita a etapa 1.
4. 🏗️ Visão geral da arquitetura
Agora que o ambiente está pronto, vamos entender como o sistema funciona antes de analisar o código.
O sistema de quatro agentes
O back-end é criado como um sistema multiagente usando o ADK (Kit de Desenvolvimento de Agente) para Go. Quatro agentes trabalham juntos, cada um com uma responsabilidade específica:
┌──────────────┐
│ 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 | Modelo | Papel |
Agente raiz |
| Agente de trânsito. Lê a mensagem do usuário e delega ao agente especialista certo. Usa um modelo rápido e leve porque só precisa tomar decisões de roteamento. |
Agente do catálogo |
| Expert em produtos. Carrega o catálogo de produtos de um arquivo YAML e responde a consultas de produtos. Também é leve, já que apenas pesquisa dados. |
Agente de provador |
| Especialista em simulador virtual. Pega uma foto do usuário e uma imagem do produto e gera uma imagem composta da pessoa usando o item. Usa um modelo mais eficiente porque precisa raciocinar sobre imagens. |
Agente de estilista |
| Consultor de moda. Com base no local, na ocasião e nas preferências, ele seleciona três combinações de roupas do catálogo. Pode gerar imagens de simulação para cada roupa. Também usa o modelo avançado para raciocínio criativo. |
O ponto de entrada: main.go
Tudo começa em main.go, que conecta os agentes e inicia o servidor 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()
}
Algumas observações importantes:
- Os agentes são criados de baixo para cima: o agente de catálogo é criado primeiro porque os agentes de provador e estilista dependem dele (eles delegam pesquisas de produtos a ele).
agent.NewMultiLoaderregistra todos os quatro agentes para que a API REST possa encaminhar para qualquer um deles por nome.- O
adkrest.NewServerfornece a API REST automaticamente. Você não precisa escrever os manipuladores de endpoint. O ADK oferece gerenciamento de sessão, armazenamento de artefatos e execução de agentes prontos para uso. - O
session.InMemoryService()armazena sessões na memória. Isso significa que as sessões serão perdidas se o servidor for reiniciado, o que é bom para uma demonstração. Em produção, você usaria um armazenamento persistente. - O
gcsartifact.NewServicearmazena artefatos (imagens geradas) no Google Cloud Storage. Assim, eles persistem em todas as solicitações e podem ser compartilhados por URIs do GCS.
5. 🤖 Análise detalhada do ADK (Kit de Desenvolvimento de Agente)
O que é o ADK?
O Kit de Desenvolvimento de Agente (ADK) é um framework de código aberto do Google para criar agentes de IA em Go (e Python/Java). É a camada entre seu aplicativo e a API Gemini.
Você pode chamar a API Gemini diretamente. Mas quando o app precisar:
- Pesquisar produtos em um catálogo
- Gerar imagens com base em fotos do usuário
- Lembrar das roupas sugeridas anteriormente
- Coordenar vários agentes de IA
Você precisa de estrutura. O ADK fornece essa estrutura.
O ciclo do agente
Todos os agentes do ADK seguem um loop:
1. Receive a message (from user or another agent)
2. Think — the LLM reasons about what to do
3. Act — call a tool, delegate to a sub-agent, or respond
4. Return — send the result back
Esse loop pode ser repetido várias vezes em uma única solicitação. Por exemplo, o agente de estilista pode:
- Receber "Me dê sugestões de looks para uma viagem à praia"
- Chamar a ferramenta
catalog_agentpara receber a lista de produtos - Selecione três combinações de roupas
- Chame
fitting_toolpara cada roupa e gere imagens. - Retornar a resposta JSON estruturada
Conceitos básicos (com código deste repositório)
Agentes de LLM
O elemento básico principal. Criado com 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
})
O campo Instruction é a persona do agente. Ele informa ao LLM quem ele é e como se comportar. Neste repositório, as instruções são escritas como arquivos Markdown e incorporadas no tempo de compilação usando a diretiva //go:embed do Go:
//go:embed instructions.md
var instructions string
Isso mantém os comandos como documentos separados e com controle de versão, em vez de strings inline.
Ferramentas
As ferramentas são funções Go que o LLM pode chamar. O ADK processa a tradução entre o formato de chamada de ferramenta do LLM e sua função Go digitada:
// 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)
O ADK gera automaticamente um esquema JSON das suas structs Go e o envia para o LLM. Quando o LLM decide chamar listProducts, o ADK desserializa os argumentos, chama sua função e envia o resultado de volta.
O parâmetro tool.Context dá às ferramentas acesso aos serviços de tempo de execução do ADK, principalmente artefatos:
// Save an image as an artifact
ctx.Artifacts().Save(ctx, "my_image", imagePart)
// Load an artifact
resp, _ := ctx.Artifacts().Load(ctx, "my_image")
Delegação de subagente
Um agente pode usar outro agente como ferramenta via agenttool.New():
// From fittingroom/agent.go
Tools: []tool.Tool{
loadartifactstool.New(), // List available artifacts
imgtool, // Get product images
agenttool.New(catalogAgent, nil), // Delegate to catalog agent
fittingTool, // Generate try-on image
},
Quando o agente do provador precisa de informações do produto, ele pode chamar o agente do catálogo como se fosse uma ferramenta comum. O LLM vê na lista de ferramentas e pode decidir invocar.
Sessões
As sessões rastreiam o histórico de conversas. A API REST do ADK gerencia esses itens automaticamente:
POST /api/apps/{appName}/users/{userId}/sessions → Creates a new session
POST /api/run (with sessionId) → Runs agent within that session
Uma decisão de design importante neste app: o provador cria uma nova sessão por solicitação (cada teste é independente), enquanto o estilista reutiliza a mesma sessão (para lembrar sugestões anteriores e refinar com base no feedback).
Estado
O estado é um armazenamento de chave-valor anexado a uma sessão. Os agentes leem e gravam o estado para coordenar:
// Write to state
ctx.State().Set("previously_used_products", "[\"id_bomber\",\"id_hat\"]")
// Read from state
val, err := ctx.State().Get("previously_used_products")
O agente de estilista usa o estado para lembrar quais produtos já foram sugeridos e, assim, escolher outros na próxima vez.
Artefatos
Os artefatos são objetos binários nomeados (geralmente imagens) armazenados por sessão. Ao contrário das respostas de texto, elas são armazenadas separadamente e buscadas por 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}
Isso mantém as respostas leves: o agente retorna apenas o nome do artefato, e o front-end busca os dados da imagem binária separadamente.
Callbacks
Callbacks são hooks que são executados em pontos específicos no ciclo do agente. Eles podem inspecionar, modificar ou interromper a execução:
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 um callback retornar uma resposta não nula, o comportamento padrão será ignorado. Por exemplo, um BeforeModelCallback que retorna uma resposta armazenada em cache pula completamente a chamada real do LLM.
Aplicação de esquema JSON
Os agentes do provador e do estilista forçam o LLM a responder em JSON estruturado:
GenerateContentConfig: &genai.GenerateContentConfig{
ResponseMIMEType: "application/json",
ResponseJsonSchema: fittingSchemaMap(), // Defines the expected structure
}
Isso garante que o front-end do Flutter sempre receba dados analisáveis, não texto livre.
O agente de catálogo: exemplo mais simples
O agente de catálogo (catalog/agent.go) é o mais simples do sistema, um bom ponto de partida para entender os padrões do ADK.
Ele tem duas ferramentas:
listProducts: retorna o catálogo completo de produtos de um arquivo YAML.getProductImage: carrega uma imagem do produto do GCS (ou fallback local) e a salva como um artefato.
A ferramenta getProductImage mostra um padrão importante: carregamento de várias origens com cache de artefatos:
// 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
}
A ferramenta tenta primeiro os artefatos, depois o GCS e, por fim, os arquivos locais. Depois de carregada, a imagem é armazenada em cache como um artefato para que as chamadas subsequentes sejam instantâneas.
6. 🧪 O pipeline de IA: agentes em ação
Agora vamos analisar os dois agentes mais sofisticados, que geram imagens e selecionam roupas.
6.1 O agente do provador
Arquivo:
adk_backend/fittingroom/agent.go
O agente do provador é o mecanismo por trás do "Simulador virtual". Quando um usuário envia uma foto e escolhe um produto, esse agente gera uma imagem composta da pessoa usando o item.
O fitting_tool: instruções detalhadas
A lógica principal está na função doFitting. Veja o que acontece quando o agente faz a chamada:
Etapa 1: resolver a imagem do usuário
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
}
A imagem do usuário pode vir de duas fontes:
- Um nome de artefato (como
upload_abc123_1): é o upload inicial, salvo pelo callbackSaveIncomingBlobs. - Um URI
gs://: é um resultado de ajuste gerado anteriormente e armazenado no GCS para reutilização em várias sessões.
Esse design de caminho duplo é intencional: quando o agente de estilista gera simulações de roupas, ele reutiliza o URL do GCS do resultado inicial do provador para que a identidade do usuário permaneça consistente em todas as roupas.
Etapa 2: criar o comando multimodal
parts := []*genai.Part{
genai.NewPartFromText(toolInstructions), // Identity preservation prompt
genai.NewPartFromText("Reference Person Photo:"),
userPart, // The user's photo
}
for _, acc := range args.Accessories {
accResp, _ := ctx.Artifacts().Load(ctx, acc) // Load product image artifact
parts = append(parts, genai.NewPartFromText("Product Image to Apply:"))
parts = append(parts, accResp.Part) // The product photo
}
O toolInstructions (incorporado de tool_instructions.md) é crucial. Ele diz ao Gemini para preservar a identidade do usuário (rosto, tipo de corpo, tom de pele, cabelo) ao aplicar apenas a peça de roupa. Sem essa engenharia de comandos, o modelo pode mudar a aparência da pessoa.
Etapa 3: chamar o Gemini para gerar imagens
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
})
Todos os quatro agentes e a ferramenta de geração de imagens compartilham um único caminho de autenticação: Backend: genai.BackendVertexAI com o ID do projeto, autenticado usando Application Default Credentials. Os modelos de orquestração (gemini-3.1-pro-preview, gemini-3-flash-preview) e o modelo de imagem (gemini-2.5-flash-image) estão todos no mesmo endpoint da Vertex AI, e o mesmo ADC também autoriza o acesso ao Cloud Storage: uma credencial, todas as chamadas.
Etapa 4: salvar o resultado
// 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
A gravação dupla (artefato + GCS) é fundamental para a transferência do agente entre o provador e o estilista. O artefato fornece acesso imediato na sessão atual, enquanto o URI do GCS permite que o estilista (que é executado em uma sessão diferente) faça referência à mesma imagem mais tarde.
O callback SaveIncomingBlobs
Antes mesmo de começar a raciocinar, esse BeforeAgentCallback é executado para salvar as imagens enviadas pelo usuário:
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
}
Ao retornar (nil, nil), o callback sinaliza "Terminei o pré-processamento. Agora execute o agente normalmente". Se ele retornasse conteúdo não nulo, o agente seria interrompido completamente.
6.2 O agente estilista
Arquivo:
adk_backend/stylist/agent.go
O agente de estilista é o mais sofisticado do sistema. Ele seleciona recomendações de roupas personalizadas e oferece suporte ao refinamento iterativo por conversa.
Três callbacks: a memória da estilista
O estilista usa três callbacks para manter o contexto em conversas multiturnos:
Callback 1:
InjectPreviousProducts (BeforeModel)
O problema: se o usuário disser "mostre opções diferentes", o LLM poderá sugerir os mesmos produtos novamente porque não rastreia o que já recomendou.
A solução: depois de cada resposta, os IDs dos produtos são salvos no estado da sessão. Antes da próxima chamada de LLM, esse callback lê os dados e injeta uma dica:
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)
O problema: quando o usuário envia feedback ("deixe mais informal"), a mensagem de acompanhamento não inclui a foto do usuário novamente. mas a ferramenta de ajuste precisa.
A solução: na primeira solicitação, esse callback extrai a referência da imagem do usuário e a salva no estado. Em solicitações subsequentes, ele é reinserido:
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)
Depois que o LLM responde com sugestões de roupas, essa callback analisa o JSON para extrair IDs de produtos e os salva para que a callback InjectPreviousProducts use na próxima vez:
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
}
Juntos, esses três callbacks criam um feedback contínuo:
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 O agente raiz
Arquivo:
adk_backend/rootagent/agent.go
O agente mais simples, com apenas 31 linhas:
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},
})
}
Ele usa o gemini-3-flash-preview (o modelo mais rápido) porque as decisões de encaminhamento são simples. O LLM só precisa ler a intenção do usuário e escolher o subagente certo. Não são necessárias ferramentas. O SubAgents processa a delegação automaticamente.
7. 📱 Arquitetura de front-end do Flutter
O front-end do Flutter é um app de compras de varejo totalmente funcional. Os recursos de IA ficam em flutter_frontend/lib/workshop_tasks/, separados da experiência de compra pré-criada em core_app/.
O padrão MVVM
O app segue a arquitetura Model-View-ViewModel com o pacote 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 │ │ │ │ │
└──────────────────┘ └────────────────────┘ └──────────────────┘
Cada camada tem uma função clara:
- Modelo: classes de dados como
Product,Outfit,StyleRequeste enums comoTryOnState - ViewModel (
ChangeNotifier): contém o estado atual e transmite mudanças para a interface usandonotifyListeners(). - View (widget): se inscreve no ViewModel com
context.watche é reconstruída quando o estado muda.() - Serviço: faz chamadas HTTP para o back-end do ADK e retorna dados tipados.
A camada de serviço
Os serviços são definidos como interfaces abstratas, com implementações específicas do 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 { ... }
Essa separação significa que você pode trocar o back-end do ADK pelo Firebase AI, um serviço simulado ou qualquer outra implementação sem mudar o restante do app.
O padrão de API de três etapas
AdkFittingRoomService e AdkStylingService seguem o mesmo padrão para se comunicar com o back-end do ADK:
Etapa 1: criar uma sessão
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
}
Etapa 2: executar o 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...
}
Etapa 3: buscar o artefato
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
}
Uma diferença de design importante: o serviço de provador cria uma nova sessão para cada solicitação (_createSession() é chamado a cada vez), enquanto o serviço de estilo reutiliza a mesma sessão (_sessionId ??= await _createSession()) para permitir uma conversa em várias etapas.
Gerenciamento de estado: o TryItOnProvider
Arquivo:
workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart
O TryItOnProvider gerencia todo o fluxo de simulação. Ele usa uma enumeração TryOnState como uma máquina de estado:
enum TryOnState { initial, imagePicked, generating, success, error }
class TryItOnProvider with ChangeNotifier {
TryOnState _state = TryOnState.initial;
Uint8List? _userImageBytes;
Uint8List? _generatedImage;
String? _errorMessage;
As transições de estado particulares garantem a consistência. Você nunca atualiza o estado sem limpar dados desatualizados e notificar a interface:
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();
}
O principal método de geração une tudo:
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;
}
A interface: telas como um roteador de estado
Arquivo:
workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart
A tela de simulação usa a correspondência de padrões do Dart 3 com AnimatedSwitcher para fazer o roteamento entre sub-telas com base no estado do provedor:
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 se inscreve no provedor. Sempre que notifyListeners() é chamado, esse widget é recriado, e AnimatedSwitcher faz a transição entre as telas de maneira suave. Não há Navigator.push. O conteúdo da tela muda no local com base no tipo enumerado de estado.
Transferência de agente: provador de roupas → estilista
O padrão de UX mais interessante é como o app passa o contexto do agente do provador para o agente de estilista.
Em 5_fitting_room.dart, depois que a imagem do Provador Virtual é gerada, o botão "Estilize-me" abre um formulário. Quando o usuário envia:
// 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,
));
O StyleRequest agrupa tudo o que o estilista precisa:
- Local e ocasião: contexto de texto para estilização
- URL da imagem do usuário do GCS: para que o estilista possa reutilizar a mesma representação do usuário.
- Produto selecionado: para que o estilista inclua o item em todas as roupas
Essa é a transferência agêntica: a transferência perfeita de contexto multimodal de um agente de IA para outro, com o usuário vendo apenas um formulário simples.
O fluxo de estilização: StylingProvider
Arquivo:
workshop_tasks/step_2_style_me/providers/styling_provider.dart
O StylingProvider é mais simples do que o TryItOnProvider porque delega a maior parte da complexidade ao back-end:
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;
}
}
O método refineWithFeedback envia uma mensagem de texto simples para a mesma sessão. Os callbacks InjectPreviousProducts e ExtractAndInjectUserImage do back-end processam todo o gerenciamento de contexto automaticamente.
8. 🚀 Executar o app localmente
Para uma experiência tranquila no Cloud Shell, o back-end do Go veicula o app da Web do Flutter compilado da mesma porta (8080). Um processo, um URL de prévia, sem problemas de origem cruzada e sem edição de arquivos de configuração.
Antes de começar: verificação de integridade do ADC
O back-end precisa de Application Default Credentials para chamar a Vertex AI. Se você concluiu a etapa 7 da configuração do projeto nesta sessão do Cloud Shell e nesta Conta do Google, tudo certo. Se você estiver voltando depois de um tempo, tiver trocado de conta ou não tiver certeza, verifique em cinco segundos:
gcloud auth application-default print-access-token | head -c 20 && echo "..."
Se isso imprimir cerca de 20 caracteres de um token, está tudo certo. Se houver um erro, execute novamente a etapa 7 da configuração do projeto:
gcloud auth application-default login
gcloud auth application-default set-quota-project $(gcloud config get-value project)
Você vai usar dois terminais do Cloud Shell:
- Terminal A: executa o back-end continuamente (
./run.sh). Deixe-o aberto. - Terminal B: executa o build da Web do Flutter uma vez (
flutter build web) e sai quando termina.
A ordem não importa. Você pode começar por qualquer um dos dois. Mas, para uma experiência de primeira execução mais limpa, crie o Flutter primeiro para que o back-end tenha uma interface do usuário para atender desde o início.
1. Terminal B: criar o pacote do Flutter Web (única vez)
Abra uma nova guia do Cloud Shell (o + na parte de cima do painel do terminal) e faça o seguinte:
cd ~/fashion_app_demo/flutter_frontend
flutter pub get
flutter build web
Isso produz flutter_frontend/build/web/, um diretório de arquivos estáticos (HTML, JS, recursos), e é encerrado quando termina. O back-end vai veicular esses dados assim que perceber que o diretório existe.
2. Terminal A: iniciar o back-end (longa duração)
No terminal original do Cloud Shell:
cd ~/fashion_app_demo/adk_backend
./run.sh
Você verá um código como este:
Serving Flutter web build from ../flutter_frontend/build/web
Deixe esse terminal em execução: o back-end fica ativo enquanto run.sh estiver ativo. Para interromper, pressione Ctrl+C.
O servidor expõe tudo na porta 8080:
/: app da Web do Flutter (a interface de compras)/api/: endpoints REST do ADK (chamados pelo app Flutter)- Interface de desenvolvimento do ADK: também em
/quando não há um build do Flutter. Útil para depuração direta de agentes.
3. Abrir a Visualização da Web
- No Cloud Shell, clique no ícone Visualização da Web (canto superior direito) → Visualizar na porta 8080.
- O app de compras do Flutter é carregado em uma nova guia.
- Navegue pelo catálogo de produtos e selecione um item
- Toque no ícone de pessoa (👤) para iniciar o fluxo de simulação.
- Envie uma foto e veja a IA gerar uma imagem do Provador Virtual
- Toque em "Estilize-me" para receber recomendações de roupas
- Digite um feedback complementar, como "deixe mais informal": refinamento na mesma sessão
9. ☁️ Implantar no Cloud Run
Agrupar o build do Flutter no back-end
O contêiner do Cloud Run envia a API e a interface de uma imagem. Copie o build da Web do Flutter para adk_backend/flutter_web/. Esse é o primeiro caminho que o servidor Go verifica ao escolher qual interface veicular:
cd ~/fashion_app_demo/flutter_frontend
flutter build web
rm -rf ../adk_backend/flutter_web
cp -r build/web ../adk_backend/flutter_web
Se você estiver fazendo iterações localmente, talvez já tenha build/web da etapa "Executar localmente". Executar flutter build web novamente não é um problema.)
Implantar o back-end (fornece API e interface)
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
Quando a implantação terminar, você vai receber um URL do serviço como https://fashion-app-backend-xyz-uc.a.run.app. Abra em um navegador. O app de compras do Flutter será carregado de /, e as chamadas de API vão para /api/ no mesmo host. Nenhuma edição de configuração de front-end é necessária, nenhuma chave de API foi transmitida.
Verificar a implantação
Abra o URL do Cloud Run no navegador e execute o fluxo completo:
- Procurar → Selecione um produto
- Provador Virtual → Faça upload da sua foto → Confira a imagem gerada com IA
- Me dê um estilo → Informe o local/a ocasião → Confira as opções selecionadas
- Feedback → Digite "deixe mais casual" → Confira as roupas atualizadas
- Adicionar à bolsa → Concluir o fluxo de compras
10. 🎉 Conclusão
O que você criou
Você conheceu uma experiência completa de varejo com tecnologia de IA, incluindo:
- ✅ Um backend multiagente com quatro agentes especializados trabalhando juntos
- ✅ Um provador virtual que gera imagens personalizadas para simulação
- ✅ Um consultor de estilo de IA que seleciona e refina looks por conversa
- ✅ Um app Flutter multiplataforma que se conecta ao back-end do agente
- ✅ Implantação do Cloud Run para hospedagem escalonável e sem servidor
Principais conceitos
Conceito | Onde você viu |
Orquestração multiagente do ADK | Roteamento do agente raiz para agentes de provador, catálogo e estilista |
Geração de imagens multimodais com o Gemini |
|
Estado da sessão para IA de conversação | O estilista reutiliza sessões para feedback iterativo |
Armazenamento de artefatos para dados binários | Separar o armazenamento de imagens das respostas de texto |
Callbacks para lógica de middleware |
|
MVVM + Provider no Flutter |
|
Transição agêntica |
|
Próximas etapas
- 🎨 Personalizar comandos do agente: edite
instructions.mdpara mudar a personalidade do estilista. - 🛍️ Adicionar mais produtos: atualize
catalog.yamlcom novos itens. - 📱 Crie para dispositivos móveis: execute
flutter build iosouflutter build apk - 🔄 Adicionar sessões persistentes: substitua
InMemoryServicepor uma implementação com suporte de banco de dados. - 🔒 Adicionar autenticação: proteja o endpoint do Cloud Run com o IAM
Recursos
- Documentação do ADK: documentação oficial do Kit de Desenvolvimento de Agente
- Código-fonte do ADK Go: repositório do GitHub.
- Referência do pacote ADK Go: referência da API
- Documentação da API Gemini: recursos e guias do modelo
- Pacote do provedor do Flutter: documentos de gerenciamento de estado
- Documentação do Cloud Run: guias de implantação e escalonamento