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
- במסוף Google Cloud, בוחרים או יוצרים פרויקט בענן של Google.
- הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud.
הפעלת Cloud Shell
- לוחצים על Activate Cloud Shell בחלק העליון של מסוף Google Cloud.

- אימות האימות:

gcloud auth list
- מוודאים שהפרויקט הפעיל מוגדר:
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. הגדרת הפרויקט
בשלב הזה, מאתחלים את משתני הסביבה של הפרויקט ויוצרים ספריית עבודה לפרויקט.
- בסשן הפעיל של Cloud Shell, מאתחלים את משתני הסביבה של הפרויקט הבאים:
export PROJECT_ID=$(gcloud config get-value project)
הערה: שימוש באזור הקרוב ביותר
מוצאים את האזור הקרוב ביותר ומחליפים את insert-region-here בפקודה הבאה:
export REGION=[insert-region-here]
- יוצרים ספריית פרויקט חדשה בשם
coffee-barista-agentועוברים אליה:
mkdir coffee-barista-agent && cd coffee-barista-agent
4. יצירת מקור נתונים לדוגמה של תפריט
כדי להקפיד על דיוק המידע שמתקבל מ-AI Barista ולמנוע ממנו להמציא פריטים שלא קיימים, צריך ליצור מערך נתונים מקומי של תפריט. הסוכן יקרא את הקובץ הזה בזמן הריצה באמצעות כלי בהתאמה אישית.
- יוצרים את הקובץ
menu.jsonופותחים אותו ב-Cloud Shell Editor:
cloudshell edit menu.json
- מדביקים את תוכן ה-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"]
}
]
- מוודאים שפורמט קובץ ה-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.
- יוצרים את הקובץ
requirements.txtופותחים אותו ב-Cloud Shell Editor:
cloudshell edit requirements.txt
- מדביקים את התלויות הבאות בעורך ושומרים את הקובץ:
google-adk==2.2.0
streamlit==1.58.0
- יוצרים את הקובץ
agent.pyופותחים אותו ב-Cloud Shell Editor:
cloudshell edit agent.py
- מדביקים את הקוד הבא ב-
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
)
- יוצרים את הקובץ
app.pyופותחים אותו ב-Cloud Shell Editor:
cloudshell edit app.py
- מדביקים את הקוד הבא ב-
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.
- יוצרים חשבון שירות ייעודי:
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"
- מקצים את תפקיד המשתמש 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"
- מבצעים פריסה של השירות באמצעות
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
- אחרי הפריסה, מאתרים את כתובת ה-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 שאלות כדי לבדוק את ההצמדה שלה לקרקע ואת מגבלות הבטיחות שלה.
- בקשה מתוך התפריט: שואלים: "תמליץ לי על משהו חזק וחם". התשובה הצפויה: הסוכן ממליץ על אספרסו.
- מלכודת מחוץ לתפריט: שאלה: "יש לך מאצ'ה פרפוצ'ינו?" צפוי: הסוכן מסרב בנימוס ומסביר שאין את המשקה הזה בתפריט.
- בקשה שמתחשבת באלרגנים: בקשה: "יש לי אי-סבילות ללקטוז, מה אפשר להזמין?" תשובה צפויה: הסוכן ממליץ רק על פריטים בתפריט שלא מכילים חלב (כמו לאטה עם חלב שיבולת שועל, אספרסו, קולד ברו). הוא לא ממליץ על קפוצ'ינו או על קרואסון.

8. אופציונלי: עיגון הסוכן ב-Firestore באמצעות חיפוש וקטורי
בתרחיש של סביבת ייצור, אחסון פריטי התפריט בקובץ menu.json מקומי הוא לא אידיאלי, כי כל שינוי בתפריט מחייב בנייה מחדש של קובץ האימג' של הקונטיינר ופריסה מחדש של שירות Cloud Run.
כדי להפוך את האפליקציה לדינמית ולניתנת להרחבה, אפשר להעביר את נתוני התפריט אל Cloud 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.
- כדי להריץ את סקריפט ההזנה, צריך להתקין את ספריות הלקוח של Firestore ו-GenAI באופן מקומי ב-Cloud Shell:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
- יצירת סקריפט להפצת תוכן
seed.py:
cloudshell edit seed.py
- מדביקים את הקוד הבא ב-
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!")
- מריצים את הסקריפט:
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.
- פותחים את
requirements.txtב-Cloud Shell Editor:
cloudshell edit requirements.txt
- מוסיפים את ספריות הלקוח של Firestore ו-GenAI לסוף הקובץ ושומרים אותו:
google-cloud-firestore==2.27.0
google-genai==2.11.0
- פותחים את
agent.pyב-Cloud Shell Editor:
cloudshell edit agent.py
- מאתרים את הבלוק
# [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]
- פותחים את
app.pyב-Cloud Shell Editor:
cloudshell edit app.py
- מאתרים את הבלוק
# [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 ומוודאים שהנציג ממליץ עליו.
- כדי לכתוב מסמך חדש לאוסף
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!')
"
- כדי לנקות את סשן הצ'אט ולטעון את מצב מסד הנתונים החדש, מרעננים את אפליקציית Streamlit בדפדפן.
- חשוב לזכור:
- 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 באמצעות בנייה על סמך קוד המקור.