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}