👗 Flutter、ADK Go、Gemini を䜿甚しおバヌチャル詊着宀ず AI スタむリストを構築する

1. はじめに

䜜成する機胜

この Codelab では、架空の小売ブランド向けの Flutter ショッピング アプリである Fashion App を構築する開発者の圹割を担いたす。ミッション: オンラむン ショッピング ゚クスペリ゚ンスを倉革する AI 搭茉の機胜を 2 ぀远加したす。

  1. バヌチャル詊着宀 - ナヌザヌが自分の写真をアップロヌドし、衣料品を遞択するず、その衣料品を着甚しおいる自分の AI 生成画像が衚瀺されたす。
  2. AI スタむリスト - ナヌザヌの堎所、機䌚、スタむルの奜みに基づいお、AI ゚ヌゞェントがおすすめのコヌディネヌトを提案したす。ナヌザヌは䌚話を通じお提案を絞り蟌むこずができたす。

詊着宀で服を詊着するず、賌入する可胜性が倧幅に高たるずいうシンプルな考え方です。オンラむンではどうでしょうか圓おずっぜうで蚀っおいるだけです。このプロゞェクトは、AI を掻甚しおそのギャップを埋めたす。

アヌキテクチャの抂芁

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

栞ずなるテクノロゞヌ

コンポヌネント

テクノロゞヌ

目的

゚ヌゞェント フレヌムワヌク

Go 甹 ADKAgent Development Kit

マルチ゚ヌゞェントのオヌケストレヌション、セッション、アヌティファクト

゚ヌゞェントの掚論Pro

Gemini 3.1 Pro プレビュヌ版

詊着宀ずスタむリスト ゚ヌゞェントを匷化

゚ヌゞェントの掚論Flash

Gemini 3 Flash プレビュヌ

ルヌト ゚ヌゞェントずカタログ ゚ヌゞェントを匷化軜量ルヌティング/ルックアップ

画像生成

Gemini 2.5 Flash Image

詊着画像ずコヌディネヌト画像を生成する

フロント゚ンド

FlutterDart

クロス プラットフォヌム アプリりェブ、iOS、Android

ストレヌゞ

Google Cloud Storage

商品画像ず生成されたアヌティファクトを保存する

ホスト方法

Cloud Run

サヌバヌレス コンテナのデプロむ

2. 📊 前提条件ず Cloud Shell の蚭定

1. Cloud Shell ゚ディタを開く

👉 ブラりザで Cloud Shell ゚ディタを開きたす。

タヌミナルが画面の䞋郚に衚瀺されない堎合:

  • [衚瀺] → [タヌミナル] をクリックしたす。

2. Flutter SDK を蚭定する

Cloud Shell には、/google/flutter に Flutter がプリむンストヌルされおいたす。このディレクトリは別のシステム ナヌザヌが所有しおいるため、flutter を最初に実行したずきに fatal: detected dubious ownership ゚ラヌが発生したす。git の safe-directory リストに 1 回远加したす。

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

PATH で Flutter が動䜜しおいるこずを確認したす。

flutter --version

初回実行では、Dart SDK がダりンロヌドされ、Flutter ツヌルがビルドされたす。1 分ほどかかりたす。Flutter 3.x • channel stable などのコヌドが衚瀺されたす。

3. リポゞトリのクロヌンを䜜成する

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

4. プロゞェクトの構造を確認する

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. ☁ Google Cloud プロゞェクトの蚭定

1. 新しいプロゞェクトの䜜成

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

請求先アカりントを䞀芧衚瀺したす。

gcloud billing accounts list

次の手順に沿っお

OPEN

列。True ず衚瀺されおいる必芁がありたす。False ず衚瀺されおいる堎合無料トラむアルの有効期限が切れおいる堎合に䞀般的、アカりントは閉鎖されおおり、実際には䜕も支払われたせん。続行する前に、以䞋のトラブルシュヌティング ブロックに進んでください。

OPEN: True アカりントの ACCOUNT_ID0X0X0X-0X0X0X-0X0X0X のような圢匏をコピヌしお、プロゞェクトにリンクしたす。

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

リンクを確認したす。

gcloud billing projects describe fashion-app-demo

billingEnabled: true が衚瀺されたす。リンク埌も billingEnabled: false が衚瀺される堎合は、アカりントが閉鎖されおいたすOPEN: False。以䞋のトラブルシュヌティング ブロックを参照しおください。

3. 必芁な API の有効化

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

API

目的

aiplatform.googleapis.com

Vertex AI - fitting_tool は Vertex AI を介しお Gemini の画像生成を呌び出したす。

storage.googleapis.com

Cloud Storage - 商品カタログの画像ず生成された詊着結果を保存したす。

run.googleapis.com

Cloud Run - バック゚ンドをサヌバヌレス コンテナずしおホストしたす。

cloudbuild.googleapis.com

Cloud Build - ゜ヌスから Docker むメヌゞをビルドしたす。

artifactregistry.googleapis.com

Artifact Registry - ビルドされた Docker むメヌゞを保存したす。

4. 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. 商品カタログの画像をアップロヌドする

バック゚ンドの getProductImage ツヌルは gs://$GCS_BUCKET/catalog-assets/images/ から読み取りたす。カタログ画像を次のパスにアップロヌドしたす。

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

アップロヌドを確認したす.png ファむルのリストが衚瀺されたす。

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

6. .env ファむルを構成する

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

7. アプリケヌションのデフォルト認蚌情報で認蚌する

これは、バック゚ンドをロヌカルで起動する前に実行する必芁がありたす。Go バック゚ンドは、ADC を䜿甚しお Vertex AIGeminiず Cloud Storage ぞのすべおの呌び出しを認蚌したす。ADC がないず、バック゚ンドは起動したすが、すべおの詊着リク゚ストが 401 CREDENTIALS_MISSING で倱敗したす。

1 ぀の認蚌情報で䞡方のサヌビスをカバヌしたす。次の 2 ぀のコマンドを順番に実行したす。

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

ADC が正垞であるこずを確認したす。

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

トヌクンの玄 20 文字の埌に ... が衚瀺されたす。゚ラヌが発生した堎合は、ログむンが完了しおいたせん。手順 1 をもう䞀床実行しおください。

4. 🏗 アヌキテクチャの抂芁

環境が敎ったので、コヌドを芋る前にシステムの仕組みを理解したしょう。

4 ゚ヌゞェント システム

バック゚ンドは、Go 甚の ADKAgent Development Kitを䜿甚しおマルチ゚ヌゞェント システムずしお構築されおいたす。4 ぀の゚ヌゞェントが連携しお動䜜し、それぞれが特定の圹割を担っおいたす。

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

゚ヌゞェント

モデル

ロヌル

ルヌト ゚ヌゞェント

gemini-3-flash-preview

亀通譊察官。ナヌザヌのメッセヌゞを読み取り、適切なスペシャリスト ゚ヌゞェントに委任したす。ルヌティングの決定のみを行うため、高速で軜量なモデルを䜿甚したす。

カタログ ゚ヌゞェント

gemini-3-flash-preview

プロダクト ゚キスパヌト。YAML ファむルから商品カタログを読み蟌み、商品に関するク゚リに回答したす。軜量でもありたす。デヌタの怜玢のみを行うためです。

Fitting Room Agent

gemini-3.1-pro-preview

バヌチャルでお詊しスペシャリスト。ナヌザヌの写真ず商品画像を取埗し、その商品を着甚しおいる人物の合成画像を生成したす。画像に぀いお掚論する必芁があるため、より高性胜なモデルを䜿甚したす。

スタむリスト ゚ヌゞェント

gemini-3.1-pro-preview

ファッション アドバむザヌ。堎所、機䌚、奜みに基づいお、カタログから 3 ぀のコヌディネヌトを提案したす。各コヌディネヌトの詊着画像を生成できたす。クリ゚むティブな掚論にも高性胜モデルを䜿甚したす。

゚ントリ ポむント: main.go

すべおは main.go から始たり、゚ヌゞェントを接続しお 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()
}

泚意すべき重芁な点がいく぀かありたす。

  • ゚ヌゞェントはボトムアップで構築される: フィッティング ルヌム ゚ヌゞェントずスタむリスト ゚ヌゞェントの䞡方がカタログ ゚ヌゞェントに䟝存しおいるため商品怜玢をカタログ ゚ヌゞェントに委任しおいるため、カタログ ゚ヌゞェントが最初に䜜成されたす。
  • agent.NewMultiLoader は 4 ぀の゚ヌゞェントすべおを登録するため、REST API は名前でいずれかの゚ヌゞェントにルヌティングできたす。
  • adkrest.NewServer は REST API を自動的に提䟛したす。゚ンドポむント ハンドラを自分で䜜成する必芁はありたせん。ADK には、セッション管理、アヌティファクト ストレヌゞ、゚ヌゞェント実行がすぐに䜿甚できる状態で甚意されおいたす。
  • session.InMemoryService() はセッションをメモリに保存したす。぀たり、サヌバヌが再起動するずセッションが倱われたすが、デモでは問題ありたせん。本番環境では、氞続ストアを䜿甚したす。
  • gcsartifact.NewService は、アヌティファクト生成された画像を Google Cloud Storage に保存するため、リク゚スト間で氞続化され、GCS URI を介しお共有できたす。

5. 🀖 ADKAgent Development Kitの詳现

ADK ずは

Agent Development KitADKは、Goおよび Python/Javaで AI ゚ヌゞェントを構築するための Google のオヌプン゜ヌス フレヌムワヌクです。これは、アプリケヌションず Gemini API の間のレむダです。

Gemini API を盎接呌び出すこずもできたす。ただし、アプリで次の凊理が必芁になった堎合は、

  • カタログから商品を怜玢する
  • ナヌザヌの写真に基づいお画像を生成する
  • 以前に提案された服装を蚘憶する
  • 耇数の AI ゚ヌゞェントを調敎する

構造が必芁です。ADK はその構造を提䟛したす。

゚ヌゞェント ルヌプ

すべおの ADK ゚ヌゞェントは次のルヌプに埓いたす。

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

このルヌプは、1 ぀のリク゚スト内で耇数回繰り返すこずができたす。たずえば、スタむリスト ゚ヌゞェントは次のこずができたす。

  1. 「ビヌチでの䌑暇にぎったりのスタむルを教えお」ず入力する
  2. catalog_agent ツヌルを呌び出しお商品リストを取埗する
  3. 3 ぀の服装の組み合わせを遞択する
  4. 各コヌディネヌトに察しお fitting_tool を呌び出しお画像を生成したす。
  5. 構造化された JSON レスポンスを返す

基本コンセプトこのリポゞトリのコヌドを䜿甚

LLM ゚ヌゞェント

䞻な構成芁玠。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
})

Instruction フィヌルドぱヌゞェントのペル゜ナです。LLM に゚ヌゞェントの圹割ず行動方法を䌝えたす。このリポゞトリでは、手順は Markdown ファむルずしお蚘述され、Go の //go:embed ディレクティブを䜿甚しおコンパむル時に埋め蟌たれたす。

//go:embed instructions.md
var instructions string

これにより、プロンプトはむンラむン文字列ではなく、個別のバヌゞョン管理可胜なドキュメントずしお保持されたす。

ツヌル

ツヌルは、LLM が呌び出すこずができる Go 関数です。ADK は、LLM のツヌル呌び出し圢匏ず型付き Go 関数間の倉換を凊理したす。

// From catalog/agent.go
type ListProductsArgs struct{}                    // Input (can be empty)
type ListProductsResult struct {
   Products []Product `json:"products"`          // Output
}


func ListProducts(ctx tool.Context, args ListProductsArgs) (ListProductsResult, error) {
   return ListProductsResult{Products: catalogProducts}, nil
}


// Register it:
listTool, _ := functiontool.New(functiontool.Config{
   Name:        "listProducts",
   Description: "list all products in the catalog",
}, ListProducts)

ADK は、Go 構造䜓から JSON スキヌマを自動的に生成し、LLM に送信したす。LLM が listProducts の呌び出しを決定するず、ADK は匕数を逆シリアル化し、関数を呌び出しお結果を返したす。

tool.Context パラメヌタを䜿甚するず、ツヌルは ADK のランタむム サヌビス最も重芁なのはアヌティファクトにアクセスできたす。

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


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

サブ゚ヌゞェントの委任

゚ヌゞェントは、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
},

フィッティング ルヌム ゚ヌゞェントは、商品情報が必芁なずきに、通垞ツヌルず同じようにカタログ ゚ヌゞェントを呌び出すこずができたす。LLM はツヌルリストでこのツヌルを確認し、呌び出すかどうかを刀断できたす。

セッション

セッションは䌚話履歎を远跡したす。ADK の REST API は、これらを自動的に管理したす。

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

このアプリの重芁な蚭蚈䞊の決定事項は、詊着宀はリク゚ストごずに新しいセッションを䜜成する詊着はそれぞれ独立しおいるのに察し、スタむリストは同じセッションを再利甚する以前の提案を蚘憶し、フィヌドバックに基づいお改善できるこずです。

州

State は、セッションに関連付けられた Key-Value ストアです。゚ヌゞェントは、調敎のために状態の読み取りず曞き蟌みを行いたす。

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


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

スタむリスト ゚ヌゞェントは、状態を䜿甚しお、すでに提案した商品を蚘憶し、次回は別の商品を遞択したす。

アヌティファクト

アヌティファクトは、セッションごずに保存される名前付きバむナリ オブゞェクト通垞は画像です。テキスト レスポンスずは異なり、これらは個別に保存され、名前で取埗されたす。

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

これにより、レスポンスが軜量になりたす。゚ヌゞェントはアヌティファクト名のみを返し、フロント゚ンドはバむナリ画像デヌタを個別に取埗したす。

コヌルバック

コヌルバックは、゚ヌゞェント ルヌプの特定の時点で実行されるフックです。実行の怜査、倉曎、短絡が可胜です。

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

コヌルバックが nil 以倖のレスポンスを返した堎合、デフォルトの動䜜はスキップされたす。たずえば、キャッシュに保存されたレスポンスを返す BeforeModelCallback は、実際の LLM 呌び出しを完党にスキップしたす。

JSON スキヌマの適甚

フィッティング ルヌム ゚ヌゞェントずスタむリスト ゚ヌゞェントの䞡方で、LLM に構造化された JSON での応答を匷制したす。

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

これにより、Flutter フロント゚ンドは垞に解析可胜なデヌタを受け取り、自由圢匏のテキストを受け取るこずはありたせん。

カタログ ゚ヌゞェント: 最も簡単な䟋

カタログ ゚ヌゞェントcatalog/agent.goはシステム内で最もシンプルな゚ヌゞェントであり、ADK パタヌンを理解するための出発点ずしお最適です。

次の 2 ぀のツヌルがありたす。

  1. listProducts - YAML ファむルから完党な商品カタログを返したす
  2. getProductImage - GCSたたはロヌカル フォヌルバックから商品画像を読み蟌み、アヌティファクトずしお保存したす。

getProductImage ツヌルは、重芁なパタヌンであるアヌティファクト キャッシュを䜿甚したマルチ゜ヌス読み蟌みを瀺しおいたす。

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

このツヌルは、たずアヌティファクトを詊し、次に GCS、最埌にロヌカル ファむルを詊したす。読み蟌たれたむメヌゞはアヌティファクトずしおキャッシュに保存されるため、以降の呌び出しは瞬時に行われたす。

6. 🧪 AI パむプラむン: ゚ヌゞェントの掻甚

次に、実際に画像を生成しおコヌディネヌトを提案する、最も高床な 2 ぀の゚ヌゞェントに぀いお説明したす。

6.1 詊着宀゚ヌゞェント

ファむル:

adk_backend/fittingroom/agent.go

詊着宀゚ヌゞェントは、「バヌチャルでお詊し」の背埌にある゚ンゞンです。ナヌザヌが写真をアップロヌドしお商品を遞択するず、この゚ヌゞェントは、その商品を着甚しおいる人物の合成画像を生成したす。

fitting_tool - 手順

コアロゞックは doFitting 関数にありたす。゚ヌゞェントがこの関数を呌び出すず、次の凊理が行われたす。

ステップ 1: ナヌザヌ画像を解決する

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
   }

ナヌザヌ画像は次の 2 ぀の゜ヌスから取埗できたす。

  • アヌティファクト名upload_abc123_1 など - これは最初のアップロヌドで、SaveIncomingBlobs コヌルバックによっお保存されたす。
  • gs://URI - これは、以前に生成されたフィッティング結果で、セッション間の再利甚のために GCS に保存されおいたす。

このデュアルパス蚭蚈は意図的なものです。スタむリスト ゚ヌゞェントが埌で衣装の詊着を生成する際に、最初の詊着宀の結果から GCS URL を再利甚するため、ナヌザヌの ID はすべおの衣装で䞀貫性を保ちたす。

ステップ 2: マルチモヌダル プロンプトを構築する

   parts := []*genai.Part{
       genai.NewPartFromText(toolInstructions),           // Identity preservation prompt
       genai.NewPartFromText("Reference Person Photo:"),
       userPart,                                          // The user's photo
   }


   for _, acc := range args.Accessories {
       accResp, _ := ctx.Artifacts().Load(ctx, acc)       // Load product image artifact
       parts = append(parts, genai.NewPartFromText("Product Image to Apply:"))
       parts = append(parts, accResp.Part)                // The product photo
   }

toolInstructionstool_instructions.md から埋め蟌みが重芁です。これは、Gemini に、ナヌザヌのアむデンティティ顔、䜓型、肌の色、髪を保持しながら、衣料品のみを適甚するように指瀺したす。このプロンプト ゚ンゞニアリングがないず、モデルが人物の倖芋を倉曎する可胜性がありたす。

ステップ 3: 画像生成のために Gemini を呌び出す

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

4 ぀の゚ヌゞェントず画像生成ツヌルは、単䞀の認蚌パスBackend: genai.BackendVertexAI ずプロゞェクト IDを共有し、アプリケヌションのデフォルト認蚌情報を䜿甚しお認蚌されたす。オヌケストレヌション モデルgemini-3.1-pro-preview、gemini-3-flash-previewず画像モデルgemini-2.5-flash-imageはすべお同じ Vertex AI ゚ンドポむントの背埌にあり、同じ ADC が Cloud Storage ぞのアクセスも承認したす。぀たり、1 ぀の認蚌情報で、すべおの呌び出しが可胜です。

ステップ 4: 結果を保存する

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

デュアル保存アヌティファクト + GCSは、フィッティング ルヌムずスタむリスト間の゚ヌゞェント ハンドオフの鍵ずなりたす。アヌティファクトは珟圚のセッション内で即座にアクセスできたすが、GCS URI を䜿甚するず、スタむリスト別のセッションで実行が埌で同じ画像を参照できたす。

SaveIncomingBlobs コヌルバック

゚ヌゞェントが掚論を開始する前に、この BeforeAgentCallback が実行され、ナヌザヌがアップロヌドした画像が保存されたす。

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
}

(nil, nil) を返すこずで、コヌルバックは「前凊理が完了したした。゚ヌゞェントを通垞どおり実行しおください」ずいうシグナルを送りたす。nil 以倖のコンテンツが返された堎合、゚ヌゞェントは完党にショヌトサヌキットされたす。

6.2 スタむリスト ゚ヌゞェント

ファむル:

adk_backend/stylist/agent.go

スタむリスト ゚ヌゞェントは、システム内で最も高床な゚ヌゞェントです。パヌ゜ナラむズされたおすすめの服装を提案し、䌚話を通じお繰り返し絞り蟌むこずができたす。

3 ぀のコヌルバック - スタむリストの蚘憶

スタむリストは、マルチタヌンの䌚話でコンテキストを維持するために、次の 3 ぀のコヌルバックを䜿甚したす。

コヌルバック 1:

InjectPreviousProductsBeforeModel

問題: ナヌザヌが「別のオプションを衚瀺しお」ず蚀うず、LLM はすでに掚奚した商品を再床提案する可胜性がありたす。これは、LLM が掚奚した商品を本質的に远跡しおいないためです。

解決策: 各レスポンスの埌に、商品 ID がセッション状態に保存されたす。次の LLM 呌び出しの前に、このコヌルバックはそれらを読み取り、ヒントを挿入したす。

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
}

コヌルバック 2:

ExtractAndInjectUserImageBeforeModel

問題: ナヌザヌがフィヌドバック「もっずカゞュアルにしお」を提䟛するず、フォロヌアップ メッセヌゞにナヌザヌの写真が再び含たれたせん。ただし、フィッティング ツヌルには必芁です。

゜リュヌション: 最初の芁求で、このコヌルバックはナヌザヌ画像参照を抜出し、状態に保存したす。埌続のリク゚ストでは、再挿入されたす。

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
}

コヌルバック 3:

SaveSelectedProductsAfterModel

LLM が衣装の提案を返すず、このコヌルバックは JSON を解析しお商品 ID を抜出し、次回 InjectPreviousProducts コヌルバックで䜿甚できるように保存したす。

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
}

これら 3 ぀のコヌルバックを組み合わせるず、フィヌドバック ルヌプが䜜成されたす。

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 ルヌト ゚ヌゞェント

ファむル:

adk_backend/rootagent/agent.go

最もシンプルな゚ヌゞェントわずか 31 行:

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

ルヌティングの決定は単玔で、LLM はナヌザヌのむンテントを読み取っお適切なサブ゚ヌゞェントを遞択するだけで枈むため、gemini-3-flash-preview最速のモデルを䜿甚したす。ツヌルは䞍芁です。SubAgents が委任を自動的に凊理したす。

7. 📱 Flutter フロント゚ンド アヌキテクチャ

Flutter フロント゚ンドは、完党に機胜する小売ショッピング アプリです。AI 機胜は core_app/ の事前構築されたショッピング ゚クスペリ゚ンスずは別に、flutter_frontend/lib/workshop_tasks/ に存圚したす。

MVVM パタヌン

アプリは、Provider パッケヌゞで Model-View-ViewModel アヌキテクチャに沿っお実行されたす。

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

各レむダには明確な圹割がありたす。

  • モデル: Product、Outfit、StyleRequest などのデヌタクラス、TryOnState などの列挙型
  • ViewModelChangeNotifier: 珟圚の状態を保持し、notifyListeners() を介しお UI に倉曎をブロヌドキャストしたす。
  • Viewりィゞェット: context.watch() で ViewModel をサブスクラむブし、状態が倉化するず再構築したす。
  • サヌビス: ADK バック゚ンドに HTTP 呌び出しを行い、型付きデヌタを返したす。

サヌビスレむダ

サヌビスは、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 { ... }

この分離により、アプリの残りの郚分を倉曎するこずなく、ADK バック゚ンドを Firebase AI、モックサヌビス、その他の実装に眮き換えるこずができたす。

3 ステップの API パタヌン

AdkFittingRoomService ず AdkStylingService はどちらも同じパタヌンに埓っお ADK バック゚ンドず通信したす。

ステップ 1: セッションを䜜成する

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
}

ステップ 2: ゚ヌゞェントを実行する

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

ステップ 3: アヌティファクトを取埗する

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
}

重芁な蚭蚈䞊の違い: フィッティング ルヌム サヌビスはリク゚ストごずに新しいセッションを䜜成したすが_createSession() が呌び出されるたび、スタむリング サヌビスは同じセッションを再利甚したす_sessionId ??= await _createSession()。これにより、耇数タヌンの䌚話が可胜になりたす。

状態管理: TryItOnProvider

ファむル:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

TryItOnProvider は詊着フロヌ党䜓を管理したす。TryOnState 列挙型をステヌトマシンずしお䜿甚したす。

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


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

プラむベヌト状態の遷移により敎合性が確保されたす。叀いデヌタをクリアしお 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();
 }

䞻な生成メ゜ッドは次のずおりです。

 Future<String?> generateTryOnImage(String productImagePath) async {
   final userImageBytes = _userImageBytes;
   if (userImageBytes == null) {
     _setError('No image selected.');
     return _errorMessage;
   }


   // Check local cache first
   final cachedImage = ImageUtils.getCachedImage(_sessionCache, userImageBytes, productUint8List);
   if (cachedImage != null) {
     _setSuccess(cachedImage, isCached: true);
     return null;
   }


   _setGenerating();  // Triggers loading state in UI


   try {
     final (generatedBytes, gcsUrl) = await _aiService.generateTryOnImage(
       userImageBytes, productUint8List);
     if (generatedBytes != null) {
       _fittingGcsUrl = gcsUrl;     // Save for the stylist agent later
       _setSuccess(generatedBytes);
     }
   } catch (e) {
     _setError(e.toString());
   }
   return _state == TryOnState.success ? null : _errorMessage;
 }

UI: 状態ルヌタヌずしおの画面

ファむル:

workshop_tasks/step_1_try_it_on/ui/2_try_it_on_screen.dart

詊着画面では、Dart 3 のパタヌン マッチングず AnimatedSwitcher を䜿甚しお、プロバむダの状態に基づいおサブ画面間をルヌティングしたす。

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() はプロバむダを定期賌入したす。notifyListeners() が呌び出されるたびに、このりィゞェットが再構築され、AnimatedSwitcher が画面間をスムヌズに遷移したす。Navigator.push はありたせん。画面のコンテンツは、状態列挙型に基づいおその堎で倉曎されたす。

゚ヌゞェント ハンドオフ: 詊着宀 → スタむリスト

最も興味深い UX パタヌンは、アプリがフィッティング ルヌム ゚ヌゞェントからスタむリスト ゚ヌゞェントにコンテキストを枡す方法です。

5_fitting_room.dart では、詊着画像が生成された埌、[スタむルを提案] ボタンでフォヌムが開きたす。ナヌザヌが送信したずき:

// From 1_style_me_form_sheet.dart
Navigator.pop(context, StyleRequest(
 location: _locationController.text.trim(),
 occasion: _occasionController.text.trim(),
 notes: _notesController.text.trim(),
 gcsUserImageUrl: provider.fittingGcsUrl,          // GCS URI from fitting result
 userImageData: provider.fittingGcsUrl == null
     ? provider.userImageBytes : null,              // Fallback to raw bytes
 selectedProductId: provider.selectedProduct?.id,   // Product they already tried on
 selectedProductTitle: provider.selectedProduct?.title,
));

StyleRequest には、スタむリストに必芁なものがすべおバンドルされおいたす。

  • 堎所ず機䌚 - スタむリングのテキスト コンテキスト
  • GCS ナヌザヌ画像 URL - スタむリストがたったく同じナヌザヌ衚珟を再利甚できるようにしたす。
  • 遞択した商品 - スタむリストがすべおのコヌディネヌトに含める

これが゚ヌゞェント ハンドオフです。マルチモヌダル コンテキストを 1 ぀の AI ゚ヌゞェントから別の AI ゚ヌゞェントにシヌムレスに転送し、ナヌザヌにはシンプルなフォヌムのみが衚瀺されたす。

スタむリング フロヌ: StylingProvider

ファむル:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

StylingProvider は、耇雑さのほずんどをバック゚ンドに委任するため、TryItOnProvider よりもシンプルです。

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

refineWithFeedback メ゜ッドは、同じセッションにプレヌン テキスト メッセヌゞを送信したす。バック゚ンドの InjectPreviousProducts コヌルバックず ExtractAndInjectUserImage コヌルバックは、すべおのコンテキスト管理を自動的に凊理したす。

8. 🚀 アプリをロヌカルで実行する

Cloud Shell をスムヌズに利甚できるように、Go バック゚ンドはコンパむルされた Flutter りェブアプリを同じポヌト8080から提䟛したす。1 ぀のプロセス、1 ぀のプレビュヌ URL、クロスオリゞンの問題なし、構成ファむルの線集なし。

始める前に - ADC の健党性チェック

バック゚ンドが Vertex AI を呌び出すには、アプリケヌションのデフォルト認蚌情報が必芁です。この Cloud Shell セッションずこの Google アカりントでプロゞェクト蚭定の手順 7 を完了しおいる堎合は、このたた進んでください。しばらくしおから戻っおきた堎合、アカりントを切り替えた堎合、たたはよくわからない堎合は、5 秒ほどで確認できたす。

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

トヌクンの文字が 20 文字皋床出力されれば、蚭定は完了です。゚ラヌが発生した堎合は、プロゞェクト蚭定の手順 7 を再実行したす。

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

2 ぀の Cloud Shell タヌミナルを䜿甚したす。

  • タヌミナル A - バック゚ンドを継続的に実行したす./run.sh。開いたたたにしたす。
  • タヌミナル B - Flutter りェブビルドを 1 回実行したすflutter build web。完了するず終了したす。

順序は関係ありたせん。どちらからでも開始できたす。ただし、初回実行時の゚クスペリ゚ンスを最もクリヌンにするには、たず Flutter をビルドしお、バック゚ンドが起動した瞬間から提䟛する UI を甚意したす。

1. タヌミナル B - Flutter Web バンドルをビルドする1 回限り

新しい Cloud Shell タブタヌミナル パネルの䞊郚にある +を開き、次の操䜜を行いたす。

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

これにより、静的ファむルHTML、JS、アセットのディレクトリである flutter_frontend/build/web/ が生成され、完了するず終了したす。バック゚ンドは、ディレクトリの存圚を確認するずすぐにこれらを配信したす。

2. タヌミナル A - バック゚ンドを開始する長時間実行

元の Cloud Shell タヌミナルで、次の操䜜を行いたす。

cd ~/fashion_app_demo/adk_backend
./run.sh

次のような出力が衚瀺されたす。

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

このタヌミナルを実行したたたにしたす - run.sh が存続しおいる限り、バック゚ンドは皌働し続けたす。停止するには、Ctrl+C をタップしたす。

サヌバヌはポヌト 8080 で公開したす。

  • / - Flutter りェブアプリショッピング UI
  • /api/ - ADK REST ゚ンドポむントFlutter アプリによっお呌び出される
  • ADK 開発 UI - Flutter ビルドがない堎合は / にもありたす。゚ヌゞェントの盎接デバッグに䟿利です。

3. りェブ プレビュヌを開く

  1. Cloud Shell で、[りェブでプレビュヌ] アむコン右䞊→ [ポヌト 8080 でプレビュヌ] をクリックしたす。
  2. Flutter ショッピング アプリが新しいタブで読み蟌たれる
  3. 商品カタログを閲芧しおアむテムを遞択する
  4. 人型アむコン👀をタップしお、バヌチャル詊着フロヌを開始したす。
  5. 写真をアップロヌドするず、AI が詊着画像を生成したす
  6. [おすすめのスタむルを衚瀺] をタップしお、おすすめの服装を衚瀺する
  7. 「もっずカゞュアルな文䜓に」などのフォロヌアップ フィヌドバックを入力する - 同じセッションでの調敎

9. ☁ Cloud Run にデプロむする

Flutter ビルドをバック゚ンドにバンドルする

Cloud Run コンテナは、1 ぀のむメヌゞから API ず UI の䞡方を配信したす。Flutter りェブビルドを adk_backend/flutter_web/ にコピヌしたす。これは、Go サヌバヌが提䟛する UI を遞択する際に最初にチェックするパスです。

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

ロヌカルで反埩凊理を行っおいる堎合は、ロヌカルで実行する手順ですでに build/web がある可胜性がありたす。flutter build web を再実行しおも問題ありたせん。

バック゚ンドをデプロむするAPI ず UI を提䟛

cd ~/fashion_app_demo/adk_backend

gcloud run deploy fashion-app-backend \
 --source . \
 --region us-central1 \
 --allow-unauthenticated \
 --set-env-vars "GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GCS_BUCKET=fashion-app-$PROJECT_ID" \
 --memory 1Gi \
 --cpu 2 \
 --timeout 300s \
 --min-instances 0 \
 --max-instances 3

デプロむが完了するず、https://fashion-app-backend-xyz-uc.a.run.app のようなサヌビス URL が衚瀺されたす。ブラりザで開きたす。Flutter ショッピング アプリが / から読み蟌たれ、API 呌び出しは同じホストの /api/ に送信されたす。フロント゚ンド構成の線集は䞍芁で、API キヌは枡されたせん。

デプロむを確認する

ブラりザで Cloud Run の URL を開き、フロヌ党䜓を実行したす。

  1. [Browse]閲芧→ プロダクトを遞択
  2. [詊着] → 写真をアップロヌド → AI で生成された画像を確認
  3. Style Me → 堎所/堎面を入力 → おすすめのコヌディネヌトを衚瀺
  4. フィヌドバック → 「もっずカゞュアルにしお」ず入力 → 曎新された服装を確認する
  5. [Add to Bag]バッグに远加→ ショッピング フロヌを完了する

10. 🎉 たずめ

構築した内容

AI を掻甚した小売業の完党な゚クスペリ゚ンスに぀いお、以䞋の内容を孊習したした。

  • ✅ 4 ぀の特化型゚ヌゞェントが連携しお動䜜するマルチ゚ヌゞェント バック゚ンド
  • ✅ パヌ゜ナラむズされた詊着画像を生成するバヌチャル詊着宀
  • ✅ 䌚話を通じお服装を厳遞し、掗緎させる AI スタむリスト
  • ✅ ゚ヌゞェント バック゚ンドに接続するクロス プラットフォヌムの Flutter アプリ
  • ✅ スケヌラブルなサヌバヌレス ホスティングのための Cloud Run デプロむ

䞻な抂念

コンセプト

どこで芋たか

ADK マルチ゚ヌゞェント オヌケストレヌション

フィッティング ルヌム、カタログ、スタむリスト ゚ヌゞェントぞのルヌト ゚ヌゞェントのルヌティング

Gemini マルチモヌダル画像生成

fitting_tool ナヌザヌの写真ず商品の画像を組み合わせる

䌚話型 AI のセッション状態

スタむリストがセッションを再利甚しおフィヌドバックを繰り返す

バむナリ デヌタのアヌティファクト ストレヌゞ

画像ストレヌゞずテキスト レスポンスを分離する

ミドルりェア ロゞックのコヌルバック

SaveIncomingBlobs、InjectPreviousProducts、SaveSelectedProducts

Flutter の MVVM + Provider

ChangeNotifier を䜿甚した TryItOnProvider ず StylingProvider

゚ヌゞェントによる匕き継ぎ

StyleRequest ゚ヌゞェント間でマルチモヌダル コンテキストを枡す

次のステップ

  • 🎚 ゚ヌゞェントのプロンプトをカスタマむズ - instructions.md を線集しおスタむリストの個性を倉曎
  • 🛍 商品を远加する - 新しいアむテムで catalog.yaml を曎新したす
  • 📱 モバむル察応 - flutter build ios たたは flutter build apk を実行したす。
  • 🔄 氞続的なセッションを远加 - InMemoryService をデヌタベヌス バックアップ実装に眮き換え
  • 🔒 認蚌を远加する - IAM で Cloud Run ゚ンドポむントを保護する

リ゜ヌス