1. מבוא
מה תפַתחו
בשיעור Codelab הזה תיכנסו לנעליו של מפתח שיוצר את Fashion App, אפליקציית שופינג ב-Flutter למותג קמעונאי פיקטיבי. המשימה שלכם: להוסיף שתי תכונות מבוססות-AI שמשפרות את חוויית הקנייה באינטרנט.
- חדר מדידה וירטואלי – משתמש מעלה תמונה שלו, בוחר פריט לבוש ורואה תמונה שנוצרה על ידי AI של עצמו כשהוא לובש את הפריט.
- סטייליסטית מבוססת-AI – סוכן AI מספק המלצות ללבוש מלא על סמך המיקום של המשתמש, האירוע והעדפות הסגנון שלו. המשתמש יכול לשפר את ההמלצות באמצעות שיחה.
הרעיון פשוט: כשלקוחות מודדים בגדים בתא מדידה, הסיכוי שהם יקנו אותם גבוה בהרבה. אבל באינטרנט? אתה רק מנחש. הפרויקט הזה מגשר על הפער הזה באמצעות AI.
Architecture at a Glance
Flutter App ──── HTTP/REST ────▶ ADK Go Backend
│
┌──────────┼──────────┐
Fitting Room Stylist Catalog
Agent Agent Agent
│
Gemini API + Cloud Storage
טכנולוגיות ליבה
רכיב | טכנולוגיה | מטרה |
Agent Framework | ADK (Agent Development Kit) for Go | תיאום בין כמה סוכנים, סשנים וארטיפקטים |
Agent Reasoning (Pro) | Gemini 3.1 Pro Preview | הטכנולוגיה שמפעילה את חדר המדידה ואת הסוכנים הווירטואליים של הסטייליסטים |
Agent Reasoning (Flash) | Gemini 3 Flash Preview | מפעיל את סוכני הבסיס והקטלוג (ניתוב/חיפוש קלים) |
יצירת תמונות | Gemini 2.5 Flash Image | יצירת תמונות של פריטים למדידה ושל מערכות לבוש |
Frontend | Flutter (Dart) | אפליקציה חוצת-פלטפורמות (אינטרנט, iOS, Android) |
אחסון | Google Cloud Storage | שומר תמונות של מוצרים ופריטים שנוצרו |
אירוח | Cloud Run | פריסת קונטיינר ללא שרת |
2. 📦 דרישות מוקדמות והגדרת Cloud Shell
1. פתיחת Cloud Shell Editor
👉 פותחים את Cloud Shell Editor בדפדפן.
אם הטרמינל לא מופיע בתחתית המסך:
- לוחצים על View (תצוגה) ← Terminal (טרמינל).
2. הגדרת Flutter SDK
סביבת Cloud Shell כוללת מראש התקנה של Flutter בנתיב /google/flutter. מכיוון שהספרייה הזו נמצאת בבעלות של משתמש מערכת אחר, תופיע שגיאה fatal: detected dubious ownership בפעם הראשונה שתפעילו את flutter. מוסיפים אותו לרשימת הספרייה הבטוחה של git פעם אחת:
git config --global --add safe.directory /google/flutter
מוודאים ש-Flutter מותקן ב-PATH ופועל:
flutter --version
בהרצה הראשונה, מערכת ההפעלה מורידה את Dart SDK ובונה את כלי 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. הפעלת ממשקי ה-API הנדרשים
gcloud services enable \
aiplatform.googleapis.com \
storage.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
API | מטרה |
| 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 של ה-Backend קורא מ-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. אימות באמצעות Application Default Credentials
צריך להריץ את הפקודה הזו לפני שמפעילים את הקצה העורפי באופן מקומי. הקצה העורפי של Go משתמש ב-ADC כדי לאמת כל קריאה ל-Vertex AI (Gemini) ול-Cloud Storage. בלי ADC, ה-Backend יופעל אבל כל בקשה לניסיון וירטואלי תיכשל עם שגיאה 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 "..."
אמורים להופיע כ-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 │
└────────────────┘
סוכן | מודל | תפקיד |
Root Agent |
| שוטר תנועה. קריאת ההודעה של המשתמש והעברה לנציג מומחה מתאים. המערכת משתמשת במודל מהיר וקל משקל כי היא צריכה רק לקבל החלטות לגבי ניתוב. |
Catalog Agent |
| מומחה מוצר. טוען את קטלוג המוצרים מקובץ YAML ועונה על שאילתות לגבי מוצרים. היא גם קלה – היא רק מחפשת נתונים. |
סוכן חדר המדידה |
| מומחה להתנסות וירטואלית. הכלי משלב תמונה של משתמש עם תמונה של מוצר, ויוצר תמונה מורכבת של האדם כשהוא לובש את הפריט. המודל שבו נעשה שימוש הוא בעל יכולות גבוהות יותר כי הוא צריך להסיק מסקנות לגבי תמונות. |
Stylist Agent |
| יועץ אופנה. הוא בוחר מתוך הקטלוג 3 שילובי לבוש בהתאם למיקום, לאירוע ולהעדפות. יכול ליצור תמונות של מדידת בגדים לכל תלבושת. בנוסף, הוא משתמש במודל מתקדם לניתוח יצירתי. |
נקודת הכניסה: main.go
הכול מתחיל ב-main.go, שבו סוכני ה-AI מחוברים זה לזה ושרת ה-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רושם את כל ארבעת הסוכנים, כך ש-API בארכיטקטורת REST יכול לנתב לכל אחד מהם לפי שם. -
adkrest.NewServerמספק את API בארכיטקטורת REST באופן אוטומטי – אתם לא צריכים לכתוב בעצמכם את handlers של נקודות הקצה. ערכת ה-ADK מספקת ניהול סשנים, אחסון ארטיפקטים והרצת סוכנים מחוץ לקופסה. -
session.InMemoryService()שומרת את הסשנים בזיכרון. המשמעות היא שאם השרת יופעל מחדש, הסשנים יאבדו, וזה בסדר להדגמה. בסביבת ייצור, צריך להשתמש במאגר נתונים קבוע. -
gcsartifact.NewServiceמאחסן ארטיפקטים (תמונות שנוצרו על ידי AI) ב-Google Cloud Storage, כך שהם נשמרים בין בקשות שונות ואפשר לשתף אותם באמצעות URI של GCS.
5. 🤖 ניתוח מעמיק של ADK (ערכה לפיתוח סוכנים)
מה זה ADK?
הערכה לפיתוח סוכנים (ADK) היא מסגרת קוד פתוח מבית Google ליצירת סוכני AI ב-Go (וב-Python/Java). זו השכבה שבין האפליקציה שלכם לבין Gemini API.
אפשר לקרוא ישירות ל-Gemini API. אבל אם האפליקציה צריכה:
- חיפוש מוצרים מקטלוג
- יצירת תמונות על סמך תמונות של משתמשים
- לזכור אילו תלבושות הוצעו בעבר
- תיאום בין כמה סוכני AI
אתם צריכים מבנה. ה-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הכלי כדי לקבל את רשימת המוצרים - בחירת 3 שילובים של תלבושות
- מזינים הנחיה לכל תלבושת כדי ליצור תמונות
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 מבטל את הסריאליזציה של הארגומנטים, קורא לפונקציה ושולח את התוצאה בחזרה.
הפרמטר 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
},
כשנציג חדר המדידות צריך פרטי מוצר, הוא יכול להתקשר לנציג הקטלוג כאילו מדובר בכלי רגיל. מודל ה-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)
מצב הוא מאגר של צמדי מפתח/ערך שמצורף לסשן. הסוכנים קוראים את המצב וכותבים אותו כדי לתאם:
// 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 מחזירה תגובה שאינה 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 ולבסוף את הקבצים המקומיים. אחרי שהתמונה נטענת, היא נשמרת במטמון כארטיפקט, כך שהקריאות הבאות יהיו מיידיות.
6. 🧪 צינור ה-AI: סוכנים בפעולה
עכשיו נסביר על שני הסוכנים המתוחכמים ביותר – אלה שיוצרים תמונות ומארגנים מערכות לבוש.
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
}
תמונת המשתמש יכולה להגיע משני מקורות:
- שם של פריט מידע שנוצר בתהליך פיתוח (Artifact) (למשל
upload_abc123_1) – זו ההעלאה הראשונית, שנשמרת על ידי הקריאה החוזרת (callback) שלSaveIncomingBlobs - URI של
gs://– זו תוצאת התאמה שנוצרה בעבר ונשמרה ב-GCS לשימוש חוזר בסשנים שונים
העיצוב הזה עם שני הנתיבים הוא מכוון: כשהסוכן הסטייליסט יוצר בהמשך תמונות של פריטים שנמדדים, הוא משתמש מחדש בכתובת ה-URL של GCS מתוצאת חדר המדידות הראשונית, כך שהזהות של המשתמש נשארת עקבית בכל הפריטים.
שלב 2: בניית ההנחיה המולטימודלית
parts := []*genai.Part{
genai.NewPartFromText(toolInstructions), // Identity preservation prompt
genai.NewPartFromText("Reference Person Photo:"),
userPart, // The user's photo
}
for _, acc := range args.Accessories {
accResp, _ := ctx.Artifacts().Load(ctx, acc) // Load product image artifact
parts = append(parts, genai.NewPartFromText("Product Image to Apply:"))
parts = append(parts, accResp.Part) // The product photo
}
הסמל toolInstructions (מוטמע מ-tool_instructions.md) הוא חיוני – הוא אומר ל-Gemini לשמור על הזהות של המשתמש (פנים, מבנה גוף, גוון עור, שיער) ולהחיל רק את פריט הלבוש. בלי הנדסת הפקודות הזו, יכול להיות שהמודל ישנה את המראה של האדם.
שלב 3: קוראים ל-Gemini כדי ליצור תמונה
client, _ := genai.NewClient(ctx, &genai.ClientConfig{
Backend: genai.BackendVertexAI, // Vertex AI endpoint
Project: os.Getenv("GOOGLE_CLOUD_PROJECT"), // From your .env
Location: "global", // Multi-region endpoint
})
resp, _ := client.Models.GenerateContent(ctx, "gemini-2.5-flash-image",
[]*genai.Content{genai.NewContentFromParts(parts, "user")},
&genai.GenerateContentConfig{
ResponseModalities: []string{"TEXT", "IMAGE"}, // Request both text and image output
Temperature: genai.Ptr(float32(0.2)), // Low temperature for consistency
})
לכל ארבעת הסוכנים ולכלי ליצירת תמונות יש נתיב אימות יחיד: Backend: genai.BackendVertexAI עם מזהה הפרויקט, שמאומת באמצעות Application Default Credentials. מודלי התזמור (gemini-3.1-pro-preview, gemini-3-flash-preview) ומודל התמונות (gemini-2.5-flash-image) פועלים מאחורי אותה נקודת קצה של Vertex AI, ואותה ADC גם מאשרת גישה ל-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 של 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) מאותתת לפונקציית ה-callback: "סיימתי את העיבוד המקדים – עכשיו אפשר להפעיל את הסוכן כרגיל". אם הפונקציה תחזיר תוכן שאינו nil, היא תגרום להפסקת הפעולה של הסוכן.
6.2 הסוכן Stylist
קובץ:
adk_backend/stylist/agent.go
הסוכן לעיצוב הוא המתקדם ביותר במערכת. הוא אוסף המלצות מותאמות אישית לגבי פריטי לבוש, ותומך בשיפורים חוזרים ונשנים באמצעות שיחה.
שלוש שיחות חוזרות – הזיכרון של הסטייליסט
הסטייליסט משתמש בשלושה קריאות חוזרות (callback) כדי לשמור על ההקשר בשיחות מרובות תורות:
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
}
Callback 2:
ExtractAndInjectUserImage (BeforeModel)
הבעיה: כשהמשתמש מספק משוב ("תכתוב את זה בצורה יותר קלילה"), ההודעה הבאה לא כוללת שוב את התמונה של המשתמש. אבל הכלי להתאמת מידות צריך את זה.
הפתרון: בבקשה הראשונה, פונקציית ה-callback הזו מחלצת את ההפניה לתמונת המשתמש ושומרת אותה במצב. בבקשות הבאות, הוא מוסיף אותו מחדש:
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)
אחרי שה-LLM מגיב עם הצעות לציוד, פונקציית ה-callback הזו מנתחת את ה-JSON כדי לחלץ מזהי מוצרים ושומרת אותם לשימוש בפונקציית ה-callback 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
}
שלושת ה-callbacks האלה יוצרים ביחד לולאת משוב:
Request 1: User sends styling request + user image
→ ExtractAndInjectUserImage SAVES image to state
→ LLM generates 3 outfits
→ SaveSelectedProducts SAVES product IDs to state
Request 2: User says "make it more casual"
→ ExtractAndInjectUserImage INJECTS saved image into prompt
→ InjectPreviousProducts INJECTS "don't reuse these IDs"
→ LLM generates 3 NEW outfits
→ SaveSelectedProducts UPDATES product IDs in state
6.3 הסוכן הראשי
קובץ:
adk_backend/rootagent/agent.go
הסוכן הפשוט ביותר – רק 31 שורות:
func NewRootAgent(project string, fittingAgent, catalogAgent, stylistAgent agent.Agent) (agent.Agent, error) {
m, _ := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{
Backend: genai.BackendVertexAI,
Project: project,
Location: "global",
})
return llmagent.New(llmagent.Config{
Name: "root_agent",
Model: m,
Description: "A root agent that delegates to other agents",
Instruction: "You are a helpful shopping assistant. If the user asks about fitting " +
"items or generating images, delegate to the fitting room agent. If the user " +
"asks about products, delegate to the catalog agent. If the user asks for " +
"styling advice, delegate to the stylist agent.",
SubAgents: []agent.Agent{fittingAgent, catalogAgent, stylistAgent},
})
}
הוא משתמש ב-gemini-3-flash-preview (המודל המהיר ביותר) כי החלטות הניתוב פשוטות – מודל ה-LLM רק צריך לקרוא את הכוונה של המשתמש ולבחור את סוכן המשנה המתאים. לא צריך כלים מיוחדים, SubAgents מטפל בהקצאת ההרשאות באופן אוטומטי.
7. 📱 Flutter Frontend Architecture
הקצה הקדמי של Flutter הוא אפליקציית קניות קמעונאית שפועלת באופן מלא. תכונות ה-AI נמצאות ב-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 (ווידג'ט): נרשם ל-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 בן 3 השלבים
הפונקציות AdkFittingRoomService ו-AdkStylingService פועלות לפי אותו דפוס כדי לתקשר עם ה-backend של ADK:
שלב 1: יצירת סשן
Future<String> _createSession() async {
final url = Uri.parse(
'$_baseUrl/apps/${Uri.encodeComponent(_appName)}/users/$_userId/sessions');
final response = await _client.post(url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({}));
final body = jsonDecode(response.body) as Map<String, dynamic>;
return body['id'] as String; // Returns the session ID
}
שלב 2: הפעלת הסוכן
Future<(String?, String?)> _runAgent({required String sessionId, ...}) async {
final requestBody = jsonEncode({
'appName': _appName,
'userId': _userId,
'sessionId': sessionId,
'newMessage': {
'role': 'user',
'parts': [
{'text': 'Generate a virtual try-on...'},
{'inlineData': {'mimeType': 'image/jpeg', 'data': base64Encode(userImageBytes)}},
{'inlineData': {'mimeType': 'image/png', 'data': base64Encode(productImageBytes)}},
],
},
});
final response = await _client.post(Uri.parse('$_baseUrl/run'), body: requestBody);
// Parse response events for artifact name and GCS URL...
}
שלב 3: אחזור הארטיפקט
Future<Uint8List?> _loadArtifact({required String sessionId, required String artifactName}) async {
final url = Uri.parse(
'$_baseUrl/apps/$_appName/users/$_userId/sessions/$sessionId/artifacts/$artifactName');
final response = await _client.get(url);
final part = jsonDecode(response.body) as Map<String, dynamic>;
final data = part['inlineData']['data'] as String;
return base64Decode(data); // Returns raw image bytes
}
הבדל חשוב בעיצוב: שירות תא המדידה יוצר סשן חדש לכל בקשה (מתבצעת קריאה ל-_createSession() בכל פעם), ואילו שירות הסטיילינג משתמש מחדש באותו סשן (_sessionId ??= await _createSession()) כדי לאפשר שיחה רב-שלבית.
ניהול מצב: TryItOnProvider
קובץ:
workshop_tasks/step_1_try_it_on/providers/try_it_on_provider.dart
TryItOnProvider מנהל את כל תהליך המדידה. הוא משתמש ב-enum 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 – כדי שהסטייליסט יוכל לעשות שימוש חוזר באותו ייצוג של המשתמש
- מוצר נבחר – כדי שהסטייליסט יכלול אותו בכל מערכת לבוש
זהו העברה מבוססת-סוכן – העברה חלקה של הקשר מרובה-האופנים מסוכן AI אחד לסוכן AI אחר, כשהמשתמש רואה רק טופס פשוט.
תהליך העיצוב: 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 שולחת הודעת טקסט פשוטה לאותו סשן – פונקציות ה-callback InjectPreviousProducts ו-ExtractAndInjectUserImage של ה-backend מטפלות בכל ניהול ההקשר באופן אוטומטי.
8. 🚀 הרצת האפליקציה באופן מקומי
כדי ליהנות מחוויה חלקה ב-Cloud Shell, קצה העורפי של Go משרת את אפליקציית האינטרנט המהודרת של Flutter מאותו פורט (8080). תהליך אחד, כתובת URL אחת לתצוגה מקדימה, בלי בעיות שקשורות למקורות שונים, בלי צורך לערוך קובצי הגדרה.
לפני שמתחילים – בדיקת תקינות של ADC
לשרת העורפי נדרשים Application Default Credentials כדי לבצע קריאות ל-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:
- Terminal A – מריץ את ה-backend באופן רציף (
./run.sh). משאירים אותו פתוח. - Terminal B — מריץ את בניית האתר ב-Flutter פעם אחת (
flutter build web). יוצא בסיום.
הסדר לא משנה – אפשר להתחיל עם כל אחת מהן. אבל כדי להבטיח את חוויית ההפעלה הראשונה הכי חלקה, כדאי לבנות את Flutter קודם, כדי של-Backend יהיה ממשק משתמש להצגה מהרגע שהוא מתחיל לפעול.
1. Terminal B — Build the Flutter Web Bundle (one-shot)
פותחים כרטיסייה חדשה ב-Cloud Shell (הסמל + בחלק העליון של חלונית הטרמינל), ואז:
cd ~/fashion_app_demo/flutter_frontend
flutter pub get
flutter build web
הפעולה הזו יוצרת את flutter_frontend/build/web/ – ספרייה של קבצים סטטיים (HTML, JS, נכסים) – ומסתיימת. הקצה העורפי יציג את הנתונים האלה ברגע שהוא יזהה שהספרייה קיימת.
2. טרמינל א' – הפעלת ה-Backend (פעולה ארוכת טווח)
בטרמינל המקורי של Cloud Shell:
cd ~/fashion_app_demo/adk_backend
./run.sh
אמורה להופיע הודעה שדומה להודעה הבאה:
Serving Flutter web build from ../flutter_frontend/build/web
משאירים את הטרמינל הזה פועל – ה-backend יפעל כל עוד run.sh פועל. כדי להפסיק את ההקלטה, לוחצים על Ctrl+C.
השרת חושף את כל מה שיש ביציאה 8080:
-
/– אפליקציית אינטרנט ב-Flutter (ממשק המשתמש של הקניות) -
/api/– נקודות קצה של ADK REST (שמופעלות על ידי אפליקציית Flutter) - ADK Dev UI – גם בכתובת
/כשאין גרסת Flutter; שימושי לניפוי באגים ישיר של סוכנים
3. פתיחת תצוגה מקדימה של אתר
- ב-Cloud Shell, לוחצים על סמל תצוגה מקדימה באינטרנט (בפינה השמאלית העליונה) → תצוגה מקדימה ביציאה 8080.
- אפליקציית השופינג של Flutter נטענת בכרטיסייה חדשה
- מעיינים בקטלוג המוצרים ובוחרים פריט
- מקישים על סמל בצורת אדם (👤) כדי להתחיל את תהליך המדידה.
- מעלים תמונה וצופים ב-AI יוצר תמונה של מדידה וירטואלית
- מקישים על 'המלצות לסטיילינג' כדי לקבל המלצות ללבוש
- מקלידים משוב המשך כמו "תנסח את זה בצורה יותר קלילה" – שיפור באותו סשן
9. ☁️ פריסה ב-Cloud Run
איגוד ה-Flutter Build בקצה העורפי
הקונטיינר של Cloud Run כולל גם את ה-API וגם את ממשק המשתמש מקובץ אימג' אחד. מעתיקים את ה-build של Flutter web אל adk_backend/flutter_web/ – זה הנתיב הראשון שהשרת של Go בודק כשבוחרים איזה ממשק משתמש להציג:
cd ~/fashion_app_demo/flutter_frontend
flutter build web
rm -rf ../adk_backend/flutter_web
cp -r build/web ../adk_backend/flutter_web
(אם ביצעתם איטרציות באופן מקומי, יכול להיות שכבר יש לכם את build/web מהשלב של הפעלה מקומית. עדיין אפשר להריץ מחדש את flutter build web.)
פריסת הקצה העורפי (שמשרת את ה-API ואת ממשק המשתמש)
cd ~/fashion_app_demo/adk_backend
gcloud run deploy fashion-app-backend \
--source . \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars "GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GCS_BUCKET=fashion-app-$PROJECT_ID" \
--memory 1Gi \
--cpu 2 \
--timeout 300s \
--min-instances 0 \
--max-instances 3
בסיום הפריסה, תקבלו כתובת URL של שירות כמו https://fashion-app-backend-xyz-uc.a.run.app. פותחים אותו בדפדפן – אפליקציית הקניות של Flutter נטענת מ-/, והקריאות ל-API שלה מגיעות אל /api/ באותו מארח. אין צורך לערוך את ההגדרות של קצה קדמי, לא מועבר מפתח API.
אימות הפריסה
פותחים את כתובת ה-URL של Cloud Run בדפדפן ומבצעים את כל השלבים בתהליך:
- עיון → בחירת מוצר
- למדידה → העלאת תמונה → צפייה בתמונה שנוצרה על ידי AI
- המלצות סטיילינג → מזינים את המיקום או האירוע → צופים בסטים שנבחרו במיוחד
- משוב → מקלידים "תגרום לזה להיראות יותר קז'ואל" → רואים את התלבושות המעודכנות
- הוספה לעגלת הקניות → השלמת תהליך הקנייה
10. 🎉 סיכום
מה יצרתם
התנסיתם בחוויה מלאה של קמעונאות מבוססת-AI, שכוללת:
- ✅ קצה עורפי מרובה-סוכנים עם 4 סוכנים מומחים שעובדים יחד
- ✅ חדר מדידה וירטואלי שמייצר תמונות בהתאמה אישית של פריטים שנמדדו
- ✅ סטייליסטית מבוססת-AI שבוחרת תלבושות ומשפרת אותן באמצעות שיחה
- ✅ אפליקציית Flutter בפלטפורמות שונות שמתחברת לקצה העורפי של הנציג
- ✅ Cloud Run deployment for scalable, serverless hosting
מושגים מרכזיים
קונספט | איפה ראית את המודעה |
תיאום בין כמה סוכנים ב-ADK | ניתוב של סוכן ראשי לחדר מדידה, לקטלוג ולסוכני סטיילינג |
יצירת תמונות באמצעות Gemini multimodal |
|
מצב הפעלה של AI בממשק שיחה | מעצב/ת שמשתמש/ת מחדש בסשנים כדי לקבל משוב חוזר |
אחסון ארטיפקטים של נתונים בינאריים | הפרדה בין אחסון תמונות לבין תשובות טקסט |
Callbacks for middleware logic |
|
MVVM + Provider in Flutter | |
העברה אג'נטית |
|
השלבים הבאים
- 🎨 התאמה אישית של ההנחיות לסוכן – לוחצים על
instructions.mdכדי לשנות את האישיות של מעצב התמונות - 🛍️ הוספת מוצרים נוספים – עדכון
catalog.yamlבפריטים חדשים - 📱 יצירת אפליקציה לנייד – מריצים את הפקודה
flutter build iosאוflutter build apk - 🔄 Add persistent sessions — replace
InMemoryServicewith a database-backed implementation - 🔒 Add authentication — secure the Cloud Run endpoint with IAM
משאבים
- מאמרי עזרה בנושא ADK – מאמרי עזרה רשמיים בנושא ערכת פיתוח סוכנים
- ADK Go Source Code — מאגר GitHub
- ADK Go Package Reference – הפניית API
- מאמרי העזרה של Gemini API – מדריכים ויכולות של המודלים
- Flutter Provider Package – מסמכי ניהול מצב
- מאמרי העזרה של Cloud Run – מדריכים לפריסה ולהתאמה לעומס