Skip to main content

ironflow_core/providers/http/tools/
registry.rs

1//! Tool registry: stores tools and converts them to OpenAI format.
2
3use std::collections::HashMap;
4
5use serde_json::{Value, json};
6
7use super::tool_trait::{Tool, ToolError, ToolOutput};
8
9/// Registry of client-side tools available to the HTTP agent provider.
10///
11/// Converts registered tools to the OpenAI `tools` array format and routes
12/// tool call execution to the correct tool implementation.
13///
14/// # Examples
15///
16/// ```no_run
17/// use ironflow_core::providers::http::tools::ToolRegistry;
18///
19/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20/// let registry = ToolRegistry::new();
21/// // registry.register(my_tool);
22/// let tools_json = registry.to_openai_tools();
23/// # Ok(())
24/// # }
25/// ```
26pub struct ToolRegistry {
27    tools: Vec<Box<dyn Tool>>,
28    index: HashMap<String, usize>,
29}
30
31impl ToolRegistry {
32    /// Create an empty registry.
33    pub fn new() -> Self {
34        Self {
35            tools: Vec::new(),
36            index: HashMap::new(),
37        }
38    }
39
40    /// Register a tool. Panics if a tool with the same name is already registered.
41    ///
42    /// # Panics
43    ///
44    /// Panics if a tool with the same name already exists in the registry.
45    pub fn register(mut self, tool: impl Tool + 'static) -> Self {
46        let name = tool.name().to_string();
47        assert!(
48            !self.index.contains_key(&name),
49            "tool '{}' already registered",
50            name
51        );
52        let idx = self.tools.len();
53        self.tools.push(Box::new(tool));
54        self.index.insert(name, idx);
55        self
56    }
57
58    /// Returns the number of registered tools.
59    pub fn len(&self) -> usize {
60        self.tools.len()
61    }
62
63    /// Returns `true` if no tools are registered.
64    pub fn is_empty(&self) -> bool {
65        self.tools.is_empty()
66    }
67
68    /// Convert all registered tools to the OpenAI `tools` array format.
69    ///
70    /// Each tool is represented as:
71    /// ```json
72    /// {
73    ///   "type": "function",
74    ///   "function": {
75    ///     "name": "tool_name",
76    ///     "description": "tool description",
77    ///     "parameters": { ... json schema ... }
78    ///   }
79    /// }
80    /// ```
81    pub fn to_openai_tools(&self) -> Vec<Value> {
82        self.tools
83            .iter()
84            .map(|tool| {
85                json!({
86                    "type": "function",
87                    "function": {
88                        "name": tool.name(),
89                        "description": tool.description(),
90                        "parameters": tool.parameters_schema()
91                    }
92                })
93            })
94            .collect()
95    }
96
97    /// Execute a tool by name with the given input.
98    ///
99    /// Returns `None` if no tool with that name is registered.
100    pub async fn execute(&self, name: &str, input: Value) -> Option<Result<ToolOutput, ToolError>> {
101        let idx = self.index.get(name)?;
102        let tool = &self.tools[*idx];
103        Some(tool.execute(input).await)
104    }
105
106    /// Check if a tool with the given name is registered.
107    pub fn has_tool(&self, name: &str) -> bool {
108        self.index.contains_key(name)
109    }
110}
111
112impl Default for ToolRegistry {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use std::future::Future;
121    use std::pin::Pin;
122
123    use serde_json::json;
124
125    use super::*;
126
127    struct AddTool;
128
129    impl Tool for AddTool {
130        fn name(&self) -> &str {
131            "add"
132        }
133        fn description(&self) -> &str {
134            "Adds two numbers"
135        }
136        fn parameters_schema(&self) -> Value {
137            json!({
138                "type": "object",
139                "properties": {
140                    "a": {"type": "number"},
141                    "b": {"type": "number"}
142                },
143                "required": ["a", "b"]
144            })
145        }
146        fn execute(
147            &self,
148            input: Value,
149        ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
150            Box::pin(async move {
151                let a = input
152                    .get("a")
153                    .and_then(|v| v.as_f64())
154                    .ok_or_else(|| ToolError::new("missing 'a'"))?;
155                let b = input
156                    .get("b")
157                    .and_then(|v| v.as_f64())
158                    .ok_or_else(|| ToolError::new("missing 'b'"))?;
159                Ok(ToolOutput::success(format!("{}", a + b)))
160            })
161        }
162    }
163
164    struct EchoTool;
165
166    impl Tool for EchoTool {
167        fn name(&self) -> &str {
168            "echo"
169        }
170        fn description(&self) -> &str {
171            "Echoes input"
172        }
173        fn parameters_schema(&self) -> Value {
174            json!({"type": "object", "properties": {"msg": {"type": "string"}}})
175        }
176        fn execute(
177            &self,
178            input: Value,
179        ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
180            Box::pin(async move {
181                let msg = input.get("msg").and_then(|v| v.as_str()).unwrap_or("empty");
182                Ok(ToolOutput::success(msg))
183            })
184        }
185    }
186
187    #[test]
188    fn registry_new_is_empty() {
189        let registry = ToolRegistry::new();
190        assert!(registry.is_empty());
191        assert_eq!(registry.len(), 0);
192    }
193
194    #[test]
195    fn registry_register_increments_count() {
196        let registry = ToolRegistry::new().register(AddTool).register(EchoTool);
197        assert_eq!(registry.len(), 2);
198        assert!(!registry.is_empty());
199    }
200
201    #[test]
202    fn registry_has_tool() {
203        let registry = ToolRegistry::new().register(AddTool);
204        assert!(registry.has_tool("add"));
205        assert!(!registry.has_tool("echo"));
206    }
207
208    #[test]
209    #[should_panic(expected = "tool 'add' already registered")]
210    fn registry_duplicate_panics() {
211        ToolRegistry::new().register(AddTool).register(AddTool);
212    }
213
214    #[test]
215    fn to_openai_tools_format() {
216        let registry = ToolRegistry::new().register(AddTool);
217        let tools = registry.to_openai_tools();
218
219        assert_eq!(tools.len(), 1);
220        assert_eq!(tools[0]["type"], "function");
221        assert_eq!(tools[0]["function"]["name"], "add");
222        assert_eq!(tools[0]["function"]["description"], "Adds two numbers");
223        assert_eq!(tools[0]["function"]["parameters"]["type"], "object");
224        assert!(tools[0]["function"]["parameters"]["properties"]["a"].is_object());
225    }
226
227    #[test]
228    fn to_openai_tools_multiple() {
229        let registry = ToolRegistry::new().register(AddTool).register(EchoTool);
230        let tools = registry.to_openai_tools();
231
232        assert_eq!(tools.len(), 2);
233        assert_eq!(tools[0]["function"]["name"], "add");
234        assert_eq!(tools[1]["function"]["name"], "echo");
235    }
236
237    #[tokio::test]
238    async fn execute_existing_tool() {
239        let registry = ToolRegistry::new().register(AddTool);
240        let result = registry
241            .execute("add", json!({"a": 3, "b": 4}))
242            .await
243            .expect("tool should exist")
244            .expect("tool should succeed");
245
246        assert_eq!(result.content, "7");
247        assert!(!result.is_error);
248    }
249
250    #[tokio::test]
251    async fn execute_nonexistent_tool_returns_none() {
252        let registry = ToolRegistry::new().register(AddTool);
253        let result = registry.execute("nonexistent", json!({})).await;
254        assert!(result.is_none());
255    }
256
257    #[tokio::test]
258    async fn execute_tool_error_propagates() {
259        let registry = ToolRegistry::new().register(AddTool);
260        let result = registry
261            .execute("add", json!({"a": 1}))
262            .await
263            .expect("tool should exist");
264
265        assert!(result.is_err());
266    }
267}