👗 Создайте виртуальную примерочную и ИИ-стилиста с помощью Flutter, ADK Go и Gemini

1. Введение

Что вы построите

В этом практическом занятии вы примерите на себя роль разработчика, создающего Fashion App — приложение для покупок на Flutter для вымышленного розничного бренда. Ваша задача: добавить две функции на основе искусственного интеллекта, которые преобразят опыт онлайн-шопинга.

  1. Виртуальная примерочная — пользователь загружает свою фотографию, выбирает предмет одежды и видит сгенерированное искусственным интеллектом изображение себя в этом предмете.
  2. Искусственный интеллект-стилист — на основе местоположения пользователя, случая и стилевых предпочтений, агент с искусственным интеллектом подбирает готовые варианты нарядов, которые пользователь может уточнить в ходе беседы.

Идея проста: когда люди примеряют одежду в примерочной, они с гораздо большей вероятностью её покупают. А в интернете? Это просто гадание. Этот проект с помощью искусственного интеллекта решает эту проблему.

Архитектура вкратце

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

Основные технологии

Компонент

Технологии

Цель

Агентская структура

ADK (Agent Development Kit for Go)

Многоагентная оркестровка, сессии, артефакты

Логическое мышление агента (Pro)

Gemini 3.1 Pro Preview

Обеспечивает работу примерочных и стилистов.

Логическое мышление агента (Flash)

Предварительный просмотр Gemini 3 Flash

Обеспечивает работу корневого и каталогового агентов (легковесная маршрутизация/поиск).

Генерация изображений

Изображение со вспышкой Gemini 2.5

Создает изображения для примерки и комплектов одежды.

Внешний интерфейс

Флаттер (Дротик)

Кроссплатформенное приложение (веб, iOS, Android)

Хранилище

Google Облачное хранилище

Сохраняет изображения товаров и созданные артефакты.

Хостинг

Cloud Run

развертывание бессерверных контейнеров

2. 📦 Предварительные условия и настройка Cloud Shell

1. Откройте редактор Cloud Shell.

👉 Откройте редактор Cloud Shell в своем браузере.

Если терминал не отображается внизу экрана:

  • Нажмите «Вид»«Терминал».

2. Настройка Flutter SDK

В Cloud Shell Flutter предустановлен по адресу /google/flutter . Поскольку этот каталог принадлежит другому системному пользователю, при первом запуске flutter вы столкнетесь с ошибкой fatal: detected dubious ownership . Добавьте его в список безопасных каталогов Git один раз:

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

Убедитесь, что Flutter находится в переменной PATH и работает:

flutter --version

При первом запуске загружается SDK Dart и собирается инструмент Flutter — подождите минуту. Вы должны увидеть что-то вроде 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 (что часто встречается при истекшем бесплатном пробном периоде), учетная запись закрыта, и вы ничего не сможете оплатить — перед продолжением перейдите к разделу устранения неполадок ниже.

Скопируйте ACCOUNT_ID учетной записи с OPEN: True (выглядит как 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 использует технологию генерации изображений Gemini через Vertex AI.

storage.googleapis.com

Облачное хранилище — хранит изображения из каталога товаров и результаты примерки.

run.googleapis.com

Cloud Run — размещает бэкэнд в виде бессерверного контейнера.

cloudbuild.googleapis.com

Cloud Build — создает образы Docker из исходного кода.

artifactregistry.googleapis.com

Реестр артефактов — хранит созданные образы 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. 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)

Убедитесь в исправности АЦП:

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

Вы должны увидеть токен длиной примерно в 20 символов, за которым следует ... . Если возникнет ошибка, значит, вход в систему не удался — повторите шаг 1.

4. 🏗️ Обзор архитектуры

Теперь, когда среда готова, давайте разберемся, как работает система, прежде чем переходить к коду.

Четырехагентная система

Бэкенд построен как многоагентная система с использованием ADK (Agent Development Kit) для Go. Четыре агента работают вместе, каждый со своей определённой обязанностью:

                   ┌──────────────┐
                    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-файла и отвечает на запросы по товарам. К тому же, он очень простой — просто ищет данные.

Агент примерочной

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 регистрирует все четыре агента, чтобы REST API мог направлять запросы к любому из них по имени.
  • adkrest.NewServer автоматически предоставляет REST API — вам не нужно самостоятельно писать обработчики конечных точек. ADK обеспечивает управление сессиями, хранение артефактов и выполнение агентов «из коробки».
  • session.InMemoryService() сохраняет сессии в памяти. Это означает, что сессии теряются при перезапуске сервера, что вполне приемлемо для демонстрации. В производственной среде следует использовать постоянное хранилище.
  • gcsartifact.NewService хранит артефакты (сгенерированные изображения) в Google Cloud Storage, поэтому они сохраняются между запросами и могут быть переданы через URI GCS.

5. 🤖 Подробный анализ ADK (комплекта разработки агентов)

Что такое ADK?

Agent Development Kit (ADK) — это фреймворк с открытым исходным кодом от Google для создания агентов искусственного интеллекта на языке Go (а также Python/Java). Он является связующим звеном между вашим приложением и API Gemini.

Вы можете напрямую вызывать API Gemini. Но как только вашему приложению потребуется:

  • Найдите товары в каталоге
  • Генерировать изображения на основе фотографий пользователя.
  • Вспомните, какие наряды были предложены ранее.
  • Координировать действия нескольких агентов ИИ.

Вам нужна структура. 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. Получите "Создайте свой стиль для пляжного отдыха"
  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

Это позволяет сохранять подсказки в виде отдельных, версионируемых документов, а не в виде встроенных строк.

Инструменты

Инструменты — это функции Go, которые может вызывать LLM. 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 автоматически генерирует JSON-схему из ваших структур Go и отправляет её в 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
},

Когда сотруднику примерочной требуется информация о товаре, он может обратиться к сотруднику, отвечающему за каталог, как к обычному инструменту. Сотрудник, отвечающий за логистику, видит эту информацию в списке инструментов и может принять решение о её использовании.

Сессии

Сессии отслеживают историю переписки. REST API ADK автоматически управляет ими:

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

Ключевое дизайнерское решение в этом приложении: примерочная создает новую сессию для каждого запроса (каждая примерка независима), в то время как стилист использует одну и ту же сессию (что позволяет запоминать предыдущие предложения и корректировать их на основе отзывов).

Состояние

Состояние — это хранилище типа «ключ-значение», привязанное к сессии. Агенты считывают и записывают состояние для координации действий:

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

Если функция обратного вызова возвращает ненулевой ответ, поведение по умолчанию пропускается. Например, функция BeforeModelCallback , возвращающая кэшированный ответ, полностью пропустит фактический вызов LLM.

Применение JSON-схемы

Как агенты примерочной, так и стилисты заставляют LLM отвечать в структурированном формате JSON:

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

Это гарантирует, что интерфейс Flutter всегда будет получать данные, пригодные для анализа, а не произвольный текст.

Агент по каталогам: простейший пример

Агент каталога ( catalog/agent.go ) — это самый простой агент в системе, хорошая отправная точка для понимания шаблонов ADK.

В нём есть два инструмента:

  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. 🧪 Конвейер обработки данных ИИ: агенты в действии

Теперь давайте рассмотрим двух самых продвинутых агентов — тех, кто непосредственно генерирует изображения и подбирает наряды.

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
   }

Образ пользователя может формироваться из двух источников:

  • Имя артефакта (например, upload_abc123_1 ) — это первоначальная загрузка, сохраняемая функцией обратного вызова SaveIncomingBlobs
  • URI gs gs:// — это ранее сгенерированный результат подгонки, сохраненный в GCS для повторного использования в разных сессиях.

Такая двухканальная структура является преднамеренной: когда стилист позже создает примерки одежды, он повторно использует URL-адрес GCS из первоначального результата примерки, чтобы идентификация пользователя оставалась неизменной для всех образов.

Шаг 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
       })

Все четыре агента и инструмент генерации образов используют единый путь аутентификации: Backend: genai.BackendVertexAI с идентификатором проекта, аутентификация осуществляется через учетные данные приложения по умолчанию. Модели оркестрации ( gemini-3.1-pro-preview , gemini-3-flash-preview ) и модель образов ( gemini-2.5-flash-image ) находятся за одной и той же конечной точкой Vertex AI, и тот же ADC также авторизует доступ к облачному хранилищу — одни учетные данные для каждого вызова.

Шаг 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) является ключом к передаче информации между примерочной и стилистом. Артефакт обеспечивает немедленный доступ в рамках текущей сессии, а URI GCS позволяет стилисту (работающему в другой сессии) ссылаться на то же изображение позже.

Функция обратного вызова 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) , функция обратного вызова сигнализирует: «Предварительная обработка завершена — теперь запускайте агента в обычном режиме». Если бы она возвращала ненулевое содержимое, это полностью бы прервало работу агента.

6.2 Агент-стилист

Файл:

adk_backend/stylist/agent.go

Стилист-агент — самый продвинутый специалист в системе. Он подбирает персонализированные рекомендации по выбору одежды и поддерживает итеративное совершенствование образов посредством общения.

Три отсылки — Память стилиста

Стилист использует три обратных вызова для поддержания контекста в многоэтапных диалогах:

Обратный вызов 1:

InjectPreviousProducts (BeforeModel)

Проблема: если пользователь говорит «покажите мне другие варианты», LLM может снова предложить те же самые товары, поскольку он по своей сути не отслеживает то, что уже рекомендовал.

Решение: После каждого ответа идентификаторы продуктов сохраняются в состоянии сессии. Перед следующим вызовом 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 для извлечения идентификаторов товаров и сохраняет их для использования функцией обратного вызова 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
}

Вместе эти три функции обратного вызова создают цикл обратной связи:

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

В нем используется gemini-3-flash-preview (самая быстрая модель), поскольку решения по маршрутизации просты — LLM нужно лишь прочитать намерение пользователя и выбрать подходящего субагента. Никаких дополнительных инструментов не требуется; SubAgents автоматически обрабатывает делегирование.

7. 📱 Архитектура фронтенда Flutter

Фронтенд на Flutter представляет собой полнофункциональное приложение для розничной торговли. Функции искусственного интеллекта находятся в flutter_frontend/lib/workshop_tasks/ , отдельно от встроенного интерфейса покупок в core_app/ .

Паттерн MVVM

Приложение использует архитектуру Model-View-ViewModel с пакетом 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                                                 
└──────────────────┘    └────────────────────┘    └──────────────────┘

Каждый слой выполняет свою четкую роль:

  • Модель : Классы данных, такие как Product , Outfit , StyleRequest , и перечисления, такие как TryOnState
  • ViewModel ( ChangeNotifier ): хранит текущее состояние и передает изменения в пользовательский интерфейс через notifyListeners()
  • Виджет (View): Подписывается на ViewModel с помощью context.watch () и перестраивается при изменении состояния
  • Сервис : выполняет HTTP-запросы к бэкэнду ADK и возвращает типизированные данные.

Уровень сервисов

Сервисы определены как абстрактные интерфейсы с реализациями, специфичными для 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, фиктивный сервис или любую другую реализацию, не меняя остальную часть приложения.

Трехшаговый шаблон 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() ), что позволяет вести диалог в несколько этапов.

Управление состоянием: поставщик услуг TryItOn

Файл:

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;

Приватные переходы состояния обеспечивают согласованность — вы никогда не обновляете состояние, не очистив при этом устаревшие данные и не уведомив пользовательский интерфейс:

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

Пользовательский интерфейс: экраны в качестве маршрутизатора состояний.

Файл:

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 после генерации изображения для примерки кнопка "Style Me" открывает форму. Когда пользователь отправляет форму:

// 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 входит все необходимое для стилиста:

  • Место и повод — текстовый контекст для оформления.
  • URL-адрес изображения пользователя в GCS — чтобы стилист мог повторно использовать точно такое же изображение пользователя.
  • Выбранный продукт — чтобы стилист включал его в каждый образ.

Это и есть передача информации от одного ИИ-агента другому — плавный перенос многомодального контекста, при этом пользователь видит лишь простую форму.

Процесс создания стиля: Поставщик стилей

Файл:

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). Один процесс, один URL для предварительного просмотра, никаких проблем с междоменными запросами, никакого редактирования конфигурационных файлов.

Прежде чем начать — проверьте адекватность ADC.

Для вызова Vertex AI бэкэнду необходимы учетные данные приложения по умолчанию . Если вы выполнили шаг 7 настройки проекта в этом сеансе Cloud Shell и в этой учетной записи Google, все в порядке. Если вы возвращаетесь после перерыва, смены учетной записи или не уверены, потратьте 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)

Вам понадобятся два терминала Cloud Shell :

  • Терминал A — запускает бэкенд непрерывно ( ./run.sh ). Оставьте его открытым.
  • Терминал B — запускает сборку веб-приложения Flutter один раз ( flutter build web ). Завершает работу после завершения.

Порядок запуска не имеет значения — вы можете запустить любой из них первым. Но для наиболее чистого взаимодействия при первом запуске лучше сначала собрать Flutter, чтобы бэкенд имел готовый пользовательский интерфейс с момента запуска.

1. Терминал B — Сборка веб-пакета Flutter (одноразовое задание)

Откройте новую вкладку Cloud Shell (значок «+» в верхней части панели терминала), затем:

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

В результате создается каталог flutter_frontend/build/web/ — каталог со статическими файлами (HTML, JS, ресурсы) — и процесс завершается. Бэкенд начнет обслуживать эти файлы, как только обнаружит существование каталога.

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 (пользовательский интерфейс для покупок)
  • /api/ — REST-конечные точки ADK (вызываемые приложением Flutter)
  • ADK Dev UI — также доступен в / , когда нет сборки Flutter; полезен для прямой отладки агента.

3. Открыть предварительный просмотр веб-страницы

  1. В Cloud Shell нажмите значок « Предварительный просмотр веб-страниц» (в правом верхнем углу) → «Предварительный просмотр на порту 8080».
  2. Приложение для покупок Flutter загружается в новой вкладке.
  3. Просмотрите каталог продукции и выберите нужный товар.
  4. Нажмите на значок человека (👤), чтобы начать примерку.
  5. Загрузите фотографию и наблюдайте, как искусственный интеллект создаст изображение для примерки.
  6. Нажмите «Стиль», чтобы получить рекомендации по выбору наряда.
  7. В качестве дополнительной обратной связи используйте фразу «сделайте общение более неформальным» — это позволит внести коррективы в ходе одной сессии.

9. ☁️ Развертывание в облаке

Включите Flutter Build в бэкэнд.

Контейнер Cloud Run содержит как API, так и пользовательский интерфейс из одного образа. Скопируйте сборку Flutter web в папку adk_backend/flutter_web/ — это первый путь, который проверяет сервер Go при выборе того, какой пользовательский интерфейс обслуживать:

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 и пользовательский интерфейс).

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

После завершения развертывания вы получите URL-адрес сервиса, например, https://fashion-app-backend-xyz-uc.a.run.app . Откройте его в браузере — приложение Flutter для покупок загрузится из / , а вызовы API будут осуществляться через /api/ на том же хосте. Никаких изменений в конфигурации фронтенда не требуется, ключ API не передается.

Проверьте развертывание.

Откройте URL-адрес Cloud Run в браузере и пройдите весь процесс:

  1. Просмотреть → Выбрать товар
  2. Примерка → Загрузите свою фотографию → Посмотрите изображение, сгенерированное ИИ
  3. Style Me → Укажите место/событие → Посмотрите подборку образов
  4. Обратная связь → Напишите «сделать более повседневным» → Смотрите обновленные образы
  5. Добавить в корзину → Завершить процесс покупки

10. 🎉 Заключение

Что ты построил

Вы изучили полноценный опыт розничной торговли с использованием искусственного интеллекта, включающий в себя:

  • Многоагентная система управления с 4 специализированными агентами, работающими вместе.
  • Виртуальная примерочная , генерирующая персонализированные изображения для примерки.
  • Искусственный интеллект-стилист , который подбирает и совершенствует образы посредством диалога.
  • Кроссплатформенное Flutter-приложение , подключающееся к бэкэнду агента.
  • Развертывание Cloud Run для масштабируемого бессерверного хостинга

Ключевые понятия

Концепция

Где вы это видели

ADK многоагентная оркестрация

Маршрутизация корневых агентов к агентам в примерочных, каталогах и стилистам.

Gemini мультимодальная генерация изображений

fitting_tool объединяющий фотографии пользователей с изображениями товаров.

Состояние сессии для разговорного ИИ

Стилист повторно использует сессии для получения итеративной обратной связи.

Хранилище артефактов для бинарных данных

Разделение хранения изображений и текстовых ответов.

Обратные вызовы для логики промежуточного ПО

SaveIncomingBlobs , InjectPreviousProducts , SaveSelectedProducts

MVVM + Провайдер во Flutter

TryItOnProvider и StylingProvider с ChangeNotifier

Передача полномочий агенту

StyleRequest передает многомодальный контекст между агентами.

Следующие шаги

  • 🎨 Настройте подсказки для агента — отредактируйте файл instructions.md , чтобы изменить характер стилиста.
  • 🛍️ Добавьте больше товаров — обновите catalog.yaml , добавив новые позиции.
  • 📱 Создайте сборку для мобильных устройств — запустите flutter build ios или flutter build apk
  • 🔄 Добавьте постоянные сессии — замените InMemoryService реализацией на основе базы данных.
  • 🔒 Добавьте аутентификацию — защитите конечную точку Cloud Run с помощью IAM.

Ресурсы