۱. مقدمه
آنچه خواهید ساخت
در این آزمایشگاه کد، شما در نقش یک توسعهدهنده قرار خواهید گرفت که در حال ساخت Fashion App ، یک اپلیکیشن خرید Flutter برای یک برند خردهفروشی خیالی، است. ماموریت شما: اضافه کردن دو ویژگی مبتنی بر هوش مصنوعی که تجربه خرید آنلاین را متحول میکنند.
- اتاق پرو مجازی - کاربر عکسی از خود آپلود میکند، یک لباس را انتخاب میکند و تصویری از خود که توسط هوش مصنوعی تولید شده و آن لباس را پوشیده است، میبیند.
- متخصص مد هوش مصنوعی - بر اساس موقعیت مکانی، مناسبت و ترجیحات سبک کاربر، یک عامل هوش مصنوعی توصیههای کاملی در مورد لباس ارائه میدهد - و کاربر میتواند آنها را از طریق مکالمه اصلاح کند.
ایده ساده است: وقتی مردم لباسها را در اتاق پرو پرو میکنند، احتمال خرید آنها بسیار بیشتر است. اما آنلاین؟ فقط حدس میزنید. این پروژه این شکاف را با هوش مصنوعی پر میکند.
معماری در یک نگاه
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) | هدف |
| هوش مصنوعی ورتکس - ابزار |
| فضای ذخیرهسازی ابری - تصاویر کاتالوگ محصولات و نتایج آزمایشی تولید شده را ذخیره میکند. |
| Cloud Run - میزبان backend به عنوان یک کانتینر بدون سرور است |
| ساخت ابری - ایمیجهای داکر را از منبع میسازد |
| 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 │
└────────────────┘
عامل | مدل | نقش |
عامل ریشه | | پلیس راهنمایی و رانندگی. پیام کاربر را میخواند و به کارشناس مربوطه ارجاع میدهد. از یک مدل سریع و سبک استفاده میکند زیرا فقط نیاز به تصمیمگیری در مورد مسیریابی دارد. |
نماینده کاتالوگ | | متخصص محصول. کاتالوگ محصول را از یک فایل YAML بارگذاری میکند و به پرسشهای مربوط به محصول پاسخ میدهد. همچنین سبک است - فقط دادهها را جستجو میکند. |
نماینده اتاق پرو | | متخصص پرو مجازی. عکس کاربر + تصویر محصول را میگیرد و تصویری ترکیبی از فردی که آن کالا را پوشیده است، تولید میکند. از یک مدل توانمندتر استفاده میکند زیرا نیاز به استدلال در مورد تصاویر دارد. |
نماینده استایلیست | | مشاور مد. با توجه به موقعیت مکانی، مناسبت و ترجیحات، ۳ ترکیب لباس را از کاتالوگ انتخاب میکند. میتواند برای هر لباس تصاویر پرو ایجاد کند. همچنین از مدل توانمند برای استدلال خلاقانه استفاده میکند. |
نقطه ورود: 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
این حلقه میتواند چندین بار در یک درخواست واحد تکرار شود. برای مثال، عامل استایلیست ممکن است:
- «برای تعطیلات ساحلی به من استایل بده» را دریافت کنید
- برای دریافت لیست محصولات، ابزار
catalog_agentرا فراخوانی کنید. - ۳ ترکیب لباس انتخاب کنید
- برای تولید تصاویر، برای هر لباس، تابع
fitting_toolفراخوانی کنید. - پاسخ ساختاریافتهی 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.
دو ابزار دارد:
-
listProducts- کاتالوگ کامل محصولات را از یک فایل YAML برمیگرداند -
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 وجود ندارد؛ برای اشکالزدایی مستقیم توسط عامل مفید است
۳. پیشنمایش وب را باز کنید
- در Cloud Shell، روی نماد پیشنمایش وب (بالا سمت راست) کلیک کنید → پیشنمایش روی پورت ۸۰۸۰
- برنامه خرید Flutter در یک برگه جدید بارگیری میشود
- کاتالوگ محصولات را مرور کنید و یک مورد را انتخاب کنید
- برای شروع فرآیند امتحان، روی نماد شخص (👤) ضربه بزنید
- عکسی آپلود کنید و ببینید هوش مصنوعی یک تصویر آزمایشی میسازد
- برای دریافت توصیههای لباس، روی «استایل من» ضربه بزنید
- بازخورد پیگیری مانند «آن را غیررسمیتر کنید» را تایپ کنید - اصلاح در همان جلسه
۹. ☁️ استقرار در 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 را در مرورگر خود باز کنید و کل جریان را اجرا کنید:
- مرور → انتخاب محصول
- امتحان کنید → عکس خود را آپلود کنید → تصویر تولید شده توسط هوش مصنوعی را ببینید
- استایل من → مکان/مناسبت را پر کنید → لباسهای گلچین شده را ببینید
- بازخورد → عبارت «آن را غیررسمیتر کنید» را تایپ کنید → لباسهای بهروز شده را ببینید
- افزودن به سبد خرید → تکمیل فرآیند خرید
۱۰. 🎉 نتیجهگیری
آنچه ساختید
شما یک تجربه کامل خردهفروشی مبتنی بر هوش مصنوعی را با موارد زیر بررسی کردهاید:
- ✅ یک بکاند چندعاملی با ۴ عامل تخصصی که با هم کار میکنند
- ✅ یک اتاق پرو مجازی که تصاویر پرو شخصیسازیشده تولید میکند
- ✅ یک آرایشگر هوش مصنوعی که لباسها را انتخاب و از طریق مکالمه آنها را اصلاح میکند
- ✅ یک برنامه چند پلتفرمی Flutter که به backend عامل متصل میشود
- ✅ استقرار Cloud Run برای میزبانی مقیاسپذیر و بدون سرور
مفاهیم کلیدی
مفهوم | جایی که دیدیش |
هماهنگی چند عاملی ADK | مسیریابی نماینده اصلی به اتاق پرو، کاتالوگ و نمایندگان استایل |
تولید تصویر چندوجهی Gemini | |
وضعیت جلسه برای هوش مصنوعی محاورهای | استفاده مجدد از جلسات توسط طراح برای بازخوردهای تکراری |
ذخیرهسازی مصنوعات برای دادههای دودویی | جداسازی فضای ذخیرهسازی تصویر از پاسخهای متنی |
فراخوانیهای برگشتی برای منطق میانافزار | |
ارائه دهنده MVVM + در فلاتر | |
تحویل عاملی | |
مراحل بعدی
- 🎨 سفارشیسازی پیامهای اپراتور - برای تغییر شخصیت آرایشگر،
instructions.mdرا ویرایش کنید - 🛍️ محصولات بیشتری اضافه کنید —
catalog.yamlبا موارد جدید بهروزرسانی کنید - 📱 ساخت برای موبایل — اجرای
flutter build iosیاflutter build apk - 🔄 افزودن جلسات مداوم -
InMemoryServiceرا با یک پیادهسازی مبتنی بر پایگاه داده جایگزین کنید - 🔒 افزودن احراز هویت — ایمنسازی نقطه پایانی Cloud Run با IAM
منابع
- مستندات ADK — مستندات رسمی کیت توسعهی Agent
- کد منبع ADK Go — مخزن گیتهاب
- مرجع بسته ADK Go — مرجع API
- مستندات API Gemini - قابلیتها و راهنماهای مدل
- بسته ارائه دهنده Flutter - اسناد مدیریت وضعیت
- مستندات Cloud Run - راهنماهای استقرار و مقیاسبندی