1. مقدمة
في هذا الدرس التطبيقي حول الترميز، ستنشئ وكيلًا تفاعليًا مستندًا إلى الذكاء الاصطناعي لمتجر قهوة. باستخدام مجموعة أدوات تطوير الوكلاء (ADK) مفتوحة المصدر من Google ونموذج Gemini 3.5 Flash، ستنفّذ عملية التوليد المعزّز بالاسترجاع (RAG) لترسيخ اقتراحات الوكيل في مجموعة بيانات قائمة وهمية. أخيرًا، ستغلّف الوكيل بواجهة مستخدم Streamlit وتنشرها على Cloud Run.
الإجراءات التي ستنفذّها
- أنشئ مصدر بيانات RAG (
menu.json) يحتوي على عناصر القهوة والعلامات ومسببات الحساسية. - أنشئ وكيل ذكاء اصطناعي باستخدام ADK
LlmAgentواربط أداة Python لتحميل بيانات القائمة. - تضمين الوكيل في تطبيق دردشة Streamlit يدير سجلّ المحادثات
- انشر تطبيق Streamlit على Cloud Run باستخدام عملية النشر المستندة إلى المصدر.
- اختبار مدى صحة المعلومات التي تقدّمها نماذج RAG ومدى معرفتها بمسبّبات الحساسية

المتطلبات
- متصفّح ويب، مثل Chrome
- مشروع Google Cloud تم تفعيل الفوترة فيه
- الإلمام بأساسيات لغة Python
هذا الدرس التطبيقي حول الترميز مخصّص للمطوّرين من جميع المستويات، بما في ذلك المبتدئين.
التكلفة المقدَّرة: أقل من 1.00 دولار أمريكي
2. قبل البدء
إنشاء مشروع على Google Cloud
- في Google Cloud Console، اختَر مشروعًا على Google Cloud أو أنشِئ مشروعًا.
- تأكَّد من تفعيل الفوترة لمشروعك على السحابة الإلكترونية.
بدء Cloud Shell
- انقر على تفعيل Cloud Shell في أعلى "وحدة تحكّم Google Cloud".

- إثبات صحة المصادقة:

gcloud auth list
- تأكَّد من ضبط مشروعك النشط على النحو التالي:
gcloud config get project
إذا كان رقم تعريف المشروع المعروض غير صحيح أو لم يتم ضبط أي رقم تعريف، نفِّذ ما يلي:
gcloud config set project <YOUR_PROJECT_ID>
تفعيل واجهات برمجة التطبيقات
نفِّذ الأمر التالي لتفعيل جميع واجهات برمجة التطبيقات المطلوبة:
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" باتّباع الخطوات التالية:
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.
يتيح استخدام قاعدة بيانات مباشرة لمدراء مقاهي إضافة عناصر موسمية أو تعديل الأسعار أو ضبط علامات مسبّبات الحساسية بشكل ديناميكي بدون إعادة إنشاء صورة الحاوية أو إعادة نشر الرمز البرمجي للتطبيق. سنستخدم قاعدة بيانات مباشرة كخطوة اختيارية في وقت لاحق من الدرس التطبيقي حول الترميز.
5- إنشاء وكيل ADK
الآن، عليك تثبيت الحِزم المطلوبة وإنشاء منطق وكيل ADK الأساسي. ستحدّد أداة get_menu() وتمرّرها إلى LlmAgent.
- أنشئ الملف
requirements.txtوافتحه في "محرِّر Cloud Shell" باتّباع الخطوات التالية:
cloudshell edit requirements.txt
- الصِق التبعيات التالية في المحرّر واحفظ الملف:
google-adk==2.2.0
streamlit==1.58.0
- أنشئ الملف
agent.pyوافتحه في "محرِّر Cloud Shell" باتّباع الخطوات التالية:
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" باتّباع الخطوات التالية:
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 عنصر، بما في ذلك المكوّنات المخصّصة؟ يؤدي لصق مجموعات بيانات كبيرة مباشرةً في طلب النظام إلى زيادة عدد الرموز المميزة للطلب، ما يزيد من تكاليف المعاملات ووقت استجابة واجهة برمجة التطبيقات لكل طلب.
باستخدام أداة ADK، يطلب الوكيل قراءة القائمة بشكل ديناميكي عند الحاجة فقط. يتلقّى النموذج اللغوي الكبير بيانات القائمة ذات الصلة فقط كسياق، ما يقلّل من حجم الرمز المميز للطلب.
💬 مناقشة: حالة الذاكرة ومتاجر الإنتاج
هل يبقى سجلّ المحادثات المخزّن داخل st.session_state في Streamlit متاحًا عندما يغلق المستخدم علامة تبويب المتصفّح؟
لا، لا يمكن ذلك. st.session_state هي ميزة فريدة ومتاحة فقط عند الاتصال بالمتصفّح النشط. إذا أعاد المستخدم تحميل الصفحة أو أغلق علامة التبويب، سيتم محو سجلّ المحادثات مع الباريستا.
بالنسبة إلى تطبيق مخصّص للإنتاج، عليك ربط أداة تشغيل ADK بخادم خلفي لتخزين البيانات بشكل دائم، مثل Cloud Firestore أو Redis. توفّر حزمة تطوير التطبيقات (ADK) تجريدات خدمة مدمجة (مثل SessionService) تسهّل حفظ سجلّ المحادثات واستئنافه عند إعادة تحميل الصفحة أو على أجهزة مختلفة.
6. نشر الوكيل على Cloud Run
ستنشر تطبيق Streamlit مباشرةً من المصدر باستخدام حِزم الإنشاء المضمّنة في 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" (
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 للخدمة في ناتج الأمر.
💬 مناقشة: نشر الحاويات مقابل المصدر، وأمان إدارة الهوية وإمكانية الوصول
تم النشر إلى 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 لاختبار قيود الأمان والأساس.
- طلب داخل القائمة: الطلب: "أريد مشروبًا قويًا وساخنًا". النتيجة المتوقّعة: يقترح عليك الوكيل مشروب الإسبريسو.
- الخروج عن قائمة الطعام: السؤال: "هل يتوفّر لديكم فرابتشينو ماتشا؟"الرد المتوقّع: يرفض الوكيل الطلب بأدب ويوضح أنّه غير متوفّر في قائمة الطعام.
- طلب مراعاة الحساسية تجاه مادة معينة: اطرح السؤال التالي: "أعاني من حساسية اللاكتوز، ما هي الخيارات المتاحة لي؟"النتيجة المتوقّعة: يقترح عليك الوكيل فقط الأطباق الخالية من منتجات الألبان (مثل قهوة لاتيه بحليب الشوفان والإسبريسو والقهوة الباردة). لا يوصي هذا التطبيق باستخدام Cappuccino أو Croissant.

8. اختياري: يجب أن يستند الوكيل إلى Firestore باستخدام ميزة "البحث عن المتّجهات"
في سيناريو الإنتاج، لا يُعدّ تخزين عناصر القائمة في ملف menu.json محلي خيارًا مثاليًا لأنّ أي تغيير في القائمة يتطلّب إعادة إنشاء صورة الحاوية وإعادة نشر خدمة Cloud Run.
لجعل التطبيق ديناميكيًا وقابلاً للتوسيع، يمكنك نقل بيانات القائمة إلى Cloud Firestore واستخدام Vector Search لاسترداد عناصر القائمة الأكثر صلةً فقط استنادًا إلى التشابه الدلالي.

1. تفعيل واجهة برمجة التطبيقات Firestore وتهيئة قاعدة البيانات
نفِّذ الأوامر التالية لتفعيل واجهة برمجة التطبيقات Firestore API وإنشاء قاعدة بيانات Firestore باسم coffee-menu في "الوضع الأصلي":
gcloud services enable firestore.googleapis.com
gcloud firestore databases create --database="coffee-menu" --location=$REGION
ملاحظة: قد يستغرق تفعيل واجهة برمجة التطبيقات من دقيقة إلى دقيقتَين. إذا طلب منك أمر إنشاء قاعدة البيانات إدخال API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry?، اكتب Y للمتابعة، أو انتظِر دقيقة وأعِد تنفيذ الأمر.
2. تعبئة Firestore ببيانات القائمة
لإضافة عناصر القائمة من ملف menu.json إلى قاعدة بيانات Firestore بسرعة، يمكنك تشغيل نص برمجي بلغة 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 في الخلفية ويمكن أن يستغرق ذلك بضع دقائق. يمكنك المتابعة إلى الخطوات التالية من الدرس العملي أثناء إنشاء الفهرس.
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 في "الوضع الأصلي"، تستخدم Google Cloud أدوار Cloud Datastore الموحّدة في نظام إدارة الهوية وإمكانية الوصول (IAM) (roles/datastore.viewer أو roles/datastore.user) لإدارة التحكّم في الوصول.
5- تعديل الرمز
الآن، عدِّل الرمز البرمجي لاسترداد القائمة من Firestore بدلاً من القراءة من menu.json.
- افتح
requirements.txtفي "محرِّر Cloud Shell":
cloudshell edit requirements.txt
- أضِف مكتبتَي برامج Firestore وGenAI إلى نهاية الملف واحفظه:
google-cloud-firestore==2.27.0
google-genai==2.11.0
- افتح
agent.pyفي "محرِّر Cloud Shell":
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":
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 وتأكَّد من أنّ الوكيل يقترحه.
- نفِّذ الأمر التالي في Cloud Shell لكتابة مستند جديد في المجموعة
menuفي Firestore باستخدام Python:
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 في المتصفّح لمحو جلسة المحادثة وتحميل حالة قاعدة البيانات الجديدة.
- يُرجى ملاحظة ما يلي:
- يظهر لاتيه الشاي الأخضر تلقائيًا في قائمة الشريط الجانبي.
- اطرح السؤال التالي على روبوت الدردشة: "هل لديك أي مشروبات ماتشا؟"
- يجب أن يقترح عليك المساعد الآلي لاتيه الشاي الأخضر ماتشا الجديد بنجاح مع الوصف والسعر اللذين أضفتهما للتو. يؤكّد ذلك أنّ الموظّف يستند إلى طلب البحث مباشرةً في قاعدة بيانات 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
خطوة اختيارية: احذف المشروع بأكمله. ⚠️يجب إجراء ذلك فقط إذا أنشأت مشروعًا مخصّصًا لهذه الميزة الاختبارية
gcloud projects delete $PROJECT_ID
10. تهانينا
تهانينا! لقد أنشأت ونشرت وكيل Barista مستندًا إلى الذكاء الاصطناعي للتوليد المعزّز بالاسترجاع (RAG) باستخدام حزمة تطوير الوكلاء (ADK) وCloud Run من Google.
ما تعلّمته
- إنشاء أدوات بسيطة للتوليد المعزّز بالاسترجاع في Python
- استخدام حزمة تطوير التطبيقات (ADK)
LlmAgentوInMemoryRunner - إنشاء تجارب محادثة مستندة إلى الحالة في Streamlit
- نشر Streamlit على Cloud Run باستخدام عمليات إنشاء مستندة إلى المصدر