با استفاده از Google ADK و Cloud Run، یک عامل هوش مصنوعی RAG را در Streamlit مستقر کنید.

۱. مقدمه

در این آزمایشگاه کد، شما یک عامل تعاملی هوش مصنوعی باریستا برای یک کافی‌شاپ خواهید ساخت. با استفاده از کیت توسعه عامل متن‌باز گوگل (ADK) و مدل Gemini 3.5 Flash ، شما Retrieval-Augmented Generation (RAG) را پیاده‌سازی خواهید کرد تا توصیه‌های عامل را در یک مجموعه داده منوی ساختگی قرار دهید. در نهایت، عامل را در یک رابط کاربری Streamlit قرار داده و آن را در Cloud Run مستقر خواهید کرد.

کاری که انجام خواهید داد

  • یک منبع داده RAG ( menu.json ) حاوی اقلام قهوه، برچسب‌ها و مواد حساسیت‌زا ایجاد کنید.
  • با استفاده از ADK LlmAgent یک عامل هوش مصنوعی بسازید و یک ابزار پایتون را برای بارگذاری داده‌های منو متصل کنید.
  • اپراتور را در یک برنامه چت Streamlit قرار دهید که تاریخچه مکالمات را مدیریت می‌کند.
  • برنامه Streamlit را با استفاده از استقرار مبتنی بر منبع، روی Cloud Run مستقر کنید.
  • اتصال زمین RAG و آگاهی از آلرژن‌ها را آزمایش کنید.

Architecture Diagram

آنچه نیاز دارید

  • یک مرورگر وب مانند کروم .
  • یک پروژه گوگل کلود با قابلیت پرداخت.
  • آشنایی اولیه با پایتون.

این آزمایشگاه کد برای توسعه‌دهندگان در تمام سطوح، از جمله مبتدیان، مناسب است.

هزینه تخمینی: کمتر از ۱.۰۰ دلار آمریکا

۲. قبل از شروع

ایجاد یک پروژه ابری گوگل

  1. در کنسول گوگل کلود ، یک پروژه گوگل کلود انتخاب یا ایجاد کنید .
  2. مطمئن شوید که پرداخت برای پروژه ابری شما فعال است.

شروع پوسته ابری

  1. روی فعال کردن Cloud Shell در بالای کنسول Google Cloud کلیک کنید.

Activate Cloud\nShell

  1. تأیید اعتبار:

Authorize 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

۳. پروژه خود را تنظیم کنید

در این مرحله، متغیرهای محیطی پروژه خود را مقداردهی اولیه کرده و یک دایرکتوری کاری برای پروژه خود ایجاد خواهید کرد.

  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

۴. منبع داده منوی آزمایشی را ایجاد کنید

برای اینکه هوش مصنوعی باریستا را به کار بیندازید و از توهم زدن موارد ناموجود جلوگیری کنید، یک مجموعه داده منوی محلی ایجاد خواهید کرد. عامل این فایل را در زمان اجرا از طریق یک ابزار سفارشی می‌خواند.

  1. menu.json در ویرایشگر Cloud Shell ایجاد و باز کنید:
  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 از یک پایگاه داده زنده به عنوان یک مرحله اختیاری استفاده خواهیم کرد.

۵. عامل ADK را بسازید

اکنون بسته‌های مورد نیاز را نصب کرده و منطق عامل ADK اصلی را می‌سازید. یک ابزار get_menu() تعریف کرده و آن را به LlmAgent ارسال می‌کنید.

  1. requirements.txt را در ویرایشگر Cloud Shell ایجاد و باز کنید:
cloudshell edit requirements.txt
  1. وابستگی‌های زیر را در ویرایشگر قرار دهید و فایل را ذخیره کنید:
google-adk==2.2.0
streamlit==1.56.0
  1. agent.py در ویرایشگر Cloud Shell ایجاد و باز کنید:
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 ایجاد و باز کنید:
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}")

💬 بحث: موازنه‌های مدل و کارایی توکن بازیابی

چرا به جای اینکه کل متن منو را در دستورالعمل‌های سیستم عامل قرار دهیم، یک ابزار تابعی را برای بازیابی منو فراخوانی کنیم؟

صرفه‌جویی در توکن! قرار دادن ۸ مورد در اعلان سفارش ارزان است، اما اگر تعداد اقلام کافی‌شاپ به ۵۰۰ مورد، شامل مواد اولیه سفارشی، افزایش یابد چه؟ قرار دادن مجموعه داده‌های بزرگ مستقیماً در اعلان سفارش سیستم، تعداد توکن‌های اعلان شما را افزایش می‌دهد و هزینه‌های تراکنش و تأخیر پاسخ API را در هر پرس‌وجو افزایش می‌دهد.

با استفاده از ابزار ADK، عامل به صورت پویا درخواست می‌کند که فقط در صورت نیاز، منو را بخواند. LLM فقط داده‌های منوی مربوطه را به عنوان زمینه دریافت می‌کند و اندازه توکن اعلان را به حداقل می‌رساند.

💬 بحث: انباره‌های وضعیت و تولید حافظه

آیا تاریخچه چت ذخیره شده در st.session_state مربوط به Streamlit، وقتی کاربر تب مرورگر خود را می‌بندد، همچنان باقی می‌ماند؟

نه، اینطور نیست. st.session_state کاملاً در حافظه است و منحصر به اتصال مرورگر فعال است. اگر کاربری صفحه را رفرش کند یا تب را ببندد، تاریخچه مکالمات او با متصدی بار از بین می‌رود.

برای یک برنامه کاربردی در حال تولید، شما باید اجراکننده ADK را به یک backend ذخیره‌سازی پایدار مانند Cloud Firestore یا Redis متصل کنید. ADK انتزاع‌های سرویس داخلی (مانند SessionService ) را ارائه می‌دهد که ذخیره و از سرگیری تاریخچه چت را در بارگذاری مجدد صفحه و دستگاه‌ها ساده می‌کند.

۶. عامل را در Cloud Run مستقر کنید

شما برنامه Streamlit را مستقیماً از منبع با استفاده از بسته‌های ساخت داخلی Cloud Run مستقر خواهید کرد. برای پیروی از اصل حداقل امتیاز، به جای استفاده از حساب سرویس پیش‌فرض Compute Engine، با استفاده از یک حساب سرویس سفارشی اختصاصی ایجاد و مستقر خواهید کرد.

  1. ایجاد حساب کاربری اختصاصی برای سرویس:
  gcloud iam service-accounts create barista-agent-sa \
    --description="Service account for Coffee Barista ADK agent on Cloud Run" \
    --display-name="Barista Agent Service Account"
  1. نقش کاربری Gemini Enterprise Agent Platform ( roles/aiplatform.user ) را به حساب سرویس جدید اعطا کنید:
  gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/aiplatform.user"
  1. سرویس را با استفاده از gcloud run deploy مستقر کنید و ایمیل حساب سرویس جدید را از طریق پرچم --service-account ارسال کنید:
gcloud run deploy coffee-barista \
  --source . \
  --region $REGION \
  --allow-unauthenticated \
  --labels dev-tutorial=codelab-streamlit-rag-adk \
  --command "/cnb/lifecycle/launcher" \
  --args "sh,-c,python3 -m streamlit run app.py --server.port=\$PORT --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false" \
  --service-account "barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --set-env-vars GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global
  1. پس از استقرار، آدرس اینترنتی سرویس را در خروجی دستور پیدا کنید.

💬 بحث: استقرار کانتینرها در مقابل منبع و امنیت IAM

ما با استفاده از gcloud run deploy –source و بدون ایجاد Dockerfile یا Procfile، برنامه پایتون خود را در Cloud Run مستقر کردیم. Cloud Run چگونه فهمید که چگونه برنامه پایتون ما را کامپایل و اجرا کند؟

Cloud Run از Buildpacks در زیر کاپوت برای تجزیه و تحلیل مخزن شما استفاده می‌کند. پس از تشخیص وجود فایل‌های requirements.txt و منبع پایتون، موتور به طور خودکار یک کانتینر زمان اجرای پایتون را کامپایل و بسته‌بندی می‌کند.

نوشتن یک Dockerfile سفارشی به شما کنترل کاملی بر روی بسته‌های سیستمی و لایه‌های پایه کانتینر می‌دهد. یک Procfile روشی ساده‌تر برای اعلام دستور راه‌اندازی بدون پیکربندی کامل کانتینر است. اما برای استقرار سریع، استقرار از منبع ( --source ) بسیار کارآمد است.

چرا به جای استفاده از حساب سرویس پیش‌فرض Compute Engine، مرحله‌ی اضافی ایجاد یک حساب سرویس سفارشی barista-agent-sa را طی کردیم؟

ایمنی حرف اول را می‌زند! حساب کاربری پیش‌فرض سرویس Compute Engine به طور پیش‌فرض مجوزهای ویرایشگر بسیار گسترده‌ای دارد. اجرای کانتینر Cloud Run ما تحت حساب کاربری پیش‌فرض سرویس به این معنی است که اگر برنامه ما دارای یک اشکال امنیتی باشد، یک مهاجم می‌تواند به طور بالقوه منابع دیگر را در پروژه Google Cloud ما بخواند، بنویسد یا حذف کند.

با ایجاد یک حساب کاربری سرویس اختصاصی و اختصاص دادن تنها نقش roles/aiplatform.user به آن، ما از اصل حداقل امتیاز پیروی می‌کنیم: برنامه دقیقاً همان دسترسی لازم برای فراخوانی Gemini را دارد و نه بیشتر.

۷. رفتار RAG را آزمایش کنید

آدرس اینترنتی سرویس Cloud Run را در یک مرورگر وب باز کنید و از هوش مصنوعی باریستا سوالاتی بپرسید تا محدودیت‌های اتصال به زمین و ایمنی آن را آزمایش کنید.

  1. درخواست در منو: بپرسید: «چیزی قوی و گرم پیشنهاد دهید.» انتظار: نماینده اسپرسو را توصیه می‌کند.
  2. تله خارج از منو: بپرسید: «آیا ماچا فراپوچینو دارید؟» انتظار: متصدی مؤدبانه رد می‌کند و توضیح می‌دهد که در منو نیست.
  3. درخواست آگاه از آلرژن: بپرسید: «من به لاکتوز حساسیت دارم، چه چیزی می‌توانم تهیه کنم؟» انتظار: نماینده فقط موارد بدون لبنیات در منو را توصیه می‌کند (مانند لاته با شیر جو دوسر، اسپرسو، قهوه دم سرد). کاپوچینو یا کروسان را توصیه نمی‌کند.

Testing the RAG Behavior

۸. اختیاری: با استفاده از Vector Search، عامل خود را در Firestore به زمین متصل کنید

در یک سناریوی عملیاتی، ذخیره آیتم‌های منو در یک فایل محلی menu.json ایده‌آل نیست، زیرا هرگونه تغییر در منو نیاز به بازسازی تصویر کانتینر و استقرار مجدد سرویس Cloud Run دارد.

برای پویا و مقیاس‌پذیر کردن برنامه، می‌توانید داده‌های منوی خود را به Cloud Firestore منتقل کنید و از Vector Search برای بازیابی تنها مرتبط‌ترین موارد منو بر اساس شباهت معنایی استفاده کنید.

Integrating Firestore Using Vector Search

۱. فعال کردن API فایراستور و مقداردهی اولیه پایگاه داده

دستورات زیر را برای فعال کردن API فایراستور و ایجاد یک پایگاه داده فایراستور با نام coffee-menu در حالت Native اجرا کنید:

gcloud services enable firestore.googleapis.com

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

توجه: فعال‌سازی API می‌تواند ۱ تا ۲ دقیقه طول بکشد. اگر دستور ایجاد پایگاه داده از شما درخواست کرد که API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry? برای ادامه Y را تایپ کنید، یا یک دقیقه صبر کنید و دستور را دوباره اجرا کنید.

۲. ذخیره سازی در Firestore با استفاده از داده‌های منو

برای اینکه بتوانید به سرعت آیتم‌های منو را از فایل menu.json خود به پایگاه داده Firestore خود اضافه کنید، می‌توانید یک اسکریپت پایتون را به صورت محلی در Cloud Shell اجرا کنید.

  1. کتابخانه‌های کلاینت Firestore و GenAI را به صورت محلی در Cloud Shell نصب کنید تا اسکریپت سیدینگ اجرا شود:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. یک اسکریپت seeding seed.py ایجاد کنید:
cloudshell edit seed.py
  1. کد زیر را در seed.py قرار دهید:
# seed.py
import json
import os
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector

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

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

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

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

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

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

print("Firestore menu collection seeded with vector embeddings successfully!")
  1. اسکریپت را اجرا کنید:
python3 seed.py

۳. ایجاد شاخص برداری 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 را ادامه دهید.

۴. دسترسی به حساب سرویس را به 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) برای مدیریت کنترل دسترسی استفاده می‌کند.

۵. کد را به‌روزرسانی کنید

حالا کد خود را به‌روزرسانی کنید تا منو را به جای خواندن از menu.json ، از Firestore بازیابی کند.

  1. requirements.txt را در ویرایشگر Cloud Shell باز کنید:
cloudshell edit requirements.txt
  1. کتابخانه‌های کلاینت Firestore و GenAI را به انتهای فایل اضافه کرده و آن را ذخیره کنید:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. agent.py در ویرایشگر Cloud Shell باز کنید:
cloudshell edit agent.py
  1. بلوک # [START get_menu] را در agent.py پیدا کنید و آن را به طور کامل (از # [START get_menu] تا # [END get_menu] ) با پیاده‌سازی Firestore زیر جایگزین کنید:
# [START get_menu]
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

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

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

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

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

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

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

       return json.dumps(menu_data)
   except Exception as e:
       return json.dumps({"error": f"Could not retrieve menu: {str(e)}"})
# [END get_menu]
  1. app.py در ویرایشگر Cloud Shell باز کنید:
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]

۶. استقرار مجدد به 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_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global

۷. یکپارچگی Firestore را بررسی کنید

برای آزمایش اتصال نماینده به Firestore، یک آیتم منوی کاملاً جدید را مستقیماً در Firestore اضافه کنید و تأیید کنید که نماینده آن را توصیه می‌کند.

  1. دستور زیر را در Cloud Shell اجرا کنید تا با استفاده از پایتون، یک سند جدید در مجموعه menu در Firestore بنویسید:
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-004',
   contents=f'{name}: {desc}'
)
embedding = res.embeddings[0].values

db.collection('menu').document('matcha-latte').set({
   'name': name,
   'description': desc,
   'price': 5.50,
   'tags': ['sweet', 'hot', 'dairy-free'],
   'allergens': [],
   'embedding': Vector(embedding)
})
print('Successfully added Matcha Latte with vector embeddings!')
"
  1. برنامه Streamlit خود را در مرورگر رفرش کنید تا جلسه چت پاک شود و وضعیت جدید پایگاه داده بارگذاری شود.
  2. توجه داشته باشید که:
    • لاته چای سبز ماچا به طور خودکار در منوی کناری ظاهر می‌شود.
    • از چت‌بات بپرسید: «نوشیدنی ماچا دارید؟»
    • نماینده باید با موفقیت لاته چای سبز ماچا جدید را با توضیحات و قیمتی که شما اضافه کرده‌اید، توصیه کند. این تأیید می‌کند که نماینده مستقیماً در پایگاه داده زنده Firestore شما جستجو شده است!

۹. تمیز کردن

برای جلوگیری از هزینه‌های جاری حساب پرداخت 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

۱۰. تبریک

تبریک! شما با استفاده از ADK و Cloud Run گوگل، یک عامل هوش مصنوعی باریستا با قابلیت بازیابی افزوده (RAG) ساختید و مستقر کردید.

آنچه آموخته‌اید

  • ساخت ابزارهای ساده RAG در پایتون
  • استفاده از ADK LlmAgent و InMemoryRunner .
  • ایجاد تجربه‌های چت مبتنی بر وضعیت در Streamlit.
  • استقرار Streamlit در Cloud Run با استفاده از نسخه‌های مبتنی بر منبع.

اسناد مرجع