Triển khai một Tác nhân AI RAG trong Streamlit bằng Google ADK và Cloud Run

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ xây dựng một tác nhân AI Barista có thể tương tác cho một quán cà phê. Bằng cách sử dụng Bộ công cụ phát triển tác nhân (ADK) nguồn mở của Google và mô hình Gemini 3.5 Flash, bạn sẽ triển khai tính năng Tạo sinh tăng cường truy xuất (RAG) để đưa ra các đề xuất dựa trên dữ liệu trong một tập dữ liệu trình đơn mô phỏng. Cuối cùng, bạn sẽ gói tác nhân này trong giao diện người dùng Streamlit và triển khai tác nhân này lên Cloud Run.

Bạn sẽ thực hiện

  • Tạo một nguồn dữ liệu RAG (menu.json) chứa các mặt hàng cà phê, thẻ và chất gây dị ứng.
  • Xây dựng một tác nhân AI bằng ADK LlmAgent và kết nối một công cụ Python để tải dữ liệu trình đơn.
  • Bọc tác nhân trong một ứng dụng trò chuyện Streamlit để quản lý nhật ký trò chuyện.
  • Triển khai ứng dụng Streamlit lên Cloud Run bằng cách triển khai dựa trên nguồn.
  • Kiểm thử khả năng tiếp đất của RAG và nhận biết chất gây dị ứng.

Biểu đồ kiến trúc

Bạn cần có

  • Một trình duyệt web như Chrome.
  • Một dự án trên Google Cloud đã bật tính năng thanh toán.
  • Hiểu biết cơ bản về Python.

Lớp học lập trình này dành cho nhà phát triển ở mọi cấp độ, kể cả người mới bắt đầu.

Chi phí ước tính: Dưới 1 USD.

2. Trước khi bắt đầu

Tạo một dự án trên Google Cloud

  1. Trong Google Cloud Console, hãy chọn hoặc tạo một dự án trên đám mây của Google Cloud.
  2. Đảm bảo bạn đã bật tính năng thanh toán cho dự án trên Cloud.

Khởi động Cloud Shell

  1. Nhấp vào Kích hoạt Cloud Shell ở đầu bảng điều khiển Google Cloud.

Kích hoạt Cloud Shell

  1. Xác minh quy trình xác thực:

Uỷ quyền cho Cloud Shell

  gcloud auth list
  1. Xác nhận rằng bạn đã đặt dự án đang hoạt động:
  gcloud config get project

Nếu mã dự án xuất hiện không chính xác hoặc không có mã dự án nào được đặt, hãy chạy:

  gcloud config set project <YOUR_PROJECT_ID>

Bật API

Chạy lệnh này để bật tất cả các API bắt buộc:

gcloud services enable \
 run.googleapis.com \
 aiplatform.googleapis.com \
 cloudbuild.googleapis.com

3. Thiết lập dự án

Ở bước này, bạn sẽ khởi chạy các biến môi trường của dự án và tạo một thư mục làm việc cho dự án.

  1. Trong phiên Cloud Shell đang hoạt động, hãy khởi chạy các biến môi trường dự án sau:
  export PROJECT_ID=$(gcloud config get-value project)

Lưu ý: Sử dụng khu vực gần nhất

Tìm khu vực gần bạn nhất rồi thay thế insert-region-here bằng khu vực đó trong lệnh sau:

  export REGION=[insert-region-here]
  1. Tạo và thay đổi thành một thư mục dự án mới có tên là coffee-barista-agent:
  mkdir coffee-barista-agent && cd coffee-barista-agent

4. Tạo nguồn dữ liệu trình đơn mô phỏng

Để cung cấp thông tin cơ bản cho AI Barista và ngăn AI này tạo ra các mặt hàng không tồn tại, bạn sẽ tạo một tập dữ liệu thực đơn tại địa phương. Tác nhân sẽ đọc tệp này trong thời gian chạy thông qua một công cụ tuỳ chỉnh.

  1. Tạo và mở menu.json trong Cloud Shell Editor:
  cloudshell edit menu.json
  1. Dán nội dung JSON sau đây vào trình chỉnh sửa rồi lưu tệp:
[
  {
    "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. Xác minh rằng tệp JSON được định dạng đúng cách:
  cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"

💬 Thảo luận: JSON cục bộ so với Cơ sở dữ liệu trực tiếp

Tại sao chúng ta sử dụng tệp menu.json đơn giản tại địa phương thay vì cơ sở dữ liệu trực tiếp?

Đối với một hướng dẫn hoặc nguyên mẫu nhanh, tệp JSON cục bộ sẽ giúp loại bỏ thời gian và độ phức tạp khi thiết lập cơ sở dữ liệu ban đầu. Tuy nhiên, trong một ứng dụng sản xuất thực tế của doanh nghiệp, bạn sẽ kết nối tác nhân với một cơ sở dữ liệu được quản lý như Cloud Firestore, AlloyDB hoặc Cloud SQL.

Việc sử dụng cơ sở dữ liệu trực tiếp cho phép người quản lý quán cà phê thêm các mặt hàng theo mùa, cập nhật giá hoặc điều chỉnh thẻ chất gây dị ứng một cách linh hoạt mà không cần tạo lại hình ảnh vùng chứa hoặc triển khai lại mã ứng dụng. Chúng ta sẽ sử dụng cơ sở dữ liệu trực tiếp như một bước không bắt buộc ở phần sau của lớp học lập trình.

5. Tạo tác nhân ADK

Bây giờ, bạn sẽ cài đặt các gói cần thiết và xây dựng logic tác nhân ADK cốt lõi. Bạn sẽ xác định một công cụ get_menu() và truyền công cụ đó đến một LlmAgent.

  1. Tạo và mở requirements.txt trong Cloud Shell Editor:
  cloudshell edit requirements.txt
  1. Dán các phần phụ thuộc sau vào trình chỉnh sửa rồi lưu tệp:
google-adk==2.2.0
streamlit==1.58.0
  1. Tạo và mở agent.py trong Cloud Shell Editor:
  cloudshell edit agent.py
  1. Dán mã sau vào 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. Tạo và mở app.py trong Cloud Shell Editor:
  cloudshell edit app.py
  1. Dán mã sau vào 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}")

💬 Thảo luận: Các điểm cân bằng của mô hình và hiệu quả của mã thông báo truy xuất

Tại sao bạn gọi một công cụ hàm để truy xuất trình đơn thay vì chỉ dán toàn bộ văn bản trình đơn vào chỉ dẫn hệ thống của tác nhân?

Nền kinh tế mã thông báo! Việc đưa 8 mặt hàng vào câu lệnh là không tốn kém, nhưng nếu quán cà phê mở rộng lên 500 mặt hàng, bao gồm cả các thành phần tuỳ chỉnh thì sao? Việc dán các tập dữ liệu lớn trực tiếp vào câu lệnh hệ thống sẽ làm tăng số lượng mã thông báo của câu lệnh, làm tăng chi phí giao dịch và độ trễ phản hồi API trên mọi truy vấn.

Bằng cách sử dụng một công cụ ADK, tác nhân sẽ yêu cầu đọc trình đơn một cách linh hoạt CHỈ khi cần. LLM chỉ nhận được dữ liệu thực đơn có liên quan làm ngữ cảnh, giảm thiểu kích thước mã thông báo của câu lệnh.

💬 Thảo luận: Trạng thái bộ nhớ và các kho lưu trữ sản xuất

Nhật ký trò chuyện được lưu trữ trong st.session_state của Streamlit có được duy trì khi người dùng đóng thẻ trình duyệt không?

Không. st.session_state hoàn toàn nằm trong bộ nhớ và chỉ dành riêng cho kết nối trình duyệt đang hoạt động. Nếu người dùng làm mới trang hoặc đóng thẻ, nhật ký trò chuyện của họ với nhân viên pha chế sẽ bị mất.

Đối với một ứng dụng sản xuất, bạn sẽ kết nối trình chạy ADK với một phần phụ trợ lưu trữ liên tục như Cloud Firestore hoặc Redis. ADK cung cấp các lớp trừu tượng dịch vụ tích hợp (chẳng hạn như SessionService) giúp bạn dễ dàng lưu và tiếp tục nhật ký trò chuyện trên các thiết bị và khi tải lại trang.

6. Triển khai tác nhân lên Cloud Run

Bạn sẽ triển khai ứng dụng Streamlit trực tiếp từ nguồn bằng cách sử dụng buildpack tích hợp của Cloud Run. Để tuân theo nguyên tắc về đặc quyền tối thiểu, bạn sẽ tạo và triển khai bằng tài khoản dịch vụ tuỳ chỉnh chuyên dụng thay vì sử dụng tài khoản dịch vụ Compute Engine mặc định.

  1. Tạo một tài khoản dịch vụ chuyên dụng:
  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. Cấp vai trò người dùng Nền tảng tác nhân Gemini Enterprise (roles/aiplatform.user) cho tài khoản dịch vụ mới:
  gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/aiplatform.user"
  1. Triển khai dịch vụ bằng gcloud run deploy, truyền email của tài khoản dịch vụ mới thông qua cờ --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. Sau khi triển khai, hãy tìm URL của dịch vụ trong đầu ra lệnh.

💬 Thảo luận: Triển khai vùng chứa so với nguồn và bảo mật IAM

Chúng tôi đã triển khai lên Cloud Run bằng cách sử dụng gcloud run deploy –source mà không cần tạo tệp Dockerfile hoặc Procfile. Làm cách nào Cloud Run biết cách biên dịch và thực thi ứng dụng Python của chúng ta?

Cloud Run sử dụng Buildpack trong phần nâng cao để phân tích kho lưu trữ của bạn. Khi phát hiện sự hiện diện của requirements.txt và các tệp nguồn Python, công cụ này sẽ tự động biên dịch và đóng gói một vùng chứa thời gian chạy Python.

Việc viết một Dockerfile tuỳ chỉnh giúp bạn có toàn quyền kiểm soát các gói hệ thống và lớp cơ sở của vùng chứa. Procfile là một cách đơn giản hơn để khai báo lệnh khởi động mà không cần định cấu hình hoàn toàn một vùng chứa. Tuy nhiên, để triển khai nhanh, việc triển khai từ nguồn (--source) rất hiệu quả.

Tại sao chúng ta phải thực hiện thêm bước tạo tài khoản dịch vụ tuỳ chỉnh barista-agent-sa thay vì chỉ dùng tài khoản dịch vụ Compute Engine mặc định?

An toàn là trên hết! Theo mặc định, tài khoản dịch vụ Compute Engine mặc định có quyền Chỉnh sửa cực kỳ rộng. Việc chạy vùng chứa Cloud Run của chúng ta trong tài khoản dịch vụ mặc định có nghĩa là nếu ứng dụng của chúng ta có lỗi bảo mật, kẻ tấn công có thể đọc, ghi hoặc xoá các tài nguyên khác trong dự án Google Cloud của chúng ta.

Bằng cách tạo một tài khoản dịch vụ chuyên dụng và chỉ chỉ định vai trò roles/aiplatform.user cho tài khoản đó, chúng ta tuân theo Nguyên tắc về đặc quyền tối thiểu: ứng dụng có chính xác quyền truy cập cần thiết để gọi Gemini và không có quyền nào khác.

7. Kiểm thử hành vi của RAG

Mở URL dịch vụ Cloud Run trong trình duyệt web và đặt câu hỏi cho AI Barista để kiểm tra các ràng buộc về cơ sở và độ an toàn.

  1. Yêu cầu trong trình đơn: Hỏi: "Đề xuất một loại đồ uống mạnh và ấm."Dự kiến: Trợ lý đề xuất Espresso.
  2. Bẫy ngoài thực đơn: Hỏi: "Bạn có matcha frappuccino không?"Dự kiến: Nhân viên lịch sự từ chối và giải thích rằng món này không có trong thực đơn.
  3. Yêu cầu có lưu ý về chất gây dị ứng: Hỏi: "Tôi không dung nạp được đường lactose, tôi có thể dùng gì?"Mong đợi: Trợ lý chỉ đề xuất các món trong thực đơn không có sữa (chẳng hạn như Oat Milk Latte, Espresso, Cold Brew). Không nên dùng Cappuccino hoặc Bánh sừng bò.

Kiểm thử hành vi của RAG

8. Không bắt buộc: Dựa vào Firestore để cung cấp thông tin cho tác nhân bằng tính năng Tìm kiếm vectơ

Trong trường hợp sản xuất, việc lưu trữ các mục trong trình đơn trong tệp menu.json cục bộ là không phù hợp vì mọi thay đổi đối với trình đơn đều yêu cầu bạn phải tạo lại hình ảnh vùng chứa và triển khai lại dịch vụ Cloud Run.

Để làm cho ứng dụng trở nên linh hoạt và có khả năng mở rộng, bạn có thể di chuyển dữ liệu trình đơn sang Cloud Firestore và sử dụng Vector Search để chỉ truy xuất những mục trình đơn phù hợp nhất dựa trên mức độ tương đồng về ngữ nghĩa.

Tích hợp Firestore bằng tính năng Tìm kiếm vectơ

1. Bật Firestore API và khởi chạy cơ sở dữ liệu

Chạy các lệnh sau để bật API Firestore và tạo cơ sở dữ liệu Firestore có tên là coffee-menu ở chế độ Gốc:

gcloud services enable firestore.googleapis.com

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

Lưu ý: Có thể mất 1 đến 2 phút để API được bật. Nếu lệnh tạo cơ sở dữ liệu nhắc bạn bằng API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry?, hãy nhập Y để tiếp tục hoặc đợi một phút rồi chạy lại lệnh.

2. Gieo dữ liệu thực đơn vào Firestore

Để nhanh chóng điền sẵn cơ sở dữ liệu Firestore bằng các mục trong trình đơn từ tệp menu.json, bạn có thể chạy một tập lệnh Python cục bộ trong Cloud Shell.

  1. Cài đặt thư viện ứng dụng Firestore và AI tạo sinh cục bộ trong Cloud Shell để chạy tập lệnh gieo hạt:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. Tạo tập lệnh gieo hạt seed.py:
cloudshell edit seed.py
  1. Dán mã sau vào 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. Chạy tập lệnh:
python3 seed.py

3. Tạo chỉ mục vectơ Firestore

Để thực hiện tìm kiếm vectơ trên các mục trong trình đơn, bạn phải tạo một chỉ mục vectơ kết hợp trên trường embedding trong cơ sở dữ liệu Firestore.

Chạy lệnh sau trong cửa sổ dòng lệnh 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": "{}"}'

Lưu ý: Quá trình tạo chỉ mục Firestore chạy ở chế độ nền và có thể mất vài phút để hoàn tất. Bạn có thể tiếp tục các bước tiếp theo của lớp học lập trình trong khi chỉ mục đang được tạo.

4. Cấp quyền truy cập Firestore cho tài khoản dịch vụ

Để dịch vụ Cloud Run của bạn truy vấn Firestore, bạn phải cấp cho tài khoản dịch vụ của dịch vụ này vai trò Người dùng 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"

Lưu ý: Mặc dù chúng tôi đang sử dụng Cloud Firestore ở chế độ Gốc, nhưng Google Cloud sử dụng các vai trò IAM hợp nhất của Cloud Datastore (roles/datastore.viewer hoặc roles/datastore.user) để quản lý quyền kiểm soát truy cập.

5. Cập nhật mã

Bây giờ, hãy cập nhật mã để truy xuất trình đơn từ Firestore thay vì đọc từ menu.json.

  1. Mở requirements.txt trong Trình chỉnh sửa Cloud Shell:
cloudshell edit requirements.txt
  1. Thêm thư viện ứng dụng Firestore và GenAI vào cuối tệp rồi lưu tệp:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. Mở agent.py trong Trình chỉnh sửa Cloud Shell:
cloudshell edit agent.py
  1. Xác định vị trí khối # [START get_menu] trong agent.py rồi thay thế toàn bộ khối đó (từ # [START get_menu] đến # [END get_menu]) bằng cách triển khai Firestore sau:
# [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. Mở app.py trong Trình chỉnh sửa Cloud Shell:
cloudshell edit app.py
  1. Xác định vị trí khối # [START load_menu] trong app.py rồi thay thế toàn bộ khối đó (từ # [START load_menu] đến # [END load_menu]) bằng logic tải Firestore sau đây:
# [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. Triển khai lại lên Cloud Run

Triển khai ứng dụng đã cập nhật:

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. Xác minh việc tích hợp Firestore

Để kiểm thử kết nối của nhân viên hỗ trợ với Firestore, hãy thêm một mục hoàn toàn mới vào trình đơn ngay trong Firestore và xác minh rằng nhân viên hỗ trợ đề xuất mục đó.

  1. Chạy lệnh sau trong Cloud Shell để ghi một tài liệu mới vào tập hợp menu trong Firestore bằng 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!')
"
  1. Làm mới ứng dụng Streamlit trong trình duyệt để xoá phiên trò chuyện và tải trạng thái cơ sở dữ liệu mới.
  2. Xin lưu ý rằng:
    • Matcha Green Tea Latte (Trà xanh matcha latte) sẽ tự động xuất hiện trong trình đơn bên.
    • Hỏi chatbot: "Bạn có đồ uống nào làm từ matcha không?"
    • Trợ lý ảo sẽ đề xuất thành công món Matcha Green Tea Latte mới kèm theo nội dung mô tả và giá mà bạn vừa thêm. Điều này xác nhận rằng tác nhân được đặt câu hỏi trực tiếp trong cơ sở dữ liệu Firestore đang hoạt động của bạn!

9. Dọn dẹp

Để tránh bị tính phí liên tục vào tài khoản thanh toán Google Cloud, hãy xoá dịch vụ Cloud Run đã triển khai và tài khoản dịch vụ tuỳ chỉnh.

Xoá dịch vụ Cloud Run:

gcloud run services delete coffee-barista --region $REGION --quiet

Xoá tài khoản dịch vụ tuỳ chỉnh:

gcloud iam service-accounts delete barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com --quiet

(Không bắt buộc) Xoá cơ sở dữ liệu Firestore (nếu đã tạo):

gcloud firestore databases delete --database="coffee-menu" --quiet

Bước không bắt buộc: Xoá toàn bộ dự án. ⚠️CHỈ thực hiện việc này nếu bạn đã tạo một dự án riêng cho lớp học này

gcloud projects delete $PROJECT_ID

10. Xin chúc mừng

Xin chúc mừng! Bạn đã xây dựng và triển khai một tác nhân AI Barista Tạo sinh tăng cường truy xuất (RAG) bằng ADK và Cloud Run của Google.

Kiến thức bạn học được

  • Tạo các công cụ RAG đơn giản bằng Python.
  • Sử dụng ADK LlmAgentInMemoryRunner.
  • Tạo trải nghiệm trò chuyện có trạng thái trong Streamlit.
  • Triển khai Streamlit lên Cloud Run bằng cách sử dụng các bản dựng dựa trên nguồn.

Tài liệu tham khảo