Outils de fonction

Étendez les capacités des agents avec des fonctions Rust personnalisées.


Qu'est-ce qu'un outil de fonction ?

Les outils de fonction vous permettent de doter les agents de capacités allant au-delà de la conversation - appel d'APIs, exécution de calculs, accès à des bases de données ou toute logique personnalisée. Le LlmAgent décide quand utiliser un outil en fonction de la requête de l'utilisateur.

Points clés :

  • 🔧 Encapsulez toute fonction async comme un outil appelable
  • 📝 Paramètres JSON - entrée/sortie flexible
  • 🎯 Schémas de types sûrs - validation optionnelle du JSON Schema
  • 🔗 Accès au contexte - état de session, artefacts, mémoire

Étape 1 : Outil de base

Créez un outil avec FunctionTool::new() et ajoutez toujours un schéma afin que le LlmAgent sache quels paramètres transmettre :

use adk_rust::prelude::*;
use adk_rust::Launcher;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;

#[derive(JsonSchema, Serialize, Deserialize)]
struct WeatherParams {
    /// The city or location to get weather for
    location: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;

    // Outil météo avec un schéma approprié
    let weather_tool = FunctionTool::new(
        "get_weather",
        "Obtenir la météo actuelle pour un lieu",
        |_ctx, args| async move {
            let location = args.get("location")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            Ok(json!({
                "location": location,
                "temperature": "22°C",
                "conditions": "sunny"
            }))
        },
    )
    .with_parameters_schema::<WeatherParams>(); // Requis pour que le LlmAgent l'appelle correctement !

    let agent = LlmAgentBuilder::new("weather_agent")
        .instruction("Vous aidez les utilisateurs à vérifier la météo. Utilisez toujours l'outil get_weather.")
        .model(Arc::new(model))
        .tool(Arc::new(weather_tool))
        .build()?;

    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

⚠️ Important : Utilisez toujours .with_parameters_schema<T>() - sans cela, le LlmAgent ne saura pas quels paramètres transmettre et pourrait ne pas appeler l'outil.

Comment ça marche :

  1. L'utilisateur demande : "Quel temps fait-il à Tokyo ?"
  2. Le LlmAgent décide d'appeler get_weather avec {"location": "Tokyo"}
  3. L'outil retourne {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}
  4. Le LlmAgent formate la réponse : "Le temps à Tokyo est ensoleillé avec 22°C."

Étape 2 : Gestion des paramètres

Extrayez les paramètres de l'objet JSON args :

let order_tool = FunctionTool::new(
    "process_order",
    "Traiter une commande. Paramètres : product_id (requis), quantity (requis), priority (optionnel)",
    |_ctx, args| async move {
        // Paramètres requis - retourner une erreur s'ils sont manquants
        let product_id = args.get("product_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| adk_core::AdkError::Tool("product_id is required".into()))?;
        
        let quantity = args.get("quantity")
            .and_then(|v| v.as_i64())
            .ok_or_else(|| adk_core::AdkError::Tool("quantity is required".into()))?;
        
        // Paramètre optionnel avec valeur par défaut
        let priority = args.get("priority")
            .and_then(|v| v.as_str())
            .unwrap_or("normal");
        
        Ok(json!({
            "order_id": "ORD-12345",
            "product_id": product_id,
            "quantity": quantity,
            "priority": priority,
            "status": "confirmed"
        }))
    },
);

Étape 3 : Paramètres typés avec Schema

Pour les FunctionTool complexes, utilisez des structs typés avec JSON Schema :

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(JsonSchema, Serialize, Deserialize)]
struct CalculatorParams {
    /// The arithmetic operation to perform
    operation: Operation,
    /// First operand
    a: f64,
    /// Second operand
    b: f64,
}

#[derive(JsonSchema, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

let calculator = FunctionTool::new(
    "calculator",
    "Perform arithmetic operations",
    |_ctx, args| async move {
        let params: CalculatorParams = serde_json::from_value(args)?;
        let result = match params.operation {
            Operation::Add => params.a + params.b,
            Operation::Subtract => params.a - params.b,
            Operation::Multiply => params.a * params.b,
            Operation::Divide if params.b != 0.0 => params.a / params.b,
            Operation::Divide => return Err(adk_core::AdkError::Tool("Cannot divide by zero".into())),
        };
        Ok(json!({ "result": result }))
    },
)
.with_parameters_schema::<CalculatorParams>();

Le schema est auto-généré à partir des types Rust en utilisant schemars.


Étape 4 : Agent multi-outils

Ajoutez plusieurs Tool à un Agent :

let agent = LlmAgentBuilder::new("assistant")
    .instruction("Help with calculations, conversions, and weather.")
    .model(Arc::new(model))
    .tool(Arc::new(calc_tool))
    .tool(Arc::new(convert_tool))
    .tool(Arc::new(weather_tool))
    .build()?;

Le LLM choisit automatiquement le bon Tool en fonction de la demande de l'utilisateur.


Gestion des erreurs

Retournez AdkError::Tool pour les erreurs spécifiques au Tool :

let divide_tool = FunctionTool::new(
    "divide",
    "Divide two numbers",
    |_ctx, args| async move {
        let a = args.get("a").and_then(|v| v.as_f64())
            .ok_or_else(|| adk_core::AdkError::Tool("Parameter 'a' is required".into()))?;
        let b = args.get("b").and_then(|v| v.as_f64())
            .ok_or_else(|| adk_core::AdkError::Tool("Parameter 'b' is required".into()))?;
        
        if b == 0.0 {
            return Err(adk_core::AdkError::Tool("Cannot divide by zero".into()));
        }
        
        Ok(json!({ "result": a / b }))
    },
);

Les messages d'erreur sont transmis au LLM, qui peut réessayer ou demander une entrée différente.


Contexte de Tool

Accédez aux informations de Session via ToolContext :

#[derive(JsonSchema, Serialize, Deserialize)]
struct GreetParams {
    #[serde(default)]
    message: Option<String>,
}

let greet_tool = FunctionTool::new(
    "greet",
    "Greet the user with session info",
    |ctx, _args| async move {
        let user_id = ctx.user_id();
        let session_id = ctx.session_id();
        let agent_name = ctx.agent_name();
        Ok(json!({
            "greeting": format!("Hello, user {}!", user_id),
            "session": session_id,
            "served_by": agent_name
        }))
    },
)
.with_parameters_schema::<GreetParams>();

Contexte disponible :

  • ctx.user_id() - ID utilisateur actuel
  • ctx.session_id() - ID de Session actuel
  • ctx.agent_name() - Nom de l'Agent
  • ctx.artifacts() - Accès au stockage d'artefacts
  • ctx.search_memory(query) - Service de recherche de mémoire

Outils à longue exécution

Pour les opérations qui prennent beaucoup de temps (traitement de données, APIs externes), utilisez le modèle non bloquant :

  1. Démarrer l'outil retourne immédiatement un task_id
  2. Le travail en arrière-plan s'exécute de manière asynchrone
  3. L'outil de statut permet aux utilisateurs de vérifier la progression
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(JsonSchema, Serialize, Deserialize)]
struct ReportParams {
    topic: String,
}

#[derive(JsonSchema, Serialize, Deserialize)]
struct StatusParams {
    task_id: String,
}

// Shared task store
let tasks: Arc<RwLock<HashMap<String, TaskState>>> = Arc::new(RwLock::new(HashMap::new()));
let tasks1 = tasks.clone();
let tasks2 = tasks.clone();

// Tool 1: Start (returns immediately)
let start_tool = FunctionTool::new(
    "generate_report",
    "Start generating a report. Returns task_id immediately.",
    move |_ctx, args| {
        let tasks = tasks1.clone();
        async move {
            let topic = args.get("topic").and_then(|v| v.as_str()).unwrap_or("general").to_string();
            let task_id = format!("task_{}", rand::random::<u32>());
            
            // Store initial state
            tasks.write().await.insert(task_id.clone(), TaskState {
                status: "processing".to_string(),
                progress: 0,
                result: None,
            });

            // Spawn background work (non-blocking!)
            let tasks_bg = tasks.clone();
            let tid = task_id.clone();
            tokio::spawn(async move {
                // Simulate work...
                tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
                if let Some(t) = tasks_bg.write().await.get_mut(&tid) {
                    t.status = "completed".to_string();
                    t.result = Some("Report complete".to_string());
                }
            });

            // Return immediately with task_id
            Ok(json!({"task_id": task_id, "status": "processing"}))
        }
    },
)
.with_parameters_schema::<ReportParams>()
.with_long_running(true);  // Mark as long-running

// Tool 2: Check status
let status_tool = FunctionTool::new(
    "check_report_status",
    "Check report generation status",
    move |_ctx, args| {
        let tasks = tasks2.clone();
        async move {
            let task_id = args.get("task_id").and_then(|v| v.as_str()).unwrap_or("");
            if let Some(t) = tasks.read().await.get(task_id) {
                Ok(json!({"status": t.status, "result": t.result}))
            } else {
                Ok(json!({"error": "Task not found"}))
            }
        }
    },
)
.with_parameters_schema::<StatusParams>();

Points clés :

  • .with_long_running(true) indique à l'agent que cet outil retourne un statut en attente
  • L'outil lance le travail avec tokio::spawn() et retourne immédiatement
  • Fournissez un outil de vérification de statut afin que les utilisateurs puissent interroger la progression

Ceci ajoute une note pour empêcher le LlmAgent d'appeler l'outil de manière répétée.

---

## Exemples d'exécution

```bash
cd official_docs_examples/tools/function_tools_test

# Basic tool with closure
cargo run --bin basic

# Tool with typed JSON schema
cargo run --bin with_schema

# Multi-tool agent (3 tools)
cargo run --bin multi_tool

# Tool context (session info)
cargo run --bin context

# Long-running tool
cargo run --bin long_running

Bonnes Pratiques

  1. Descriptions claires - Aidez le LlmAgent à comprendre quand utiliser l'outil
  2. Validez les entrées - Retournez des messages d'erreur utiles pour les paramètres manquants
  3. Retournez du JSON structuré - Utilisez des noms de champs clairs
  4. Gardez les outils ciblés - Chaque outil devrait faire une chose et la faire bien
  5. Utilisez des schémas - Pour les outils complexes, définissez des schémas de paramètres

Liés


Précédent : ← mistral.rs | Suivant : Outils Intégrés →