Skip to main content

serdes_ai_tools/
definition.rs

1//! Tool definition types for describing tools to LLMs.
2//!
3//! This module provides types for defining tools with JSON Schema parameters
4//! that can be serialized and sent to language models.
5
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use serde_json::Value as JsonValue;
9use std::collections::HashMap;
10
11/// JSON Schema for an object type (tool parameters).
12///
13/// This represents the parameters that a tool accepts, using JSON Schema format
14/// that is understood by language models for function calling.
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct ObjectJsonSchema {
17    /// The schema type (always "object" for tool parameters).
18    #[serde(rename = "type")]
19    pub schema_type: String,
20
21    /// Property definitions.
22    pub properties: IndexMap<String, JsonValue>,
23
24    /// List of required property names.
25    #[serde(skip_serializing_if = "Vec::is_empty", default)]
26    pub required: Vec<String>,
27
28    /// Description of the schema.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub description: Option<String>,
31
32    /// Whether additional properties are allowed.
33    #[serde(
34        rename = "additionalProperties",
35        skip_serializing_if = "Option::is_none"
36    )]
37    pub additional_properties: Option<bool>,
38
39    /// Extra schema properties.
40    #[serde(flatten)]
41    pub extra: HashMap<String, JsonValue>,
42}
43
44impl ObjectJsonSchema {
45    /// Create a new empty object schema.
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            schema_type: "object".to_string(),
50            properties: IndexMap::new(),
51            required: Vec::new(),
52            description: None,
53            additional_properties: None,
54            extra: HashMap::new(),
55        }
56    }
57
58    /// Add a property to the schema.
59    #[must_use]
60    pub fn with_property(mut self, name: &str, schema: JsonValue, required: bool) -> Self {
61        self.properties.insert(name.to_string(), schema);
62        if required {
63            self.required.push(name.to_string());
64        }
65        self
66    }
67
68    /// Add a property without consuming self.
69    pub fn add_property(&mut self, name: &str, schema: JsonValue, required: bool) {
70        self.properties.insert(name.to_string(), schema);
71        if required && !self.required.contains(&name.to_string()) {
72            self.required.push(name.to_string());
73        }
74    }
75
76    /// Set the description.
77    #[must_use]
78    pub fn with_description(mut self, desc: &str) -> Self {
79        self.description = Some(desc.to_string());
80        self
81    }
82
83    /// Set whether additional properties are allowed.
84    #[must_use]
85    pub fn with_additional_properties(mut self, allowed: bool) -> Self {
86        self.additional_properties = Some(allowed);
87        self
88    }
89
90    /// Add an extra property to the schema.
91    #[must_use]
92    pub fn with_extra(mut self, key: &str, value: JsonValue) -> Self {
93        self.extra.insert(key.to_string(), value);
94        self
95    }
96
97    /// Check if a property is required.
98    #[must_use]
99    pub fn is_required(&self, name: &str) -> bool {
100        self.required.contains(&name.to_string())
101    }
102
103    /// Get a property schema.
104    #[must_use]
105    pub fn get_property(&self, name: &str) -> Option<&JsonValue> {
106        self.properties.get(name)
107    }
108
109    /// Get the number of properties.
110    #[must_use]
111    pub fn property_count(&self) -> usize {
112        self.properties.len()
113    }
114
115    /// Check if the schema has no properties.
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.properties.is_empty()
119    }
120
121    /// Convert to a JSON value.
122    pub fn to_json(&self) -> Result<JsonValue, serde_json::Error> {
123        serde_json::to_value(self)
124    }
125}
126
127impl Default for ObjectJsonSchema {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl TryFrom<JsonValue> for ObjectJsonSchema {
134    type Error = serde_json::Error;
135
136    fn try_from(value: JsonValue) -> Result<Self, Self::Error> {
137        serde_json::from_value(value)
138    }
139}
140
141impl From<ObjectJsonSchema> for JsonValue {
142    fn from(schema: ObjectJsonSchema) -> Self {
143        serde_json::to_value(schema).unwrap_or(JsonValue::Null)
144    }
145}
146
147/// Complete tool definition sent to the model.
148///
149/// This contains all the information a language model needs to understand
150/// and call a tool, including its name, description, and parameter schema.
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct ToolDefinition {
153    /// Tool name (must be a valid identifier).
154    pub name: String,
155
156    /// Human-readable description of what the tool does.
157    pub description: String,
158
159    /// JSON Schema for the tool's parameters.
160    pub parameters_json_schema: JsonValue,
161
162    /// Whether to use strict mode for schema validation (OpenAI feature).
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub strict: Option<bool>,
165
166    /// Key for outer typed dict (pydantic-ai compatibility).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub outer_typed_dict_key: Option<String>,
169}
170
171impl ToolDefinition {
172    /// Create a new tool definition.
173    #[must_use]
174    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
175        Self {
176            name: name.into(),
177            description: description.into(),
178            parameters_json_schema: crate::schema::SchemaBuilder::new()
179                .build()
180                .expect("SchemaBuilder JSON serialization failed"),
181            strict: None,
182            outer_typed_dict_key: None,
183        }
184    }
185
186    /// Set the parameters schema.
187    #[must_use]
188    pub fn with_parameters(mut self, schema: impl Into<JsonValue>) -> Self {
189        self.parameters_json_schema = schema.into();
190        self
191    }
192
193    /// Set strict mode.
194    #[must_use]
195    pub fn with_strict(mut self, strict: bool) -> Self {
196        self.strict = Some(strict);
197        self
198    }
199
200    /// Set the outer typed dict key.
201    #[must_use]
202    pub fn with_outer_typed_dict_key(mut self, key: impl Into<String>) -> Self {
203        self.outer_typed_dict_key = Some(key.into());
204        self
205    }
206
207    /// Get the tool name.
208    #[must_use]
209    pub fn name(&self) -> &str {
210        &self.name
211    }
212
213    /// Get the tool description.
214    #[must_use]
215    pub fn description(&self) -> &str {
216        &self.description
217    }
218
219    /// Get the parameters schema.
220    #[must_use]
221    pub fn parameters(&self) -> &JsonValue {
222        &self.parameters_json_schema
223    }
224
225    /// Check if strict mode is enabled.
226    #[must_use]
227    pub fn is_strict(&self) -> bool {
228        self.strict.unwrap_or(false)
229    }
230
231    /// Convert to OpenAI function format.
232    #[must_use]
233    pub fn to_openai_function(&self) -> JsonValue {
234        let mut func = serde_json::json!({
235            "type": "function",
236            "function": {
237                "name": self.name,
238                "description": self.description,
239                "parameters": self.parameters_json_schema.clone()
240            }
241        });
242
243        if let Some(strict) = self.strict {
244            func["function"]["strict"] = JsonValue::Bool(strict);
245        }
246
247        func
248    }
249
250    /// Convert to Anthropic tool format.
251    #[must_use]
252    pub fn to_anthropic_tool(&self) -> JsonValue {
253        serde_json::json!({
254            "name": self.name,
255            "description": self.description,
256            "input_schema": self.parameters_json_schema.clone()
257        })
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_object_json_schema_new() {
267        let schema = ObjectJsonSchema::new();
268        assert_eq!(schema.schema_type, "object");
269        assert!(schema.properties.is_empty());
270        assert!(schema.required.is_empty());
271    }
272
273    #[test]
274    fn test_object_json_schema_with_property() {
275        let schema = ObjectJsonSchema::new()
276            .with_property("name", serde_json::json!({"type": "string"}), true)
277            .with_property("age", serde_json::json!({"type": "integer"}), false);
278
279        assert_eq!(schema.property_count(), 2);
280        assert!(schema.is_required("name"));
281        assert!(!schema.is_required("age"));
282    }
283
284    #[test]
285    fn test_tool_definition_new() {
286        let def = ToolDefinition::new("get_weather", "Get the current weather");
287        assert_eq!(def.name(), "get_weather");
288        assert_eq!(def.description(), "Get the current weather");
289        let properties = def
290            .parameters()
291            .get("properties")
292            .and_then(|value| value.as_object())
293            .unwrap();
294        assert!(properties.is_empty());
295    }
296
297    #[test]
298    fn test_tool_definition_with_parameters() {
299        let params = crate::schema::SchemaBuilder::new()
300            .string("location", "City name", true)
301            .enum_values(
302                "unit",
303                "Temperature unit",
304                &["celsius", "fahrenheit"],
305                false,
306            )
307            .build()
308            .expect("SchemaBuilder JSON serialization failed");
309
310        let def = ToolDefinition::new("get_weather", "Get weather")
311            .with_parameters(params)
312            .with_strict(true);
313
314        assert!(def.is_strict());
315        let properties = def
316            .parameters()
317            .get("properties")
318            .and_then(|value| value.as_object())
319            .unwrap();
320        assert_eq!(properties.len(), 2);
321    }
322
323    #[test]
324    fn test_to_openai_function() {
325        let def = ToolDefinition::new("test", "Test tool")
326            .with_parameters(
327                crate::schema::SchemaBuilder::new()
328                    .string("x", "A value", true)
329                    .build()
330                    .expect("SchemaBuilder JSON serialization failed"),
331            )
332            .with_strict(true);
333
334        let func = def.to_openai_function();
335        assert_eq!(func["type"], "function");
336        assert_eq!(func["function"]["name"], "test");
337        assert_eq!(func["function"]["strict"], true);
338    }
339
340    #[test]
341    fn test_to_anthropic_tool() {
342        let def = ToolDefinition::new("test", "Test tool");
343        let tool = def.to_anthropic_tool();
344        assert_eq!(tool["name"], "test");
345        assert!(tool.get("input_schema").is_some());
346    }
347
348    #[test]
349    fn test_serde_roundtrip() {
350        let schema = ObjectJsonSchema::new()
351            .with_property("x", serde_json::json!({"type": "string"}), true)
352            .with_description("Test schema");
353
354        let json = serde_json::to_string(&schema).unwrap();
355        let parsed: ObjectJsonSchema = serde_json::from_str(&json).unwrap();
356        assert_eq!(schema, parsed);
357    }
358}