Wdrażanie agenta AI RAG w Streamlit przy użyciu pakietu Google ADK i Cloud Run

1. Wprowadzenie

W tym ćwiczeniu w Codelabs utworzysz interaktywnego agenta AI Barista dla kawiarni. Korzystając z pakietu Agent Development Kit (ADK) od Google o otwartym kodzie źródłowym i modelu Gemini 3.5 Flash, zaimplementujesz generowanie wspomagane wyszukiwaniem (RAG), aby rekomendacje agenta były oparte na przykładowym zbiorze danych menu. Na koniec umieścisz agenta w interfejsie użytkownika Streamlit i wdrożysz go w Cloud Run.

Jakie zadania wykonasz

  • Utwórz źródło danych RAG (menu.json) zawierające produkty z kawą, tagi i alergeny.
  • Utwórz agenta AI za pomocą pakietu ADKLlmAgent i połącz narzędzie Python, aby wczytać dane menu.
  • Umieść agenta w aplikacji do czatu Streamlit, która zarządza historią rozmów.
  • Wdróż aplikację Streamlit w Cloud Run za pomocą wdrożenia opartego na źródle.
  • Sprawdź powiązanie RAG i świadomość alergenów.

Diagram architektury

Czego potrzebujesz

  • przeglądarka, np. Chrome;
  • projekt Google Cloud z włączonymi płatnościami;
  • podstawowa znajomość Pythona,

To ćwiczenie jest przeznaczone dla deweloperów na wszystkich poziomach zaawansowania, w tym dla początkujących.

Szacowany koszt: poniżej 1 USD.

2. Zanim zaczniesz

Tworzenie projektu Google Cloud

  1. W konsoli Google Cloud wybierz lub utwórz projekt w chmurze Google Cloud.
  2. Sprawdź, czy w projekcie Cloud włączone są płatności.

Uruchamianie Cloud Shell

  1. U góry konsoli Google Cloud kliknij Aktywuj Cloud Shell.

Aktywuj Cloud Shell

  1. Potwierdź uwierzytelnianie:

Autoryzuj Cloud Shell

  gcloud auth list
  1. Sprawdź, czy aktywny projekt jest ustawiony:
  gcloud config get project

Jeśli wyświetlany identyfikator projektu jest nieprawidłowy lub nie jest ustawiony, uruchom to polecenie:

  gcloud config set project <YOUR_PROJECT_ID>

Włącz interfejsy API

Aby włączyć wszystkie wymagane interfejsy API, uruchom to polecenie:

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

3. Konfigurowanie projektu

W tym kroku zainicjujesz zmienne środowiskowe projektu i utworzysz katalog roboczy projektu.

  1. W aktywnej sesji Cloud Shell zainicjuj te zmienne środowiskowe projektu:
  export PROJECT_ID=$(gcloud config get-value project)

Uwaga: użyj najbliższego regionu

Znajdź najbliższy region i zastąp nim ciąg insert-region-here w tym poleceniu:

  export REGION=[insert-region-here]
  1. Utwórz nowy katalog projektu o nazwie coffee-barista-agent i przejdź do niego:
  mkdir coffee-barista-agent && cd coffee-barista-agent

4. Tworzenie źródła danych z przykładowym menu

Aby uziemić AI Baristę i zapobiec jej halucynacjom dotyczącym nieistniejących produktów, utworzysz lokalny zbiór danych menu. Agent odczyta ten plik w czasie działania za pomocą niestandardowego narzędzia.

  1. Utwórz i otwórz plik menu.json w edytorze Cloud Shell:
  cloudshell edit menu.json
  1. Wklej do edytora ten kod JSON i zapisz plik:
[
  {
    "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. Sprawdź, czy plik JSON jest prawidłowo sformatowany:
  cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"

💬 Dyskusja: lokalny plik JSON a bazy danych na żywo

Dlaczego używamy prostego lokalnego pliku menu.json zamiast bazy danych na żywo?

W przypadku szybkiego samouczka lub prototypu lokalny plik JSON eliminuje początkowy czas i złożoność konfiguracji bazy danych. W rzeczywistej aplikacji produkcyjnej dla przedsiębiorstw połączysz jednak agenta z zarządzaną bazą danych, taką jak Cloud Firestore, AlloyDB lub Cloud SQL.

Korzystanie z bazy danych na żywo umożliwia menedżerom kawiarni dodawanie produktów sezonowych, aktualizowanie cen i dynamiczne dostosowywanie tagów alergenów bez ponownego tworzenia obrazu kontenera ani ponownego wdrażania kodu aplikacji. W dalszej części tego ćwiczenia użyjemy bazy danych na żywo jako kroku opcjonalnego.

5. Tworzenie agenta ADK

Teraz zainstalujesz wymagane pakiety i utworzysz podstawową logikę agenta ADK. Zdefiniujesz get_menu() i przekażesz je do LlmAgent.

  1. Utwórz i otwórz plik requirements.txt w edytorze Cloud Shell:
  cloudshell edit requirements.txt
  1. Wklej do edytora te zależności i zapisz plik:
google-adk==2.2.0
streamlit==1.58.0
  1. Utwórz i otwórz plik agent.py w edytorze Cloud Shell:
  cloudshell edit agent.py
  1. Wklej do pliku agent.py ten kod:
# 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. Utwórz i otwórz plik app.py w edytorze Cloud Shell:
  cloudshell edit app.py
  1. Wklej do pliku app.py ten kod:
# 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}")

💬 Dyskusja: kompromisy dotyczące modelu i wydajność tokenów pobierania

Dlaczego w celu pobrania menu wywołujesz narzędzie funkcji, zamiast po prostu wkleić cały tekst menu do instrukcji systemowych agenta?

Ekonomia tokenów! Umieszczenie w prompcie 8 produktów jest tanie, ale co się stanie, jeśli kawiarnia rozszerzy ofertę do 500 produktów, w tym składników niestandardowych? Wklejanie dużych zbiorów danych bezpośrednio w prompcie systemowym zwiększa liczbę tokenów promptu, co podnosi koszty transakcji i wydłuża czas oczekiwania na odpowiedź interfejsu API w przypadku każdego zapytania.

Dzięki narzędziu ADK agent dynamicznie prosi o odczytanie menu TYLKO wtedy, gdy jest to potrzebne. Model LLM otrzymuje tylko odpowiednie dane menu jako kontekst, co minimalizuje rozmiar tokena promptu.

💬 Dyskusja: stan pamięci i sklepy produkcyjne

Czy historia czatu przechowywana w st.session_state w Streamlit jest zachowywana, gdy użytkownik zamknie kartę przeglądarki?

Nie, nie ma. st.session_state jest przechowywany w pamięci i unikalny dla aktywnego połączenia przeglądarki. Jeśli użytkownik odświeży stronę lub zamknie kartę, historia rozmowy z baristą zostanie utracona.

W przypadku aplikacji produkcyjnej połączysz moduł wykonawczy ADK z trwałym backendem pamięci masowej, takim jak Cloud Firestore lub Redis. ADK udostępnia wbudowane abstrakcje usług (np. SessionService), które ułatwiają zapisywanie i wznawianie historii czatu po ponownym załadowaniu strony i na różnych urządzeniach.

6. Wdrażanie agenta w Cloud Run

Wdrożysz aplikację Streamlit bezpośrednio ze źródła za pomocą wbudowanych pakietów kompilacji Cloud Run. Aby postępować zgodnie z zasadą jak najmniejszych uprawnień, utwórz i wdróż, używając dedykowanego niestandardowego konta usługi, zamiast domyślnego konta usługi Compute Engine.

  1. Utwórz dedykowane konto usługi:
  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. Przypisz do nowego konta usługi rolę użytkownika platformy agentów 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. Wdróż usługę za pomocą polecenia gcloud run deploy, przekazując nowy adres e-mail konta usługi za pomocą flagi --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. Po wdrożeniu znajdź adres URL usługi w wyniku polecenia.

💬 Dyskusja: wdrażanie kontenerów a źródło i zabezpieczenia IAM

Wdrożyliśmy aplikację w Cloud Run za pomocą polecenia gcloud run deploy –source bez tworzenia pliku Dockerfile ani Procfile. Jak Cloud Run dowiedział się, jak skompilować i wykonać naszą aplikację w Pythonie?

Cloud Run używa w tle pakietów Buildpacks do analizowania repozytorium. Po wykryciu plików źródłowychrequirements.txt i Python silnik automatycznie kompiluje i pakuje kontener środowiska wykonawczego Pythona.

Pisanie niestandardowego Dockerfile daje pełną kontrolę nad pakietami systemowymi i warstwami bazowymi kontenera. Procfile to prostszy sposób na zadeklarowanie polecenia uruchamiania bez pełnej konfiguracji kontenera. W przypadku szybkich wdrożeń bardzo wydajne jest jednak wdrażanie z kodu źródłowego (--source).

Dlaczego wykonaliśmy dodatkowy krok polegający na utworzeniu niestandardowego konta usługi barista-agent-sa zamiast użyć domyślnego konta usługi Compute Engine?

Bezpieczeństwo przede wszystkim! Domyślne konto usługi Compute Engine ma domyślnie bardzo szerokie uprawnienia Edytującego. Uruchomienie kontenera Cloud Run na domyślnym koncie usługi oznacza, że jeśli w naszej aplikacji wystąpi błąd zabezpieczeń, osoba atakująca może potencjalnie odczytywać, zapisywać lub usuwać inne zasoby w naszym projekcie Google Cloud.

Tworząc dedykowane konto usługi i przypisując mu tylko rolę roles/aiplatform.user, postępujemy zgodnie z zasadą jak najmniejszych uprawnień: aplikacja ma dokładnie taki dostęp, jakiego potrzebuje do wywoływania Gemini, i nic więcej.

7. Testowanie działania RAG

Otwórz adres URL usługi Cloud Run w przeglądarce i zadaj pytania AI Barista, aby sprawdzić jej podstawy i ograniczenia bezpieczeństwa.

  1. Prośba w menu: zapytanie: „Poleć coś mocnego i ciepłego”. Oczekiwana odpowiedź: agent poleca espresso.
  2. Pułapka związana z produktami spoza menu: zapytaj: „Czy macie matcha frappuccino?” Oczekiwana odpowiedź: agent grzecznie odmawia i wyjaśnia, że nie ma tego w menu.
  3. Zapytanie uwzględniające alergeny: zapytaj: „Mam nietolerancję laktozy. Co mogę zamówić?” Oczekiwana odpowiedź: agent poleca tylko pozycje z menu bez nabiału (np. latte na mleku owsianym, espresso, cold brew). Nie poleca cappuccino ani rogalika.

Testowanie działania RAG

8. Opcjonalnie: powiąż agenta z Firestore za pomocą wyszukiwania wektorowego

W środowisku produkcyjnym przechowywanie pozycji menu w lokalnym pliku menu.json nie jest idealne, ponieważ każda zmiana menu wymaga ponownego skompilowania obrazu kontenera i ponownego wdrożenia usługi Cloud Run.

Aby aplikacja była dynamiczna i skalowalna, możesz przenieść dane menu do Cloud Firestore i użyć wyszukiwania wektorowego, aby pobierać tylko najbardziej odpowiednie pozycje menu na podstawie podobieństwa semantycznego.

Integracja Firestore za pomocą wyszukiwania wektorowego

1. Włączanie interfejsu Firestore API i inicjowanie bazy danych

Aby włączyć interfejs Firestore API i utworzyć bazę danych Firestore o nazwie coffee-menu w trybie natywnym, uruchom te polecenia:

gcloud services enable firestore.googleapis.com

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

Uwaga: włączenie interfejsu API może potrwać 1–2 minuty. Jeśli polecenie tworzenia bazy danych wyświetli znak zachęty API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry?, wpisz Y, aby kontynuować, lub poczekaj minutę i ponownie uruchom polecenie.

2. Wypełnianie Firestore danymi menu

Aby szybko wypełnić bazę danych Firestore elementami menu z pliku menu.json, możesz uruchomić lokalnie w Cloud Shell skrypt w Pythonie.

  1. Aby uruchomić skrypt inicjujący, zainstaluj lokalnie w Cloud Shell biblioteki klienta Firestore i GenAI:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. Utwórz skrypt początkowy seed.py:
cloudshell edit seed.py
  1. Wklej do pliku seed.py ten kod:
# 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. Uruchom skrypt:
python3 seed.py

3. Tworzenie indeksu wektorowego Firestore

Aby przeprowadzać wyszukiwania wektorowe w przypadku pozycji menu, musisz utworzyć złożony indeks wektorowy w polu embedding w bazie danych Firestore.

Uruchom w terminalu Cloud Shell to polecenie:

gcloud firestore indexes composite create \
 --collection-group=menu \
 --query-scope=COLLECTION \
 --database="coffee-menu" \
 --field-config=field-path=embedding,vector-config='{"dimension":"768", "flat": "{}"}'

Uwaga: tworzenie indeksu Firestore odbywa się w tle i może potrwać kilka minut. Podczas tworzenia indeksu możesz przejść do kolejnych kroków w tym samouczku.

4. Przyznawanie kontu usługi dostępu do Firestore

Aby usługa Cloud Run mogła wysyłać zapytania do Firestore, musisz przyznać jej kontu usługi rolę Użytkownik 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"

Uwaga: mimo że używamy Cloud Firestore w trybie natywnym, Google Cloud wykorzystuje ujednolicone role IAM Cloud Datastore (roles/datastore.viewer lub roles/datastore.user) do zarządzania kontrolą dostępu.

5. Aktualizowanie kodu

Teraz zaktualizuj kod, aby pobierać menu z Firestore zamiast odczytywać je z menu.json.

  1. Otwórz plik requirements.txt w edytorze Cloud Shell:
cloudshell edit requirements.txt
  1. Dodaj biblioteki klienta Firestore i GenAI na końcu pliku i zapisz go:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. Otwórz plik agent.py w edytorze Cloud Shell:
cloudshell edit agent.py
  1. Znajdź blok # [START get_menu]agent.py i zastąp go w całości (od # [START get_menu] do # [END get_menu]) tą implementacją 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. Otwórz plik app.py w edytorze Cloud Shell:
cloudshell edit app.py
  1. Znajdź blok # [START load_menu] w pliku app.py i całkowicie go zastąp (od # [START load_menu] do # [END load_menu]) tym kodem logiki wczytywania 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. Ponowne wdrażanie w Cloud Run

Wdróż zaktualizowaną aplikację:

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. Weryfikowanie integracji z Firestore

Aby przetestować połączenie agenta z Firestore, dodaj zupełnie nowy element menu bezpośrednio w Firestore i sprawdź, czy agent go rekomenduje.

  1. Uruchom w Cloud Shell to polecenie, aby zapisać nowy dokument w kolekcji menu w Firestore za pomocą Pythona:
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. Odśwież aplikację Streamlit w przeglądarce, aby wyczyścić sesję czatu i wczytać nowy stan bazy danych.
  2. Zwróć uwagę na to, że:
    • W menu na pasku bocznym automatycznie pojawi się Matcha Green Tea Latte.
    • Zapytaj czatbota: „Czy masz jakieś napoje z matchą?”
    • Agent powinien skutecznie polecić nową Matcha Green Tea Latte z opisem i ceną, które właśnie dodano. Potwierdza to, że agent wysyła zapytania bezpośrednio do Twojej aktywnej bazy danych Firestore.

9. Czyszczenie danych

Aby uniknąć obciążenia konta rozliczeniowego Google Cloud bieżącymi opłatami, usuń wdrożoną usługę Cloud Run i niestandardowe konto usługi.

Usuń usługę Cloud Run:

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

Usuń niestandardowe konto usługi:

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

(Opcjonalnie) Usuń bazę danych Firestore (jeśli została utworzona):

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

Opcjonalnie: usuń cały projekt. ⚠️ Zrób to TYLKO wtedy, gdy masz projekt utworzony specjalnie na potrzeby tego laboratorium.

gcloud projects delete $PROJECT_ID

10. Gratulacje

Gratulacje! Masz utworzonego i wdrożonego agenta AI Barista opartego na technice generowania wspomaganego wyszukiwaniem (RAG) przy użyciu pakietu ADK od Google i Cloud Run.

Czego się nauczysz

  • Tworzenie prostych narzędzi RAG w Pythonie.
  • Korzystanie z pakietu ADK LlmAgentInMemoryRunner.
  • Tworzenie czatów z zachowywaniem stanu w Streamlit.
  • Wdrażanie Streamlit w Cloud Run za pomocą kompilacji opartych na źródłach.

Dokumentacja