Skip to main content

aagt_core/skills/tool/
mod.rs

1//! Tool system for AI agents
2//!
3//! Provides the core abstraction for defining tools that AI agents can call.
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::error::Error;
11
12pub mod code_interpreter;
13pub mod cron;
14pub mod delegation;
15pub mod memory;
16
17pub use cron::CronTool;
18pub use delegation::DelegateTool;
19pub use memory::{RememberThisTool, SearchHistoryTool, TieredSearchTool, FetchDocumentTool};
20
21/// Definition of a tool that can be sent to the LLM
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ToolDefinition {
24    /// Name of the tool
25    pub name: String,
26    /// Description for the LLM
27    pub description: String,
28    /// JSON Schema for parameters (Legacy/API)
29    pub parameters: serde_json::Value,
30    /// TypeScript interface definition (Preferred for System Prompt)
31    pub parameters_ts: Option<String>,
32}
33
34/// Trait for implementing tools that AI agents can call
35#[async_trait]
36pub trait Tool: Send + Sync {
37    /// The name of this tool
38    /// The name of this tool
39    fn name(&self) -> String;
40
41    /// Get the tool definition for the LLM
42    async fn definition(&self) -> ToolDefinition;
43
44    /// Execute the tool with the given arguments (JSON string)
45    async fn call(&self, arguments: &str) -> anyhow::Result<String>;
46}
47
48#[derive(Clone)]
49pub struct ToolSet {
50    tools: HashMap<String, Arc<dyn Tool>>,
51}
52
53impl Default for ToolSet {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl ToolSet {
60    /// Create an empty toolset
61    pub fn new() -> Self {
62        Self {
63            tools: HashMap::new(),
64        }
65    }
66
67    /// Add a tool to the set
68    pub fn add<T: Tool + 'static>(&mut self, tool: T) -> &mut Self {
69        self.tools.insert(tool.name().to_string(), Arc::new(tool));
70        self
71    }
72
73    /// Add a shared tool to the set
74    pub fn add_shared(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
75        self.tools.insert(tool.name().to_string(), tool);
76        self
77    }
78
79    /// Get a tool by name
80    pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
81        self.tools.get(name)
82    }
83
84    /// Check if a tool exists
85    pub fn contains(&self, name: &str) -> bool {
86        self.tools.contains_key(name)
87    }
88
89    /// Get all tool definitions
90    pub async fn definitions(&self) -> Vec<ToolDefinition> {
91        let mut defs = Vec::new();
92        for tool in self.tools.values() {
93            defs.push(tool.definition().await);
94        }
95        defs
96    }
97
98    /// Call a tool by name
99    pub async fn call(&self, name: &str, arguments: &str) -> anyhow::Result<String> {
100        let tool = self
101            .tools
102            .get(name)
103            .ok_or_else(|| Error::ToolNotFound(name.to_string()))?;
104
105        tool.call(arguments).await
106    }
107
108    /// Get the number of tools
109    pub fn len(&self) -> usize {
110        self.tools.len()
111    }
112
113    /// Check if empty
114    pub fn is_empty(&self) -> bool {
115        self.tools.is_empty()
116    }
117
118    /// Iterate over tools
119    pub fn iter(&self) -> impl Iterator<Item = (&String, &Arc<dyn Tool>)> {
120        self.tools.iter()
121    }
122}
123
124impl crate::agent::context::ContextInjector for ToolSet {
125    fn inject(&self) -> crate::error::Result<Vec<crate::agent::message::Message>> {
126        if self.tools.is_empty() {
127            return Ok(Vec::new());
128        }
129
130        let mut content = String::from("## Tool Definitions (TypeScript)\n\n");
131        content.push_str("You have access to the following tools. Use them to fulfill the user's request.\n\n");
132
133        // Sort for determinism
134        let mut sorted_tools: Vec<_> = self.tools.iter().collect();
135        sorted_tools.sort_by_key(|(k, _)| *k);
136
137        for (name, tool) in sorted_tools {
138            // We use block_on because Tool::definition is currently async
139            // and ContextInjector::inject is sync.
140            // In a more refined architecture, we would cache these definitions.
141            let def = futures::executor::block_on(tool.definition());
142            
143            content.push_str(&format!("### {}\n{}\n", name, def.description));
144            if let Some(ts) = def.parameters_ts {
145                content.push_str("```typescript\n");
146                content.push_str(&ts);
147                if !ts.ends_with('\n') {
148                    content.push('\n');
149                }
150                content.push_str("```\n\n");
151            } else {
152                // Fallback to JSON if TS missing
153                content.push_str("```json\n");
154                content.push_str(&serde_json::to_string_pretty(&def.parameters).unwrap_or_default());
155                content.push_str("\n```\n\n");
156            }
157        }
158
159        Ok(vec![crate::agent::message::Message::system(content)])
160    }
161}
162
163/// Builder for creating a ToolSet
164pub struct ToolSetBuilder {
165    tools: Vec<Arc<dyn Tool>>,
166}
167
168impl Default for ToolSetBuilder {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl ToolSetBuilder {
175    /// Create a new builder
176    pub fn new() -> Self {
177        Self { tools: Vec::new() }
178    }
179
180    /// Add a tool
181    pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
182        self.tools.push(Arc::new(tool));
183        self
184    }
185
186    /// Add a shared tool
187    pub fn shared_tool(mut self, tool: Arc<dyn Tool>) -> Self {
188        self.tools.push(tool);
189        self
190    }
191
192    /// Build the ToolSet
193    pub fn build(self) -> ToolSet {
194        let mut toolset = ToolSet::new();
195        for tool in self.tools {
196            toolset.add_shared(tool);
197        }
198        toolset
199    }
200}
201
202/// Helper macro for creating simple tools
203/// 
204/// # Example
205/// ```ignore
206/// simple_tool!(
207///     name: "get_time",
208///     description: "Get the current time",
209///     handler: |_args| async {
210///         Ok(chrono::Utc::now().to_rfc3339())
211///     }
212/// );
213/// ```
214#[macro_export]
215macro_rules! simple_tool {
216    (
217        name: $name:expr,
218        description: $desc:expr,
219        parameters: $params:expr,
220        handler: $handler:expr
221    ) => {{
222        struct SimpleTool;
223
224        #[async_trait::async_trait]
225        impl $crate::tool::Tool for SimpleTool {
226            fn name(&self) -> String {
227                $name.to_string()
228            }
229
230            async fn definition(&self) -> $crate::tool::ToolDefinition {
231                $crate::tool::ToolDefinition {
232                    name: $name.to_string(),
233                    description: $desc.to_string(),
234                    parameters: $params,
235                }
236            }
237
238            async fn call(&self, arguments: &str) -> anyhow::Result<String> {
239                let handler = $handler;
240                handler(arguments).await
241            }
242        }
243
244        SimpleTool
245    }};
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    struct EchoTool;
253
254    #[async_trait]
255    impl Tool for EchoTool {
256        fn name(&self) -> String {
257            "echo".to_string()
258        }
259
260        async fn definition(&self) -> ToolDefinition {
261            ToolDefinition {
262                name: "echo".to_string(),
263                description: "Echo back the input".to_string(),
264                parameters: serde_json::json!({
265                    "type": "object",
266                    "properties": {
267                        "message": {
268                            "type": "string",
269                            "description": "Message to echo"
270                        }
271                    },
272                    "required": ["message"]
273                }),
274            }
275        }
276
277        async fn call(&self, arguments: &str) -> anyhow::Result<String> {
278            #[derive(Deserialize)]
279            struct Args {
280                message: String,
281            }
282            let args: Args = serde_json::from_str(arguments)
283                .map_err(|e| Error::ToolArguments {
284                    tool_name: "echo".to_string(),
285                    message: e.to_string(),
286                })?;
287            Ok(args.message)
288        }
289    }
290
291    #[tokio::test]
292    async fn test_toolset() {
293        let mut toolset = ToolSet::new();
294        toolset.add(EchoTool);
295
296        assert!(toolset.contains("echo"));
297        assert_eq!(toolset.len(), 1);
298
299        let result = toolset
300            .call("echo", r#"{"message": "hello"}"#)
301            .await
302            .expect("call should succeed");
303        assert_eq!(result, "hello");
304    }
305}