Rappels

Les Callbacks (rappels) dans adk-rust fournissent des points d'ancrage pour observer, personnaliser et contrôler le comportement des agents à des points d'exécution clés. Ils permettent la journalisation, les garde-fous, la mise en cache, la modification des réponses, et bien plus encore.

Aperçu

adk-rust prend en charge six types de Callbacks qui interceptent différentes étapes de l'exécution d'un agent :

Type de CallbackQuand ExécutéCas d'utilisation
before_agentAvant que l'Agent ne commence le traitementValidation de l'entrée, journalisation, terminaison anticipée
after_agentAprès que l'Agent ait terminéModification de la réponse, journalisation, nettoyage
before_modelAvant l'appel LLMModification de la requête, mise en cache, limitation de débit
after_modelAprès la réponse LLMFiltrage de la réponse, journalisation, mise en cache
before_toolAvant l'exécution du ToolVérifications des permissions, validation des paramètres
after_toolAprès l'exécution du ToolModification du résultat, journalisation

Types de Callbacks

Callbacks d'Agent

Les Callbacks d'Agent enveloppent l'intégralité du cycle d'exécution de l'Agent.

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

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

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

Callbacks de Modèle

Les Callbacks de Modèle interceptent les requêtes et les réponses LLM.

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

// BeforeModelResult - contrôle ce qui se passe après le callback
pub enum BeforeModelResult {
    Continue(LlmRequest),  // Continuer avec la requête (éventuellement modifiée)
    Skip(LlmResponse),     // Ignorer l'appel au modèle, utiliser cette réponse à la place
}

// BeforeModelCallback - peut modifier la requête ou ignorer l'appel au modèle
type BeforeModelCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>, LlmRequest)
        -> Pin<Box<dyn Future<Output = Result<BeforeModelResult>> + Send>>
    + Send + Sync
>;

// AfterModelCallback - peut modifier la réponse
type AfterModelCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>, LlmResponse)
        -> Pin<Box<dyn Future<Output = Result<Option<LlmResponse>>> + Send>>
    + Send + Sync
>;

Callbacks de Tool

Les Callbacks de Tool interceptent l'exécution du Tool.

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

// BeforeToolCallback - peut ignorer le Tool en renvoyant Some(Content)
type BeforeToolCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

// AfterToolCallback - peut modifier le résultat du Tool
type AfterToolCallback = Box<
    dyn Fn(Arc<dyn CallbackContext>) 
        -> Pin<Box<dyn Future<Output = Result<Option<Content>>> + Send>> 
    + Send + Sync
>;

Sémantique des valeurs de retour

Les callbacks utilisent différentes valeurs de retour pour contrôler le flux d'exécution :

Callbacks d'Agent/Outil

Valeur de retourEffet
Ok(None)Poursuit l'exécution normale
Ok(Some(content))Remplace/saute avec le contenu fourni
Err(e)Annule l'exécution avec une erreur

Callbacks de Modèle

BeforeModelCallback utilise BeforeModelResult :

Valeur de retourEffet
Ok(BeforeModelResult::Continue(request))Poursuit avec la requête (éventuellement modifiée)
Ok(BeforeModelResult::Skip(response))Ignore l'appel au modèle, utilise cette réponse à la place
Err(e)Annule l'exécution avec une erreur

AfterModelCallback utilise Option<LlmResponse> :

Valeur de retourEffet
Ok(None)Conserve la réponse originale
Ok(Some(response))Remplace par la réponse modifiée
Err(e)Annule l'exécution avec une erreur

Résumé

  • Avant les callbacks d'agent/outil : Retourne None pour continuer, Some(content) pour ignorer
  • Callback avant le modèle : Retourne Continue(request) pour procéder, Skip(response) pour contourner le modèle
  • Après les callbacks : Retourne None pour conserver l'original, Some(...) pour remplacer

Ajouter des Callbacks aux Agents

Les callbacks sont ajoutés aux agents à l'aide du 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.")
        // Ajoute un callback before_agent
        .before_callback(Box::new(|ctx| {
            Box::pin(async move {
                println!("Agent starting: {}", ctx.agent_name());
                Ok(None) // Poursuit l'exécution
            })
        }))
        // Ajoute un callback after_agent
        .after_callback(Box::new(|ctx| {
            Box::pin(async move {
                println!("Agent completed: {}", ctx.agent_name());
                Ok(None) // Conserve le résultat original
            })
        }))
        .build()?;

    Ok(())
}

Interface CallbackContext

Le trait CallbackContext fournit l'accès au contexte d'exécution :

use adk_rust::prelude::*;

#[async_trait]
pub trait CallbackContext: ReadonlyContext {
    /// Accède au stockage d'artefacts (si configuré)
    fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}

// CallbackContext étend ReadonlyContext
#[async_trait]
pub trait ReadonlyContext: Send + Sync {
    /// ID d'invocation actuel
    fn invocation_id(&self) -> &str;
    
    /// Nom de l'agent actuel
    fn agent_name(&self) -> &str;
    
    /// ID utilisateur de la session
    fn user_id(&self) -> &str;
    
    /// Nom de l'application
    fn app_name(&self) -> &str;
    
    /// ID de session
    fn session_id(&self) -> &str;
    
    /// Branche actuelle (pour multi-agent)
    fn branch(&self) -> &str;
    
    /// Contenu d'entrée de l'utilisateur
    fn user_content(&self) -> &Content;
}

Modèles courants

Rappel de journalisation

Journaliser toutes les interactions de l'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!("[LOG] Agent '{}' starting", ctx.agent_name());
            println!("[LOG] Session: {}", ctx.session_id());
            println!("[LOG] User: {}", ctx.user_id());
            Ok(None)
        })
    }))
    .after_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[LOG] Agent '{}' completed", ctx.agent_name());
            Ok(None)
        })
    }))
    .build()?;

Garde-fous d'entrée

Bloquer le contenu inapproprié avant le traitement :

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 {
            // Check user input for blocked content
            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") {
                        // Return early with rejection message
                        return Ok(Some(Content {
                            role: "model".to_string(),
                            parts: vec![Part::Text {
                                text: "I cannot process that request.".to_string(),
                            }],
                        }));
                    }
                }
            }
            Ok(None) // Continue normal execution
        })
    }))
    .build()?;

Mise en cache des réponses (avant le modèle)

Mettre en cache les réponses du LLM pour réduire les appels API :

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

// Simple in-memory cache
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 {
            // Create cache key from request contents
            let key = format!("{:?}", request.contents);

            // Check cache
            if let Some(cached) = cache.lock().unwrap().get(&key) {
                println!("[CACHE] Hit for request");
                return Ok(BeforeModelResult::Skip(cached.clone()));
            }

            println!("[CACHE] Miss, calling model");
            Ok(BeforeModelResult::Continue(request)) // Continue to model
        })
    }))
    .build()?;

Injection de contenu multimodal (avant le modèle)

Injecter des images ou d'autres contenus binaires dans les requêtes LLM pour une analyse multimodale :

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

// Artifact service with pre-loaded image
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 {
            // Load image from artifact storage
            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 {
                // Inject image into the user's message
                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()?;

Ce modèle est essentiel pour l'IA multimodale car les réponses des outils sont du texte JSON - le modèle ne peut pas "voir" les images renvoyées par les outils. En injectant l'image directement dans la requête, le modèle reçoit des données d'image réelles.

Modification de la réponse (après le modèle)

Modifier ou filtrer les réponses du modèle :

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 {
            // Modify the response content
            if let Some(ref mut content) = response.content {
                for part in &mut content.parts {
                    if let Part::Text { text } = part {
                        // Add disclaimer to all responses
                        *text = format!("{}\n\n[AI-generated response]", text);
                    }
                }
            }
            Ok(Some(response))
        })
    }))
    .build()?;

Vérification des permissions de l'outil (avant l'outil)

Valider les permissions d'exécution de l'outil :

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 {
            // Check if user has permission for tools
            let user_id = ctx.user_id();
            
            // Example: block certain users from using tools
            if user_id == "restricted_user" {
                return Ok(Some(Content {
                    role: "function".to_string(),
                    parts: vec![Part::Text {
                        text: "Tool access denied for this user.".to_string(),
                    }],
                }));
            }
            
            Ok(None) // Allow tool execution
        })
    }))
    .build()?;

Journalisation du résultat de l'outil (après l'outil)

Journaliser toutes les exécutions d'outils :

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!("[TOOL LOG] Tool executed for agent: {}", ctx.agent_name());
            println!("[TOOL LOG] Session: {}", ctx.session_id());
            Ok(None) // Keep original result
        })
    }))
    .build()?;

Rappels Multiples

Vous pouvez ajouter plusieurs rappels du même type. Ils s'exécutent dans l'ordre :

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

let agent = LlmAgentBuilder::new("multi_callback_agent")
    .model(model)
    // First before callback - logging
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[1] Logging callback");
            Ok(None)
        })
    }))
    // Second before callback - validation
    .before_callback(Box::new(|ctx| {
        Box::pin(async move {
            println!("[2] Validation callback");
            Ok(None)
        })
    }))
    .build()?;

Lorsqu'un rappel retourne Some(content), les rappels subséquents du même type sont ignorés.

Gestion des Erreurs

Les rappels peuvent retourner des erreurs pour annuler l'exécution :

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 {
            // Validate something critical
            if ctx.user_id().is_empty() {
                return Err(AdkError::Agent("User ID is required".to_string()));
            }
            Ok(None)
        })
    }))
    .build()?;

Bonnes Pratiques

  1. Maintenez les rappels légers : Évitez les calculs lourds dans les rappels
  2. Gérez les erreurs de manière élégante : Retournez des messages d'erreur significatifs
  3. Utilisez la journalisation avec parcimonie : Une trop grande quantité de journalisation peut affecter les performances
  4. Utilisez le cache avec sagesse : Envisagez des stratégies d'invalidation du cache
  5. Testez les rappels indépendamment : Testez la logique des rappels séparément

Liens Connexes

  • LlmAgent - Configuration de l'Agent
  • Tools - Système d'outils
  • Events - Structure des événements

Précédent : ← Gestion de l'état | Suivant : Artefacts →