ironflow_core/providers/http/tools/
registry.rs1use std::collections::HashMap;
4
5use serde_json::{Value, json};
6
7use super::tool_trait::{Tool, ToolError, ToolOutput};
8
9pub struct ToolRegistry {
27 tools: Vec<Box<dyn Tool>>,
28 index: HashMap<String, usize>,
29}
30
31impl ToolRegistry {
32 pub fn new() -> Self {
34 Self {
35 tools: Vec::new(),
36 index: HashMap::new(),
37 }
38 }
39
40 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 pub fn len(&self) -> usize {
60 self.tools.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.tools.is_empty()
66 }
67
68 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 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 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}