👗 با Flutter، ADK Go و Gemini یک اتاق پرو مجازی و آرایشگر هوش مصنوعی بسازید

۱. مقدمه

آنچه خواهید ساخت

در این آزمایشگاه کد، شما در نقش یک توسعه‌دهنده قرار خواهید گرفت که در حال ساخت Fashion App ، یک اپلیکیشن خرید Flutter برای یک برند خرده‌فروشی خیالی، است. ماموریت شما: اضافه کردن دو ویژگی مبتنی بر هوش مصنوعی که تجربه خرید آنلاین را متحول می‌کنند.

  1. اتاق پرو مجازی - کاربر عکسی از خود آپلود می‌کند، یک لباس را انتخاب می‌کند و تصویری از خود که توسط هوش مصنوعی تولید شده و آن لباس را پوشیده است، می‌بیند.
  2. متخصص مد هوش مصنوعی - بر اساس موقعیت مکانی، مناسبت و ترجیحات سبک کاربر، یک عامل هوش مصنوعی توصیه‌های کاملی در مورد لباس ارائه می‌دهد - و کاربر می‌تواند آنها را از طریق مکالمه اصلاح کند.

ایده ساده است: وقتی مردم لباس‌ها را در اتاق پرو پرو می‌کنند، احتمال خرید آنها بسیار بیشتر است. اما آنلاین؟ فقط حدس می‌زنید. این پروژه این شکاف را با هوش مصنوعی پر می‌کند.

معماری در یک نگاه

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

فناوری‌های اصلی

کامپوننت

فناوری

هدف

چارچوب عامل

ADK (کیت توسعه عامل) برای Go

هماهنگی چندعاملی، جلسات، مصنوعات

استدلال عامل (حرفه‌ای)

پیش‌نمایش Gemini 3.1 Pro

به اتاق پرو و ​​​​مدیران استایلیست قدرت می‌دهد

استدلال عامل (فلش)

پیش‌نمایش فلش جمینی ۳

عامل‌های ریشه و کاتالوگ (مسیریابی/جستجوی سبک) را تقویت می‌کند

تولید تصویر

تصویر فلش Gemini 2.5

تصاویر پرو و ​​لباس را تولید می‌کند

ظاهر (فرانت‌اند)

فلوتر (دارت)

اپلیکیشن چند پلتفرمی (وب، iOS، اندروید)

ذخیره‌سازی

فضای ذخیره‌سازی ابری گوگل

تصاویر محصول و مصنوعات تولید شده را ذخیره می‌کند

میزبانی وب

اجرای ابری

استقرار کانتینر بدون سرور

۲. 📦 پیش‌نیازها و راه‌اندازی پوسته ابری

۱. ویرایشگر Cloud Shell را باز کنید

👉 ویرایشگر Cloud Shell را در مرورگر خود باز کنید.

اگر ترمینال در پایین صفحه نمایش داده نشد:

  • روی مشاهدهترمینال کلیک کنید

۲. راه‌اندازی SDK فلاتر

Cloud Shell با Flutter از پیش نصب شده در /google/flutter ارائه می‌شود. از آنجا که آن دایرکتوری متعلق به یک کاربر سیستم متفاوت است، اولین باری که flutter اجرا می‌کنید، با خطای fatal: detected dubious ownership مواجه خواهید شد. آن را یک بار به لیست دایرکتوری‌های امن گیت اضافه کنید:

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

تأیید کنید که Flutter در PATH شما قرار دارد و کار می‌کند:

flutter --version

اولین اجرا، Dart SDK را دانلود می‌کند و ابزار Flutter را می‌سازد - کمی صبر کنید. باید چیزی شبیه به Flutter 3.x • channel stable ببینید.

۳. کلون کردن مخزن

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

۴. ساختار پروژه را بررسی کنید

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

۳. ☁️ راه‌اندازی پروژه گوگل کلود

۱. ایجاد یک پروژه جدید

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 ) — به بلوک عیب‌یابی زیر مراجعه کنید.

۳. فعال کردن API های مورد نیاز

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

رابط برنامه‌نویسی کاربردی (API)

هدف

aiplatform.googleapis.com

هوش مصنوعی ورتکس - ابزار fitting_tool از طریق هوش مصنوعی ورتکس، تولید تصویر جمینی را فراخوانی می‌کند.

storage.googleapis.com

فضای ذخیره‌سازی ابری - تصاویر کاتالوگ محصولات و نتایج آزمایشی تولید شده را ذخیره می‌کند.

run.googleapis.com

Cloud Run - میزبان backend به عنوان یک کانتینر بدون سرور است

cloudbuild.googleapis.com

ساخت ابری - ایمیج‌های داکر را از منبع می‌سازد

artifactregistry.googleapis.com

Artifact Registry — ایمیج‌های ساخته‌شده‌ی Docker را ذخیره می‌کند.

۴. یک سطل 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

۵. تصاویر کاتالوگ محصولات را آپلود کنید

ابزار getProductImage در بک‌اند، از gs://$GCS_BUCKET/catalog-assets/images/ را می‌خواند. 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/

۶. پیکربندی فایل .env

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

۷. با اعتبارنامه‌های پیش‌فرض برنامه، احراز هویت کنید

شما باید این دستور را قبل از شروع بک‌اند به صورت محلی اجرا کنید. بک‌اند 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)

بررسی سلامت ADC:

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

شما باید حدود ۲۰ کاراکتر از یک توکن و به دنبال آن ... را ببینید. اگر خطا داد، ورود انجام نشد - مرحله ۱ را دوباره اجرا کنید.

۴. 🏗️ نمای کلی معماری

حالا که محیط آماده است، بیایید قبل از بررسی کد، نحوه‌ی کار سیستم را بررسی کنیم.

سیستم چهار عاملی

بخش پشتی به عنوان یک سیستم چندعاملی با استفاده از ADK (کیت توسعه عامل) برای 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

مشاور مد. با توجه به موقعیت مکانی، مناسبت و ترجیحات، ۳ ترکیب لباس را از کاتالوگ انتخاب می‌کند. می‌تواند برای هر لباس تصاویر پرو ایجاد کند. همچنین از مدل توانمند برای استدلال خلاقانه استفاده می‌کند.

نقطه ورود: 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 به اشتراک گذاشته شوند.

۵. 🤖 بررسی عمیق ADK (کیت توسعه عامل)

ADK چیست؟

کیت توسعه عامل (ADK) یک چارچوب متن‌باز از گوگل برای ساخت عامل‌های هوش مصنوعی در Go (و پایتون/جاوا) است. این کیت، لایه‌ای بین برنامه شما و رابط برنامه‌نویسی 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. ۳ ترکیب لباس انتخاب کنید
  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:embed Go تعبیه می‌شوند:

//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 آرگومان‌ها را deserialize می‌کند، تابع شما را فراخوانی می‌کند و نتیجه را برمی‌گرداند.

پارامتر tool.Context به ابزارها امکان دسترسی به سرویس‌های زمان اجرای ADK را می‌دهد - که از همه مهم‌تر، artifacts هستند :

// 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 آن را در لیست ابزارها می‌بیند و می‌تواند تصمیم به فراخوانی آن بگیرد.

جلسات

جلسات، تاریخچه مکالمات را ردیابی می‌کنند. API REST مربوط به ADK آنها را به طور خودکار مدیریت می‌کند:

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

یک تصمیم طراحی حیاتی در این برنامه: اتاق پرو برای هر درخواست، یک جلسه جدید ایجاد می‌کند (هر بار پرو مستقل است)، در حالی که آرایشگر از همان جلسه دوباره استفاده می‌کند (بنابراین پیشنهادات قبلی را به خاطر می‌سپارد و می‌تواند بر اساس بازخوردها، آن را اصلاح کند).

ایالت

وضعیت (State) یک مخزن کلید-مقدار متصل به یک جلسه (session) است. عامل‌ها (agents) وضعیت را برای هماهنگی موارد زیر می‌خوانند و می‌نویسند:

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

اگر یک تابع فراخوانی (callback) پاسخی غیر از صفر (non-nil) برگرداند، از رفتار پیش‌فرض صرف‌نظر می‌شود. برای مثال، یک 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 و در نهایت فایل‌های محلی را امتحان می‌کند. پس از بارگذاری، تصویر به عنوان یک مصنوع ذخیره می‌شود تا فراخوانی‌های بعدی فوری باشند.

۶. 🧪 خط لوله هوش مصنوعی: عامل‌ها در عمل

حالا بیایید دو تا از پیچیده‌ترین عوامل را بررسی کنیم - آن‌هایی که واقعاً تصاویر را تولید می‌کنند و لباس‌ها را انتخاب می‌کنند.

۶.۱ نماینده اتاق پرو

فایل:

adk_backend/fittingroom/agent.go

عامل اتاق پرو، موتور محرک «امتحان مجازی» است. وقتی کاربر عکس خود را آپلود می‌کند و محصولی را انتخاب می‌کند، این عامل یک تصویر ترکیبی از شخصی که آن لباس را پوشیده است، ایجاد می‌کند.

ابزار fitting_tool - گام به گام

منطق اصلی در تابع doFitting قرار دارد. وقتی عامل آن را فراخوانی می‌کند، چه اتفاقی می‌افتد؟

مرحله ۱: تصویر کاربر را حل کنید

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 ذخیره می‌شود.
  • یک آدرس اینترنتی gs:// - این یک نتیجه‌ی برازش از پیش تولید شده است که برای استفاده‌ی مجدد بین جلسات در GCS ذخیره شده است.

این طراحی دو مسیره عمدی است: وقتی بعداً نماینده استایلیست لباس‌ها را برای پرو کردن تولید می‌کند، از URL GCS از نتیجه اولیه اتاق پرو دوباره استفاده می‌کند تا هویت کاربر در تمام لباس‌ها ثابت بماند.

مرحله ۲: ساخت اعلان چندوجهی

   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 می‌گوید که هویت کاربر (چهره، نوع بدن، رنگ پوست، مو) را حفظ کند و فقط لباس را اعمال کند. بدون این مهندسی سریع، مدل ممکن است ظاهر فرد را تغییر دهد.

مرحله ۳: برای تولید تصویر با 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 با شناسه پروژه که از طریق Application Default Credentials احراز هویت شده است. مدل‌های ارکستراسیون ( gemini-3.1-pro-preview ، gemini-3-flash-preview ) و مدل تصویر ( gemini-2.5-flash-image ) همگی پشت یک نقطه پایانی Vertex AI قرار دارند و همان ADC نیز دسترسی به Cloud Storage را مجاز می‌کند - یک اعتبارنامه، برای هر فراخوانی.

مرحله ۴: نتیجه را ذخیره کنید

   // 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) ، تابع فراخوانی مجدد اعلام می‌کند که «پیش‌پردازش تمام شده است - اکنون عامل را به صورت عادی اجرا کنید.» اگر محتوای غیر صفر را برگرداند، عامل را به طور کامل از کار می‌اندازد.

۶.۲ نماینده استایلیست

فایل:

adk_backend/stylist/agent.go

کارشناس استایل، پیچیده‌ترین بخش سیستم است. این کارشناس، توصیه‌های شخصی‌سازی‌شده برای لباس‌ها را ارائه می‌دهد و از طریق مکالمه، اصلاحات مکرر را پشتیبانی می‌کند.

سه نکته‌ی کلیدی - خاطره‌ی آرایشگر

این طراح از سه فراخوانی برای حفظ زمینه در مکالمات چند نوبتی استفاده می‌کند:

تماس مجدد ۱:

InjectPreviousProducts (قبل از مدل)

مشکل: اگر کاربر بگوید «گزینه‌های مختلف را به من نشان بده»، LLM ممکن است دوباره همان محصولات را پیشنهاد دهد زیرا ذاتاً آنچه را که قبلاً توصیه کرده است، پیگیری نمی‌کند.

راه حل: پس از هر پاسخ، شناسه‌های محصول در session state ذخیره می‌شوند. قبل از فراخوانی بعدی LLM، این فراخوانی مجدد آنها را می‌خواند و یک راهنما (hint) تزریق می‌کند:

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
}

پاسخ به تماس ۲:

ExtractAndInjectUserImage (قبل از مدل)

مشکل: وقتی کاربر بازخورد می‌دهد ("آن را غیررسمی‌تر کنید")، پیام پیگیری دوباره عکس کاربر را شامل نمی‌شود. اما ابزار تناسب به آن نیاز دارد.

راه حل: در اولین درخواست، این فراخوانی، مرجع تصویر کاربر را استخراج کرده و آن را در state ذخیره می‌کند. در درخواست‌های بعدی، آن را دوباره تزریق می‌کند:

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
}

پاسخ به تماس ۳:

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

۶.۳ عامل ریشه

فایل:

adk_backend/rootagent/agent.go

ساده‌ترین عامل - فقط ۳۱ خط:

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 فقط باید قصد کاربر را بخواند و sub-agent مناسب را انتخاب کند. هیچ ابزاری لازم نیست؛ SubAgents به طور خودکار واگذاری را انجام می‌دهد.

۷. 📱 معماری فرانت‌اند فلاتر

رابط کاربری 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 و enumهایی مانند TryOnState
  • ViewModel ( ChangeNotifier ): وضعیت فعلی را نگه می‌دارد و تغییرات را از طریق notifyListeners() به رابط کاربری ارسال می‌کند.
  • View (ویجت): با context.watch () در 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 backend پیروی می‌کنند:

مرحله ۱: ایجاد یک جلسه

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

مرحله ۳: مصنوع را بردارید

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
}

یک تفاوت اساسی در طراحی: سرویس اتاق پرو برای هر درخواست یک session جدید ایجاد می‌کند (هر بار _createSession() فراخوانی می‌شود)، در حالی که سرویس استایل‌بندی از همان session استفاده مجدد می‌کند ( _sessionId ??= await _createSession() ) تا مکالمه چند نوبتی را فعال کند.

مدیریت وضعیت: TryItOnProvider

فایل:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

TryItOnProvider کل جریان try-on را مدیریت می‌کند. این ماژول از یک TryOnState enum به عنوان ماشین وضعیت استفاده می‌کند:

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 وجود ندارد - محتوای صفحه بر اساس شمارش وضعیت (state enum) درجا تغییر می‌کند.

تحویل حضوری: اتاق پرو → استایلیست

جالب‌ترین الگوی تجربه کاربری، نحوه‌ی انتقال اطلاعات از مسئول اتاق پرو به مسئول استایلیست توسط اپلیکیشن است.

در 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 هر آنچه را که یک آرایشگر نیاز دارد، در خود جای داده است:

  • مکان و مناسبت - متن زمینه برای استایل‌دهی
  • آدرس تصویر کاربر GCS - تا طراح بتواند دقیقاً از همان تصویر کاربر دوباره استفاده کند
  • محصول انتخاب شده - بنابراین آرایشگر آن را در هر لباسی قرار می‌دهد

این همان انتقال عامل‌محور است - انتقال یکپارچه‌ی زمینه‌ی چندوجهی از یک عامل هوش مصنوعی به عامل دیگر، به طوری که کاربر فقط یک فرم ساده را مشاهده کند.

جریان استایل‌دهی: StylingProvider

فایل:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

StylingProvider ساده‌تر از TryItOnProvider است زیرا بیشتر پیچیدگی را به backend واگذار می‌کند:

class StylingProvider with ChangeNotifier {
 StylingState _state = StylingState.initial;
 List<Outfit> _outfits = [];


 Future<bool> getStyleSuggestions(StyleRequest request) async {
   if (_state == StylingState.loading) return false;  // Prevent spam
   _currentRequest = request;
   _setLoading();
   try {
     final suggestions = await _stylingService.getStyleSuggestions(request);
     _setSuccess(suggestions);
     return true;
   } catch (e) {
     _setError('Something went wrong.');
   }
   return false;
 }


 // Multi-turn refinement  uses the same session
 Future<bool> refineWithFeedback(String feedback) async {
   if (_state == StylingState.loading) return false;
   _setLoading();
   try {
     final suggestions = await _stylingService.refineWithFeedback(feedback);
     _setSuccess(suggestions);
     return true;
   } catch (e) {
     _setError('Something went wrong.');
   }
   return false;
 }
}

متد refineWithFeedback یک پیام متنی ساده به همان جلسه ارسال می‌کند - فراخوانی‌های InjectPreviousProducts و ExtractAndInjectUserImage در backend به طور خودکار تمام مدیریت زمینه را انجام می‌دهند.

۸. 🚀 اجرای برنامه به صورت محلی

برای یک تجربه روان از Cloud Shell، بک‌اند Go، برنامه وب کامپایل‌شده Flutter را از همان پورت (8080) ارائه می‌دهد. یک فرآیند، یک URL پیش‌نمایش، بدون دردسرهای بین‌منبعی، بدون ویرایش فایل‌های پیکربندی.

قبل از شروع - ADC را از نظر سلامت بررسی کنید

بخش مدیریت برای فراخوانی Vertex AI به اعتبارنامه‌های پیش‌فرض برنامه نیاز دارد. اگر مرحله ۷ راه‌اندازی پروژه را در این جلسه Cloud Shell و این حساب گوگل به پایان رسانده‌اید، مشکلی نیست. اگر بعد از یک استراحت برمی‌گردید، حساب‌هایتان را عوض کرده‌اید یا مطمئن نیستید، ۵ ثانیه وقت بگذارید تا تأیید کنید:

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

اگر حدود ۲۰ کاراکتر از یک توکن چاپ شد، کار تمام است. اگر خطا داد، مرحله ۷ از تنظیمات پروژه را دوباره اجرا کنید :

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

شما از دو ترمینال Cloud Shell استفاده خواهید کرد:

  • ترمینال A — به طور مداوم backend را اجرا می‌کند ( ./run.sh ). آن را باز بگذارید.
  • ترمینال B — ساخت وب Flutter را یک بار اجرا می‌کند ( flutter build web ). پس از اتمام کار، خارج می‌شود.

ترتیب مهم نیست — می‌توانید هر کدام را اول شروع کنید. اما برای تمیزترین تجربه در اولین اجرا، ابتدا Flutter را بسازید تا backend از لحظه شروع، یک رابط کاربری برای ارائه داشته باشد.

۱. ترمینال B - ساخت بسته وب Flutter (یک مرحله‌ای)

یک تب جدید Cloud Shell (علامت + در بالای پنل ترمینال) باز کنید، سپس:

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

این flutter_frontend/build/web/ را تولید می‌کند - دایرکتوری از فایل‌های استاتیک (HTML، JS، assets) - و پس از اتمام کار، خارج می‌شود. بک‌اند به محض اینکه ببیند این دایرکتوری وجود دارد، این فایل‌ها را ارائه می‌دهد.

۲. ترمینال A - شروع Backend (اجرای طولانی مدت)

در ترمینال اصلی Cloud Shell خود:

cd ~/fashion_app_demo/adk_backend
./run.sh

شما باید چیزی شبیه به این را ببینید:

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

این ترمینال را در حال اجرا بگذارید - تا زمانی که run.sh فعال است، backend فعال می‌ماند. برای متوقف کردن آن، Ctrl+C را فشار دهید.

سرور همه چیز را در پورت ۸۰۸۰ نمایش می‌دهد:

  • / — اپلیکیشن وب فلاتر (رابط کاربری خرید)
  • /api/ — نقاط پایانی ADK REST (که توسط برنامه Flutter فراخوانی می‌شود)
  • رابط کاربری توسعه‌دهندگان ADK - همچنین در / زمانی که هیچ ساخت Flutter وجود ندارد؛ برای اشکال‌زدایی مستقیم توسط عامل مفید است

۳. پیش‌نمایش وب را باز کنید

  1. در Cloud Shell، روی نماد پیش‌نمایش وب (بالا سمت راست) کلیک کنید → پیش‌نمایش روی پورت ۸۰۸۰
  2. برنامه خرید Flutter در یک برگه جدید بارگیری می‌شود
  3. کاتالوگ محصولات را مرور کنید و یک مورد را انتخاب کنید
  4. برای شروع فرآیند امتحان، روی نماد شخص (👤) ضربه بزنید
  5. عکسی آپلود کنید و ببینید هوش مصنوعی یک تصویر آزمایشی می‌سازد
  6. برای دریافت توصیه‌های لباس، روی «استایل من» ضربه بزنید
  7. بازخورد پیگیری مانند «آن را غیررسمی‌تر کنید» را تایپ کنید - اصلاح در همان جلسه

۹. ☁️ استقرار در Cloud Run

ساخت فلاتر را در بک‌اند بسته‌بندی کنید

کانتینر Cloud Run هم API و هم رابط کاربری را از یک تصویر ارسال می‌کند. فایل ساخت وب Flutter را در 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

(اگر به صورت محلی در حال تکرار بوده‌اید، ممکن است از مرحله Run-Locally، 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/ در همان میزبان می‌رود. هیچ ویرایش پیکربندی frontend لازم نیست، هیچ کلید API ارسال نمی‌شود.

تأیید استقرار

آدرس اینترنتی Cloud Run را در مرورگر خود باز کنید و کل جریان را اجرا کنید:

  1. مرور → انتخاب محصول
  2. امتحان کنید → عکس خود را آپلود کنید → تصویر تولید شده توسط هوش مصنوعی را ببینید
  3. استایل من → مکان/مناسبت را پر کنید → لباس‌های گلچین شده را ببینید
  4. بازخورد → عبارت «آن را غیررسمی‌تر کنید» را تایپ کنید → لباس‌های به‌روز شده را ببینید
  5. افزودن به سبد خرید → تکمیل فرآیند خرید

۱۰. 🎉 نتیجه‌گیری

آنچه ساختید

شما یک تجربه کامل خرده‌فروشی مبتنی بر هوش مصنوعی را با موارد زیر بررسی کرده‌اید:

  • ✅ یک بک‌اند چندعاملی با ۴ عامل تخصصی که با هم کار می‌کنند
  • ✅ یک اتاق پرو مجازی که تصاویر پرو شخصی‌سازی‌شده تولید می‌کند
  • ✅ یک آرایشگر هوش مصنوعی که لباس‌ها را انتخاب و از طریق مکالمه آنها را اصلاح می‌کند
  • ✅ یک برنامه چند پلتفرمی Flutter که به backend عامل متصل می‌شود
  • استقرار Cloud Run برای میزبانی مقیاس‌پذیر و بدون سرور

مفاهیم کلیدی

مفهوم

جایی که دیدیش

هماهنگی چند عاملی ADK

مسیریابی نماینده اصلی به اتاق پرو، کاتالوگ و نمایندگان استایل

تولید تصویر چندوجهی Gemini

fitting_tool ترکیب عکس‌های کاربر با تصاویر محصول

وضعیت جلسه برای هوش مصنوعی محاوره‌ای

استفاده مجدد از جلسات توسط طراح برای بازخوردهای تکراری

ذخیره‌سازی مصنوعات برای داده‌های دودویی

جداسازی فضای ذخیره‌سازی تصویر از پاسخ‌های متنی

فراخوانی‌های برگشتی برای منطق میان‌افزار

SaveIncomingBlobs ، InjectPreviousProducts ، SaveSelectedProducts

ارائه دهنده MVVM + در فلاتر

TryItOnProvider و StylingProvider با ChangeNotifier

تحویل عاملی

StyleRequest ارسال زمینه چندوجهی بین عامل‌ها

مراحل بعدی

  • 🎨 سفارشی‌سازی پیام‌های اپراتور - برای تغییر شخصیت آرایشگر، instructions.md را ویرایش کنید
  • 🛍️ محصولات بیشتری اضافه کنیدcatalog.yaml با موارد جدید به‌روزرسانی کنید
  • 📱 ساخت برای موبایل — اجرای flutter build ios یا flutter build apk
  • 🔄 افزودن جلسات مداوم - InMemoryService را با یک پیاده‌سازی مبتنی بر پایگاه داده جایگزین کنید
  • 🔒 افزودن احراز هویت — ایمن‌سازی نقطه پایانی Cloud Run با IAM

منابع