Skip to main content

oxicode_ai/
tools.rs

1//! Tool definitions and validation
2
3use jsonschema::Validator;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6use serde_json::json;
7use std::sync::Arc;
8use thiserror::Error;
9
10/// Callback type for progress updates
11pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
12
13/// Create a progress callback from a closure
14pub fn progress_callback<F: Fn(String) + Send + Sync + 'static>(f: F) -> ProgressCallback {
15    Arc::new(f)
16}
17
18/// Forces or leaves-to-the-model which tool the next assistant turn must
19/// call. `Auto` is the existing default behavior (model decides freely,
20/// including calling no tool). `Named` forces exactly one tool by name —
21/// only honored by providers with native forced-tool-choice support; owned
22/// (in-band XML) dialects and providers without the feature silently treat
23/// it as `Auto` (see each provider's mapping).
24#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
25#[serde(tag = "type", content = "name", rename_all = "snake_case")]
26pub enum ToolChoice {
27    /// Let the model choose freely (the existing default behavior).
28    #[default]
29    Auto,
30    /// Force the next assistant turn to call the named tool. Only honored by
31    /// providers with native forced-tool-choice support.
32    Named(String),
33}
34
35/// Tool definition with JSON Schema parameters
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Tool {
38    /// Tool name
39    pub name: String,
40
41    /// Human-readable description
42    pub description: String,
43
44    /// JSON Schema for parameters
45    pub parameters: JsonValue,
46}
47
48impl Tool {
49    /// Create a new tool with the given name, description, and JSON Schema.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use oxicode_ai::Tool;
55    /// let tool = Tool::new(
56    ///     "read_file",
57    ///     "Read contents from a file",
58    ///     serde_json::json!({
59    ///         "type": "object",
60    ///         "properties": {
61    ///             "path": {
62    ///                 "type": "string",
63    ///                 "description": "File path to read"
64    ///             }
65    ///         },
66    ///         "required": ["path"]
67    ///     }),
68    /// );
69    /// ```
70    pub fn new(
71        name: impl Into<String>,
72        description: impl Into<String>,
73        parameters: JsonValue,
74    ) -> Self {
75        Self {
76            name: name.into(),
77            description: description.into(),
78            parameters,
79        }
80    }
81
82    /// Create a simple tool with a single string parameter
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use oxicode_ai::Tool;
88    /// let tool = Tool::with_string_param(
89    ///     "get_weather",
90    ///     "Get current weather",
91    ///     "location",
92    ///     "City name",
93    /// );
94    /// assert_eq!(tool.name, "get_weather");
95    /// ```
96    pub fn with_string_param(
97        name: impl Into<String>,
98        description: impl Into<String>,
99        param_name: impl Into<String>,
100        param_description: impl Into<String>,
101    ) -> Self {
102        let param_name = param_name.into();
103        let param_description = param_description.into();
104
105        // Build properties manually to avoid borrow issues
106        let mut properties = serde_json::Map::new();
107        properties.insert("type".to_string(), json!("object"));
108
109        let mut obj_properties = serde_json::Map::new();
110        obj_properties.insert(
111            param_name.clone(),
112            json!({
113                "type": "string",
114                "description": param_description
115            }),
116        );
117        properties.insert(
118            "properties".to_string(),
119            serde_json::Value::Object(obj_properties),
120        );
121
122        let required_arr =
123            serde_json::Value::Array(vec![serde_json::Value::String(param_name.clone())]);
124        properties.insert("required".to_string(), required_arr);
125
126        let params = serde_json::Value::Object(properties);
127        Self::new(name, description, params)
128    }
129
130    /// Validate arguments against the tool's JSON Schema
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use oxicode_ai::Tool;
136    /// let tool = Tool::with_string_param(
137    ///     "get_weather",
138    ///     "Get weather",
139    ///     "location",
140    ///     "City",
141    /// );
142    /// let result = tool.validate(&serde_json::json!({"location": "London"}));
143    /// assert!(result.is_ok());
144    /// ```
145    pub fn validate(&self, args: &JsonValue) -> Result<JsonValue, ToolValidationError> {
146        validate_args_internal(&self.parameters, args)
147    }
148
149    /// Check if this tool requires parameters
150    pub fn requires_parameters(&self) -> bool {
151        self.parameters
152            .get("required")
153            .and_then(|r| r.as_array())
154            .map(|arr| !arr.is_empty())
155            .unwrap_or(false)
156    }
157}
158
159/// Validation error
160#[derive(Error, Debug)]
161pub enum ToolValidationError {
162    #[error("Invalid JSON: {0}")]
163    /// invalid json variant.
164    InvalidJson(#[from] serde_json::Error),
165
166    #[error("Schema validation failed: {0}")]
167    /// schema validation variant.
168    SchemaValidation(String),
169
170    #[error("Missing required field: {0}")]
171    /// missing required field variant.
172    MissingRequiredField(String),
173}
174
175/// Validate tool arguments against a JSON Schema
176pub fn validate_args(tool: &Tool, args: &JsonValue) -> Result<JsonValue, ToolValidationError> {
177    validate_args_internal(&tool.parameters, args)
178}
179
180/// Internal validation implementation
181fn validate_args_internal(
182    schema: &JsonValue,
183    args: &JsonValue,
184) -> Result<JsonValue, ToolValidationError> {
185    let validator =
186        Validator::new(schema).map_err(|e| ToolValidationError::SchemaValidation(e.to_string()))?;
187
188    let validation_result = validator.validate(args);
189
190    match validation_result {
191        Ok(()) => Ok(args.clone()),
192        Err(errors) => {
193            // jsonschema returns an error with formatted message
194            let error_msg = format!("{}", errors);
195            Err(ToolValidationError::SchemaValidation(error_msg))
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_tool_validation() {
206        let tool = Tool::with_string_param(
207            "get_weather",
208            "Get current weather for a location",
209            "location",
210            "City name or coordinates",
211        );
212
213        let valid_args = serde_json::json!({
214            "location": "London"
215        });
216
217        let result = tool.validate(&valid_args);
218        assert!(result.is_ok());
219    }
220
221    #[test]
222    fn test_tool_validation_failure() {
223        let tool = Tool::with_string_param(
224            "get_weather",
225            "Get current weather for a location",
226            "location",
227            "City name or coordinates",
228        );
229
230        // Missing required field
231        let invalid_args = serde_json::json!({});
232
233        let result = tool.validate(&invalid_args);
234        assert!(result.is_err());
235    }
236}