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-flash-latest",
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}")
💬 مناقشة: المفاضلة بين النماذج وكفاءة الرموز المميزة للاسترجاع
لماذا اخترنا
gemini-3.5-flash
؟ ولماذا حدّدنا سلسلة الإصدار الدقيق بدلاً من استخدام علامة عامة لأحدث إصدار؟
تم تصميم Gemini 3.5 Flash للسرعة والفعالية من حيث التكلفة، ما يجعله مثاليًا لوكلاء الدردشة التفاعلية.
بالنسبة إلى سلسلة الإصدار الدقيق، حدّدنا أيضًا gemini-3.5-flash لأنّ الأسماء المستعارة العامة، مثل gemini-flash-latest، لا تكون متاحة دائمًا على نقاط نهاية Vertex AI الإقليمية، مثل us-central1. في هذا الدرس التطبيقي، سنضبط متغيّر الموقع الجغرافي على global للوصول إلى gemini-3.5-flash من خلال نقطة النهاية العامة.
لماذا يتم استدعاء أداة دالة لاسترداد القائمة بدلاً من مجرد لصق نص القائمة بالكامل في تعليمات النظام الخاصة بالوكيل؟
اقتصاد الرموز المميزة! إنّ وضع 8 عناصر في الطلب غير مكلف، ولكن ماذا لو توسّع المقهى ليشمل 500 عنصر، بما في ذلك المكوّنات المخصّصة؟ يؤدي لصق مجموعات بيانات كبيرة مباشرةً في طلب النظام إلى زيادة عدد الرموز المميزة للطلب، ما يزيد من تكاليف المعاملات ووقت استجابة واجهة برمجة التطبيقات لكل طلب بحث.
باستخدام إحدى أدوات ADK، يطلب الوكيل بشكل ديناميكي قراءة القائمة عند الحاجة فقط. يتلقّى النموذج اللغوي الكبير بيانات القائمة ذات الصلة فقط كسياق، ما يقلّل من حجم الرمز المميز للطلب.
💬 مناقشة: حالة الذاكرة ومتاجر الإنتاج
هل يتم تخزين سجلّ المحادثات داخل
st.session_state
تظلّ متاحة عندما يغلق المستخدم علامة تبويب المتصفّح؟
لا، ليس كذلك. st.session_state هي ميزة تعمل بالكامل في الذاكرة وهي فريدة لاتصال المتصفح النشط. إذا أعاد المستخدم تحميل الصفحة أو أغلق علامة التبويب، سيتم محو سجلّ المحادثات مع الباريستا.
بالنسبة إلى تطبيق مخصّص للإنتاج، عليك ربط أداة تشغيل ADK بخادم خلفي لتخزين البيانات بشكل دائم، مثل Cloud Firestore أو Redis. توفّر "حزمة تطوير التطبيقات" تجريدات مدمجة للخدمات (مثل 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"
- امنح حساب الخدمة الجديد دور مستخدم Vertex AI (
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 \
--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 لاختبار قيود الأمان والأساس.
- طلب من داخل القائمة: الطلب: "أريد مشروبًا قويًا ودافئًا". النتيجة المتوقّعة: يقترح عليك الوكيل تناول قهوة إسبريسو.
- الأسئلة التي لا تتضمّنها القائمة: السؤال: "هل يتوفّر لديكم فرابتشينو بنكهة الماتشا؟"الردّ المتوقّع: يرفض الوكيل الطلب بأدب ويوضح أنّ هذا المنتج غير متوفّر في القائمة.
- طلب مراعاة الحساسية: الطلب: "أعاني من حساسية اللاكتوز، ما هي الخيارات المتاحة لي؟"النتيجة المتوقّعة: يقترح الوكيل فقط عناصر قائمة الطعام الخالية من منتجات الألبان (مثل قهوة لاتيه بحليب الشوفان والإسبريسو والقهوة الباردة). لا يوصي بتناول "كابتشينو" أو "كرواسون".
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
[!NOTE] ملاحظة: قد يستغرق نشر تفعيل واجهة برمجة التطبيقات من دقيقة إلى دقيقتين. إذا طلب منك أمر إنشاء قاعدة البيانات إدخال 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": "{}"}'
[!NOTE] ملاحظة: يتم إنشاء فهارس 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 باستخدام عمليات إنشاء مستندة إلى المصدر