Skip to main content

agentai/tool/
mod.rs

1//! # Tools and Tool Boxes
2//!
3//! This module provides the core infrastructure for defining, organizing, and executing tools within the `agentai` crate.
4//! It introduces the concept of a `ToolBox`, which is a collection of callable `Tool` instances.
5//!
6//! Agents interact with the external world by calling these `Tool`s, which encapsulate specific functionalities
7//! like searching the web, interacting with external APIs, or performing calculations.
8//!
9//! To implement your own `ToolBox`, you have two primary options:
10//!
11//! 1.  **Using the `#[toolbox]` macro:** This is the recommended approach for most cases. The macro simplifies the process by
12//!     automatically generating the necessary boilerplate for a `ToolBox` trait implementation based on methods defined in a struct.
13//!     See [`#[toolbox]`](crate::tool::toolbox) for more details.
14//!
15//! 2.  **Manual implementation:** If you require finer control over the `ToolBox` behavior, you can provide your own implementation
16//!     for the [`ToolBox` trait](crate::tool::ToolBox).
17//!
18//! Ready-to-use `ToolBox` implementations are available:
19//! - [crate::tool::buildin]: Provides a set of useful built-in tools. (Requires the `tools-buildin` feature).
20//! - [crate::tool::mcp]: Provides a `ToolBox` for interacting with the MCP Client. (Requires the `mcp-client` feature).
21//! - [crate::tool::web]: Provides toolboxes for interacting with the web, such as searching and fetching content. (Requires the `tools-web` feature).
22//!
23//! For examples demonstrating how to use tools and toolboxes, look into the `examples` folder.
24//! Examples related to tools typically start with the `tools_*` prefix, e.g., [crate::examples::tools_mcp].
25//!
26//! For example demonstrating how to implement `ToolBox` trait using `#[toolbox]` macro, look into [crate::examples::tools_custom] example.
27
28#[cfg(feature = "tools-buildin")]
29pub mod buildin;
30
31#[cfg(feature = "mcp-client")]
32pub mod mcp;
33
34#[cfg(feature = "tools-web")]
35pub mod web;
36
37use serde_json::Value;
38use thiserror::Error;
39
40/// Represents a tool definition that can be exposed to an agent.
41///
42/// This structure is used by `ToolBox` implementations in their `tools_definitions`
43/// function to describe the available tools to the language model.
44///
45/// **Note:** While this struct defines a tool, actual tool invocation is handled
46/// by a `ToolBox` implementing the [`ToolBox`] trait. You must use a `ToolBox`
47/// to call any defined tool.
48///
49/// The `name` field is required, while `description` and `schema` are optional
50/// but highly recommended for effective tool use by the agent.
51#[derive(Debug, Clone, PartialEq)]
52#[non_exhaustive]
53pub struct Tool {
54    /// Name of the tool. It must be unique within the toolbox.
55    pub name: String,
56    /// What the tool does and when to use it. The model reads it.
57    pub description: Option<String>,
58    /// JSON schema of the arguments.
59    pub schema: Option<serde_json::Value>,
60    /// Asks providers that support it to make the arguments match the schema exactly.
61    pub strict: Option<bool>,
62}
63
64impl Tool {
65    /// Creates a tool definition with a name.
66    pub fn new(name: impl Into<String>) -> Self {
67        Self {
68            name: name.into(),
69            description: None,
70            schema: None,
71            strict: None,
72        }
73    }
74
75    /// Sets the description.
76    pub fn with_description(mut self, description: impl Into<String>) -> Self {
77        self.description = Some(description.into());
78        self
79    }
80
81    /// Sets the JSON schema of the arguments.
82    pub fn with_schema(mut self, schema: serde_json::Value) -> Self {
83        self.schema = Some(schema);
84        self
85    }
86
87    /// Marks the tool as strict, see [`Tool::strict`].
88    pub fn with_strict(mut self, strict: bool) -> Self {
89        self.strict = Some(strict);
90        self
91    }
92}
93
94impl Tool {
95    pub(crate) fn into_genai(self) -> genai::chat::Tool {
96        let tool = self;
97        let mut out = genai::chat::Tool::new(tool.name);
98        out.description = tool.description;
99        // Anthropic requires a schema, so a tool without arguments gets an empty object schema
100        out.schema = Some(
101            tool.schema
102                .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}})),
103        );
104        out.strict = tool.strict;
105        out
106    }
107}
108
109pub type ToolResult = Result<String, ToolError>;
110
111// Re-export tool and toolbox macros, they are used to generate auto implementation of
112#[cfg(feature = "macros")]
113pub use agentai_macros::toolbox;
114
115/// Manages a collection of callable `Tool` instances.
116///
117/// Implementors of `ToolBox` provide a way to group related tools and expose them to the
118/// agent for invocation. The `ToolBox` is responsible for defining the available tools
119/// and executing them when requested.
120///
121/// **Important:** This trait requires the use of the [`#[async_trait::async_trait]`](https://docs.rs/async-trait) attribute macro
122/// for proper asynchronous behavior and `dyn ToolBox` compatibility.
123///
124/// For most use cases, implementing this trait can be significantly simplified by using
125/// the [`#[toolbox]`](crate::tool::toolbox) attribute macro. This macro automatically
126/// generates the necessary `ToolBox` implementation for a struct based on its methods.
127#[async_trait::async_trait]
128pub trait ToolBox: Send + Sync + 'static {
129    /// Returns a list of all `Tool` instances contained within this ToolBox.
130    /// These definitions include the tool's name, description, and parameters,
131    /// which are used by the language model to decide which tool to call.
132    ///
133    /// The `schema` field of the `Tool` can be conveniently generated from Rust structs using the [`schemars`](https://crates.io/crates/schemars) crate.
134    ///
135    /// This method is typically invoked internally by the [`Agent`](crate::agent::Agent) structure to discover the available tools and their parameters.
136    fn tools_definitions(&self) -> Result<Vec<Tool>, ToolError>;
137
138    /// Calls a specific tool by its name with the given parameters.
139    ///
140    /// This method is the entry point for executing a tool's functionality. It is typically invoked internally by the [`Agent`](crate::agent::Agent) structure
141    /// when the language model determines that a tool needs to be called.
142    /// The `arguments` are provided as a `serde_json::Value`. You can easily deserialize this `Value`
143    /// into a Rust struct (e.g., the same struct used to generate the JSON schema
144    /// in `tools_definitions`) using the [`serde`](https://crates.io/crates/serde) crate.
145    /// The arguments provided by the agent will conform to the JSON schema defined for the tool.
146    ///
147    /// For example, to deserialize the arguments:
148    /// ```ignore
149    /// # This is only informational example, it represents what in theory is being generated
150    /// let args: ToolArguments = serde_json::from_value(arguments)?;
151    /// ```
152    /// Replace `ToolArguments` with the actual struct type corresponding to your tool's schema.
153    ///
154    /// # Arguments
155    /// * `tool_name` - The name of the tool to call.
156    /// * `arguments` - A JSON `Value` containing the arguments for the tool call.
157    ///
158    /// # Returns
159    /// A `Result` containing the tool's output as a `String` on success,
160    /// or a `ToolError` if the tool call fails or the tool is not found.
161    async fn call_tool(&self, tool_name: String, arguments: Value) -> ToolResult;
162}
163
164/// Errors that can occur when working with `ToolBox`es and tools.
165///
166/// Variants fall into two groups, and [`ToolError::is_recoverable`] tells them apart:
167///
168/// - **Recoverable** errors ([`NotFound`](ToolError::NotFound),
169///   [`InvalidArguments`](ToolError::InvalidArguments), [`Failed`](ToolError::Failed)) are
170///   something the model can react to, for example by calling the tool again with fixed
171///   arguments. [`Agent::run`](crate::agent::Agent::run) sends their message to the model as the
172///   tool result and the loop continues.
173/// - **Fatal** errors ([`Configuration`](ToolError::Configuration), [`Fatal`](ToolError::Fatal))
174///   cannot be fixed by calling again, for example a missing API key or a dead MCP server.
175///   `Agent::run` stops and returns them as [`AgentError::Tool`](crate::agent::AgentError::Tool).
176#[derive(Error, Debug)]
177#[non_exhaustive]
178pub enum ToolError {
179    // -- Recoverable: sent to the model as the tool result; the loop continues
180    /// The requested tool does not exist in this `ToolBox`. This is the signal for
181    /// [`ToolBoxSet`] to try the next toolbox.
182    ///
183    /// Reserved for unknown tool names: a tool that fails for its own reasons should return
184    /// [`InvalidArguments`](ToolError::InvalidArguments) or [`Failed`](ToolError::Failed),
185    /// otherwise an outer `ToolBoxSet` silently tries the next toolbox instead.
186    #[error("tool '{0}' not found")]
187    NotFound(String),
188    /// Arguments are missing, malformed or invalid (wrong timezone, bad date format, ...).
189    /// The message is passed to the model, so it should explain what to fix.
190    #[error("invalid arguments: {0}")]
191    InvalidArguments(String),
192    /// The tool ran but failed in a way the model can react to (HTTP 404, an MCP `isError`
193    /// result, ...). The message is passed to the model.
194    #[error("{0}")]
195    Failed(String),
196
197    // -- Fatal: `Agent::run` stops and returns `AgentError::Tool`
198    /// Configuration problem that retrying cannot fix: a missing API key, a server that is
199    /// not running, tool definitions that are not ready.
200    #[error("tool is not configured correctly: {0}")]
201    Configuration(String),
202    /// Any other unexpected error.
203    #[error(transparent)]
204    Fatal(Box<dyn std::error::Error + Send + Sync>),
205}
206
207impl ToolError {
208    /// Creates a [`ToolError::Fatal`] from any error.
209    pub fn fatal(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
210        Self::Fatal(err.into())
211    }
212
213    /// Returns `true` when the model can react to the error, so
214    /// [`Agent::run`](crate::agent::Agent::run) sends it back as the tool result and continues.
215    /// Returns `false` for errors that end the run.
216    pub fn is_recoverable(&self) -> bool {
217        matches!(
218            self,
219            Self::NotFound(_) | Self::InvalidArguments(_) | Self::Failed(_)
220        )
221    }
222}
223
224/// A collection of `ToolBox` instances.
225///
226/// It allows for managing multiple toolboxes as a single unit, aggregating
227/// their tool definitions and dispatching tool calls to the appropriate `ToolBox`.
228///
229/// When a tool is called, the `ToolBoxSet` will search through its contained
230/// toolboxes in the order they were added. The first `ToolBox` that contains
231/// a tool with a matching name will be used to execute the call.
232#[derive(Default)]
233pub struct ToolBoxSet {
234    toolboxes: Vec<Box<dyn ToolBox + Send + Sync>>,
235}
236
237impl ToolBoxSet {
238    /// Creates a new, empty `ToolBoxSet`.
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Adds a `ToolBox` to the set.
244    ///
245    /// The order in which toolboxes are added is significant. When a tool call
246    /// is made, the `ToolBoxSet` will search for the tool in the order the
247    /// toolboxes were added.
248    pub fn add_tool(&mut self, toolbox: impl ToolBox) {
249        self.toolboxes.push(Box::new(toolbox));
250    }
251}
252
253#[async_trait::async_trait]
254impl ToolBox for ToolBoxSet {
255    /// Returns a list of all `Tool` instances contained within this ToolBoxSet.
256    ///
257    /// It aggregates the tool definitions from all the contained toolboxes.
258    fn tools_definitions(&self) -> Result<Vec<Tool>, ToolError> {
259        let mut all_definitions = Vec::new();
260        for toolbox in &self.toolboxes {
261            all_definitions.extend(toolbox.tools_definitions()?);
262        }
263        Ok(all_definitions)
264    }
265
266    /// Calls a specific tool by its name with the given parameters.
267    ///
268    /// It finds the correct `ToolBox` that contains the tool and delegates the call.
269    /// If multiple toolboxes contain a tool with the same name, the one that was
270    /// added first will be used.
271    async fn call_tool(&self, tool_name: String, arguments: Value) -> ToolResult {
272        for toolbox in &self.toolboxes {
273            match toolbox
274                .call_tool(tool_name.clone(), arguments.clone())
275                .await
276            {
277                Err(ToolError::NotFound(_)) => {
278                    // No tool in this toolbox, we can check others
279                    continue;
280                }
281                result => {
282                    return result;
283                }
284            };
285        }
286        Err(ToolError::NotFound(tool_name))
287    }
288}