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 Agent 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,00 USD.

2. Hinweis

Google Cloud-Projekt erstellen

  1. In der Google Cloud Console, wählen Sie ein Google Cloud-Projekt aus oder erstellen Sie eines.
  2. Die Abrechnung für das Cloud-Projekt muss aktiviert sein. So prüfen Sie, ob die Abrechnung für ein Projekt aktiviert ist.

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 Projektumgebungsvariablen:
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

Um den KI-Barista zu fundieren und zu verhindern, dass er nicht vorhandene Artikel halluziniert, 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. Überprüfen Sie, ob die JSON-Datei korrekt 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 schnelles Tutorial oder einen Prototyp entfällt durch die Verwendung einer lokalen JSON-Datei der anfängliche Zeitaufwand und die Komplexität der Datenbankeinrichtung. 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.

Durch die Verwendung einer Live-Datenbank können Café-Manager saisonale Artikel hinzufügen, Preise aktualisieren oder Allergenkennzeichnungen 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.56.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: Modell-Kompromisse & Effizienz der Token-Abfrage

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 Elemente im Prompt sind günstig, aber was passiert, wenn das Café auf 500 Artikel erweitert wird, einschließlich benutzerdefinierter Zutaten? Das direkte Einfügen großer Datensätze in die Systemeingabeaufforderung führt zu einer Erhöhung der Anzahl der Eingabeaufforderungstoken, was die Transaktionskosten und die Latenz der API-Antworten bei jeder einzelnen Abfrage erhöht.

Mithilfe eines ADK-Tools fragt der Agent dynamisch nur dann nach dem Menü, wenn dies erforderlich ist. Der LLM empfängt nur die relevanten Menüdaten als Kontext, wodurch die Größe des Eingabeaufforderungstokens minimiert wird.

💬 Diskussion: Speicherzustand & Produktionsspeicher

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

Nein. st.session_state befindet sich vollständig im Arbeitsspeicher und ist eindeutig der aktiven Browserverbindung zugeordnet. Wenn ein Benutzer die Seite aktualisiert oder den Tab schließt, geht sein Gesprächsverlauf mit dem Barista verloren.

Bei einer Produktionsanwendung würden Sie den ADK-Runner mit einem nichtflüchtigen Speicher-Backend wie Cloud Firestore oder Redis verbinden. ADK bietet integrierte Serviceabstraktionen (wie SessionService), die es einfach machen, den Chatverlauf über Seitenneuladungen und Geräte hinweg zu speichern und fortzusetzen.

6. Agent in Cloud Run bereitstellen

Sie werden die Streamlit-Anwendung direkt aus dem Quellcode mithilfe der in Cloud Run integrierten Buildpacks bereitstellen. Um dem Prinzip der geringsten Berechtigung zu folgen, werden Sie ein dediziertes benutzerdefiniertes Dienstkonto erstellen und damit bereitstellen, anstatt das standardmäßige Compute Engine-Dienstkonto 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 Benutzerrolle „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_ENTERPRISE=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 auf Cloud Run mit gcloud run deploy –source durchgeführt, ohne eine Dockerfile oder eine 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. Menüanfrage: Frage: "Empfehlen Sie etwas Starkes und Warmes."Erwartet: Der Mitarbeiter empfiehlt Espresso.
  2. Falle: Frage: „Haben Sie einen Matcha-Frappuccino?“ Erwartet:Der Agent 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). Es empfiehlt weder Cappuccino noch Croissant.

RAG-Verhalten testen

8. Optional: Binden Sie Ihren Agenten mithilfe der Vektorsuche in Firestore ein.

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

Um die Anwendung dynamisch und skalierbar zu gestalten, können Sie Ihre Menüdaten nach Cloud Firestore migrieren und Vektorsuche verwenden, um nur die relevantesten Menüpunkte basierend auf semantischer Ähnlichkeit abzurufen.

Integration von Firestore mithilfe der Vektorsuche

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: Die API-Aktivierung kann 1–2 Minuten dauern. Wenn Sie beim Befehl zur Datenbankerstellung mit 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 erneut aus.

2. Seed Firestore mit den Menüdaten

Um Ihre Firestore-Datenbank schnell mit den Menüpunkten aus Ihrer menu.json-Datei zu befüllen, 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-Script 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 Gemini Enterprise Agent Platform text-embedding-005 model
   text_to_embed = f"{item['name']}: {item['description']}"
   response = client.models.embed_content(
       model="text-embedding-005",
       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 des Firestore-Index 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. Firestore Zugriff auf das Dienstkonto 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 Ihren Code, um das Menü aus Firestore abzurufen, anstatt es aus menu.json zu lesen.

  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-005",
           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 \
  --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_ENTERPRISE=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global

7. Überprüfen Sie die Firestore-Integration.

Um die Verbindung des Agenten zu Firestore zu testen, fügen Sie direkt in Firestore einen neuen Menüpunkt hinzu und überprüfen Sie, ob der Agent diesen empfiehlt.

  1. Führen Sie den folgenden Befehl in Cloud Shell aus, um mit Python ein neues Dokument in die menu-Sammlung 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 oat milk infused with premium Japanese matcha powder.'
res = client.models.embed_content(
   model='text-embedding-005',
   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. Beachten Sie Folgendes:
    • 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. Dies bestätigt, dass der Agent direkt in Ihrer Live-Firestore-Datenbank abgefragt wird!

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 erstellt):

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.
  • Erstellung zustandsbehafteter Chat-Erlebnisse in Streamlit.
  • Streamlit mit quellbasierten Builds in Cloud Run bereitstellen.

Referenzdokumente