ติดตั้งใช้งาน RAG AI Agent ใน Streamlit โดยใช้ Google ADK และ Cloud Run

1. บทนำ

ใน Codelab นี้ คุณจะได้สร้างเอเจนต์บาริสต้า AI แบบอินเทอร์แอกทีฟสำหรับร้านกาแฟ การใช้ Agent Development Kit (ADK) แบบโอเพนซอร์สของ Google และโมเดล Gemini 3.5 Flash คุณจะใช้การสร้างที่ดึงข้อมูลมาเสริม (Retrieval-Augmented Generation หรือ RAG) เพื่ออ้างอิงคำแนะนำของ Agent ในชุดข้อมูลเมนูจำลอง สุดท้าย คุณจะห่อหุ้ม Agent ในอินเทอร์เฟซผู้ใช้ Streamlit และติดตั้งใช้งานใน Cloud Run

สิ่งที่คุณต้องทำ

  • สร้างแหล่งข้อมูล RAG (menu.json) ที่มีรายการกาแฟ แท็ก และสารก่อภูมิแพ้
  • สร้าง AI Agent โดยใช้ ADK LlmAgent และเชื่อมต่อเครื่องมือ Python เพื่อโหลดข้อมูลเมนู
  • ห่อหุ้มเอเจนต์ในแอปพลิเคชันแชท Streamlit ที่จัดการประวัติการสนทนา
  • ติดตั้งใช้งานแอป Streamlit ไปยัง Cloud Run โดยใช้การติดตั้งใช้งานตามแหล่งที่มา
  • ทดสอบการอ้างอิง RAG และการรับรู้ถึงสารก่อภูมิแพ้

แผนภาพสถาปัตยกรรม

สิ่งที่คุณต้องมี

  • เว็บเบราว์เซอร์ เช่น Chrome
  • โปรเจ็กต์ Google Cloud ที่เปิดใช้การเรียกเก็บเงิน
  • มีความคุ้นเคยกับ Python ในระดับพื้นฐาน

Codelab นี้มีไว้สำหรับนักพัฒนาซอฟต์แวร์ทุกระดับ รวมถึงผู้เริ่มต้น

ค่าใช้จ่ายโดยประมาณ: น้อยกว่า $1.00 USD

2. ก่อนเริ่มต้น

สร้างโปรเจ็กต์ Google Cloud

  1. ในคอนโซล Google Cloud ให้เลือกหรือสร้างโปรเจ็กต์ Google Cloud
  2. ตรวจสอบว่าได้เปิดใช้การเรียกเก็บเงินสำหรับโปรเจ็กต์ที่อยู่ในระบบคลาวด์แล้ว

เริ่มต้น Cloud Shell

  1. คลิกเปิดใช้งาน Cloud Shell ที่ด้านบนของคอนโซล Google Cloud

เปิดใช้งาน Cloud\nShell

  1. ยืนยันการตรวจสอบสิทธิ์

ให้สิทธิ์ 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

3. สร้างโปรเจ็กต์

ในขั้นตอนนี้ คุณจะเริ่มต้นตัวแปรสภาพแวดล้อมของโปรเจ็กต์และสร้างไดเรกทอรีการทำงานสำหรับโปรเจ็กต์

  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

4. สร้างแหล่งข้อมูลเมนูจำลอง

คุณจะต้องสร้างชุดข้อมูลเมนูในเครื่องเพื่อเป็นพื้นฐานให้ AI Barista และป้องกันไม่ให้ AI สร้างรายการที่ไม่มีอยู่จริง Agent จะอ่านไฟล์นี้ในรันไทม์ผ่านเครื่องมือที่กำหนดเอง

  1. สร้างและเปิด menu.json ใน Cloud Shell Editor โดยทำดังนี้
  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 ในภายหลัง

5. สร้างเอเจนต์ ADK

ตอนนี้คุณจะติดตั้งแพ็กเกจที่จำเป็นและสร้างตรรกะของ Agent หลักของ ADK คุณจะกำหนดเครื่องมือ get_menu() และส่งไปยัง LlmAgent

  1. สร้างและเปิด requirements.txt ใน Cloud Shell Editor โดยทำดังนี้
  cloudshell edit requirements.txt
  1. วางการอ้างอิงต่อไปนี้ลงในตัวแก้ไข แล้วบันทึกไฟล์
google-adk==2.2.0
streamlit==1.58.0
  1. สร้างและเปิด agent.py ใน Cloud Shell Editor โดยทำดังนี้
  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 Editor โดยทำดังนี้
  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}")

💬 การสนทนา: การแลกเปลี่ยนโมเดลและประสิทธิภาพของโทเค็นการดึงข้อมูล

เหตุใดจึงต้องเรียกใช้เครื่องมือฟังก์ชันเพื่อดึงเมนูแทนที่จะวางข้อความเมนูทั้งหมดลงในคำสั่งของระบบของเอเจนต์

เศรษฐกิจโทเค็น การใส่รายการ 8 รายการในพรอมต์นั้นไม่แพง แต่จะเกิดอะไรขึ้นหากร้านกาแฟขยายรายการเป็น 500 รายการ รวมถึงส่วนผสมที่กำหนดเอง การวางชุดข้อมูลขนาดใหญ่ลงในพรอมต์ของระบบโดยตรงจะทำให้จำนวนโทเค็นของพรอมต์เพิ่มขึ้น ซึ่งจะเพิ่มต้นทุนธุรกรรมและเวลาในการตอบสนองของ API ในการค้นหาแต่ละครั้ง

เมื่อใช้เครื่องมือ ADK เอเจนต์จะขออ่านเมนูแบบไดนามิกเฉพาะเมื่อจำเป็นเท่านั้น LLM จะได้รับเฉพาะข้อมูลเมนูที่เกี่ยวข้องเป็นบริบท ซึ่งจะช่วยลดขนาดโทเค็นของพรอมต์

💬 การสนทนา: สถานะหน่วยความจำและร้านค้าที่ใช้งานจริง

ประวัติการแชทที่จัดเก็บไว้ใน st.session_state ของ Streamlit จะยังอยู่ไหมเมื่อผู้ใช้ปิดแท็บเบราว์เซอร์

ไม่ st.session_state อยู่ในหน่วยความจำทั้งหมดและไม่ซ้ำกันสำหรับการเชื่อมต่อเบราว์เซอร์ที่ใช้งานอยู่ หากผู้ใช้รีเฟรชหน้าเว็บหรือปิดแท็บ ประวัติการสนทนากับบาริสต้าจะหายไป

สำหรับแอปพลิเคชันที่ใช้งานจริง คุณจะต้องเชื่อมต่อโปรแกรมเรียกใช้ ADK กับแบ็กเอนด์พื้นที่เก็บข้อมูลถาวร เช่น Cloud Firestore หรือ Redis ADK มีการแยกบริการในตัว (เช่น SessionService) ซึ่งช่วยให้บันทึกและดูประวัติการแชทต่อได้ง่ายๆ เมื่อโหลดหน้าเว็บซ้ำและเปลี่ยนอุปกรณ์

6. ติดตั้งใช้งาน Agent ใน Cloud Run

คุณจะติดตั้งใช้งานแอปพลิเคชัน Streamlit โดยตรงจากแหล่งที่มาโดยใช้ Buildpack ในตัวของ 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. มอบบทบาทผู้ใช้แพลตฟอร์ม Agent ของ 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"
  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. เมื่อติดตั้งใช้งานแล้ว ให้ค้นหา URL ของบริการในเอาต์พุตจากคำสั่ง

💬 การสนทนา: การติดตั้งใช้งานคอนเทนเนอร์เทียบกับแหล่งที่มา และความปลอดภัยของ IAM

เราติดตั้งใช้งานใน Cloud Run โดยใช้ gcloud run deploy –source โดยไม่ต้องสร้าง Dockerfile หรือ Procfile Cloud Run ทราบได้อย่างไรว่าจะคอมไพล์และเรียกใช้แอป Python ของเราอย่างไร

Cloud Run ใช้ Buildpack เบื้องหลังเพื่อวิเคราะห์ที่เก็บ เมื่อตรวจพบไฟล์ต้นฉบับ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 เพื่อทดสอบข้อจำกัดด้านความปลอดภัยและการอ้างอิง

  1. คำขอในเมนู: ถามว่า: "แนะนำเครื่องดื่มที่เข้มข้นและอุ่นๆ หน่อย"คาดการณ์: เอเจนต์แนะนำเอสเปรสโซ
  2. คำถามนอกเมนู: ถามว่า "คุณมีมัทฉะแฟรบปูชิโนไหม" คาดการณ์: ตัวแทนปฏิเสธอย่างสุภาพและอธิบายว่าไม่มีในเมนู
  3. คำขอที่ตระหนักถึงสารก่อภูมิแพ้: ถามว่า "ฉันแพ้แลคโตส มีอะไรที่ฉันทานได้บ้าง"คาดการณ์: ตัวแทนจะแนะนำเฉพาะเมนูที่ไม่มีส่วนผสมของนม (เช่น ลาเต้ใส่นมข้าวโอ๊ต เอสเปรสโซ โคลด์บรูว์) แต่จะไม่แนะนำคาปูชิโนหรือครัวซองต์

การทดสอบลักษณะการทำงานของ RAG

8. ไม่บังคับ: ให้ Agent ทำงานโดยอิงตาม Firestore โดยใช้ Vector Search

ในสถานการณ์การใช้งานจริง การจัดเก็บรายการเมนูในไฟล์ menu.json ในเครื่องไม่ใช่แนวทางที่ดี เนื่องจากหากมีการเปลี่ยนแปลงเมนูจะต้องสร้างอิมเมจคอนเทนเนอร์ใหม่และทำให้บริการ Cloud Run ใช้งานได้อีกครั้ง

หากต้องการให้แอปพลิเคชันมีความไดนามิกและปรับขนาดได้ คุณสามารถย้ายข้อมูลเมนูไปยัง Cloud Firestore และใช้การค้นหาเวกเตอร์เพื่อดึงเฉพาะรายการเมนูที่เกี่ยวข้องมากที่สุดตามความคล้ายคลึงเชิงความหมาย

การผสานรวม Firestore โดยใช้ Vector Search

1. เปิดใช้ Firestore API และเริ่มต้นฐานข้อมูล

เรียกใช้คำสั่งต่อไปนี้เพื่อเปิดใช้ Firestore API และสร้างฐานข้อมูล Firestore ชื่อ coffee-menu ในโหมดดั้งเดิม

gcloud services enable firestore.googleapis.com

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

หมายเหตุ: การเปิดใช้ API อาจใช้เวลา 1-2 นาทีจึงจะมีผล หากคำสั่งสร้างฐานข้อมูลแจ้งให้คุณป้อน API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry? ให้พิมพ์ Y เพื่อดำเนินการต่อ หรือรอสักครู่แล้วเรียกใช้คำสั่งอีกครั้ง

2. เริ่มต้นใช้งาน Firestore ด้วยข้อมูลเมนู

หากต้องการเริ่มต้นใช้งานฐานข้อมูล Firestore อย่างรวดเร็วด้วยรายการเมนูจากไฟล์ menu.json คุณสามารถเรียกใช้สคริปต์ Python ในเครื่องใน Cloud Shell ได้

  1. ติดตั้งไลบรารีของไคลเอ็นต์ Firestore และ GenAI ในเครื่องใน Cloud Shell เพื่อเรียกใช้สคริปต์การเริ่มต้น
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. สร้างสคริปต์การจัดอันดับ 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

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 จะทำงานในเบื้องหลังและอาจใช้เวลา 2-3 นาทีจึงจะเสร็จสมบูรณ์ คุณสามารถทำตามขั้นตอนถัดไปของโค้ดแล็บในขณะที่ระบบกำลังสร้างดัชนี

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 จะใช้บทบาท IAM ของ Cloud Datastore แบบรวม (roles/datastore.viewer หรือ roles/datastore.user) เพื่อจัดการการควบคุมการเข้าถึง

5. อัปเดตโค้ด

ตอนนี้ให้อัปเดตโค้ดเพื่อดึงเมนูจาก Firestore แทนการอ่านจาก menu.json

  1. เปิด requirements.txt ใน Cloud Shell Editor โดยทำดังนี้
cloudshell edit requirements.txt
  1. ผนวกไลบรารีของไคลเอ็นต์ Firestore และ GenAI ไว้ท้ายไฟล์ แล้วบันทึก
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. เปิด agent.py ใน Cloud Shell Editor โดยทำดังนี้
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 Editor โดยทำดังนี้
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]

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 โดยตรง แล้วตรวจสอบว่าตัวแทนแนะนำรายการนั้น

  1. เรียกใช้คำสั่งต่อไปนี้ใน 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!')
"
  1. รีเฟรชแอป Streamlit ในเบราว์เซอร์เพื่อล้างเซสชันแชทและโหลดสถานะฐานข้อมูลใหม่
  2. โปรดทราบว่า
    • มัทฉะลาเต้จะปรากฏในเมนูแถบด้านข้างโดยอัตโนมัติ
    • ถามแชทบ็อตว่า "คุณมีเครื่องดื่มมัทฉะไหม"
    • ตัวแทนควรแนะนำ Matcha Green Tea Latte ใหม่พร้อมคำอธิบายและราคาที่คุณเพิ่งเพิ่มได้สำเร็จ ซึ่งเป็นการยืนยันว่า Agent อิงตามการค้นหาในฐานข้อมูล Firestore ที่ใช้งานจริงโดยตรง

9. ล้างข้อมูล

หากต้องการหลีกเลี่ยงการเรียกเก็บเงินอย่างต่อเนื่องในบัญชีการเรียกเก็บเงินของ Google Cloud ให้ลบบริการ Cloud Run ที่ทําให้ใช้งานได้และบัญชีบริการที่กําหนดเอง

ลบบริการ Cloud Run

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

ลบบัญชีบริการที่กำหนดเอง

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

(ไม่บังคับ) ลบฐานข้อมูล Firestore (หากสร้างไว้)

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

ขั้นตอนที่ไม่บังคับ: ลบทั้งโปรเจ็กต์ ⚠️ทำขั้นตอนนี้เฉพาะในกรณีที่คุณสร้างโปรเจ็กต์เฉพาะสำหรับ Lab นี้

gcloud projects delete $PROJECT_ID

10. ขอแสดงความยินดี

ยินดีด้วย คุณได้สร้างและติดตั้งใช้งาน AI Barista Agent ที่มีความสามารถในการดึงข้อมูลมาเสริม (Retrieval-Augmented Generation หรือ RAG) โดยใช้ ADK และ Cloud Run ของ Google

สิ่งที่คุณได้เรียนรู้

  • การสร้างเครื่องมือ RAG อย่างง่ายใน Python
  • การใช้ ADK LlmAgent และ InMemoryRunner
  • การสร้างประสบการณ์แชทแบบมีสถานะใน Streamlit
  • การติดตั้งใช้งาน Streamlit ใน Cloud Run โดยใช้บิลด์ที่อิงตามแหล่งที่มา

เอกสารอ้างอิง