‫👗 إنشاء غرفة قياس افتراضية ومصمّم أزياء مستند إلى الذكاء الاصطناعي باستخدام Flutter وADK Go وGemini

1. مقدمة

ما ستنشئه

في هذا الدرس التطبيقي حول الترميز، ستتولّى دور مطوّر ينشئ تطبيق Fashion، وهو تطبيق تسوّق من Flutter لعلامة تجارية خيالية للبيع بالتجزئة. مهمتك: إضافة ميزتَين مستندتَين إلى الذكاء الاصطناعي لتغيير تجربة التسوّق على الإنترنت.

  1. غرفة القياس الافتراضية: يحمّل المستخدم صورة له، ويختار قطعة ملابس، ثم يرى صورة من إنشاء الذكاء الاصطناعي وهو يرتدي تلك القطعة.
  2. AI Stylist: استنادًا إلى الموقع الجغرافي للمستخدم والمناسبة والإعدادات المفضّلة من حيث الأسلوب، يقدّم وكيل الذكاء الاصطناعي اقتراحات بشأن ملابس كاملة، ويمكن للمستخدم تحسينها من خلال المحادثة.

الفكرة بسيطة: عندما يجرّب الأشخاص الملابس في غرفة تبديل الملابس، يزداد احتمال شرائها. ولكن على الإنترنت؟ أنت تخمّن فقط. ويساعد هذا المشروع في سدّ هذه الفجوة باستخدام الذكاء الاصطناعي.

لمحة سريعة عن الهندسة المعمارية

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

التقنيات الأساسية

المكوّن

تكنولوجيا

الغرض

Agent Framework

حزمة تطوير الوكلاء (ADK) للغة Go

تنظيم الجلسات والعناصر المتعددة الوكلاء

الاستدلال المستند إلى الذكاء الاصطناعي الوكيل (Pro)

معاينة Gemini 3.1 Pro

تفعيل وكلاء غرف القياس ومستشاري الموضة

الاستدلال من خلال الوكيل (Flash)

معاينة Gemini 3 Flash

تتيح استخدام وكلاء الجذر والكتالوج (التوجيه/البحث البسيط)

إنشاء الصور

Gemini 2.5 Flash Image

إنشاء صور لتجربة الملابس والأزياء

Frontend

Flutter (Dart)

تطبيق من عدّة منصات (الويب وiOS وAndroid)

مساحة التخزين

Google Cloud Storage

تخزين صور المنتجات والنتائج التي تم إنشاؤها

الاستضافة

Cloud Run

نشر الحاويات بدون خادم

2. 📦 المتطلبات الأساسية وإعداد Cloud Shell

1. فتح "محرّر Cloud Shell"

👉 افتح محرِّر Cloud Shell في المتصفّح.

إذا لم تظهر الوحدة الطرفية في أسفل الشاشة، اتّبِع الخطوات التالية:

  • انقر على عرضالوحدة الطرفية

2. إعداد حزمة تطوير البرامج (SDK) في Flutter

يأتي Cloud Shell مع تثبيت Flutter مسبقًا في /google/flutter. بما أنّ هذا الدليل يملكه مستخدم نظام مختلف، سيظهر لك الخطأ fatal: detected dubious ownership في المرة الأولى التي تشغّل فيها flutter. أضِفها إلى قائمة safe-directory في 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- تفعيل واجهات برمجة التطبيقات المطلوبة

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

واجهة برمجة التطبيقات

الغرض

aiplatform.googleapis.com

Vertex AI: ينفّذ fitting_tool طلبات إنشاء الصور في Gemini من خلال Vertex AI

storage.googleapis.com

Cloud Storage: لتخزين صور كتالوج المنتجات ونتائج تجربة المنتجات افتراضيًا

run.googleapis.com

Cloud Run: يستضيف الخلفية كحاوية بدون خادم

cloudbuild.googleapis.com

‫Cloud Build: إنشاء صور Docker من المصدر

artifactregistry.googleapis.com

Artifact Registry: لتخزين صور Docker التي تم إنشاؤها

4. إنشاء حزمة GCS

export PROJECT_ID=$(gcloud config get-value project)
gcloud storage buckets create gs://fashion-app-$PROJECT_ID \
 --location=us-central1 \
 --uniform-bucket-level-access

5- تحميل صور كتالوج المنتجات

تستند أداة getProductImage في الخلفية إلى gs://$GCS_BUCKET/catalog-assets/images/. حمِّل صور الكتالوج إلى هذا المسار بالتحديد:

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

أكِّد عملية التحميل (من المفترض أن تظهر لك قائمة بملفات .png):

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

6. ضبط ملف .env

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

7. المصادقة باستخدام "بيانات الاعتماد التلقائية للتطبيق"

يجب تنفيذ هذا الأمر قبل بدء تشغيل الخلفية محليًا. يستخدم الخلفية البرمجية Go خدمة ADC للمصادقة على كل طلب يتم إرساله إلى Vertex AI (Gemini) وCloud Storage. بدون ADC، سيتم تشغيل الخلفية ولكن سيفشل كل طلب تجربة مع ظهور الخطأ 401 CREDENTIALS_MISSING.

تغطي بيانات الاعتماد خدمة Google Workspace وخدمة Google Cloud. نفِّذ هذين الأمرين بالترتيب:

# 1. Log in (opens a browser; in Cloud Shell, paste the verification code back)
gcloud auth application-default login

# 2. Attach your project as the quota / billing project for ADC
gcloud auth application-default set-quota-project $(gcloud config get-value project)

التأكّد من أنّ ADC سليم:

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

من المفترض أن يظهر لك حوالي 20 حرفًا من الرمز المميّز متبوعًا بـ .... إذا حدث خطأ، يعني ذلك أنّه لم يتم تسجيل الدخول، لذا أعِد تنفيذ الخطوة 1.

4. ‫🏗️ نظرة عامة على البنية

بعد أن أصبحت البيئة جاهزة، لنفهم طريقة عمل النظام قبل إلقاء نظرة على الرمز البرمجي.

نظام الوكلاء الأربعة

تم إنشاء الخلفية على أنّها نظام متعدد الوكلاء باستخدام "حزمة تطوير الوكلاء" (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

شرطي مرور يقرأ رسالة المستخدم ويفوّضها إلى الوكيل المتخصّص المناسب. يستخدم نموذجًا سريعًا وخفيفًا لأنه يحتاج فقط إلى اتخاذ قرارات التوجيه.

Catalog Agent

gemini-3-flash-preview

خبير منتجات تحمّل هذه الأداة كتالوج المنتجات من ملف YAML وتجيب عن طلبات البحث المتعلقة بالمنتجات. كما أنّه خفيف الوزن، فهو يبحث عن البيانات فقط.

Fitting Room Agent

gemini-3.1-pro-preview

أخصائي التجربة في الواقع الافتراضي تأخذ هذه الأداة صورة مستخدم + صورة منتج وتنشئ صورة مركّبة للشخص وهو يرتدي هذا المنتج. يستخدم نموذجًا أكثر قدرة لأنّه يحتاج إلى التفكير في الصور.

Stylist Agent

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 تلقائيًا، لذا لن تحتاج إلى كتابة معالِجات نقاط النهاية بنفسك. توفّر لك "مجموعة أدوات تطوير الوكلاء" إدارة الجلسات وتخزين العناصر وتنفيذ الوكلاء بدون الحاجة إلى إعدادات إضافية.
  • يخزّن session.InMemoryService() الجلسات في الذاكرة. هذا يعني أنّه سيتم فقدان الجلسات إذا تمت إعادة تشغيل الخادم، وهو أمر مقبول في عرض توضيحي. في مرحلة الإنتاج، عليك استخدام مساحة تخزين دائمة.
  • يخزّن gcsartifact.NewService القطع الأثرية (الصور من إنشاء الذكاء الاصطناعي التوليدي) في Google Cloud Storage، لذا تظل متاحة في جميع الطلبات ويمكن مشاركتها عبر معرّفات الموارد الموحّدة في GCS.

5- ‫🤖 نظرة تفصيلية على "حزمة تطوير الوكلاء" (ADK)

ما هي "حزمة تطوير التطبيقات" (ADK)؟

حزمة تطوير الوكلاء (ADK) هي إطار عمل مفتوح المصدر من Google لإنشاء وكلاء الذكاء الاصطناعي في Go (وPython/Java). وهي الطبقة بين تطبيقك وGemini API.

يمكنك الاتصال بواجهة Gemini API مباشرةً. ولكن بمجرد أن يحتاج تطبيقك إلى:

  • البحث عن منتجات من كتالوج
  • إنشاء صور استنادًا إلى صور المستخدم
  • تذكُّر الملابس التي تم اقتراحها سابقًا
  • تنسيق عمل وكلاء الذكاء الاصطناعي المتعدّدين

أنت بحاجة إلى بنية. وتوفّر "حزمة تطوير التطبيقات" هذه البنية.

حلقة الوكيل

يتبع كل وكيل 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 منظَّمة

المفاهيم الأساسية (مع الرمز من هذا المستودع)

وكلاء النماذج اللغوية الكبيرة

الوحدة الأساسية تم الإنشاء باستخدام 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 هو شخصية الوكيل، فهو يحدد للنموذج اللغوي الكبير هوية الوكيل وكيفية تصرفه. في هذا المستودع، تتم كتابة التعليمات كملفات Markdown وتضمينها في وقت الترجمة باستخدام توجيه //go:embed في Go:

//go:embed instructions.md
var instructions string

يؤدي ذلك إلى الاحتفاظ بالطلبات كمستندات منفصلة يمكن تتبُّع إصداراتها بدلاً من سلاسل مضمّنة.

الأدوات

الأدوات هي دوال Go يمكن للنموذج اللغوي الكبير استدعاؤها. يتولّى ADK عملية الترجمة بين تنسيق استدعاء الأدوات في النموذج اللغوي الكبير ودالة 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 وترسله إلى النموذج اللغوي الكبير. عندما يقرّر النموذج اللغوي الكبير استدعاء 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 يعرض ردًا مخزّنًا مؤقتًا إلى تخطّي طلب النموذج اللغوي الكبير الفعلي بالكامل.

فرض استخدام مخطط JSON

يفرض كلّ من وكيلَي غرفة القياس ومصمّم الأزياء على النموذج اللغوي الكبير الردّ بتنسيق 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
  • معرّف الموارد المنتظم gs://: هو نتيجة ملائمة تم إنشاؤها سابقًا ويتم تخزينها في "خدمة التخزين السحابي من Google" لإعادة استخدامها في جلسات متعددة.

هذا التصميم المزدوج المسار مقصود: عندما ينشئ وكيل مصمّم الأزياء لاحقًا عمليات تجربة الملابس، يعيد استخدام عنوان 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 نفسها، وتتيح بيانات الاعتماد نفسها أيضًا إمكانية الوصول إلى Cloud Storage، أي بيانات اعتماد واحدة لكل طلب.

الخطوة 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) في "خدمة التخزين السحابي من Google" (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

يُعدّ وكيل المصمّم الأكثر تطورًا في النظام. تتيح هذه الميزة الحصول على اقتراحات مخصّصة بشأن الملابس، كما تتيح تحسين البحث بشكل متكرّر من خلال المحادثة.

ثلاث عمليات إعادة استدعاء: ذاكرة مصمّم الأزياء

يستخدم المصمّم ثلاث وظائف ردّ للحفاظ على السياق في المحادثات المتعدّدة الجولات:

Callback 1:

InjectPreviousProducts (BeforeModel)

المشكلة: إذا قال المستخدم "أريد رؤية خيارات مختلفة"، قد تقترح نماذج اللغات الكبيرة المنتجات نفسها مرة أخرى لأنّها لا تتتبّع بشكل أساسي ما سبق أن اقترحته.

الحلّ: بعد كلّ ردّ، يتم حفظ معرّفات المنتجات في حالة الجلسة. قبل إجراء طلب 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
}

Callback 3:

SaveSelectedProducts (AfterModel)

بعد أن يردّ النموذج اللغوي الكبير باقتراحات حول الملابس، تحلّل دالة معاودة الاتصال هذه ملف 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 (النموذج الأسرع) لأنّ قرارات التوجيه بسيطة، إذ يحتاج النموذج اللغوي الكبير إلى قراءة نية المستخدم واختيار الوكيل الفرعي المناسب. لا تحتاج إلى أي أدوات، إذ يتولّى 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()
  • طريقة العرض (الأداة): يتم الاشتراك في ViewModel باستخدام context.watch() وإعادة الإنشاء عند تغيُّر الحالة.
  • الخدمة: تُجري طلبات HTTP إلى الخلفية في "حزمة تطوير التطبيقات" وتعرض بيانات مكتوبة

طبقة الخدمة

يتم تعريف الخدمات على أنّها واجهات مجرّدة، مع عمليات تنفيذ خاصة بحزمة ADK:

// Abstract interface  defines WHAT the service does
abstract class TryItOnService {
 Future<(Uint8List?, String?)> generateTryOnImage(
   Uint8List userImageBytes,
   Uint8List productImageBytes,
 );
}


// Concrete implementation  defines HOW (via ADK REST API)
class AdkFittingRoomService implements TryItOnService { ... }

يعني هذا الفصل أنّه يمكنك استبدال الخلفية المستندة إلى ADK بخدمة الذكاء الاصطناعي من Firebase أو خدمة وهمية أو أي تنفيذ آخر بدون تغيير بقية التطبيق.

نموذج واجهة برمجة التطبيقات المكوّن من 3 خطوات

يتّبع كلّ من AdkFittingRoomService وAdkStylingService النمط نفسه للتواصل مع الخلفية البرمجية لحزمة تطوير البرامج الإعلانية:

الخطوة 1: إنشاء جلسة

Future<String> _createSession() async {
 final url = Uri.parse(
   '$_baseUrl/apps/${Uri.encodeComponent(_appName)}/users/$_userId/sessions');
 final response = await _client.post(url,
   headers: {'Content-Type': 'application/json'},
   body: jsonEncode({}));
 final body = jsonDecode(response.body) as Map<String, dynamic>;
 return body['id'] as String;  // Returns the session ID
}

الخطوة 2: تشغيل البرنامج

Future<(String?, String?)> _runAgent({required String sessionId, ...}) async {
 final requestBody = jsonEncode({
   'appName': _appName,
   'userId': _userId,
   'sessionId': sessionId,
   'newMessage': {
     'role': 'user',
     'parts': [
       {'text': 'Generate a virtual try-on...'},
       {'inlineData': {'mimeType': 'image/jpeg', 'data': base64Encode(userImageBytes)}},
       {'inlineData': {'mimeType': 'image/png', 'data': base64Encode(productImageBytes)}},
     ],
   },
 });
 final response = await _client.post(Uri.parse('$_baseUrl/run'), body: requestBody);
 // Parse response events for artifact name and GCS URL...
}

الخطوة 3: استرداد العنصر

Future<Uint8List?> _loadArtifact({required String sessionId, required String artifactName}) async {
 final url = Uri.parse(
   '$_baseUrl/apps/$_appName/users/$_userId/sessions/$sessionId/artifacts/$artifactName');
 final response = await _client.get(url);
 final part = jsonDecode(response.body) as Map<String, dynamic>;
 final data = part['inlineData']['data'] as String;
 return base64Decode(data);  // Returns raw image bytes
}

يتمثّل أحد الاختلافات المهمة في التصميم في أنّ خدمة غرفة القياس تنشئ جلسة جديدة لكل طلب (يتم استدعاء _createSession() في كل مرة)، بينما تعيد خدمة تنسيق الملابس استخدام الجلسة نفسها (_sessionId ??= await _createSession()) لإتاحة المحادثات المتعددة الأدوار.

إدارة الحالة: TryItOnProvider

الملف:

workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart

يتولّى TryItOnProvider إدارة مسار تجربة الملابس بالكامل. تستخدم TryOnState تعدادًا كآلة حالة:

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


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

تضمن عمليات نقل الحالة الخاصة الاتساق، إذ لا يتم تعديل الحالة بدون محو البيانات القديمة وإرسال إشعار إلى واجهة المستخدم:

 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، بل يتغيّر محتوى الشاشة في مكانه استنادًا إلى تعداد الحالة.

عملية التسليم التي تتم من خلال الذكاء الاصطناعي الوكيل: غرفة تجربة الملابس → مصمّم الأزياء

إنّ نمط تجربة المستخدم الأكثر إثارة للاهتمام هو كيفية نقل التطبيق للسياق من وكيل غرفة القياس إلى وكيل مصمّم الأزياء.

في 5_fitting_room.dart، بعد إنشاء صورة تجربة الملابس، يفتح الزرّ "اقتَرِح عليّ أسلوبًا" نموذجًا. عندما يرسل المستخدم:

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

تتضمّن StyleRequest كل ما يحتاجه مصمّم الأزياء:

  • الموقع الجغرافي والمناسبة: سياق النص لتطبيق النمط
  • عنوان URL لصورة مستخدم GCS: حتى يتمكّن مصمّم الأسلوب من إعادة استخدام تمثيل المستخدم نفسه بالضبط
  • المنتج المحدّد: لكي يدرجه مصمّم الأزياء في كل طقم

هذه هي عملية التسليم المستندة إلى الوكيل، أي نقل السياق المتعدد الوسائط بسلاسة من وكيل ذكاء اصطناعي إلى آخر، مع عدم ظهور سوى نموذج بسيط للمستخدم.

عملية التصميم: StylingProvider

الملف:

workshop_tasks/step_2_style_me/providers/styling_provider.dart

إنّ StylingProvider أبسط من TryItOnProvider لأنّه يفوّض معظم التعقيد إلى الخلفية:

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


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


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

ترسل الطريقة refineWithFeedback رسالة نصية عادية إلى الجلسة نفسها، وتتعامل عمليات معاودة الاتصال InjectPreviousProducts وExtractAndInjectUserImage في الخلفية مع جميع عمليات إدارة السياق تلقائيًا.

8. 🚀 تشغيل التطبيق محليًا

للحصول على تجربة سلسة في Cloud Shell، يقدّم برنامج Go الخلفي تطبيق الويب المجمَّع المتوافق مع Flutter من المنفذ نفسه (8080). عملية واحدة، وعنوان 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 Web (للمرة الأولى)

افتح علامة تبويب جديدة في 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)
  • واجهة مستخدم المطوّرين في حزمة تطوير تطبيقات Android (ADK): تظهر أيضًا في / عندما لا يتوفّر إصدار Flutter، وهي مفيدة لتصحيح أخطاء الوكيل المباشر

3- فتح معاينة الويب

  1. في Cloud Shell، انقر على رمز معاينة الويب (أعلى يسار الصفحة) → المعاينة على المنفذ 8080
  2. يتم تحميل تطبيق التسوّق Flutter في علامة تبويب جديدة
  3. تصفُّح كتالوج المنتجات واختيار منتج
  4. انقر على رمز الشخص (👤) لبدء تجربة المنتج
  5. حمِّل صورة وشاهِد الذكاء الاصطناعي وهو ينشئ صورة لك بالملابس التي اخترتها
  6. انقر على "اقتراحات بشأن الإطلالة" للحصول على اقتراحات بشأن الملابس
  7. اكتب ملاحظات متابعة، مثل "أريد أسلوبًا أكثر بساطة" — تحسين في الجلسة نفسها

9- ☁️ النشر على Cloud Run

تجميع إصدار Flutter في الخلفية

تحتوي حاوية Cloud Run على كلّ من واجهة برمجة التطبيقات وواجهة المستخدم من صورة واحدة. انسخ إصدار الويب من 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

(إذا كنت قد كرّرت العملية محليًا، قد يكون لديك build/web من خطوة "التشغيل محليًا". لا بأس بإعادة تشغيل flutter build web.)

نشر الخلفية (التي تعرض واجهة برمجة التطبيقات وواجهة المستخدم)

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/ على المضيف نفسه. لا يلزم إجراء أي تعديلات على إعدادات الواجهة الأمامية، ولا يتم تمرير مفتاح واجهة برمجة التطبيقات.

التحقّق من عملية النشر

افتح عنوان URL الخاص بـ Cloud Run في المتصفّح واتّبِع الخطوات الكاملة:

  1. تصفّح → اختيار منتج
  2. تجربة الملابس → تحميل صورتك → الاطّلاع على الصورة من إنشاء الذكاء الاصطناعي
  3. جرِّب أسلوبي → املأ معلومات الموقع الجغرافي/المناسبة → اطّلِع على الملابس المقترَحة
  4. الملاحظات → كتابة "أريد أسلوبًا أكثر عصرية" → الاطّلاع على الملابس المعدَّلة
  5. إضافة إلى الحقيبة → إكمال عملية التسوّق

10. 🎉 الخاتمة

ما أنشأته

لقد استكشفت تجربة بيع بالتجزئة كاملة مستندة إلى الذكاء الاصطناعي، وتتضمّن ما يلي:

  • ‫✅ نظام خلفي متعدد الوكلاء يضم 4 وكلاء متخصصين يعملون معًا
  • ‫✅ غرفة قياس افتراضية تنشئ صورًا مخصّصة لتجربة الملابس
  • مصمّم أزياء يستنِد إلى الذكاء الاصطناعي يختار الملابس ويحسّنها من خلال المحادثة
  • ‫✅ تطبيق Flutter من عدّة منصات يتصل بخادم وكيل
  • ‫✅ النشر على Cloud Run لاستضافة قابلة للتوسّع وبدون خادم

المفاهيم الرئيسية

المفهوم

مكان ظهور الإعلان

تنظيم الوكلاء المتعدّدين في ADK

توجيه طلبات الوكيل الأساسي إلى وكلاء غرف القياس والكتالوج ومصمّمي الأزياء

إنشاء الصور في Gemini المتعدّد الوسائط

fitting_tool دمج صور المستخدمين مع صور المنتجات

حالة الجلسة للذكاء الاصطناعي الحواري

إعادة استخدام جلسات المصمّم للحصول على ملاحظات متكررة

تخزين العناصر لبيانات ثنائية

فصل تخزين الصور عن الردود النصية

عمليات ردّ الاتصال لمنطق البرامج الوسيطة

SaveIncomingBlobs، InjectPreviousProducts، SaveSelectedProducts

نمط تصميم MVVM مع حزمة Provider في Flutter

TryItOnProvider وStylingProvider مع ChangeNotifier

التسليم المستند إلى الذكاء الاصطناعي الوكيل

StyleRequest نقل السياق المتعدّد الوسائط بين الوكلاء

الخطوات التالية

  • 🎨 تخصيص طلبات الوكيل: انقر على instructions.md لتغيير شخصية مصمّم الأزياء
  • 🛍️ إضافة المزيد من المنتجات: تعديل catalog.yaml بمنتجات جديدة
  • 📱 إنشاء تطبيق متوافق مع الأجهزة الجوّالة: شغِّل flutter build ios أو flutter build apk
  • 🔄 إضافة جلسات مستمرة: استبدِل InMemoryService بتنفيذ يستند إلى قاعدة بيانات
  • 🔒 إضافة مصادقة: تأمين نقطة نهاية Cloud Run باستخدام IAM

الموارد