aacode_rs/tools/
registry.rs1use super::schema::ToolSchema;
7use crate::error::Result;
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::atomic::AtomicBool;
11
12#[async_trait::async_trait]
13pub trait Tool: Send + Sync {
14 fn schema(&self) -> ToolSchema;
15 async fn call(&self, args: &Value, cancel: &AtomicBool) -> Result<String>;
16 fn name(&self) -> &'static str { "" }
17}
18
19#[derive(Default)]
20pub struct ToolRegistry {
21 tools: HashMap<String, Box<dyn Tool>>,
22 schemas: HashMap<String, ToolSchema>,
23}
24
25impl ToolRegistry {
26 pub fn new() -> Self { ToolRegistry::default() }
27 pub fn register(&mut self, tool: Box<dyn Tool>) {
28 let schema = tool.schema();
29 let name = schema.name.to_string();
30 self.schemas.insert(name.clone(), schema);
31 self.tools.insert(name, tool);
32 }
33 pub fn contains(&self, name: &str) -> bool { self.tools.contains_key(name) }
34 pub fn names(&self) -> Vec<String> {
35 let mut v: Vec<String> = self.tools.keys().cloned().collect();
36 v.sort(); v
37 }
38 pub fn schema(&self, name: &str) -> Option<&ToolSchema> { self.schemas.get(name) }
39
40 pub async fn execute(&self, name: &str, raw_args: Value, cancel: &AtomicBool) -> String {
42 let tool = match self.tools.get(name) {
43 Some(t) => t, None => return self.not_found_message(name),
44 };
45 let schema = self.schemas.get(name).unwrap();
46 let normalized = schema.normalize_params(&raw_args);
47 if let Err(msg) = schema.validate(&normalized) {
48 return format!("Parameter validation failed\n\n{msg}\n\nTool docs:\n{}", schema.documentation());
49 }
50 match tool.call(&normalized, cancel).await {
51 Ok(obs) => obs,
52 Err(crate::error::AacodeError::Cancelled) => "Execution cancelled".to_string(),
53 Err(e) => format!("Execution error: {e}"),
54 }
55 }
56
57 pub fn openai_tools(&self) -> Vec<Value> {
58 let mut names: Vec<&String> = self.schemas.keys().collect();
59 names.sort();
60 names.iter().filter_map(|n| self.schemas.get(*n)).map(|s| s.to_openai_tool()).collect()
61 }
62 pub fn anthropic_tools(&self) -> Vec<Value> {
63 let mut names: Vec<&String> = self.schemas.keys().collect();
64 names.sort();
65 names.iter().filter_map(|n| self.schemas.get(*n)).map(|s| s.to_anthropic_tool()).collect()
66 }
67
68 fn not_found_message(&self, name: &str) -> String {
69 let mut msg = format!("Error: Unknown tool '{name}'\n\n");
70 let suggestions = self.suggest_similar(name, 3);
71 if !suggestions.is_empty() {
72 msg.push_str("Did you mean:\n");
73 for s in &suggestions { msg.push_str(&format!(" - {s}\n")); }
74 msg.push('\n');
75 }
76 msg.push_str("Available tools:\n");
77 for n in self.names() {
78 if let Some(s) = self.schemas.get(&n) {
79 let desc: String = s.description.chars().take(60).collect();
80 msg.push_str(&format!(" - {n}: {desc}\n"));
81 }
82 }
83 msg
84 }
85
86 fn suggest_similar(&self, name: &str, max: usize) -> Vec<String> {
87 let mut scored: Vec<(usize, &str)> = self.tools.keys()
88 .map(|k| (levenshtein(name, k), k.as_str()))
89 .filter(|(d, _)| *d <= 4).collect();
90 scored.sort_by_key(|(d, _)| *d);
91 scored.iter().take(max).map(|(_, k)| k.to_string()).collect()
92 }
93}
94
95fn levenshtein(a: &str, b: &str) -> usize {
96 let (a, b) = if a.len() > b.len() { (b, a) } else { (a, b) };
97 let n = a.len(); let m = b.len();
98 let mut prev: Vec<usize> = (0..=n).collect();
99 let mut curr = vec![0usize; n + 1];
100 for j in 1..=m {
101 curr[0] = j;
102 for i in 1..=n {
103 curr[i] = if a.as_bytes()[i - 1] == b.as_bytes()[j - 1] { prev[i - 1] }
104 else { 1 + prev[i - 1].min(prev[i]).min(curr[i - 1]) };
105 }
106 std::mem::swap(&mut prev, &mut curr);
107 }
108 prev[n]
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use super::super::schema::{ParamType, ToolParameter};
115 use serde_json::json;
116
117 struct Echo;
118 #[async_trait::async_trait]
119 impl Tool for Echo {
120 fn schema(&self) -> ToolSchema {
121 ToolSchema::new("echo", "echo text", vec![
122 ToolParameter::new("text", ParamType::String, true, "text", &["msg"])
123 ])
124 }
125 async fn call(&self, args: &Value, _c: &AtomicBool) -> Result<String> {
126 Ok(args["text"].as_str().unwrap_or("").to_string())
127 }
128 }
129
130 fn reg() -> ToolRegistry {
131 let mut r = ToolRegistry::new();
132 r.register(Box::new(Echo));
133 r
134 }
135
136 #[tokio::test]
137 async fn executes_with_alias() {
138 let r = reg();
139 let cancel = AtomicBool::new(false);
140 let out = r.execute("echo", json!({"msg": "hi"}), &cancel).await;
141 assert_eq!(out, "hi");
142 }
143
144 #[tokio::test]
145 async fn validation_error_for_missing() {
146 let r = reg();
147 let cancel = AtomicBool::new(false);
148 let out = r.execute("echo", json!({}), &cancel).await;
149 assert!(out.contains("validation failed"));
150 assert!(out.contains("text"));
151 }
152
153 #[tokio::test]
154 async fn unknown_tool_suggests() {
155 let r = reg();
156 let cancel = AtomicBool::new(false);
157 let out = r.execute("ecko", json!({}), &cancel).await;
158 assert!(out.contains("Unknown tool 'ecko'"));
159 assert!(out.contains("echo"));
160 }
161
162 #[test]
163 fn levenshtein_basic() {
164 assert_eq!(levenshtein("cat", "bat"), 1);
165 assert_eq!(levenshtein("kitten", "sitting"), 3);
166 assert_eq!(levenshtein("echo", "echo"), 0);
167 }
168
169 #[test]
170 fn exports_native_tools() { assert!(!reg().openai_tools().is_empty()); }
171}