Skip to main content

mofa_foundation/agent/components/
tool.rs

1//! 工具组件
2//!
3//! 从 kernel 层导入 Tool trait,提供具体实现和扩展
4
5use async_trait::async_trait;
6use mofa_kernel::agent::components::tool::{
7    ToolDescriptor, ToolInput, ToolMetadata, ToolRegistry, ToolResult,
8};
9use mofa_kernel::agent::context::AgentContext;
10use mofa_kernel::agent::error::AgentResult;
11use serde::{Deserialize, Serialize};
12use serde_json::{Value, json};
13use std::any::Any;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17// ============================================================================
18// Foundation 层扩展类型
19// ============================================================================
20
21/// Tool categories for organization and discovery
22///
23/// Foundation-specific extension for tool categorization
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum ToolCategory {
26    /// File operations (read, write, edit)
27    File,
28    /// Command execution (shell, scripts)
29    Shell,
30    /// Web operations (search, fetch)
31    Web,
32    /// Memory operations (read, write memory)
33    Memory,
34    /// Agent control (spawn, coordinate)
35    Agent,
36    /// Messaging and communication
37    Communication,
38    /// General purpose tools
39    General,
40    /// Custom tools
41    Custom,
42}
43
44impl ToolCategory {
45    /// Get the category as a string
46    pub fn as_str(&self) -> &str {
47        match self {
48            Self::File => "file",
49            Self::Shell => "shell",
50            Self::Web => "web",
51            Self::Memory => "memory",
52            Self::Agent => "agent",
53            Self::Communication => "communication",
54            Self::General => "general",
55            Self::Custom => "custom",
56        }
57    }
58
59    /// Parse from string
60    pub fn from_str(s: &str) -> Option<Self> {
61        match s.to_lowercase().as_str() {
62            "file" => Some(Self::File),
63            "shell" => Some(Self::Shell),
64            "web" => Some(Self::Web),
65            "memory" => Some(Self::Memory),
66            "agent" => Some(Self::Agent),
67            "communication" => Some(Self::Communication),
68            "general" => Some(Self::General),
69            "custom" => Some(Self::Custom),
70            _ => None,
71        }
72    }
73}
74
75/// 扩展的 Tool trait (Foundation 特有方法)
76///
77/// 注意:这是 Foundation 层提供的扩展 trait,不是 kernel 层的 Tool trait
78pub trait ToolExt: mofa_kernel::agent::components::tool::Tool {
79    /// 工具分类
80    fn category(&self) -> ToolCategory;
81
82    /// 转换为 OpenAI function schema 格式 (兼容性方法)
83    fn to_openai_schema(&self) -> Value {
84        use mofa_kernel::agent::components::tool::Tool;
85        json!({
86            "type": "function",
87            "function": {
88                "name": self.name(),
89                "description": self.description(),
90                "parameters": self.parameters_schema()
91            }
92        })
93    }
94
95    /// Get this tool as `Any` for downcasting
96    fn as_any(&self) -> &dyn Any;
97}
98
99// ============================================================================
100// SimpleTool - Convenience trait for tools that don't need context
101// ============================================================================
102
103/// Simple tool trait for tools that don't need CoreAgentContext
104///
105/// This is a convenience trait for implementing simple tools that only need
106/// the input parameters and don't require access to the agent context.
107/// SimpleToolAdapter automatically implements the full Tool trait.
108///
109/// # Example
110///
111/// ```rust,ignore
112/// use mofa_foundation::agent::components::tool::{SimpleTool, ToolInput, ToolResult};
113/// use serde_json::json;
114///
115/// struct HelloTool;
116///
117/// impl SimpleTool for HelloTool {
118///     fn name(&self) -> &str {
119///         "hello"
120///     }
121///
122///     fn description(&self) -> &str {
123///         "Says hello to someone"
124///     }
125///
126///     fn parameters_schema(&self) -> serde_json::Value {
127///         json!({
128///             "type": "object",
129///             "properties": {
130///                 "name": {"type": "string"}
131///             },
132///             "required": ["name"]
133///         })
134///     }
135///
136///     async fn execute(&self, input: ToolInput) -> ToolResult {
137///         let name = input.get_str("name").unwrap_or("World");
138///         ToolResult::success_text(format!("Hello, {}!", name))
139///     }
140/// }
141/// ```
142#[async_trait]
143pub trait SimpleTool: Send + Sync {
144    /// Get the tool's name
145    fn name(&self) -> &str;
146
147    /// Get the tool's description
148    fn description(&self) -> &str;
149
150    /// Get the JSON Schema for the tool's parameters
151    fn parameters_schema(&self) -> Value;
152
153    /// Execute the tool with given input (no context needed)
154    async fn execute(&self, input: ToolInput) -> ToolResult;
155
156    /// Get tool metadata (optional override)
157    fn metadata(&self) -> ToolMetadata {
158        ToolMetadata::default()
159    }
160
161    /// Get tool category (optional override)
162    fn category(&self) -> ToolCategory {
163        ToolCategory::Custom
164    }
165}
166
167/// Adapter that implements the full Tool trait for SimpleTool
168///
169/// This adapter wraps a SimpleTool and implements the Tool trait by
170/// ignoring the CoreAgentContext parameter.
171pub struct SimpleToolAdapter<T: SimpleTool> {
172    inner: T,
173}
174
175impl<T: SimpleTool> SimpleToolAdapter<T> {
176    /// Create a new adapter from a SimpleTool
177    pub fn new(inner: T) -> Self {
178        Self { inner }
179    }
180
181    /// Get a reference to the inner tool
182    pub fn inner(&self) -> &T {
183        &self.inner
184    }
185}
186
187#[async_trait]
188impl<T: SimpleTool + Send + Sync + 'static> mofa_kernel::agent::components::tool::Tool
189    for SimpleToolAdapter<T>
190{
191    fn name(&self) -> &str {
192        self.inner.name()
193    }
194
195    fn description(&self) -> &str {
196        self.inner.description()
197    }
198
199    fn parameters_schema(&self) -> Value {
200        self.inner.parameters_schema()
201    }
202
203    async fn execute(&self, input: ToolInput, _ctx: &AgentContext) -> ToolResult {
204        self.inner.execute(input).await
205    }
206
207    fn metadata(&self) -> ToolMetadata {
208        self.inner.metadata()
209    }
210}
211
212impl<T: SimpleTool + 'static> ToolExt for SimpleToolAdapter<T> {
213    fn category(&self) -> ToolCategory {
214        self.inner.category()
215    }
216
217    fn as_any(&self) -> &dyn Any {
218        self
219    }
220}
221
222/// Convenience function to convert a SimpleTool into an Arc<dyn Tool>
223///
224/// # Example
225///
226/// ```rust,ignore
227/// use mofa_foundation::agent::components::tool::{SimpleTool, as_tool};
228/// use std::sync::Arc;
229///
230/// let tool = Arc::new(MySimpleTool);
231/// let tool_ref = as_tool(tool);
232/// registry.register(tool_ref)?;
233/// ```
234pub fn as_tool<T: SimpleTool + Send + Sync + 'static>(
235    tool: T,
236) -> Arc<dyn mofa_kernel::agent::components::tool::Tool> {
237    Arc::new(SimpleToolAdapter::new(tool))
238}
239
240// ============================================================================
241// 工具注册中心实现
242// ============================================================================
243
244/// 简单工具注册中心实现
245///
246/// Foundation 层的具体实现
247pub struct SimpleToolRegistry {
248    tools: HashMap<String, Arc<dyn mofa_kernel::agent::components::tool::Tool>>,
249}
250
251impl SimpleToolRegistry {
252    /// 创建新的注册中心
253    pub fn new() -> Self {
254        Self {
255            tools: HashMap::new(),
256        }
257    }
258}
259
260impl Default for SimpleToolRegistry {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266#[async_trait]
267impl ToolRegistry for SimpleToolRegistry {
268    fn register(
269        &mut self,
270        tool: Arc<dyn mofa_kernel::agent::components::tool::Tool>,
271    ) -> AgentResult<()> {
272        self.tools.insert(tool.name().to_string(), tool);
273        Ok(())
274    }
275
276    fn get(&self, name: &str) -> Option<Arc<dyn mofa_kernel::agent::components::tool::Tool>> {
277        self.tools.get(name).cloned()
278    }
279
280    fn unregister(&mut self, name: &str) -> AgentResult<bool> {
281        Ok(self.tools.remove(name).is_some())
282    }
283
284    fn list(&self) -> Vec<ToolDescriptor> {
285        self.tools
286            .values()
287            .map(|t| ToolDescriptor::from_tool(t.as_ref()))
288            .collect()
289    }
290
291    fn list_names(&self) -> Vec<String> {
292        self.tools.keys().cloned().collect()
293    }
294
295    fn contains(&self, name: &str) -> bool {
296        self.tools.contains_key(name)
297    }
298
299    fn count(&self) -> usize {
300        self.tools.len()
301    }
302}
303
304// ============================================================================
305// 内置工具
306// ============================================================================
307
308/// Echo 工具 (用于测试)
309pub struct EchoTool;
310
311#[async_trait]
312impl mofa_kernel::agent::components::tool::Tool for EchoTool {
313    fn name(&self) -> &str {
314        "echo"
315    }
316
317    fn description(&self) -> &str {
318        "Echo the input back as output"
319    }
320
321    fn parameters_schema(&self) -> Value {
322        json!({
323            "type": "object",
324            "properties": {
325                "message": {
326                    "type": "string",
327                    "description": "The message to echo"
328                }
329            },
330            "required": ["message"]
331        })
332    }
333
334    async fn execute(&self, input: ToolInput, _ctx: &AgentContext) -> ToolResult {
335        if let Some(message) = input.get_str("message") {
336            ToolResult::success_text(message)
337        } else if let Some(raw) = &input.raw_input {
338            ToolResult::success_text(raw)
339        } else {
340            ToolResult::failure("No message provided")
341        }
342    }
343}
344
345impl ToolExt for EchoTool {
346    fn category(&self) -> ToolCategory {
347        ToolCategory::General
348    }
349
350    fn as_any(&self) -> &dyn Any {
351        self
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use mofa_kernel::agent::components::tool::Tool; // Import Tool trait for method resolution
359
360    #[tokio::test]
361    async fn test_echo_tool() {
362        let tool = EchoTool;
363        let ctx = AgentContext::new("test");
364        let input = ToolInput::from_json(json!({"message": "Hello!"}));
365
366        let result = tool.execute(input, &ctx).await;
367        assert!(result.success);
368        assert_eq!(result.as_text(), Some("Hello!"));
369    }
370
371    #[test]
372    fn test_tool_category() {
373        let category = ToolCategory::File;
374        assert_eq!(category.as_str(), "file");
375        assert_eq!(ToolCategory::from_str("file"), Some(ToolCategory::File));
376    }
377
378    #[test]
379    fn test_tool_ext() {
380        let tool = EchoTool;
381        assert_eq!(tool.category(), ToolCategory::General);
382        let schema = tool.to_openai_schema();
383        assert_eq!(schema["function"]["name"], "echo");
384    }
385
386    #[tokio::test]
387    async fn test_simple_tool_registry() {
388        let mut registry = SimpleToolRegistry::new();
389        registry.register(Arc::new(EchoTool)).unwrap();
390
391        assert!(registry.contains("echo"));
392        assert_eq!(registry.count(), 1);
393
394        let ctx = AgentContext::new("test");
395        let result = registry
396            .execute(
397                "echo",
398                ToolInput::from_json(json!({"message": "test"})),
399                &ctx,
400            )
401            .await
402            .unwrap();
403
404        assert!(result.success);
405    }
406
407    // SimpleTool tests
408    struct TestSimpleTool {
409        name: String,
410    }
411
412    #[async_trait]
413    impl SimpleTool for TestSimpleTool {
414        fn name(&self) -> &str {
415            &self.name
416        }
417
418        fn description(&self) -> &str {
419            "A test tool"
420        }
421
422        fn parameters_schema(&self) -> Value {
423            json!({
424                "type": "object",
425                "properties": {
426                    "value": {"type": "string"}
427                }
428            })
429        }
430
431        async fn execute(&self, input: ToolInput) -> ToolResult {
432            if let Some(value) = input.get_str("value") {
433                ToolResult::success_text(format!("Got: {}", value))
434            } else {
435                ToolResult::failure("No value provided")
436            }
437        }
438
439        fn category(&self) -> ToolCategory {
440            ToolCategory::Custom
441        }
442    }
443
444    #[tokio::test]
445    async fn test_simple_tool() {
446        let tool = TestSimpleTool {
447            name: "test_tool".to_string(),
448        };
449        let input = ToolInput::from_json(json!({"value": "hello"}));
450
451        let result = tool.execute(input).await;
452        assert!(result.success);
453        assert_eq!(result.as_text(), Some("Got: hello"));
454    }
455
456    #[tokio::test]
457    async fn test_simple_tool_adapter() {
458        let simple_tool = TestSimpleTool {
459            name: "test_adapter".to_string(),
460        };
461        let adapter = SimpleToolAdapter::new(simple_tool);
462
463        assert_eq!(adapter.name(), "test_adapter");
464        assert_eq!(adapter.description(), "A test tool");
465        assert_eq!(adapter.category(), ToolCategory::Custom);
466
467        let ctx = AgentContext::new("test");
468        let input = ToolInput::from_json(json!({"value": "world"}));
469
470        let result =
471            mofa_kernel::agent::components::tool::Tool::execute(&adapter, input, &ctx).await;
472        assert!(result.success);
473        assert_eq!(result.as_text(), Some("Got: world"));
474    }
475
476    #[tokio::test]
477    async fn test_as_tool_function() {
478        let simple_tool = TestSimpleTool {
479            name: "test_as_tool".to_string(),
480        };
481        let tool_ref = as_tool(simple_tool);
482
483        let mut registry = SimpleToolRegistry::new();
484        registry.register(tool_ref).unwrap();
485
486        assert!(registry.contains("test_as_tool"));
487
488        let ctx = AgentContext::new("test");
489        let result = registry
490            .execute(
491                "test_as_tool",
492                ToolInput::from_json(json!({"value": "test"})),
493                &ctx,
494            )
495            .await
496            .unwrap();
497
498        assert!(result.success);
499    }
500}