פריסת סוכן AI מסוג RAG ב-Streamlit באמצעות Google ADK ו-Cloud Run

1. מבוא

בשיעור Codelab הזה נסביר איך ליצור סוכן בריסטה אינטראקטיבי מבוסס-AI לבית קפה. באמצעות הערכה לפיתוח סוכנים (ADK) בקוד פתוח של Google ומודל Gemini 3.5 Flash, תיישמו יצירה משולבת-אחזור (RAG) כדי לבסס את ההמלצות של הסוכן על מערך נתונים של תפריט מדומה. לבסוף, תעטפו את הסוכן בממשק משתמש של Streamlit ותפרסו אותו ב-Cloud Run.

הפעולות שתבצעו:

  • יוצרים מקור נתונים של RAG ‏ (menu.json) שמכיל פריטי קפה, תגים ואלרגנים.
  • בונים סוכן AI באמצעות ADK LlmAgent ומחברים כלי Python לטעינת נתוני התפריט.
  • עוטפים את הסוכן באפליקציית צ'אט של Streamlit שמנהלת את היסטוריית השיחות.
  • פורסים את אפליקציית Streamlit ב-Cloud Run באמצעות פריסה מבוססת-מקור.
  • בדיקת ההצמדה של RAG והמודעות לאלרגנים.

תרשים ארכיטקטורה

הדרישות

  • דפדפן אינטרנט כמו Chrome.
  • פרויקט ב-Google Cloud שהחיוב בו מופעל.
  • היכרות בסיסית עם Python.

שיעור ה-Codelab הזה מיועד למפתחים בכל הרמות, כולל מתחילים.

העלות המשוערת: פחות מ-1.00 $‎.

‫2. לפני שמתחילים

יצירת פרויקט ב-Google Cloud

  1. במסוף Google Cloud, בוחרים או יוצרים פרויקט בענן של Google.
  2. הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud.

הפעלת Cloud Shell

  1. לוחצים על Activate Cloud Shell בחלק העליון של מסוף Google Cloud.

הפעלת Cloud\nShell

  1. אימות האימות:

מתן הרשאה ל-Cloud Shell

  gcloud auth list
  1. מוודאים שהפרויקט הפעיל מוגדר:
  gcloud config get project

אם מזהה הפרויקט שמוצג לא נכון או שלא הוגדר מזהה, מריצים את הפקודה:

  gcloud config set project <YOUR_PROJECT_ID>

הפעלת ממשקי ה-API

מריצים את הפקודה הבאה כדי להפעיל את כל ממשקי ה-API הנדרשים:

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

3. הגדרת הפרויקט

בשלב הזה, מאתחלים את משתני הסביבה של הפרויקט ויוצרים ספריית עבודה לפרויקט.

  1. בסשן הפעיל של Cloud Shell, מאתחלים את משתני הסביבה של הפרויקט הבאים:
  export PROJECT_ID=$(gcloud config get-value project)

הערה: שימוש באזור הקרוב ביותר

מוצאים את האזור הקרוב ביותר ומחליפים את insert-region-here בפקודה הבאה:

  export REGION=[insert-region-here]
  1. יוצרים ספריית פרויקט חדשה בשם coffee-barista-agent ועוברים אליה:
  mkdir coffee-barista-agent && cd coffee-barista-agent

4. יצירת מקור נתונים לדוגמה של תפריט

כדי להקפיד על דיוק המידע שמתקבל מ-AI Barista ולמנוע ממנו להמציא פריטים שלא קיימים, צריך ליצור מערך נתונים מקומי של תפריט. הסוכן יקרא את הקובץ הזה בזמן הריצה באמצעות כלי בהתאמה אישית.

  1. יוצרים את הקובץ menu.json ופותחים אותו ב-Cloud Shell Editor:
  cloudshell edit menu.json
  1. מדביקים את תוכן ה-JSON הבא בעורך ושומרים את הקובץ:
[
  {
    "name": "Espresso Solo",
    "description": "A single shot of rich, bold espresso.",
    "price": 2.50,
    "tags": ["strong", "hot", "dairy-free", "sugar-free"],
    "allergens": []
  },
  {
    "name": "Oat Milk Honey Latte",
    "description": "Creamy steamed oat milk with espresso and a touch of honey.",
    "price": 5.00,
    "tags": ["sweet", "hot", "dairy-free"],
    "allergens": []
  },
  {
    "name": "Cold Brew Coffee",
    "description": "Smooth, slow-steeped cold brew served over ice.",
    "price": 4.00,
    "tags": ["strong", "cold", "dairy-free", "sugar-free"],
    "allergens": []
  },
  {
    "name": "Seasonal Pumpkin Latte",
    "description": "Spiced pumpkin sauce, espresso, and steamed milk, topped with whipped cream.",
    "price": 5.50,
    "tags": ["sweet", "hot", "seasonal"],
    "allergens": ["dairy"]
  },
  {
    "name": "Classic Croissant",
    "description": "Flaky, buttery traditional French pastry.",
    "price": 3.50,
    "tags": ["bakery", "savory"],
    "allergens": ["wheat", "dairy"]
  },
  {
    "name": "Vegan Blueberry Muffin",
    "description": "Soft, sweet muffin packed with real blueberries, entirely plant-based.",
    "price": 3.75,
    "tags": ["bakery", "sweet", "dairy-free", "vegan"],
    "allergens": ["wheat"]
  },
  {
    "name": "Nitro Cold Brew",
    "description": "Cold brew infused with nitrogen for a super smooth, creamy head.",
    "price": 4.50,
    "tags": ["strong", "cold", "dairy-free", "sugar-free"],
    "allergens": []
  },
  {
    "name": "Iced Caramel Macchiato",
    "description": "Chilled milk and vanilla syrup marked with espresso and caramel drizzle.",
    "price": 5.25,
    "tags": ["sweet", "cold"],
    "allergens": ["dairy"]
  }
]
  1. מוודאים שפורמט קובץ ה-JSON תקין:
  cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"

💬 דיון: קובץ JSON מקומי לעומת מסדי נתונים פעילים

למה אנחנו משתמשים בקובץ פשוט של menu.json במקום במסד נתונים פעיל?

כדי ליצור מדריך קצר או אב טיפוס, קובץ JSON מקומי מאפשר לדלג על ההגדרה הראשונית של מסד הנתונים ועל המורכבות שלה. עם זאת, באפליקציית ייצור ארגונית בעולם האמיתי, צריך לקשר את הסוכן למסד נתונים מנוהל כמו Cloud Firestore,‏ AlloyDB או Cloud SQL.

שימוש במסד נתונים פעיל מאפשר למנהלי בתי קפה להוסיף פריטים עונתיים, לעדכן מחירים או לשנות תגי אלרגנים באופן דינמי בלי לבנות מחדש את תמונת הקונטיינר או לפרוס מחדש את קוד האפליקציה. בהמשך ה-Codelab נשתמש במסד נתונים פעיל כשלב אופציונלי.

5. יצירת סוכן ADK

עכשיו מתקינים את החבילות הנדרשות ויוצרים את לוגיקת הליבה של סוכן ADK. תגדירו כלי get_menu() ותעבירו אותו אל LlmAgent.

  1. יוצרים את הקובץ requirements.txt ופותחים אותו ב-Cloud Shell Editor:
  cloudshell edit requirements.txt
  1. מדביקים את התלויות הבאות בעורך ושומרים את הקובץ:
google-adk==2.2.0
streamlit==1.58.0
  1. יוצרים את הקובץ agent.py ופותחים אותו ב-Cloud Shell Editor:
  cloudshell edit agent.py
  1. מדביקים את הקוד הבא ב-agent.py:
# agent.py
import json

from google.adk.agents import LlmAgent

# [START get_menu]
def get_menu() -> str:
    """Retrieves the coffee shop menu from menu.json.

    Returns:
        str: A JSON string representing the list of menu items.
    """
    try:
        with open("menu.json", "r") as f:
            menu_data = json.load(f)
            return json.dumps(menu_data)
    except Exception as e:
        return json.dumps({"error": f"Could not retrieve menu: {str(e)}"})
# [END get_menu]

# Create the barista agent
barista_agent = LlmAgent(
    name="barista_agent",
    model="gemini-3.5-flash",
    instruction="""You are a friendly barista at ☕ Coffee Shop.
Your job is to recommend drinks and pastries to customers based on their preferences.

Rules you MUST follow:
1.  You must recommend items ONLY from the menu returned by get_menu().
2.  Do NOT recommend or suggest any item that is not present in the menu.
3.  If a user's preference is vague or unclear, ask exactly ONE friendly clarifying question to narrow down what they want (e.g., cold or hot, sweet or strong, coffee or pastry).
4.  Be warm and welcoming, but remain professional.
5.  Ground your recommendations in the actual tags, descriptions, and allergens listed in the menu (e.g., if a user is dairy-free, recommend ONLY items tagged 'dairy-free' or with no dairy allergens).
""",
    tools=[get_menu]
)

from google.adk.apps import App

# Define the App object
app = App(
    name="coffee_barista_app",
    root_agent=barista_agent
)
  1. יוצרים את הקובץ app.py ופותחים אותו ב-Cloud Shell Editor:
  cloudshell edit app.py
  1. מדביקים את הקוד הבא ב-app.py:
# app.py
import streamlit as st
import json

# Set page config for a premium look
st.set_page_config(
    page_title="☕ Coffee Shop - Barista Bot",
    page_icon="☕",
    layout="wide",
    initial_sidebar_state="expanded"
)

# Custom CSS to make the header sticky (adapts to light/dark themes)
st.markdown("""
<style>
    div[data-testid="element-container"]:has(.header-container),
    div.element-container:has(.header-container) {
        position: sticky;
        top: 2.875rem;
        z-index: 999;
        background-color: transparent;
        padding-bottom: 10px;
    }
</style>
""", unsafe_allow_html=True)

# App Header (using inline styles for the permanent coffee theme look)
st.markdown("""
<div class="header-container" style="text-align: center; padding: 20px; background: linear-gradient(135deg, #8B5E3C, #6F4E37); color: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
    <h1 style="margin: 0; font-size: 2.5rem; font-weight: 700; color: white;">☕ ☕ Coffee Shop</h1>
    <p style="margin: 5px 0 0 0; font-size: 1.1rem; opacity: 0.9; color: white;">Your friendly AI Barista is ready to help you find the perfect drink or pastry!</p>
</div>
""", unsafe_allow_html=True)

# Load Menu for the sidebar
# [START load_menu]
try:
    with open("menu.json", "r") as f:
        menu_items = json.load(f)
except Exception as e:
    st.error(f"Error loading menu: {e}")
    menu_items = []
# [END load_menu]

# Sidebar Menu & Configuration
with st.sidebar:
    st.markdown("## ☕ Coffee Shop Menu")
    st.markdown("Explore our offerings and ask the barista for recommendations.")
    st.markdown("---")

    for item in menu_items:
        with st.container(border=True):
            st.markdown(f"**{item['name']}**  •  **${item['price']:.2f}**")
            st.caption(item['description'])

            # Tags & Allergens as native badges
            tags = " ".join([f"`{t}`" for t in item.get("tags", [])])
            if tags:
                st.markdown(tags)

            allergens = ", ".join(item.get("allergens", []))
            if allergens:
                st.markdown(f"⚠️ *Allergens: {allergens}*")

# Chat Interface
if "session_id" not in st.session_state:
    import uuid
    st.session_state.session_id = str(uuid.uuid4())

if "runner" not in st.session_state:
    from google.adk.runners import InMemoryRunner
    from agent import app
    st.session_state.runner = InMemoryRunner(app=app)

if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "assistant", "content": "Welcome to ☕ Coffee Shop! What can I get started for you today?"}
    ]

# Display existing messages
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

# User Input
if prompt := st.chat_input("Ask for recommendations (e.g., 'What dairy-free pastries do you have?')"):
    # Display user message
    with st.chat_message("user"):
        st.markdown(prompt)
    st.session_state.messages.append({"role": "user", "content": prompt})

    # Generate response
    with st.chat_message("assistant"):
        try:
            import asyncio

            # Run the ADK runner asynchronously using asyncio.run
            async def fetch_response():
                return await st.session_state.runner.run_debug(
                    prompt,
                    session_id=st.session_state.session_id
                )

            res_events = asyncio.run(fetch_response())

            response_text = "".join([
                part.text
                for event in res_events
                if event.content and event.content.parts
                for part in event.content.parts
                if part.text
            ])

            st.markdown(response_text)
            st.session_state.messages.append({"role": "assistant", "content": response_text})
        except Exception as e:
            st.error(f"Apologies, I ran into an error: {e}")

💬 דיון: פשרות במודל ויעילות של אסימוני אחזור

למה צריך להשתמש בכלי פונקציה כדי לאחזר את התפריט במקום פשוט להדביק את כל הטקסט של התפריט בהוראות המערכת של הסוכן?

כלכלת טוקנים! הוספה של 8 פריטים להנחיה היא זולה, אבל מה אם בית הקפה יתרחב ל-500 פריטים, כולל רכיבים בהתאמה אישית? הדבקה של מערכי נתונים גדולים ישירות בהנחיית המערכת מגדילה את מספר האסימונים של ההנחיה, וכך מגדילה את עלויות העסקאות ואת זמן האחזור של תגובות ה-API בכל שאילתה.

באמצעות כלי ADK, הסוכן מבקש באופן דינמי לקרוא את התפריט רק כשצריך. ה-LLM מקבל רק את נתוני התפריט הרלוונטיים כהקשר, וכך מצמצם את גודל הטוקן של ההנחיה.

💬 דיון: מצב הזיכרון ומאגרי ייצור

האם היסטוריית הצ'אט שנשמרת ב-st.session_state של Streamlit נשמרת גם כשמשתמש סוגר את כרטיסיית הדפדפן?

לא, זה לא קורה. ‫st.session_state נמצא כולו בזיכרון והוא ייחודי לחיבור הדפדפן הפעיל. אם משתמש ירענן את הדף או יסגור את הכרטיסייה, היסטוריית השיחה שלו עם הבריסטה תימחק.

באפליקציה בסביבת ייצור, צריך לחבר את מפעיל ה-ADK לבק-אנד של אחסון מתמיד כמו Cloud Firestore או Redis. ‫ADK מספק הפשטות מובנות של שירותים (כמו SessionService) שמקלות על שמירה של היסטוריית הצ'אט ועל המשך השיחה אחרי טעינה מחדש של הדף או במכשירים שונים.

6. פריסת הסוכן ב-Cloud Run

תפרסו את אפליקציית Streamlit ישירות מקוד המקור באמצעות חבילות ה-buildpacks המובנות של Cloud Run. כדי לפעול בהתאם לעיקרון של הרשאות מינימליות, תיצרו ותפרסו באמצעות חשבון שירות ייעודי בהתאמה אישית, במקום להשתמש בחשבון השירות שמוגדר כברירת מחדל של Compute Engine.

  1. יוצרים חשבון שירות ייעודי:
  gcloud iam service-accounts create barista-agent-sa \
    --description="Service account for Coffee Barista ADK agent on Cloud Run" \
    --display-name="Barista Agent Service Account"
  1. מקצים את תפקיד המשתמש Gemini Enterprise Agent Platform ‏ (roles/aiplatform.user) לחשבון השירות החדש:
  gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/aiplatform.user"
  1. מבצעים פריסה של השירות באמצעות gcloud run deploy, ומעבירים את כתובת האימייל בחשבון השירות החדש באמצעות הדגל --service-account:
gcloud run deploy coffee-barista \
  --source . \
  --region $REGION \
  --allow-unauthenticated \
  --labels dev-tutorial=codelab-streamlit-rag-adk \
  --command "/cnb/lifecycle/launcher" \
  --args "sh,-c,python3 -m streamlit run app.py --server.port=\$PORT --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false" \
  --service-account "barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --set-env-vars GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global
  1. אחרי הפריסה, מאתרים את כתובת ה-URL של השירות בפלט של הפקודה.

💬 דיון: פריסת קונטיינרים לעומת קוד מקור, ואבטחת IAM

פרסנו ב-Cloud Run באמצעות gcloud run deploy –source בלי ליצור קובץ Dockerfile או קובץ Procfile. איך Cloud Run הצליח לקמפל ולהריץ את אפליקציית Python שלנו?

‫Cloud Run משתמש ב-Buildpacks מאחורי הקלעים כדי לנתח את המאגר שלכם. כשמנוע ה-AI מזהה קבצים שלrequirements.txt וקובצי מקור של Python, הוא מהדר באופן אוטומטי קונטיינר של זמן ריצה של Python ואורז אותו.

כשכותבים Dockerfile בהתאמה אישית, מקבלים שליטה מלאה על חבילות המערכת והשכבות הבסיסיות של מאגר התגים. Procfile היא דרך פשוטה יותר להצהיר על פקודת ההפעלה בלי להגדיר את כל ההגדרות של מאגר. אבל לפריסות מהירות, פריסה ממקור (--source) היא יעילה מאוד.

למה יצרנו חשבון שירות מותאם אישית barista-agent-sa במקום להשתמש בחשבון השירות שמוגדר כברירת מחדל של Compute Engine?

בטיחות לפני הכול! כברירת מחדל, לחשבון השירות של Compute Engine יש הרשאות עריכה רחבות מאוד. הפעלת קונטיינר Cloud Run שלנו באמצעות חשבון השירות שמוגדר כברירת מחדל, פירושה שאם באפליקציה שלנו יש באג אבטחה, תוקף יכול לקרוא, לכתוב או למחוק משאבים אחרים בפרויקט שלנו ב-Google Cloud.

כשיוצרים חשבון שירות ייעודי ומקצים לו רק את התפקיד roles/aiplatform.user, אנחנו פועלים לפי העיקרון של הרשאות מינימליות: לאפליקציה יש בדיוק את הגישה שהיא צריכה כדי להפעיל את Gemini, ולא יותר.

7. בדיקת התנהגות ה-RAG

פותחים את כתובת ה-URL של שירות Cloud Run בדפדפן אינטרנט ושואלים את ברמנת ה-AI שאלות כדי לבדוק את ההצמדה שלה לקרקע ואת מגבלות הבטיחות שלה.

  1. בקשה מתוך התפריט: שואלים: "תמליץ לי על משהו חזק וחם". התשובה הצפויה: הסוכן ממליץ על אספרסו.
  2. מלכודת מחוץ לתפריט: שאלה: "יש לך מאצ'ה פרפוצ'ינו?" צפוי: הסוכן מסרב בנימוס ומסביר שאין את המשקה הזה בתפריט.
  3. בקשה שמתחשבת באלרגנים: בקשה: "יש לי אי-סבילות ללקטוז, מה אפשר להזמין?" תשובה צפויה: הסוכן ממליץ רק על פריטים בתפריט שלא מכילים חלב (כמו לאטה עם חלב שיבולת שועל, אספרסו, קולד ברו). הוא לא ממליץ על קפוצ'ינו או על קרואסון.

בדיקת התנהגות ה-RAG

8. אופציונלי: עיגון הסוכן ב-Firestore באמצעות חיפוש וקטורי

בתרחיש של סביבת ייצור, אחסון פריטי התפריט בקובץ menu.json מקומי הוא לא אידיאלי, כי כל שינוי בתפריט מחייב בנייה מחדש של קובץ האימג' של הקונטיינר ופריסה מחדש של שירות Cloud Run.

כדי להפוך את האפליקציה לדינמית ולניתנת להרחבה, אפשר להעביר את נתוני התפריט אל Cloud Firestore ולהשתמש בחיפוש וקטורי כדי לאחזר רק את הפריטים הרלוונטיים ביותר בתפריט על סמך דמיון סמנטי.

שילוב של Firestore באמצעות Vector Search

1. הפעלת Firestore API ואתחול מסד הנתונים

מריצים את הפקודות הבאות כדי להפעיל את Firestore API וליצור מסד נתונים של Firestore בשם coffee-menu במצב Native:

gcloud services enable firestore.googleapis.com

gcloud firestore databases create --database="coffee-menu" --location=$REGION

הערה: יכולות לעבור 1-2 דקות עד שההפעלה של ה-API תתעדכן. אם הפקודה ליצירת מסד הנתונים מציגה את ההודעה API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry?, מקלידים Y כדי להמשיך, או מחכים דקה ומריצים מחדש את הפקודה.

2. הוספת נתונים ראשוניים ל-Firestore עם נתוני התפריט

כדי לאכלס במהירות את מסד הנתונים של Firestore בפריטי התפריט מקובץ menu.json, אפשר להריץ סקריפט Python באופן מקומי ב-Cloud Shell.

  1. כדי להריץ את סקריפט ההזנה, צריך להתקין את ספריות הלקוח של Firestore ו-GenAI באופן מקומי ב-Cloud Shell:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. יצירת סקריפט להפצת תוכן seed.py:
cloudshell edit seed.py
  1. מדביקים את הקוד הבא ב-seed.py:
# seed.py
import json
import os
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector

db = firestore.Client(database="coffee-menu")
client = genai.Client(
   vertexai=True,
   project=os.environ.get("PROJECT_ID"),
   location=os.environ.get("REGION", "us-central1")
)

with open("menu.json", "r") as f:
   menu_items = json.load(f)

for item in menu_items:
   # Use the name as the document ID
   doc_id = item["name"].lower().replace(" ", "-")

   # Generate text embedding using Vertex AI text-embedding-004 model
   text_to_embed = f"{item['name']}: {item['description']}"
   response = client.models.embed_content(
       model="text-embedding-004",
       contents=text_to_embed,
   )
   embedding = response.embeddings[0].values

   # Add embedding vector to the menu item data
   item["embedding"] = Vector(embedding)

   db.collection("menu").document(doc_id).set(item)

print("Firestore menu collection seeded with vector embeddings successfully!")
  1. מריצים את הסקריפט:
python3 seed.py

3. יצירת אינדקס וקטורי ב-Firestore

כדי לבצע חיפושים וקטוריים בפריטים בתפריט, צריך ליצור אינדקס וקטורי מורכב בשדה embedding במסד הנתונים של Firestore.

מריצים את הפקודה הבאה במסוף Cloud Shell:

gcloud firestore indexes composite create \
 --collection-group=menu \
 --query-scope=COLLECTION \
 --database="coffee-menu" \
 --field-config=field-path=embedding,vector-config='{"dimension":"768", "flat": "{}"}'

הערה: יצירת אינדקס ב-Firestore מתבצעת ברקע ויכולה להימשך כמה דקות. אפשר להמשיך לשלבים הבאים של ה-codelab בזמן שהאינדקס נוצר.

4. הענקת גישה ל-Firestore לחשבון השירות

כדי שהשירות של Cloud Run יוכל לשלוח שאילתות ל-Firestore, צריך להקצות לחשבון השירות שלו את התפקיד משתמש ב-Cloud Datastore (roles/datastore.user):

gcloud projects add-iam-policy-binding $PROJECT_ID \
 --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
 --role="roles/datastore.user"

הערה: למרות שאנחנו משתמשים ב-Cloud Firestore במצב Native, ‏ Google Cloud משתמשת בתפקידי IAM המאוחדים של Cloud Datastore (roles/datastore.viewer או roles/datastore.user) כדי לנהל את בקרת הגישה.

5. עדכון הקוד

עכשיו צריך לעדכן את הקוד כדי לאחזר את התפריט מ-Firestore במקום לקרוא מ-menu.json.

  1. פותחים את requirements.txt ב-Cloud Shell Editor:
cloudshell edit requirements.txt
  1. מוסיפים את ספריות הלקוח של Firestore ו-GenAI לסוף הקובץ ושומרים אותו:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. פותחים את agent.py ב-Cloud Shell Editor:
cloudshell edit agent.py
  1. מאתרים את הבלוק # [START get_menu] ב-agent.py ומחליפים אותו לגמרי (מ-# [START get_menu] עד # [END get_menu]) בהטמעה הבאה של Firestore:
# [START get_menu]
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

def get_menu(query: str) -> str:
   """Retrieves coffee shop menu items matching the user's query.

   Args:
       query: The search query or preference to find matching menu items.

   Returns:
       str: A JSON string representing the list of top matching menu items.
   """
   try:
       # Initialize clients
       db = firestore.Client(database="coffee-menu")
       client = genai.Client()

       # Generate embedding for the search query
       response = client.models.embed_content(
           model="text-embedding-004",
           contents=query,
       )
       query_vector = response.embeddings[0].values

       # Search the Firestore database using Vector Search
       results = db.collection("menu").find_nearest(
           vector_field="embedding",
           query_vector=Vector(query_vector),
           distance_measure=DistanceMeasure.COSINE,
           limit=3,
       ).stream()

       menu_data = []
       for doc in results:
           item = doc.to_dict()
           # Remove embedding field to save tokens
           item.pop("embedding", None)
           menu_data.append(item)

       return json.dumps(menu_data)
   except Exception as e:
       return json.dumps({"error": f"Could not retrieve menu: {str(e)}"})
# [END get_menu]
  1. פותחים את app.py ב-Cloud Shell Editor:
cloudshell edit app.py
  1. מאתרים את הבלוק # [START load_menu] ב-app.py ומחליפים אותו לגמרי (מ-# [START load_menu] עד # [END load_menu]) בלוגיקת הטעינה הבאה של Firestore:
# [START load_menu]
from google.cloud import firestore

try:
   db = firestore.Client(database="coffee-menu")
   docs = db.collection("menu").stream()
   menu_items = []
   for doc in docs:
       item = doc.to_dict()
       item.pop("embedding", None)
       menu_items.append(item)
except Exception as e:
   st.error(f"Error loading menu from Firestore: {e}")
   menu_items = []
# [END load_menu]

6. פריסה מחדש ב-Cloud Run

פורסים את האפליקציה המעודכנת:

gcloud run deploy coffee-barista \
 --source . \
 --region $REGION \
 --allow-unauthenticated \
 --command "/cnb/lifecycle/launcher" \
 --args "sh,-c,python3 -m streamlit run app.py --server.port=\$PORT --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false" \
 --service-account "barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
 --set-env-vars GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global

7. אימות השילוב של Firestore

כדי לבדוק את החיבור של הנציג ל-Firestore, מוסיפים פריט תפריט חדש לגמרי ישירות ב-Firestore ומוודאים שהנציג ממליץ עליו.

  1. כדי לכתוב מסמך חדש לאוסף menu ב-Firestore באמצעות Python, מריצים את הפקודה הבאה ב-Cloud Shell:
python3 -c "
import os
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector

db = firestore.Client(database='coffee-menu')
client = genai.Client(
   vertexai=True,
   project=os.environ.get('PROJECT_ID'),
   location=os.environ.get('REGION', 'us-central1')
)

name = 'Matcha Green Tea Latte'
desc = 'Creamy steamed milk infused with premium Japanese matcha powder.'
res = client.models.embed_content(
   model='text-embedding-004',
   contents=f'{name}: {desc}'
)
embedding = res.embeddings[0].values

db.collection('menu').document('matcha-latte').set({
   'name': name,
   'description': desc,
   'price': 5.50,
   'tags': ['sweet', 'hot', 'dairy-free'],
   'allergens': [],
   'embedding': Vector(embedding)
})
print('Successfully added Matcha Latte with vector embeddings!')
"
  1. כדי לנקות את סשן הצ'אט ולטעון את מצב מסד הנתונים החדש, מרעננים את אפליקציית Streamlit בדפדפן.
  2. חשוב לזכור:
    • Matcha Green Tea Latte מופיע אוטומטית בתפריט שבסרגל הצד.
    • שואלים את הצ'אט בוט: "יש לך משקאות מאצ'ה?"
    • הסוכן צריך להמליץ בהצלחה על מאצ'ה לאטה עם התיאור והמחיר שהוספתם. כך מאשרים שהסוכן מעוגן בשאילתה ישירות במסד הנתונים הפעיל של Firestore!

9. הסרת המשאבים

כדי למנוע חיובים שוטפים בחשבון לחיוב ב-Google Cloud, צריך למחוק את שירות Cloud Run שנפרס ואת חשבון השירות המותאם אישית.

מוחקים את שירות Cloud Run:

gcloud run services delete coffee-barista --region $REGION --quiet

מוחקים את חשבון השירות המותאם אישית:

gcloud iam service-accounts delete barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com --quiet

(אופציונלי) מוחקים את מסד הנתונים של Firestore (אם הוא נוצר):

gcloud firestore databases delete --database="coffee-menu" --quiet

שלב אופציונלי: מוחקים את כל הפרויקט. ⚠️ מבצעים את הפעולה הזו רק אם יצרתם פרויקט ייעודי לשיעור ה-Lab הזה

gcloud projects delete $PROJECT_ID

10. מזל טוב

מעולה! יצרתם ופרסתם סוכן AI של בריסטה מסוג Retrieval-Augmented Generation (יצירה משולבת-אחזור, RAG) באמצעות ADK ו-Cloud Run של Google.

מה למדתם

  • יצירת כלים פשוטים של RAG ב-Python.
  • שימוש ב-ADK LlmAgent וב-InMemoryRunner.
  • יצירת חוויות צ'אט עם שמירת מצב ב-Streamlit.
  • פריסת Streamlit ב-Cloud Run באמצעות בנייה על סמך קוד המקור.

מסמכים לדוגמה