Sebastian Gomez
Callbacks y guardrails para agentes confiables
Notebook de la lección: Lesson_4.ipynb
Los agentes son potentes y, a la vez, no deterministas. En esta lección añadimos control programático para que tu agente se comporte de forma predecible: afinamos instrucciones y usamos callbacks como guardrails que filtran dominios, enriquecen respuestas y dejan trazabilidad. Al final tendrás un agente listo para producción que combina todo lo previo con controles efectivos.
Versiones que usamos aquí. Esta lección asume google-adk 1.0 o superior y la familia de modelos Gemini 2.5 (gemini-2.5-flash). Dos detalles importan para que el código corra tal cual: pasar una lista a before_tool_callback y after_tool_callback requiere una versión reciente de ADK, y google_search como herramienta integrada requiere un modelo Gemini 2 o superior. Si vienes de instalaciones más antiguas, actualiza con pip install -U google-adk.
Panorama general
- Reutilizar tools previas:
get_financial_contextysave_news_to_markdown. - Entender los puntos de extensión: before/after agent, tool y model.
- Implementar dos callbacks: filtrado de fuentes (before tool) y enriquecimiento de respuesta (after tool).
- Actualizar instrucciones para que el agente sea consciente de los callbacks.
- Probar en
adk web, validar logs de proceso y cerrar servicios.
4.1 Preparación del entorno
A diferencia de las lecciones de voz, este agente es síncrono y orquesta herramientas de texto, así que no necesitamos un modelo Live. Usaremos un modelo Flash estándar, que es la opción recomendada para flujos de texto y llamadas a tools.
adk create app5 --model gemini-2.5-flash --api_key $GOOGLE_API_KEYLive solo cuando hace falta. Los modelos *-live-* están optimizados para streaming bidireccional de audio. En las lecciones de voz tenían sentido; aquí, para un agente de texto que llama herramientas, gemini-2.5-flash es más adecuado y económico. Reserva el modelo Live para cuando realmente uses la Live API.
Credenciales: define un .env en app5/ (AI Studio: GOOGLE_API_KEY; Vertex AI: GOOGLE_GENAI_USE_VERTEXAI=TRUE, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION). Si usas load_env() en notebooks, trátalo como helper; al ejecutar con adk web, ADK leerá el .env por ti.
4.2 Reutiliza herramientas de lecciones anteriores
Copiamos get_financial_context (lecciones 2 y 3) y save_news_to_markdown (lección 3) a app5/agent.py.
import pathlib
from typing import Dict
def save_news_to_markdown(filename: str, content: str) -> Dict[str, str]:
if not filename.endswith(".md"):
filename += ".md"
file_path = pathlib.Path.cwd() / filename
file_path.write_text(content, encoding="utf-8")
return {"status": "success", "message": f"Saved to {file_path.resolve()}"}4.3 Callbacks en ADK
ADK ofrece hooks en varios puntos del ciclo de vida:
before_agent_callback/after_agent_callbackbefore_tool_callback/after_tool_callbackbefore_model_callback/after_model_callback
### Callback 1: filtrado de fuentes (before tool)
Bloquea búsquedas hacia dominios no deseados y responde con errores descriptivos.
BLOCKED_DOMAINS = [
"wikipedia.org",
"reddit.com",
"youtube.com",
"medium.com",
"investopedia.com",
"quora.com",
]
def filter_news_sources_callback(tool, args, tool_context):
if tool.name == "google_search":
query = args.get("query", "").lower()
for domain in BLOCKED_DOMAINS:
if f"site:{domain}" in query or domain.split(".")[0] in query:
return {
"error": "blocked_source",
"reason": f"Searches targeting {domain} are not allowed. Use professional news sources.",
}
return None### Callback 2: enriquecimiento de respuesta (after tool)
Extrae dominios de los resultados, mantiene un process_log en tool_context.state y lo inyecta en la salida. Fíjate en los imports al inicio del bloque: necesitamos re y urlparse, que faltaban en la versión original.
import re
from urllib.parse import urlparse
from google.adk.tools import ToolContext
def initialize_process_log(tool_context: ToolContext):
if "process_log" not in tool_context.state:
tool_context.state["process_log"] = []
def inject_process_log_after_search(tool, args, tool_context, tool_response):
if tool.name != "google_search":
return tool_response
# Normaliza la respuesta (str o dict)
if isinstance(tool_response, dict):
raw = tool_response.get("search_results") or tool_response.get("results") or ""
else:
raw = tool_response
if isinstance(raw, str) and raw:
urls = re.findall(r"https?://[^\s/]+", raw)
unique_domains = sorted(list({urlparse(url).netloc for url in urls}))
if unique_domains:
sourcing_log = f"Action: Sourced news from: {', '.join(unique_domains)}."
tool_context.state["process_log"] = [sourcing_log] + tool_context.state.get("process_log", [])
# Devuelve la estructura enriquecida
return {
"search_results": raw if isinstance(raw, str) else str(raw),
"process_log": tool_context.state.get("process_log", []),
}Para que initialize_process_log no quede como código muerto, lo conectamos en before_agent_callback. Así el estado queda creado antes de la primera búsqueda y el after tool callback solo lo enriquece.
4.4 Modifica el agente para usar callbacks
Aquí aparece un detalle importante de diseño. ADK restringe combinar una herramienta integrada como google_search con tools personalizadas en el mismo LlmAgent. Si las pones todas juntas en tools=[...], lo más probable es que obtengas un error al construir el agente.
El patrón recomendado: aislar `google_search` en un sub agente. Envolvemos google_search en su propio LlmAgent y lo exponemos al agente principal mediante AgentTool. Así el coordinador sigue teniendo sus tools personalizadas y delega la búsqueda al sub agente. Los callbacks de búsqueda viven en el sub agente, que es quien realmente invoca google_search.
AgentTool han cambiado entre versiones de ADK. Antes de publicar, confirma contra la documentación actual de ADK si tu versión ya permite mezclarlas directamente; de ser así, podrías simplificar y prescindir del sub agente.
from google.adk.agents import LlmAgent
from google.adk.tools import google_search
from google.adk.tools.agent_tool import AgentTool
# Sub agente dedicado a la búsqueda: aquí viven los callbacks de google_search.
search_agent = LlmAgent(
name="news_search_agent",
model="gemini-2.5-flash",
tools=[google_search],
instruction="Busca noticias recientes de IA usando google_search y devuelve los resultados.",
before_tool_callback=[filter_news_sources_callback],
after_tool_callback=[inject_process_log_after_search],
)
root_agent = LlmAgent(
name="ai_news_research_coordinator",
model="gemini-2.5-flash",
tools=[
AgentTool(agent=search_agent),
get_financial_context,
save_news_to_markdown,
],
before_agent_callback=[lambda ctx: initialize_process_log(ctx)],
instruction="""
Tu propósito exclusivo es preparar un reporte de noticias de IA con contexto financiero.
Plan de ejecución:
1) Buscar 5 noticias recientes a través del sub agente de búsqueda.
2) Extraer tickers y llamar `get_financial_context`.
3) Formatear todo en un Markdown siguiendo el esquema requerido.
4) Guardar en `ai_research_report.md` con `save_news_to_markdown`.
Comprensión de callbacks y salidas modificadas:
- La búsqueda puede retornar { search_results: str, process_log: list[str] }.
- Usa `process_log` para incluir fuentes y acciones en el reporte final.
Regla operativa crucial: no muestres contenido intermedio. Solo confirma el inicio y la entrega final.
""",
)Fíjate en que pasamos los callbacks como lista. Esa forma de lista es una característica reciente de ADK; si tu instalación es antigua y solo acepta un único callable, actualiza ADK o pasa la función directamente sin la lista.
4.5 Pruebas de punta a punta
Levanta la UI desde la carpeta padre con adk web y selecciona "app5", o apunta directo a la carpeta:
adk web --port 8000 app5En Windows, usa --no-reload si es necesario. Detén el servidor con Ctrl-C.
Prueba con un mensaje como "Encuentra las últimas noticias de IA". El agente confirma el inicio, trabaja en silencio y termina con un mensaje final que confirma ai_research_report.md. Cierra los procesos al terminar.
4.6 Visualiza el reporte
Verifica que el Markdown incluya titulares, tickers, métrica financiera y un process_log con los dominios fuente.
from IPython.display import Markdown, display
with open("ai_research_report.md", "r", encoding="utf-8") as f:
display(Markdown(f.read()))Buenas prácticas y próximos pasos
- Documenta tus políticas: qué dominios se bloquean y por qué, y prueba escenarios límite.
- Incluye el
process_logen los artefactos finales para tener trazabilidad. - Centraliza los callbacks comunes (observabilidad, validación de entrada y salida, mitigación de prompt injection). Cuando una política debe aplicarse de forma global a todos los agentes, evalúa moverla a un Plugin de ADK en lugar de repetir callbacks.
- Si apuntas a producción, evalúa mover el almacenamiento del reporte a un bucket o a una base de datos.
Nota sobre Google Search. La herramienta google_search funciona con modelos Gemini 2 o superiores. Si el modelo devuelve "Search suggestions", muéstralas en tu UI, ya que es parte de la política de Grounding. Más información en la documentación de built in tools de ADK.
>
google_search se mueve rápido. Confirma para Gemini 2.5 en la documentación oficial antes de publicar.
Tu agente ahora combina investigación silenciosa con guardrails programáticos y trazabilidad. A partir de aquí puedes escalar a sistemas multi agente y respuestas aún más estructuradas.
Ejercicios propuestos
- Añade un tercer dominio a
BLOCKED_DOMAINSy comprueba enadk webque el filtro devuelve el error descriptivo cuando la consulta lo apunta. - Convierte
initialize_process_logpara que registre también la hora de inicio del proceso, y verifica que aparece en elprocess_logfinal. - Mueve
filter_news_sources_callbacka un Plugin global de ADK y compara la experiencia frente a registrarlo callback por callback.
Resumen en 3 puntos
- Los callbacks before/after de ADK son tus guardrails programáticos: filtran fuentes, enriquecen respuestas y dejan trazabilidad sin tocar la lógica del modelo.
- Para combinar
google_searchcon tools personalizadas, aísla la búsqueda en un sub agente y exponlo conAgentTool; usagemini-2.5-flashpara este flujo de texto y deja los modelos Live para voz. - Pasar los callbacks como lista y mantener un
process_logentool_context.statete da observabilidad lista para producción; centraliza políticas globales en Plugins.
Recursos
- Callbacks: tipos de callbacks
- Plugins: documentación de Plugins
- Built in tools: herramientas integradas
- Lección anterior: ADK clase 3, construye un agente investigador en segundo plano
- Siguiente lección: ADK clase 5, respuestas estructuradas con esquemas y validación
Este contenido se basa en el curso "Building Live Voice Agents with Google's ADK!" de DeepLearning.AI (disponible en learn.deeplearning.ai). Este blog busca acercar el material de ADK al español.
Espero que este post te sea de utilidad y lo puedas aplicar a algún proyecto que tengas en mente. Déjame un comentario si te sirvió, si quieres añadir alguna opinión o si tienes alguna duda, y recuerda que si te gustó también puedes compartirlo usando los links a las redes sociales aquí abajo. Buena suerte construyendo agentes confiables.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.