Implementa un agente de IA de RAG en Streamlit con el ADK de Google y Cloud Run

1. Introducción

En este codelab, crearás un agente interactivo de barista con IA para una cafetería. Con el Kit de desarrollo de agentes (ADK) de código abierto de Google y el modelo Gemini 3.5 Flash, implementarás la generación mejorada por recuperación (RAG) para fundamentar las recomendaciones del agente en un conjunto de datos de menú simulado. Por último, integrarás el agente en una interfaz de usuario de Streamlit y lo implementarás en Cloud Run.

Actividades

  • Crea una fuente de datos de RAG (menu.json) que contenga elementos de café, etiquetas y alérgenos.
  • Compila un agente de IA con el ADK LlmAgent y conecta una herramienta de Python para cargar los datos del menú.
  • Envuelve el agente en una aplicación de chat de Streamlit que administra el historial de conversaciones.
  • Implementa la app de Streamlit en Cloud Run con la implementación basada en la fuente.
  • Prueba la fundamentación de RAG y la detección de alérgenos.

Diagrama de arquitectura

Requisitos

  • Un navegador web, como Chrome
  • Un proyecto de Google Cloud con facturación habilitada.
  • Conocimientos básicos sobre Python

Este codelab es para desarrolladores de todos los niveles, incluidos los principiantes.

Costo estimado: Menos de USD 1.

2. Antes de comenzar

Crea un proyecto de Google Cloud

  1. En la consola de Google Cloud, selecciona o crea un proyecto de Google Cloud.
  2. Asegúrate de que la facturación esté habilitada para tu proyecto de Cloud.

Inicie Cloud Shell

  1. Haz clic en Activar Cloud Shell en la parte superior de la consola de Google Cloud.

Activa Cloud\nShell

  1. Verifica la autenticación:

Autoriza Cloud Shell

  gcloud auth list
  1. Confirma que tu proyecto activo esté configurado:
  gcloud config get project

Si el ID del proyecto que se muestra no es correcto o no se configuró ninguno, ejecuta el siguiente comando:

  gcloud config set project <YOUR_PROJECT_ID>

Habilita las APIs

Ejecuta este comando para habilitar todas las APIs requeridas:

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

3. Configura tu proyecto

En este paso, inicializarás las variables de entorno de tu proyecto y crearás un directorio de trabajo para él.

  1. En tu sesión activa de Cloud Shell, inicializa las siguientes variables de entorno del proyecto:
  export PROJECT_ID=$(gcloud config get-value project)

Nota: Usa la región más cercana

Busca tu región más cercana y reemplaza insert-region-here por ella en el siguiente comando:

  export REGION=[insert-region-here]
  1. Crea un directorio de proyecto nuevo llamado coffee-barista-agent y cambia a él:
  mkdir coffee-barista-agent && cd coffee-barista-agent

4. Crea la fuente de datos del menú simulado

Para fundamentar a AI Barista y evitar que alucine elementos inexistentes, crearás un conjunto de datos de menú local. El agente leerá este archivo en el tiempo de ejecución a través de una herramienta personalizada.

  1. Crea y abre menu.json en el Editor de Cloud Shell:
  cloudshell edit menu.json
  1. Pega el siguiente contenido JSON en el editor y guarda el archivo:
[
  {
    "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. Verifica que el archivo JSON tenga el formato correcto:
  cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"

💬 Debate: Bases de datos activas vs. JSON local

¿Por qué usamos un archivo local simple menu.json en lugar de una base de datos activa?

Para un prototipo o un instructivo rápido, un archivo JSON local elimina el tiempo y la complejidad de la configuración inicial de la base de datos. Sin embargo, en una aplicación de producción empresarial del mundo real, conectarías el agente a una base de datos administrada, como Cloud Firestore, AlloyDB o Cloud SQL.

El uso de una base de datos activa permite que los administradores de la cafetería agreguen artículos de temporada, actualicen los precios o ajusten las etiquetas de alérgenos de forma dinámica sin volver a compilar la imagen del contenedor ni volver a implementar el código de la aplicación. Usaremos una base de datos activa como paso opcional más adelante en el codelab.

5. Compila el agente del ADK

Ahora instalarás los paquetes necesarios y compilarás la lógica principal del agente del ADK. Definirás una herramienta get_menu() y la pasarás a un LlmAgent.

  1. Crea y abre requirements.txt en el Editor de Cloud Shell:
  cloudshell edit requirements.txt
  1. Pega las siguientes dependencias en el editor y guarda el archivo:
google-adk==2.2.0
streamlit==1.58.0
  1. Crea y abre agent.py en el Editor de Cloud Shell:
  cloudshell edit agent.py
  1. Pega el siguiente código en 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. Crea y abre app.py en el Editor de Cloud Shell:
  cloudshell edit app.py
  1. Pega el siguiente código en 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}")

💬 Debate: Compromisos del modelo y eficiencia de los tokens de recuperación

¿Por qué llamar a una herramienta de función para recuperar el menú en lugar de simplemente pegar todo el texto del menú en las instrucciones del sistema del agente?

¡Economía de tokens! Incluir 8 elementos en la instrucción es económico, pero ¿qué sucede si la cafetería se expande a 500 elementos, incluidos ingredientes personalizados? Pegar grandes conjuntos de datos directamente en la instrucción del sistema aumenta el recuento de tokens de la instrucción, lo que incrementa los costos de transacción y la latencia de respuesta de la API en cada consulta.

Cuando se usa una herramienta del ADK, el agente solicita de forma dinámica leer el menú SOLO cuando es necesario. El LLM solo recibe los datos del menú pertinentes como contexto, lo que minimiza el tamaño de los tokens de la instrucción.

💬 Debate: Almacenes de producción y estado de la memoria

¿El historial de chat almacenado en st.session_state de Streamlit persiste cuando un usuario cierra la pestaña del navegador?

No, no lo sabe. st.session_state se encuentra completamente en la memoria y es único para la conexión activa del navegador. Si un usuario actualiza la página o cierra la pestaña, se perderá su historial de conversación con el barista.

En el caso de una aplicación de producción, conectarías el ejecutor del ADK a un backend de almacenamiento persistente, como Cloud Firestore o Redis. El ADK proporciona abstracciones de servicio integradas (como SessionService) que facilitan guardar y reanudar el historial de chat en las recargas de páginas y los dispositivos.

6. Implementa el agente en Cloud Run

Implementarás la aplicación de Streamlit directamente desde el código fuente con los buildpacks integrados de Cloud Run. Para seguir el principio de privilegio mínimo, crearás y realizarás la implementación con una cuenta de servicio personalizada dedicada en lugar de usar la cuenta de servicio predeterminada de Compute Engine.

  1. Crea una cuenta de servicio dedicada:
  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. Otorga el rol de usuario de Gemini Enterprise Agent Platform (roles/aiplatform.user) a la nueva cuenta de servicio:
  gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/aiplatform.user"
  1. Implementa el servicio con gcloud run deploy y pasa el nuevo correo electrónico de la cuenta de servicio a través de la marca --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. Una vez que se implemente, busca la URL del servicio en el resultado del comando.

💬 Debate: Implementación de contenedores frente a código fuente y seguridad de IAM

Realizamos la implementación en Cloud Run con gcloud run deploy --source sin crear un Dockerfile ni un Procfile. ¿Cómo supo Cloud Run cómo compilar y ejecutar nuestra app de Python?

Cloud Run usa Buildpacks de forma interna para analizar tu repositorio. Cuando detecta la presencia derequirements.txt y archivos fuente de Python, el motor compila y empaqueta automáticamente un contenedor de entorno de ejecución de Python.

Escribir un Dockerfile personalizado te brinda control total sobre los paquetes del sistema y las capas base de tu contenedor. Un Procfile es una forma más simple de declarar el comando de inicio sin configurar por completo un contenedor. Sin embargo, para las implementaciones rápidas, la implementación desde la fuente (--source) es muy eficiente.

¿Por qué realizamos el paso adicional de crear una cuenta de servicio personalizada barista-agent-sa en lugar de usar la cuenta de servicio predeterminada de Compute Engine?

La seguridad ante todo La cuenta de servicio predeterminada de Compute Engine tiene permisos de editor extremadamente amplios de forma predeterminada. Ejecutar nuestro contenedor de Cloud Run con la cuenta de servicio predeterminada significa que, si nuestra app tiene un error de seguridad, un atacante podría leer, escribir o borrar otros recursos en nuestro proyecto de Google Cloud.

Si creamos una cuenta de servicio dedicada y solo le asignamos el rol de roles/aiplatform.user, cumplimos con el principio de privilegio mínimo: la app tiene exactamente el acceso que necesita para llamar a Gemini y nada más.

7. Prueba el comportamiento de la RAG

Abre la URL del servicio de Cloud Run en un navegador web y hazle preguntas a AI Barista para probar sus restricciones de fundamentación y seguridad.

  1. Solicitud en el menú: Pregunta: "Recomiéndame algo fuerte y cálido".Respuesta esperada:El agente recomienda un espresso.
  2. Pregunta trampa fuera del menú: Pregunta: "¿Tienen un frappuccino de matcha?" Respuesta esperada: El agente rechaza la solicitud de forma cortés y explica que no está en el menú.
  3. Solicitud que tiene en cuenta las alergias: Pregunta: "Soy intolerante a la lactosa. ¿Qué puedo pedir?" Respuesta esperada: El agente solo recomienda elementos del menú sin lácteos (como el café con leche de avena, el espresso y el café frío). NO recomienda Cappuccino ni Croissant.

Cómo probar el comportamiento de la RAG

8. Opcional: Fundamenta tu agente en Firestore con la búsqueda de vectores

En una situación de producción, no es ideal almacenar los elementos del menú en un archivo menu.json local, ya que cualquier cambio en el menú requiere recompilar la imagen de contenedor y volver a implementar el servicio de Cloud Run.

Para que la aplicación sea dinámica y escalable, puedes migrar los datos del menú a Cloud Firestore y usar la búsqueda de vectores para recuperar solo los elementos del menú más relevantes según la similitud semántica.

Integración de Firestore con la búsqueda de vectores

1. Habilita la API de Firestore y, luego, inicializa la base de datos

Ejecuta los siguientes comandos para habilitar la API de Firestore y crear una base de datos de Firestore llamada coffee-menu en modo nativo:

gcloud services enable firestore.googleapis.com

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

Nota: La habilitación de la API puede tardar de 1 a 2 minutos en propagarse. Si el comando de creación de la base de datos te solicita API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry?, escribe Y para continuar o espera un minuto y vuelve a ejecutar el comando.

2. Propaga Firestore con los datos del menú

Para propagar rápidamente tu base de datos de Firestore con los elementos del menú de tu archivo menu.json, puedes ejecutar una secuencia de comandos de Python de forma local en Cloud Shell.

  1. Instala las bibliotecas cliente de Firestore y GenAI de forma local en Cloud Shell para ejecutar la secuencia de comandos de inicialización:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. Crea una secuencia de comandos de inicialización seed.py:
cloudshell edit seed.py
  1. Pega el siguiente código en 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. Ejecuta la secuencia de comandos:
python3 seed.py

3. Crea un índice vectorial de Firestore

Para realizar búsquedas de vectores en los elementos del menú, debes crear un índice de vectores compuesto en el campo embedding de tu base de datos de Firestore.

Ejecuta el siguiente comando en la terminal de 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": "{}"}'

Nota: La creación de índices de Firestore se ejecuta en segundo plano y puede tardar unos minutos en completarse. Puedes continuar con los siguientes pasos del codelab mientras se compila el índice.

4. Otorga acceso a Firestore a la cuenta de servicio

Para que tu servicio de Cloud Run consulte Firestore, debes otorgarle a su cuenta de servicio el rol de Usuario de 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"

Nota: Aunque usamos Cloud Firestore en modo nativo, Google Cloud utiliza los roles unificados de IAM de Cloud Datastore (roles/datastore.viewer o roles/datastore.user) para administrar el control de acceso.

5. Actualiza el código

Ahora, actualiza tu código para recuperar el menú de Firestore en lugar de leerlo de menu.json.

  1. Abre requirements.txt en el editor de Cloud Shell:
cloudshell edit requirements.txt
  1. Agrega las bibliotecas cliente de Firestore y GenAI al final del archivo y guárdalo:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. Abre agent.py en el editor de Cloud Shell:
cloudshell edit agent.py
  1. Busca el bloque # [START get_menu] en agent.py y reemplázalo por completo (desde # [START get_menu] hasta # [END get_menu]) con la siguiente implementación de 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. Abre app.py en el editor de Cloud Shell:
cloudshell edit app.py
  1. Busca el bloque # [START load_menu] en app.py y reemplázalo por completo (desde # [START load_menu] hasta # [END load_menu]) con la siguiente lógica de carga de 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. Vuelve a implementar en Cloud Run

Implementa la aplicación actualizada:

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. Verifica la integración de Firestore

Para probar la conexión del agente a Firestore, agrega un elemento de menú nuevo directamente en Firestore y verifica que el agente lo recomiende.

  1. Ejecuta el siguiente comando en Cloud Shell para escribir un documento nuevo en la colección menu en Firestore con 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. Actualiza tu app de Streamlit en el navegador para borrar la sesión de chat y cargar el nuevo estado de la base de datos.
  2. Ten en cuenta lo siguiente:
    • Matcha Green Tea Latte aparecerá automáticamente en el menú de la barra lateral.
    • Pregúntale al chatbot: "¿Tienen bebidas de matcha?"
    • El agente debería recomendar correctamente el nuevo Matcha Green Tea Latte con la descripción y el precio que acabas de agregar. Esto confirma que el agente se basa en la consulta directamente en tu base de datos activa de Firestore.

9. Limpia

Para evitar cargos continuos en tu cuenta de facturación de Google Cloud, borra el servicio de Cloud Run implementado y la cuenta de servicio personalizada.

Borra el servicio de Cloud Run:

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

Borra la cuenta de servicio personalizada:

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

(Opcional) Borra la base de datos de Firestore (si la creaste):

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

Paso opcional: Borra todo el proyecto. ⚠️SOLO haz esto si creaste un proyecto específico para este lab.

gcloud projects delete $PROJECT_ID

10. Felicitaciones

¡Felicitaciones! Creaste e implementaste un agente de barista de IA con generación aumentada por recuperación (RAG) usando el ADK de Google y Cloud Run.

Qué aprendiste

  • Cómo construir herramientas simples de RAG en Python
  • Utiliza LlmAgent y InMemoryRunner del ADK.
  • Crea experiencias de chat con estado en Streamlit.
  • Implementa Streamlit en Cloud Run con compilaciones basadas en la fuente.

Documentos de referencia