Retrollamadas

Las retrollamadas en adk-rust proporcionan ganchos para observar, personalizar y controlar el comportamiento del agente en puntos clave de ejecución. Permiten el registro, los mecanismos de protección, el almacenamiento en caché, la modificación de respuestas y más.

Resumen

adk-rust admite seis tipos de retrollamadas que interceptan diferentes etapas de la ejecución del agente:

Tipo de RetrollamadaCuándo se EjecutaCasos de Uso
before_agentAntes de que el agente comience a procesarValidación de entrada, registro, terminación temprana
after_agentDespués de que el agente finaliceModificación de respuesta, registro, limpieza
before_modelAntes de la llamada a LLMModificación de solicitud, almacenamiento en caché, limitación de tasa
after_modelDespués de la respuesta de LLMFiltrado de respuesta, registro, almacenamiento en caché
before_toolAntes de la ejecución de la herramientaComprobaciones de permisos, validación de parámetros
after_toolDespués de la ejecución de la herramientaModificación de resultado, registro

Tipos de Retrollamadas

Retrollamadas del Agente

Las retrollamadas del agente envuelven el ciclo completo de ejecución del agente.

use adk_rust::prelude::*;
use std::sync::Arc;

// Firma del tipo BeforeAgentCallback
type BeforeAgentCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

// Firma del tipo AfterAgentCallback  
type AfterAgentCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

Retrollamadas del Modelo

Las retrollamadas del modelo interceptan las solicitudes y respuestas de LLM.

use adk_rust::prelude::*;
use std::sync::Arc;

// BeforeModelResult - controla lo que sucede después de la retrollamada
pub enum BeforeModelResult {
    Continue(LlmRequest),  // Continuar con la solicitud (posiblemente modificada)
    Skip(LlmResponse),     // Omitir la llamada al modelo, usar esta respuesta en su lugar
}

// BeforeModelCallback - puede modificar la solicitud u omitir la llamada al modelo
type BeforeModelCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>, LlmRequest)
        -> Pin<Box<dyn Future<Output = Result<BeforeModelResult>> + Send>>
    + Send + Sync
>;

// AfterModelCallback - puede modificar la respuesta
type AfterModelCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>, LlmResponse)
        -> Pin<Box<dyn Future<Output = Result<Option<LlmResponse>>> + Send>>
    + Send + Sync
>;

Retrollamadas de la Herramienta

Las retrollamadas de la herramienta interceptan la ejecución de la herramienta.

use adk_rust::prelude::*;
use std::sync::Arc;

// BeforeToolCallback - puede omitir la herramienta devolviendo Some(Content)
type BeforeToolCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

// AfterToolCallback - puede modificar el resultado de la herramienta
type AfterToolCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

Semántica de los Valores de Retorno

Las retrollamadas (callbacks) utilizan diferentes valores de retorno para controlar el flujo de ejecución:

Retrollamadas de Agent/Tool

Valor de RetornoEfecto
Ok(None)Continuar ejecución normal
Ok(Some(content))Sobrescribir/omitir con el contenido proporcionado
Err(e)Abortar ejecución con error

Retrollamadas de Model

BeforeModelCallback utiliza BeforeModelResult:

Valor de RetornoEfecto
Ok(BeforeModelResult::Continue(request))Continuar con la solicitud (posiblemente modificada)
Ok(BeforeModelResult::Skip(response))Omitir la llamada al modelo, usar esta respuesta en su lugar
Err(e)Abortar ejecución con error

AfterModelCallback utiliza Option<LlmResponse>:

Valor de RetornoEfecto
Ok(None)Mantener la respuesta original
Ok(Some(response))Reemplazar con la respuesta modificada
Err(e)Abortar ejecución con error

Resumen

  • Retrollamadas antes de agent/tool: Retornar None para continuar, Some(content) para omitir
  • Retrollamada antes del modelo: Retornar Continue(request) para proceder, Skip(response) para omitir el modelo
  • Retrollamadas después: Retornar None para mantener el original, Some(...) para reemplazar

Añadiendo Retrollamadas a los Agents

Las retrollamadas se añaden a los agents usando el LlmAgentBuilder:

use adk_rust::prelude::*;
use std::sync::Arc;

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);

    let agent = LlmAgentBuilder::new("my_agent")
        .model(model)
        .instruction("You are a helpful assistant.")
        // Añadir retrollamada before_agent
        .before_callback(Box::new(|ctx| {
            Box::pin(async move {
                println!("Agent starting: {}", ctx.agent_name());
                Ok(None) // Continuar ejecución
            })
        }))
        // Añadir retrollamada after_agent
        .after_callback(Box::new(|ctx| {
            Box::pin(async move {
                println!("Agent completed: {}", ctx.agent_name());
                Ok(None) // Mantener resultado original
            })
        }))
        .build()?;

    Ok(())
}

Interfaz CallbackContext

El trait CallbackContext proporciona acceso al contexto de ejecución:

use adk_rust::prelude::*;

#[async_trait]
pub trait CallbackContext: ReadonlyContext {
    /// Accede al almacenamiento de artefactos (si está configurado)
    fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}

// CallbackContext extiende ReadonlyContext
#[async_trait]
pub trait ReadonlyContext: Send + Sync {
    /// ID de invocación actual
    fn invocation_id(&self) -> &str;
    
    /// Nombre del agent actual
    fn agent_name(&self) -> &str;
    
    /// ID de usuario de la sesión
    fn user_id(&self) -> &str;
    
    /// Nombre de la aplicación
    fn app_name(&self) -> &str;
    
    /// ID de sesión
    fn session_id(&self) -> &str;
    
    /// Rama actual (para multi-agent)
    fn branch(&self) -> &str;
    
    /// El contenido de entrada del usuario
    fn user_content(&self) -> &Content;
}

Patrones Comunes

Callback de Registro

Registra todas las interacciones del agent:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("logged_agent")
    .model(model)
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[REGISTRO] Agent '{}' iniciando", ctx.agent_name());
            println!("[REGISTRO] Sesión: {}", ctx.session_id());
            println!("[REGISTRO] Usuario: {}", ctx.user_id());
            Ok(None)
        })
    }))
    .after_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[REGISTRO] Agent '{}' completado", ctx.agent_name());
            Ok(None)
        })
    }))
    .build()?;

Controles de Entrada

Bloquear contenido inapropiado antes del procesamiento:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("guarded_agent")
    .model(model)
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            // Verificar entrada del usuario en busca de contenido bloqueado
            let user_content = ctx.user_content();
            for part in &user_content.parts {
                if let Part::Text { text } = part {
                    if text.to_lowercase().contains("blocked_word") {
                        // Retornar anticipadamente con mensaje de rechazo
                        return Ok(Some(Content {
                            role: "model".to_string(),
                            parts: vec![Part::Text {
                                text: "No puedo procesar esa solicitud.".to_string(),
                            }],
                        }));
                    }
                }
            }
            Ok(None) // Continuar ejecución normal
        })
    }))
    .build()?;

Caché de Respuestas (Antes del Modelo)

Almacenar en caché las respuestas del LLM para reducir las llamadas a la API:

use adk_rust::prelude::*;
use std::sync::Arc;
use std::collections::HashMap;
use std::sync::Mutex;

// Caché simple en memoria
let cache: Arc<Mutex<HashMap<String, LlmResponse>>> = Arc::new(Mutex::new(HashMap::new()));
let cache_clone = cache.clone();

let agent = LlmAgentBuilder::new("cached_agent")
    .model(model)
    .before_model_callback(Box::new(move |ctx, request| {
        let cache = cache_clone.clone();
        Box::pin(async move {
            // Crear clave de caché a partir de los contenidos de la solicitud
            let key = format!("{:?}", request.contents);

            // Verificar caché
            if let Some(cached) = cache.lock().unwrap().get(&key) {
                println!("[CACHÉ] Acierto para la solicitud");
                return Ok(BeforeModelResult::Skip(cached.clone()));
            }

            println!("[CACHÉ] Fallo, llamando al modelo");
            Ok(BeforeModelResult::Continue(request)) // Continuar al modelo
        })
    }))
    .build()?;

Inyección de Contenido Multimodal (Antes del Modelo)

Inyectar imágenes u otro contenido binario en las solicitudes del LLM para análisis multimodal:

use adk_rust::prelude::*;
use adk_rust::artifact::{ArtifactService, LoadRequest};
use std::sync::Arc;

// Servicio de artefactos con imagen precargada
let artifact_service: Arc<dyn ArtifactService> = /* ... */;
let callback_service = artifact_service.clone();

let agent = LlmAgentBuilder::new("image_analyst")
    .model(model)
    .instruction("Describe the image provided by the user.")
    .before_model_callback(Box::new(move |_ctx, mut request| {
        let service = callback_service.clone();
        Box::pin(async move {
            // Cargar imagen del almacenamiento de artefactos
            if let Ok(response) = service.load(LoadRequest {
                app_name: "my_app".to_string(),
                user_id: "user".to_string(),
                session_id: "session".to_string(),
                file_name: "user:photo.png".to_string(),
                version: None,
            }).await {
                // Inyectar imagen en el mensaje del usuario
                if let Some(last_content) = request.contents.last_mut() {
                    if last_content.role == "user" {
                        last_content.parts.push(response.part);
                    }
                }
            }

            Ok(BeforeModelResult::Continue(request))
        })
    }))
    .build()?;

Este patrón es esencial para la IA multimodal porque las respuestas de las tools son texto JSON; el modelo no puede "ver" las imágenes devueltas por las tools. Al inyectar la imagen directamente en la solicitud, el modelo recibe datos de imagen reales.

Modificación de Respuesta (Después del Modelo)

Modificar o filtrar respuestas del modelo:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("filtered_agent")
    .model(model)
    .after_model_callback(Box::new(|ctx, mut response| {
        Box::pin(async move {
            // Modificar el contenido de la respuesta
            if let Some(ref mut content) = response.content {
                for part in &mut content.parts {
                    if let Part::Text { text } = part {
                        // Añadir descargo de responsabilidad a todas las respuestas
                        *text = format!("{}\n\n[Respuesta generada por IA]", text);
                    }
                }
            }
            Ok(Some(response))
        })
    }))
    .build()?;

Verificación de Permisos de Tool (Antes de la Tool)

Validar permisos de ejecución de tools:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("permission_agent")
    .model(model)
    .tool(Arc::new(GoogleSearchTool::new()))
    .before_tool_callback(Box::new(|ctx| {
        Box::pin(async move {
            // Verificar si el usuario tiene permiso para las tools
            let user_id = ctx.user_id();
            
            // Ejemplo: bloquear a ciertos usuarios para que no usen las tools
            if user_id == "restricted_user" {
                return Ok(Some(Content {
                    role: "function".to_string(),
                    parts: vec![Part::Text {
                        text: "Acceso a la tool denegado para este usuario.".to_string(),
                    }],
                }));
            }
            
            Ok(None) // Permitir la ejecución de la tool
        })
    }))
    .build()?;

Registro de Resultados de Tool (Después de la Tool)

Registrar todas las ejecuciones de tools:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("tool_logged_agent")
    .model(model)
    .tool(Arc::new(GoogleSearchTool::new()))
    .after_tool_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[REGISTRO DE TOOL] Tool ejecutada para el agent: {}", ctx.agent_name());
            println!("[REGISTRO DE TOOL] Sesión: {}", ctx.session_id());
            Ok(None) // Mantener el resultado original
        })
    }))
    .build()?;

Múltiples Callbacks

Puedes añadir múltiples callbacks del mismo tipo. Se ejecutan en orden:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("multi_callback_agent")
    .model(model)
    // Primer callback "before" - registro
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[1] Logging callback");
            Ok(None)
        })
    }))
    // Segundo callback "before" - validación
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[2] Validation callback");
            Ok(None)
        })
    }))
    .build()?;

Cuando un callback devuelve Some(content), los callbacks subsiguientes del mismo tipo se omiten.

Manejo de Errores

Los callbacks pueden devolver errores para abortar la ejecución:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("error_handling_agent")
    .model(model)
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            // Validar algo crítico
            if ctx.user_id().is_empty() {
                return Err(AdkError::Agent("User ID is required".to_string()));
            }
            Ok(None)
        })
    }))
    .build()?;

Mejores Prácticas

  1. Mantén los callbacks ligeros: Evita computaciones pesadas en los callbacks
  2. Maneja los errores con elegancia: Devuelve mensajes de error significativos
  3. Usa el registro con moderación: Demasiado registro puede afectar el rendimiento
  4. Usa la caché sabiamente: Considera estrategias de invalidación de caché
  5. Prueba los callbacks de forma independiente: Realiza pruebas unitarias de la lógica del callback por separado

Relacionado


Anterior: ← State Management | Siguiente: Artifacts →