ricecoder_agents/
tool_invokers.rs

1//! Tool invoker implementations for ricecoder-tools
2//!
3//! This module provides implementations of the ToolInvoker trait for each tool
4//! provided by ricecoder-tools (webfetch, patch, todowrite, todoread, websearch).
5
6use crate::tool_registry::{ToolInvoker, ToolMetadata};
7use serde_json::json;
8use tracing::{debug, info};
9
10/// Webfetch tool invoker
11///
12/// Invokes the webfetch tool to fetch web content from URLs.
13pub struct WebfetchToolInvoker;
14
15#[async_trait::async_trait]
16impl ToolInvoker for WebfetchToolInvoker {
17    async fn invoke(&self, input: serde_json::Value) -> Result<serde_json::Value, String> {
18        debug!("Invoking webfetch tool");
19
20        // Extract URL from input
21        let url = input
22            .get("url")
23            .and_then(|v| v.as_str())
24            .ok_or_else(|| "Missing 'url' field in input".to_string())?;
25
26        info!(url = %url, "Fetching web content");
27
28        // TODO: Implement actual webfetch invocation
29        // For now, return a placeholder response
30        Ok(json!({
31            "success": true,
32            "content": "Web content would be fetched here",
33            "url": url,
34            "metadata": {
35                "provider": "builtin",
36                "truncated": false,
37                "size": 0
38            }
39        }))
40    }
41
42    fn metadata(&self) -> ToolMetadata {
43        ToolMetadata {
44            id: "webfetch".to_string(),
45            name: "Webfetch".to_string(),
46            description: "Fetch and process web content from URLs".to_string(),
47            input_schema: json!({
48                "type": "object",
49                "properties": {
50                    "url": {
51                        "type": "string",
52                        "description": "URL to fetch"
53                    },
54                    "max_size": {
55                        "type": "integer",
56                        "description": "Maximum content size in bytes (optional)"
57                    }
58                },
59                "required": ["url"]
60            }),
61            output_schema: json!({
62                "type": "object",
63                "properties": {
64                    "success": { "type": "boolean" },
65                    "content": { "type": "string" },
66                    "url": { "type": "string" },
67                    "metadata": {
68                        "type": "object",
69                        "properties": {
70                            "provider": { "type": "string" },
71                            "truncated": { "type": "boolean" },
72                            "size": { "type": "integer" }
73                        }
74                    }
75                }
76            }),
77            available: true,
78        }
79    }
80}
81
82/// Patch tool invoker
83///
84/// Invokes the patch tool to apply unified diff patches to files.
85pub struct PatchToolInvoker;
86
87#[async_trait::async_trait]
88impl ToolInvoker for PatchToolInvoker {
89    async fn invoke(&self, input: serde_json::Value) -> Result<serde_json::Value, String> {
90        debug!("Invoking patch tool");
91
92        // Extract file path and patch content from input
93        let file_path = input
94            .get("file_path")
95            .and_then(|v| v.as_str())
96            .ok_or_else(|| "Missing 'file_path' field in input".to_string())?;
97
98        let _patch_content = input
99            .get("patch_content")
100            .and_then(|v| v.as_str())
101            .ok_or_else(|| "Missing 'patch_content' field in input".to_string())?;
102
103        info!(file_path = %file_path, "Applying patch");
104
105        // TODO: Implement actual patch invocation
106        // For now, return a placeholder response
107        Ok(json!({
108            "success": true,
109            "applied_hunks": 0,
110            "failed_hunks": 0,
111            "file_path": file_path,
112            "metadata": {
113                "provider": "builtin"
114            }
115        }))
116    }
117
118    fn metadata(&self) -> ToolMetadata {
119        ToolMetadata {
120            id: "patch".to_string(),
121            name: "Patch".to_string(),
122            description: "Apply unified diff patches to files safely".to_string(),
123            input_schema: json!({
124                "type": "object",
125                "properties": {
126                    "file_path": {
127                        "type": "string",
128                        "description": "Path to the file to patch"
129                    },
130                    "patch_content": {
131                        "type": "string",
132                        "description": "Unified diff patch content"
133                    }
134                },
135                "required": ["file_path", "patch_content"]
136            }),
137            output_schema: json!({
138                "type": "object",
139                "properties": {
140                    "success": { "type": "boolean" },
141                    "applied_hunks": { "type": "integer" },
142                    "failed_hunks": { "type": "integer" },
143                    "file_path": { "type": "string" },
144                    "metadata": {
145                        "type": "object",
146                        "properties": {
147                            "provider": { "type": "string" }
148                        }
149                    }
150                }
151            }),
152            available: true,
153        }
154    }
155}
156
157/// Todowrite tool invoker
158///
159/// Invokes the todowrite tool to create or update todos.
160pub struct TodowriteToolInvoker;
161
162#[async_trait::async_trait]
163impl ToolInvoker for TodowriteToolInvoker {
164    async fn invoke(&self, input: serde_json::Value) -> Result<serde_json::Value, String> {
165        debug!("Invoking todowrite tool");
166
167        // Extract todos from input
168        let todos = input
169            .get("todos")
170            .ok_or_else(|| "Missing 'todos' field in input".to_string())?;
171
172        info!(todo_count = todos.as_array().map(|a| a.len()).unwrap_or(0), "Writing todos");
173
174        // TODO: Implement actual todowrite invocation
175        // For now, return a placeholder response
176        Ok(json!({
177            "success": true,
178            "created": 0,
179            "updated": 0,
180            "metadata": {
181                "provider": "builtin"
182            }
183        }))
184    }
185
186    fn metadata(&self) -> ToolMetadata {
187        ToolMetadata {
188            id: "todowrite".to_string(),
189            name: "Todowrite".to_string(),
190            description: "Create or update todos in the task list".to_string(),
191            input_schema: json!({
192                "type": "object",
193                "properties": {
194                    "todos": {
195                        "type": "array",
196                        "description": "List of todos to create or update",
197                        "items": {
198                            "type": "object",
199                            "properties": {
200                                "id": { "type": "string" },
201                                "title": { "type": "string" },
202                                "description": { "type": "string" },
203                                "status": { "type": "string" },
204                                "priority": { "type": "string" }
205                            }
206                        }
207                    }
208                },
209                "required": ["todos"]
210            }),
211            output_schema: json!({
212                "type": "object",
213                "properties": {
214                    "success": { "type": "boolean" },
215                    "created": { "type": "integer" },
216                    "updated": { "type": "integer" },
217                    "metadata": {
218                        "type": "object",
219                        "properties": {
220                            "provider": { "type": "string" }
221                        }
222                    }
223                }
224            }),
225            available: true,
226        }
227    }
228}
229
230/// Todoread tool invoker
231///
232/// Invokes the todoread tool to read todos from the task list.
233pub struct TodoreadToolInvoker;
234
235#[async_trait::async_trait]
236impl ToolInvoker for TodoreadToolInvoker {
237    async fn invoke(&self, input: serde_json::Value) -> Result<serde_json::Value, String> {
238        debug!("Invoking todoread tool");
239
240        // Extract optional filters from input
241        let status_filter = input.get("status").and_then(|v| v.as_str());
242        let priority_filter = input.get("priority").and_then(|v| v.as_str());
243
244        info!(
245            status_filter = ?status_filter,
246            priority_filter = ?priority_filter,
247            "Reading todos"
248        );
249
250        // TODO: Implement actual todoread invocation
251        // For now, return a placeholder response
252        Ok(json!({
253            "success": true,
254            "todos": [],
255            "metadata": {
256                "provider": "builtin"
257            }
258        }))
259    }
260
261    fn metadata(&self) -> ToolMetadata {
262        ToolMetadata {
263            id: "todoread".to_string(),
264            name: "Todoread".to_string(),
265            description: "Read todos from the task list with optional filtering".to_string(),
266            input_schema: json!({
267                "type": "object",
268                "properties": {
269                    "status": {
270                        "type": "string",
271                        "description": "Filter by status (optional)"
272                    },
273                    "priority": {
274                        "type": "string",
275                        "description": "Filter by priority (optional)"
276                    }
277                }
278            }),
279            output_schema: json!({
280                "type": "object",
281                "properties": {
282                    "success": { "type": "boolean" },
283                    "todos": {
284                        "type": "array",
285                        "items": {
286                            "type": "object",
287                            "properties": {
288                                "id": { "type": "string" },
289                                "title": { "type": "string" },
290                                "description": { "type": "string" },
291                                "status": { "type": "string" },
292                                "priority": { "type": "string" }
293                            }
294                        }
295                    },
296                    "metadata": {
297                        "type": "object",
298                        "properties": {
299                            "provider": { "type": "string" }
300                        }
301                    }
302                }
303            }),
304            available: true,
305        }
306    }
307}
308
309/// Websearch tool invoker
310///
311/// Invokes the websearch tool to search the web.
312pub struct WebsearchToolInvoker;
313
314#[async_trait::async_trait]
315impl ToolInvoker for WebsearchToolInvoker {
316    async fn invoke(&self, input: serde_json::Value) -> Result<serde_json::Value, String> {
317        debug!("Invoking websearch tool");
318
319        // Extract query from input
320        let query = input
321            .get("query")
322            .and_then(|v| v.as_str())
323            .ok_or_else(|| "Missing 'query' field in input".to_string())?;
324
325        let limit = input
326            .get("limit")
327            .and_then(|v| v.as_u64())
328            .unwrap_or(10);
329
330        let offset = input
331            .get("offset")
332            .and_then(|v| v.as_u64())
333            .unwrap_or(0);
334
335        info!(query = %query, limit = %limit, offset = %offset, "Searching web");
336
337        // TODO: Implement actual websearch invocation
338        // For now, return a placeholder response
339        Ok(json!({
340            "success": true,
341            "results": [],
342            "total_count": 0,
343            "query": query,
344            "metadata": {
345                "provider": "builtin",
346                "limit": limit,
347                "offset": offset
348            }
349        }))
350    }
351
352    fn metadata(&self) -> ToolMetadata {
353        ToolMetadata {
354            id: "websearch".to_string(),
355            name: "Websearch".to_string(),
356            description: "Search the web and return ranked results".to_string(),
357            input_schema: json!({
358                "type": "object",
359                "properties": {
360                    "query": {
361                        "type": "string",
362                        "description": "Search query"
363                    },
364                    "limit": {
365                        "type": "integer",
366                        "description": "Maximum number of results (optional, default: 10)"
367                    },
368                    "offset": {
369                        "type": "integer",
370                        "description": "Result offset for pagination (optional, default: 0)"
371                    }
372                },
373                "required": ["query"]
374            }),
375            output_schema: json!({
376                "type": "object",
377                "properties": {
378                    "success": { "type": "boolean" },
379                    "results": {
380                        "type": "array",
381                        "items": {
382                            "type": "object",
383                            "properties": {
384                                "title": { "type": "string" },
385                                "url": { "type": "string" },
386                                "snippet": { "type": "string" },
387                                "rank": { "type": "integer" }
388                            }
389                        }
390                    },
391                    "total_count": { "type": "integer" },
392                    "query": { "type": "string" },
393                    "metadata": {
394                        "type": "object",
395                        "properties": {
396                            "provider": { "type": "string" },
397                            "limit": { "type": "integer" },
398                            "offset": { "type": "integer" }
399                        }
400                    }
401                }
402            }),
403            available: true,
404        }
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[tokio::test]
413    async fn test_webfetch_invoker() {
414        let invoker = WebfetchToolInvoker;
415        let input = json!({
416            "url": "https://example.com"
417        });
418
419        let result = invoker.invoke(input).await;
420        assert!(result.is_ok());
421
422        let output = result.unwrap();
423        assert_eq!(output["success"], true);
424        assert_eq!(output["url"], "https://example.com");
425    }
426
427    #[tokio::test]
428    async fn test_webfetch_missing_url() {
429        let invoker = WebfetchToolInvoker;
430        let input = json!({});
431
432        let result = invoker.invoke(input).await;
433        assert!(result.is_err());
434    }
435
436    #[test]
437    fn test_webfetch_metadata() {
438        let invoker = WebfetchToolInvoker;
439        let metadata = invoker.metadata();
440
441        assert_eq!(metadata.id, "webfetch");
442        assert_eq!(metadata.name, "Webfetch");
443        assert!(metadata.available);
444    }
445
446    #[tokio::test]
447    async fn test_patch_invoker() {
448        let invoker = PatchToolInvoker;
449        let input = json!({
450            "file_path": "src/main.rs",
451            "patch_content": "--- a/src/main.rs\n+++ b/src/main.rs\n"
452        });
453
454        let result = invoker.invoke(input).await;
455        assert!(result.is_ok());
456
457        let output = result.unwrap();
458        assert_eq!(output["success"], true);
459    }
460
461    #[tokio::test]
462    async fn test_patch_missing_fields() {
463        let invoker = PatchToolInvoker;
464        let input = json!({
465            "file_path": "src/main.rs"
466        });
467
468        let result = invoker.invoke(input).await;
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_patch_metadata() {
474        let invoker = PatchToolInvoker;
475        let metadata = invoker.metadata();
476
477        assert_eq!(metadata.id, "patch");
478        assert_eq!(metadata.name, "Patch");
479        assert!(metadata.available);
480    }
481
482    #[tokio::test]
483    async fn test_todowrite_invoker() {
484        let invoker = TodowriteToolInvoker;
485        let input = json!({
486            "todos": [
487                {
488                    "id": "1",
489                    "title": "Task 1",
490                    "description": "Description",
491                    "status": "pending",
492                    "priority": "high"
493                }
494            ]
495        });
496
497        let result = invoker.invoke(input).await;
498        assert!(result.is_ok());
499
500        let output = result.unwrap();
501        assert_eq!(output["success"], true);
502    }
503
504    #[tokio::test]
505    async fn test_todowrite_missing_todos() {
506        let invoker = TodowriteToolInvoker;
507        let input = json!({});
508
509        let result = invoker.invoke(input).await;
510        assert!(result.is_err());
511    }
512
513    #[test]
514    fn test_todowrite_metadata() {
515        let invoker = TodowriteToolInvoker;
516        let metadata = invoker.metadata();
517
518        assert_eq!(metadata.id, "todowrite");
519        assert_eq!(metadata.name, "Todowrite");
520        assert!(metadata.available);
521    }
522
523    #[tokio::test]
524    async fn test_todoread_invoker() {
525        let invoker = TodoreadToolInvoker;
526        let input = json!({
527            "status": "pending"
528        });
529
530        let result = invoker.invoke(input).await;
531        assert!(result.is_ok());
532
533        let output = result.unwrap();
534        assert_eq!(output["success"], true);
535    }
536
537    #[tokio::test]
538    async fn test_todoread_no_filters() {
539        let invoker = TodoreadToolInvoker;
540        let input = json!({});
541
542        let result = invoker.invoke(input).await;
543        assert!(result.is_ok());
544    }
545
546    #[test]
547    fn test_todoread_metadata() {
548        let invoker = TodoreadToolInvoker;
549        let metadata = invoker.metadata();
550
551        assert_eq!(metadata.id, "todoread");
552        assert_eq!(metadata.name, "Todoread");
553        assert!(metadata.available);
554    }
555
556    #[tokio::test]
557    async fn test_websearch_invoker() {
558        let invoker = WebsearchToolInvoker;
559        let input = json!({
560            "query": "rust programming",
561            "limit": 10
562        });
563
564        let result = invoker.invoke(input).await;
565        assert!(result.is_ok());
566
567        let output = result.unwrap();
568        assert_eq!(output["success"], true);
569        assert_eq!(output["query"], "rust programming");
570    }
571
572    #[tokio::test]
573    async fn test_websearch_missing_query() {
574        let invoker = WebsearchToolInvoker;
575        let input = json!({});
576
577        let result = invoker.invoke(input).await;
578        assert!(result.is_err());
579    }
580
581    #[test]
582    fn test_websearch_metadata() {
583        let invoker = WebsearchToolInvoker;
584        let metadata = invoker.metadata();
585
586        assert_eq!(metadata.id, "websearch");
587        assert_eq!(metadata.name, "Websearch");
588        assert!(metadata.available);
589    }
590}