1. مقدمة
ما ستنشئه
في هذا الدرس التطبيقي حول الترميز، ستتولّى دور مطوّر ينشئ تطبيق Fashion، وهو تطبيق تسوّق من Flutter لعلامة تجارية خيالية للبيع بالتجزئة. مهمتك: إضافة ميزتَين مستندتَين إلى الذكاء الاصطناعي لتغيير تجربة التسوّق على الإنترنت.
- غرفة القياس الافتراضية: يحمّل المستخدم صورة له، ويختار قطعة ملابس، ثم يرى صورة من إنشاء الذكاء الاصطناعي وهو يرتدي تلك القطعة.
- 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
2. ربط حساب فوترة
أدرِج حسابات الفوترة باتّباع الخطوات التالية:
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
واجهة برمجة التطبيقات | الغرض |
| Vertex AI: ينفّذ |
| Cloud Storage: لتخزين صور كتالوج المنتجات ونتائج تجربة المنتجات افتراضيًا |
| Cloud Run: يستضيف الخلفية كحاوية بدون خادم |
| Cloud Build: إنشاء صور Docker من المصدر |
| 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 │
└────────────────┘
الوكيل | الطراز | الدور |
الوكيل الأساسي |
| شرطي مرور يقرأ رسالة المستخدم ويفوّضها إلى الوكيل المتخصّص المناسب. يستخدم نموذجًا سريعًا وخفيفًا لأنه يحتاج فقط إلى اتخاذ قرارات التوجيه. |
Catalog Agent |
| خبير منتجات تحمّل هذه الأداة كتالوج المنتجات من ملف YAML وتجيب عن طلبات البحث المتعلقة بالمنتجات. كما أنّه خفيف الوزن، فهو يبحث عن البيانات فقط. |
Fitting Room Agent |
| أخصائي التجربة في الواقع الافتراضي تأخذ هذه الأداة صورة مستخدم + صورة منتج وتنشئ صورة مركّبة للشخص وهو يرتدي هذا المنتج. يستخدم نموذجًا أكثر قدرة لأنّه يحتاج إلى التفكير في الصور. |
Stylist Agent |
| مستشار في الموضة وباستخدام الموقع الجغرافي والمناسبة والإعدادات المفضّلة، يختار لك 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
يمكن تكرار هذه الحلقة عدة مرات ضمن طلب واحد. على سبيل المثال، قد يقوم وكيل مصمّم الأزياء بما يلي:
- تلقّي الردّ "أريد أفكارًا لملابس مناسبة لقضاء عطلة على الشاطئ"
- استدعاء أداة
catalog_agentللحصول على قائمة المنتجات - اختيار 3 مجموعات ملابس
- الاتصال بـ
fitting_toolلكل زي لإنشاء الصور - إرجاع استجابة 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.
تتضمّن هذه الأداة ما يلي:
listProducts: تعرض كتالوج المنتجات الكامل من ملف YAMLgetProductImage: يتم تحميل صورة منتج من 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- فتح معاينة الويب
- في Cloud Shell، انقر على رمز معاينة الويب (أعلى يسار الصفحة) → المعاينة على المنفذ 8080
- يتم تحميل تطبيق التسوّق Flutter في علامة تبويب جديدة
- تصفُّح كتالوج المنتجات واختيار منتج
- انقر على رمز الشخص (👤) لبدء تجربة المنتج
- حمِّل صورة وشاهِد الذكاء الاصطناعي وهو ينشئ صورة لك بالملابس التي اخترتها
- انقر على "اقتراحات بشأن الإطلالة" للحصول على اقتراحات بشأن الملابس
- اكتب ملاحظات متابعة، مثل "أريد أسلوبًا أكثر بساطة" — تحسين في الجلسة نفسها
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 في المتصفّح واتّبِع الخطوات الكاملة:
- تصفّح → اختيار منتج
- تجربة الملابس → تحميل صورتك → الاطّلاع على الصورة من إنشاء الذكاء الاصطناعي
- جرِّب أسلوبي → املأ معلومات الموقع الجغرافي/المناسبة → اطّلِع على الملابس المقترَحة
- الملاحظات → كتابة "أريد أسلوبًا أكثر عصرية" → الاطّلاع على الملابس المعدَّلة
- إضافة إلى الحقيبة → إكمال عملية التسوّق
10. 🎉 الخاتمة
ما أنشأته
لقد استكشفت تجربة بيع بالتجزئة كاملة مستندة إلى الذكاء الاصطناعي، وتتضمّن ما يلي:
- ✅ نظام خلفي متعدد الوكلاء يضم 4 وكلاء متخصصين يعملون معًا
- ✅ غرفة قياس افتراضية تنشئ صورًا مخصّصة لتجربة الملابس
- ✅ مصمّم أزياء يستنِد إلى الذكاء الاصطناعي يختار الملابس ويحسّنها من خلال المحادثة
- ✅ تطبيق Flutter من عدّة منصات يتصل بخادم وكيل
- ✅ النشر على Cloud Run لاستضافة قابلة للتوسّع وبدون خادم
المفاهيم الرئيسية
المفهوم | مكان ظهور الإعلان |
تنظيم الوكلاء المتعدّدين في ADK | توجيه طلبات الوكيل الأساسي إلى وكلاء غرف القياس والكتالوج ومصمّمي الأزياء |
إنشاء الصور في Gemini المتعدّد الوسائط |
|
حالة الجلسة للذكاء الاصطناعي الحواري | إعادة استخدام جلسات المصمّم للحصول على ملاحظات متكررة |
تخزين العناصر لبيانات ثنائية | فصل تخزين الصور عن الردود النصية |
عمليات ردّ الاتصال لمنطق البرامج الوسيطة | |
نمط تصميم MVVM مع حزمة Provider في Flutter | |
التسليم المستند إلى الذكاء الاصطناعي الوكيل |
|
الخطوات التالية
- 🎨 تخصيص طلبات الوكيل: انقر على
instructions.mdلتغيير شخصية مصمّم الأزياء - 🛍️ إضافة المزيد من المنتجات: تعديل
catalog.yamlبمنتجات جديدة - 📱 إنشاء تطبيق متوافق مع الأجهزة الجوّالة: شغِّل
flutter build iosأوflutter build apk - 🔄 إضافة جلسات مستمرة: استبدِل
InMemoryServiceبتنفيذ يستند إلى قاعدة بيانات - 🔒 إضافة مصادقة: تأمين نقطة نهاية Cloud Run باستخدام IAM
الموارد
- مستندات ADK: مستندات رسمية حول Agent Development Kit
- رمز مصدر ADK Go: مستودع GitHub
- مرجع حزمة ADK Go: مرجع واجهة برمجة التطبيقات
- مستندات Gemini API: إمكانات النماذج والأدلّة
- حزمة Flutter Provider: مستندات إدارة الحالة
- مستندات Cloud Run: أدلة النشر والتوسيع