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

1. מבוא

בשיעור Codelab הזה נסביר איך ליצור סוכן AI אינטראקטיבי של בריסטה לבית קפה. שימוש בקוד פתוח של גוגל ערכת פיתוח סוכנים (ADK) וה- Gemini 3.5 Flash מודל, תיישם Retrieval-Augmented Generation (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. לחץ על הפעל את 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.56.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_ENTERPRISE=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 מאחורי הקלעים כדי לנתח את המאגר שלכם. לאחר זיהוי נוכחות של קבצי מקור של 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 Barista שאלות כדי לבדוק את האבטחה והאילוצים שלו.

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

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

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

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

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

שילוב Firestore באמצעות חיפוש וקטורי

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 Gemini Enterprise Agent Platform text-embedding-005 model
   text_to_embed = f"{item['name']}: {item['description']}"
   response = client.models.embed_content(
       model="text-embedding-005",
       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 User (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-005",
           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 \
  --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_ENTERPRISE=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 oat milk infused with premium Japanese matcha powder.'
res = client.models.embed_content(
   model='text-embedding-005',
   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 באמצעות בנייה על סמך קוד המקור.

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