Skip to main content

anthropic_async/types/
tools.rs

1//! Tool calling support for Anthropic API
2//!
3//! This module provides types for defining tools that Claude can call.
4//!
5//! # Example (with schemars feature)
6//!
7//! ```rust,no_run
8//! # #[cfg(feature = "schemars")]
9//! # {
10//! use anthropic_async::types::tools;
11//! use serde::{Serialize, Deserialize};
12//! use schemars::JsonSchema;
13//!
14//! // Define tools as an enum with adjacently tagged format
15//! #[derive(Serialize, Deserialize, JsonSchema)]
16//! #[serde(tag = "action", content = "params", rename_all = "snake_case")]
17//! enum Actions {
18//!     SendEmail { to: String, subject: String },
19//!     SearchWeb { query: String },
20//! }
21//!
22//! // Generate tool definition from schema
23//! let tool = tools::schema::tool_from_schema::<Actions>(
24//!     "actions",
25//!     Some("Available actions")
26//! );
27//!
28//! // Parse tool use response back to typed enum
29//! # use anthropic_async::types::ContentBlock;
30//! let tool_use = ContentBlock::ToolUse {
31//!     id: "123".into(),
32//!     name: "send_email".into(),
33//!     input: serde_json::json!({ "to": "user@example.com", "subject": "Hello" })
34//! };
35//!
36//! if let ContentBlock::ToolUse { name, input, .. } = tool_use {
37//!     let action = tools::schema::try_parse_tool_use::<Actions>(&name, &input).unwrap();
38//!     // action is now typed as Actions enum
39//! }
40//! # }
41//! ```
42
43use serde::{Deserialize, Serialize};
44
45use super::common::CacheControl;
46
47/// Tool definition for Claude to use
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub struct Tool {
50    /// Tool name
51    pub name: String,
52    /// Optional tool description
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub description: Option<String>,
55    /// JSON schema for tool input
56    pub input_schema: serde_json::Value,
57    /// Optional cache control for prompt caching
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub cache_control: Option<CacheControl>,
60    /// Enable strict mode for tool input validation (beta)
61    ///
62    /// When enabled, tool inputs must exactly match the schema with no additional properties.
63    /// Requires a structured outputs beta header to be enabled.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub strict: Option<bool>,
66}
67
68/// Tool choice strategy
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum ToolChoice {
72    /// Let Claude decide whether to use tools
73    Auto {
74        /// Disable parallel tool use
75        #[serde(skip_serializing_if = "Option::is_none")]
76        disable_parallel_tool_use: Option<bool>,
77    },
78    /// Force Claude to use at least one tool
79    Any {
80        /// Disable parallel tool use
81        #[serde(skip_serializing_if = "Option::is_none")]
82        disable_parallel_tool_use: Option<bool>,
83    },
84    /// Disable tool use
85    #[serde(rename = "none")]
86    None,
87    /// Force Claude to use a specific tool
88    Tool {
89        /// Name of the tool to use
90        name: String,
91        /// Disable parallel tool use
92        #[serde(skip_serializing_if = "Option::is_none")]
93        disable_parallel_tool_use: Option<bool>,
94    },
95}
96
97impl Default for ToolChoice {
98    fn default() -> Self {
99        Self::Auto {
100            disable_parallel_tool_use: None,
101        }
102    }
103}
104
105/// Type-safe tool schema generation (requires schemars feature)
106#[cfg(feature = "schemars")]
107pub mod schema {
108    use schemars::JsonSchema;
109
110    use super::Tool;
111
112    /// Generate a Tool definition from a type implementing `JsonSchema`
113    ///
114    /// # Panics
115    /// Panics if the schema cannot be serialized to JSON (should never happen with valid schemas)
116    ///
117    /// # Example
118    /// ```ignore
119    /// use schemars::JsonSchema;
120    /// use serde::{Serialize, Deserialize};
121    ///
122    /// #[derive(Serialize, Deserialize, JsonSchema)]
123    /// #[serde(tag = "action", content = "params")]
124    /// enum MyTools {
125    ///     GetWeather { city: String },
126    ///     GetTime { timezone: String },
127    /// }
128    ///
129    /// let tool = tool_from_schema::<MyTools>("my_tools", Some("Weather and time tools"));
130    /// ```
131    #[must_use]
132    pub fn tool_from_schema<T: JsonSchema>(name: &str, description: Option<&str>) -> Tool {
133        let schema = schemars::schema_for!(T);
134        let schema_value = serde_json::to_value(&schema).expect("valid schema");
135        Tool {
136            name: name.to_string(),
137            description: description.map(std::string::ToString::to_string),
138            input_schema: schema_value,
139            cache_control: None,
140            strict: None,
141        }
142    }
143
144    /// Parse tool use response back to typed enum
145    ///
146    /// # Errors
147    /// Returns error if the input cannot be deserialized to type T
148    pub fn try_parse_tool_use<T: serde::de::DeserializeOwned>(
149        name: &str,
150        input: &serde_json::Value,
151    ) -> serde_json::Result<T> {
152        let wrapped = serde_json::json!({
153            "action": name,
154            "params": input
155        });
156        serde_json::from_value(wrapped)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn tool_choice_auto_ser() {
166        let tc = ToolChoice::Auto {
167            disable_parallel_tool_use: None,
168        };
169        let s = serde_json::to_string(&tc).unwrap();
170        assert!(s.contains(r#""type":"auto""#));
171    }
172
173    #[test]
174    fn tool_choice_any_ser() {
175        let tc = ToolChoice::Any {
176            disable_parallel_tool_use: Some(true),
177        };
178        let s = serde_json::to_string(&tc).unwrap();
179        assert!(s.contains(r#""type":"any""#));
180        assert!(s.contains(r#""disable_parallel_tool_use":true"#));
181    }
182
183    #[test]
184    fn tool_choice_none_ser() {
185        let tc = ToolChoice::None;
186        let s = serde_json::to_string(&tc).unwrap();
187        assert_eq!(s, r#"{"type":"none"}"#);
188    }
189
190    #[test]
191    fn tool_choice_tool_ser() {
192        let tc = ToolChoice::Tool {
193            name: "get_weather".into(),
194            disable_parallel_tool_use: None,
195        };
196        let s = serde_json::to_string(&tc).unwrap();
197        assert!(s.contains(r#""type":"tool""#));
198        assert!(s.contains(r#""name":"get_weather""#));
199    }
200
201    #[test]
202    fn tool_ser() {
203        let tool = Tool {
204            name: "calculator".into(),
205            description: Some("Math tool".into()),
206            input_schema: serde_json::json!({
207                "type": "object",
208                "properties": {
209                    "expression": { "type": "string" }
210                }
211            }),
212            cache_control: None,
213            strict: None,
214        };
215        let s = serde_json::to_string(&tool).unwrap();
216        assert!(s.contains(r#""name":"calculator""#));
217        assert!(s.contains(r#""description":"Math tool""#));
218        assert!(s.contains(r#""input_schema""#));
219        // strict should not appear when None
220        assert!(!s.contains("strict"));
221    }
222
223    #[cfg(feature = "schemars")]
224    #[test]
225    fn schema_tool_generation() {
226        use schemars::JsonSchema;
227
228        #[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
229        #[serde(tag = "action", content = "params")]
230        enum TestTools {
231            Echo { message: String },
232        }
233
234        let tool = schema::tool_from_schema::<TestTools>("test", Some("Test tool"));
235        assert_eq!(tool.name, "test");
236        assert_eq!(tool.description, Some("Test tool".into()));
237        assert!(tool.input_schema.is_object());
238    }
239}