RAG AI-Agenten in Streamlit mit Google ADK und Cloud Run bereitstellen

1. Einführung

In diesem Codelab erstellen Sie einen interaktiven KI-Barista-Agenten für ein Café. Mit dem Open-Source-Agent Development Kit (ADK) von Google und dem Modell Gemini 3.5 Flash implementieren Sie Retrieval Augmented Generation (RAG), um die Empfehlungen des Agents auf einem Mock-Menü-Dataset zu basieren. Schließlich verpacken Sie den Agenten in einer Streamlit-Benutzeroberfläche und stellen ihn in Cloud Run bereit.

Aufgaben

  • Erstellen Sie eine RAG-Datenquelle (menu.json) mit Kaffeeprodukten, Tags und Allergenen.
  • Erstellen Sie einen KI-Agenten mit dem ADK LlmAgent und verbinden Sie ein Python-Tool, um die Menüdaten zu laden.
  • Den Agenten in eine Streamlit-Chatanwendung einbinden, die den Unterhaltungsverlauf verwaltet.
  • Streamlit-App mit quellcodebasierter Bereitstellung in Cloud Run bereitstellen
  • RAG-Fundierung und Allergenbewusstsein testen

Architekturdiagramm

Voraussetzungen

  • Ein Webbrowser wie Chrome.
  • Google Cloud-Projekt mit aktivierter Abrechnungsfunktion.
  • Grundkenntnisse in Python.

Dieses Codelab richtet sich an Entwickler aller Erfahrungsstufen, auch an Anfänger.

Geschätzte Kosten:weniger als 1 $.

2. Hinweis

Google Cloud-Projekt erstellen

  1. Wählen Sie in der Google Cloud Console ein Google Cloud-Projekt aus oder erstellen Sie eines.
  2. Die Abrechnung für das Cloud-Projekt muss aktiviert sein.

Cloud Shell starten

  1. Klicken Sie oben in der Google Cloud Console auf Cloud Shell aktivieren.

Cloud Shell aktivieren

  1. Authentifizierung überprüfen:

Cloud Shell autorisieren

  gcloud auth list
  1. Prüfen Sie, ob Ihr aktives Projekt festgelegt ist:
  gcloud config get project

Wenn die angezeigte Projekt-ID nicht korrekt ist oder keine festgelegt ist, führen Sie Folgendes aus:

  gcloud config set project <YOUR_PROJECT_ID>

APIs aktivieren

Führen Sie diesen Befehl aus, um alle erforderlichen APIs zu aktivieren:

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

3. Projekt einrichten

In diesem Schritt initialisieren Sie die Umgebungsvariablen Ihres Projekts und erstellen ein Arbeitsverzeichnis für Ihr Projekt.

  1. Initialisieren Sie in Ihrer aktiven Cloud Shell-Sitzung die folgenden Umgebungsvariablen für das Projekt:
  export PROJECT_ID=$(gcloud config get-value project)

Hinweis: Verwenden Sie die nächstgelegene Region.

Suchen Sie nach der Region, die Ihnen am nächsten liegt, und ersetzen Sie „insert-region-here“ im folgenden Befehl durch diese Region:

  export REGION=[insert-region-here]
  1. Erstellen Sie ein neues Projektverzeichnis mit dem Namen coffee-barista-agent und wechseln Sie dorthin:
  mkdir coffee-barista-agent && cd coffee-barista-agent

4. Mock-Menü-Datenquelle erstellen

Damit der KI-Barista nicht halluziniert und nicht vorhandene Artikel erfindet, erstellen Sie ein lokales Menü-Dataset. Der KI‑Agent liest diese Datei zur Laufzeit über ein benutzerdefiniertes Tool.

  1. Erstellen Sie menu.json im Cloud Shell-Editor und öffnen Sie die Datei:
  cloudshell edit menu.json
  1. Fügen Sie den folgenden JSON-Inhalt in den Editor ein und speichern Sie die Datei:
[
  {
    "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. Prüfen Sie, ob die JSON-Datei richtig formatiert ist:
  cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"

💬 Diskussion: Lokale JSON-Daten im Vergleich zu Live-Datenbanken

Warum verwenden wir eine einfache lokale Datei „menu.json“ anstelle einer Live-Datenbank?

Für ein kurzes Tutorial oder einen Prototyp ist eine lokale JSON-Datei ideal, da sie die anfängliche Einrichtungszeit und Komplexität der Datenbank reduziert. In einer echten Produktionsanwendung für Unternehmen würden Sie den Agent jedoch mit einer verwalteten Datenbank wie Cloud Firestore, AlloyDB oder Cloud SQL verbinden.

Mit einer Live-Datenbank können Kaffeeladenleiter saisonale Artikel hinzufügen, Preise aktualisieren oder Allergen-Tags dynamisch anpassen, ohne das Container-Image neu erstellen oder den Anwendungscode neu bereitstellen zu müssen. Wir verwenden später im Codelab eine Live-Datenbank als optionalen Schritt.

5. ADK-KI-Agenten erstellen

Jetzt installieren Sie die erforderlichen Pakete und erstellen die zentrale ADK-Agent-Logik. Sie definieren ein get_menu()-Tool und übergeben es an ein LlmAgent.

  1. Erstellen Sie requirements.txt im Cloud Shell-Editor und öffnen Sie die Datei:
  cloudshell edit requirements.txt
  1. Fügen Sie die folgenden Abhängigkeiten in den Editor ein und speichern Sie die Datei:
google-adk==2.2.0
streamlit==1.58.0
  1. Erstellen Sie agent.py im Cloud Shell-Editor und öffnen Sie die Datei:
  cloudshell edit agent.py
  1. Fügen Sie den folgenden Code in agent.py ein:
# 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. Erstellen Sie app.py im Cloud Shell-Editor und öffnen Sie die Datei:
  cloudshell edit app.py
  1. Fügen Sie den folgenden Code in app.py ein:
# 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}")

💬 Diskussion: Model Tradeoffs & Retrieval Token Efficiency

Warum sollte ein Funktionstool aufgerufen werden, um das Menü abzurufen, anstatt den gesamten Menütext einfach in die Systemanweisungen des Agenten einzufügen?

Token-Ökonomie! Acht Artikel im Prompt sind günstig, aber was passiert, wenn das Café auf 500 Artikel erweitert wird, einschließlich benutzerdefinierter Zutaten? Wenn Sie große Datasets direkt in den Systemprompt einfügen, erhöht sich die Anzahl der Prompt-Tokens. Das führt zu höheren Transaktionskosten und einer höheren API-Antwortlatenz bei jeder einzelnen Anfrage.

Wenn Sie ein ADK-Tool verwenden, fordert der Agent dynamisch an, das Menü NUR bei Bedarf zu lesen. Das LLM erhält nur die relevanten Menüdaten als Kontext, wodurch die Größe des Prompt-Tokens minimiert wird.

💬 Diskussion: Memory-Status und Produktionsspeicher

Bleibt der in Streamlits st.session_state gespeicherte Chatverlauf erhalten, wenn ein Nutzer seinen Browsertab schließt?

Nein. st.session_state ist vollständig im Arbeitsspeicher und nur für die aktive Browserverbindung verfügbar. Wenn ein Nutzer die Seite aktualisiert oder den Tab schließt, geht der Unterhaltungsverlauf mit dem Barista verloren.

Bei einer Produktionsanwendung würden Sie den ADK-Runner mit einem persistenten Speicher-Backend wie Cloud Firestore oder Redis verbinden. Das ADK bietet integrierte Dienstabstraktionen (z. B. SessionService), mit denen sich der Chatverlauf ganz einfach über Seitenneuladungen und Geräte hinweg speichern und fortsetzen lässt.

6. Agent in Cloud Run bereitstellen

Sie stellen die Streamlit-Anwendung direkt aus der Quelle mit den integrierten Buildpacks von Cloud Run bereit. Um das Prinzip der geringsten Berechtigung zu befolgen, erstellen und stellen Sie die Anwendung mit einem dedizierten benutzerdefinierten Dienstkonto bereit, anstatt das Compute Engine-Standarddienstkonto zu verwenden.

  1. Dediziertes Dienstkonto erstellen:
  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. Weisen Sie dem neuen Dienstkonto die Nutzerrolle „Gemini Enterprise Agent Platform“ (roles/aiplatform.user) zu:
  gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/aiplatform.user"
  1. Stellen Sie den Dienst mit gcloud run deploy bereit und übergeben Sie die neue E-Mail-Adresse des Dienstkontos über das Flag --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. Suchen Sie nach der Bereitstellung in der Befehlsausgabe nach der Dienst-URL.

💬 Diskussion: Container im Vergleich zu Quellcode bereitstellen und IAM-Sicherheit

Wir haben die Bereitstellung in Cloud Run mit „gcloud run deploy --source“ durchgeführt, ohne ein Dockerfile oder ein Procfile zu erstellen. Wie hat Cloud Run herausgefunden, wie unsere Python-App kompiliert und ausgeführt werden muss?

Cloud Run verwendet Buildpacks, um Ihr Repository zu analysieren. Wenn die Engine das Vorhandensein vonrequirements.txt und Python-Quelldateien erkennt, kompiliert und verpackt sie automatisch einen Python-Laufzeitcontainer.

Wenn Sie ein benutzerdefiniertes Dockerfile schreiben, haben Sie die vollständige Kontrolle über die Systempakete und Basisebenen Ihres Containers. Mit Procfile können Sie den Startbefehl einfacher deklarieren, ohne einen Container vollständig zu konfigurieren. Für schnelle Bereitstellungen ist die Bereitstellung aus der Quelle (--source) jedoch sehr effizient.

Warum haben wir den zusätzlichen Schritt zum Erstellen eines benutzerdefinierten Dienstkontos „barista-agent-sa“ durchlaufen, anstatt einfach das Compute Engine-Standarddienstkonto zu verwenden?

Sicherheit geht vor! Das standardmäßige Compute Engine-Dienstkonto hat standardmäßig sehr weitreichende Bearbeitungsberechtigungen. Wenn wir unseren Cloud Run-Container unter dem Standarddienstkonto ausführen, kann ein Angreifer, falls unsere App einen Sicherheitsfehler aufweist, möglicherweise andere Ressourcen in unserem Google Cloud-Projekt lesen, schreiben oder löschen.

Indem wir ein dediziertes Dienstkonto erstellen und ihm nur die Rolle roles/aiplatform.user zuweisen, folgen wir dem Prinzip der geringsten Berechtigung: Die App hat genau den Zugriff, den sie zum Aufrufen von Gemini benötigt, und nichts weiter.

7. RAG-Verhalten testen

Öffnen Sie die Cloud Run-Dienst-URL in einem Webbrowser und stellen Sie dem KI-Barista Fragen, um seine Fundierung und Sicherheitsbeschränkungen zu testen.

  1. Anfrage im Menü:Frage: „Empfiehl mir etwas Starkes und Warmes.“ Erwartet:Der Agent empfiehlt Espresso.
  2. Falle außerhalb des Menüs:Frage: „Haben Sie einen Matcha-Frappuccino?“ Erwartet:Der Kundenservicemitarbeiter lehnt höflich ab und erklärt, dass er nicht auf der Speisekarte steht.
  3. Anfrage mit Angabe von Allergien:Frage: „Ich habe eine Laktoseintoleranz. Was kann ich bestellen?“ Erwartet:Der Kundenservicemitarbeiter empfiehlt nur laktosefreie Menüpunkte (z. B. Hafermilch-Latte, Espresso, Cold Brew). Cappuccino oder Croissant werden NICHT empfohlen.

RAG-Verhalten testen

8. Optional: Agent mit Firestore und der Vektorsuche fundieren

In einer Produktionsumgebung ist das Speichern der Menüelemente in einer lokalen menu.json-Datei nicht ideal, da jede Änderung am Menü das Neuerstellen des Container-Images und die erneute Bereitstellung des Cloud Run-Dienstes erfordert.

Damit die Anwendung dynamisch und skalierbar ist, können Sie Ihre Menüdaten zu Cloud Firestore migrieren und Vector Search verwenden, um nur die relevantesten Menüelemente basierend auf semantischer Ähnlichkeit abzurufen.

Firestore mit der Vektorsuche einbinden

1. Firestore API aktivieren und Datenbank initialisieren

Führen Sie die folgenden Befehle aus, um die Firestore API zu aktivieren und eine Firestore-Datenbank mit dem Namen coffee-menu im nativen Modus zu erstellen:

gcloud services enable firestore.googleapis.com

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

Hinweis:Es kann 1–2 Minuten dauern, bis die API-Aktivierung wirksam wird. Wenn Sie beim Befehl zum Erstellen der Datenbank zur Eingabe von API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry? aufgefordert werden, geben Sie Y ein, um fortzufahren, oder warten Sie eine Minute und führen Sie den Befehl noch einmal aus.

2. Firestore mit den Menüdaten initialisieren

Wenn Sie Ihre Firestore-Datenbank schnell mit den Menüelementen aus der Datei menu.json füllen möchten, können Sie ein Python-Skript lokal in Cloud Shell ausführen.

  1. Installieren Sie die Firestore- und GenAI-Clientbibliotheken lokal in Cloud Shell, um das Seeding-Skript auszuführen:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. Erstellen Sie ein Seeding-Script seed.py:
cloudshell edit seed.py
  1. Fügen Sie den folgenden Code in seed.py ein:
# 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. Führen Sie das Script aus:
python3 seed.py

3. Firestore-Vektorindex erstellen

Wenn Sie Vektorsuchen für Ihre Menüelemente durchführen möchten, müssen Sie einen zusammengesetzten Vektorindex für das Feld embedding in Ihrer Firestore-Datenbank erstellen.

Führen Sie im Cloud Shell-Terminal den folgenden Befehl aus:

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

Hinweis:Die Erstellung von Firestore-Indizes erfolgt im Hintergrund und kann einige Minuten dauern. Sie können mit den nächsten Schritten des Codelabs fortfahren, während der Index erstellt wird.

4. Dienstkonto Firestore-Zugriff gewähren

Damit Ihr Cloud Run-Dienst Firestore abfragen kann, müssen Sie seinem Dienstkonto die Rolle Cloud Datastore-Nutzer (roles/datastore.user) zuweisen:

gcloud projects add-iam-policy-binding $PROJECT_ID \
 --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
 --role="roles/datastore.user"

Hinweis:Obwohl wir Cloud Firestore im nativen Modus verwenden, nutzt Google Cloud die einheitlichen Cloud Datastore-IAM-Rollen (roles/datastore.viewer oder roles/datastore.user) zur Verwaltung der Zugriffssteuerung.

5. Code aktualisieren

Aktualisieren Sie nun den Code, damit das Menü aus Firestore abgerufen wird, anstatt aus menu.json gelesen zu werden.

  1. Öffnen Sie requirements.txt im Cloud Shell-Editor:
cloudshell edit requirements.txt
  1. Hängen Sie die Firestore- und GenAI-Clientbibliotheken an das Ende der Datei an und speichern Sie sie:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. Öffnen Sie agent.py im Cloud Shell-Editor:
cloudshell edit agent.py
  1. Suchen Sie in agent.py nach dem Block # [START get_menu] und ersetzen Sie ihn vollständig (von # [START get_menu] bis # [END get_menu]) durch die folgende Firestore-Implementierung:
# [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. Öffnen Sie app.py im Cloud Shell-Editor:
cloudshell edit app.py
  1. Suchen Sie in app.py nach dem Block # [START load_menu] und ersetzen Sie ihn vollständig (von # [START load_menu] bis # [END load_menu]) durch die folgende Firestore-Ladelogik:
# [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. Nochmalige Bereitstellung in Cloud Run

Stellen Sie die aktualisierte Anwendung bereit:

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-Integration prüfen

Wenn Sie die Verbindung des Agents zu Firestore testen möchten, fügen Sie direkt in Firestore ein brandneues Menüelement hinzu und prüfen Sie, ob der Agent es empfiehlt.

  1. Führen Sie in Cloud Shell den folgenden Befehl aus, um mit Python ein neues Dokument in die Sammlung menu in Firestore zu schreiben:
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. Aktualisieren Sie Ihre Streamlit-App im Browser, um die Chatsitzung zu löschen und den neuen Datenbankstatus zu laden.
  2. Hinweis:
    • Matcha Green Tea Latte wird automatisch im Seitenleistenmenü angezeigt.
    • Fragen Sie den Chatbot: „Do you have any matcha drinks?“ (Gibt es Matcha-Getränke?)
    • Der Kundenservicemitarbeiter sollte den neuen Matcha Green Tea Latte mit der Beschreibung und dem Preis, die Sie gerade hinzugefügt haben, empfehlen. So wird bestätigt, dass der Agent direkt in Ihrer Live-Firestore-Datenbank auf die Anfrage reagiert.

9. Bereinigen

Damit Ihrem Google Cloud-Rechnungskonto keine laufenden Gebühren in Rechnung gestellt werden, löschen Sie den bereitgestellten Cloud Run-Dienst und das benutzerdefinierte Dienstkonto.

Löschen Sie den Cloud Run-Dienst:

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

Löschen Sie das benutzerdefinierte Dienstkonto:

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

Optional: Löschen Sie die Firestore-Datenbank (falls sie erstellt wurde):

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

Optionaler Schritt: Löschen Sie das gesamte Projekt. ⚠️ Führen Sie diesen Schritt NUR aus, wenn Sie für dieses Lab ein eigenes Projekt erstellt haben.

gcloud projects delete $PROJECT_ID

10. Glückwunsch

Glückwunsch! Sie haben einen RAG-KI-Barista-Agenten (Retrieval Augmented Generation) mit dem ADK von Google und Cloud Run erstellt und bereitgestellt.

Das haben Sie gelernt

  • Einfache RAG-Tools in Python erstellen
  • Verwendung des ADK LlmAgent und InMemoryRunner.
  • Zustandsbehaftete Chatfunktionen in Streamlit erstellen
  • Streamlit mit quellbasierten Builds in Cloud Run bereitstellen.

Referenzdokumente