mofa_foundation/agent/components/
tool.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum ToolCategory {
26 File,
28 Shell,
30 Web,
32 Memory,
34 Agent,
36 Communication,
38 General,
40 Custom,
42}
43
44impl ToolCategory {
45 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 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
75pub trait ToolExt: mofa_kernel::agent::components::tool::Tool {
79 fn category(&self) -> ToolCategory;
81
82 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 fn as_any(&self) -> &dyn Any;
97}
98
99#[async_trait]
143pub trait SimpleTool: Send + Sync {
144 fn name(&self) -> &str;
146
147 fn description(&self) -> &str;
149
150 fn parameters_schema(&self) -> Value;
152
153 async fn execute(&self, input: ToolInput) -> ToolResult;
155
156 fn metadata(&self) -> ToolMetadata {
158 ToolMetadata::default()
159 }
160
161 fn category(&self) -> ToolCategory {
163 ToolCategory::Custom
164 }
165}
166
167pub struct SimpleToolAdapter<T: SimpleTool> {
172 inner: T,
173}
174
175impl<T: SimpleTool> SimpleToolAdapter<T> {
176 pub fn new(inner: T) -> Self {
178 Self { inner }
179 }
180
181 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
222pub 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
240pub struct SimpleToolRegistry {
248 tools: HashMap<String, Arc<dyn mofa_kernel::agent::components::tool::Tool>>,
249}
250
251impl SimpleToolRegistry {
252 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
304pub 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; #[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 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}