1use async_trait::async_trait;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::fs;
6use std::path::Path;
7
8use crate::generate_schema;
9use ai_agents_core::{Tool, ToolResult};
10
11pub struct FileTool;
12
13impl FileTool {
14 pub fn new() -> Self {
15 Self
16 }
17}
18
19impl Default for FileTool {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25#[derive(Debug, Deserialize, JsonSchema)]
26struct FileInput {
27 operation: String,
29 path: String,
31 #[serde(default)]
33 content: Option<String>,
34 #[serde(default)]
36 pattern: Option<String>,
37}
38
39#[derive(Debug, Serialize)]
40struct ReadOutput {
41 content: String,
42 path: String,
43 size: usize,
44}
45
46#[derive(Debug, Serialize)]
47struct WriteOutput {
48 success: bool,
49 path: String,
50 bytes_written: usize,
51}
52
53#[derive(Debug, Serialize)]
54struct ExistsOutput {
55 exists: bool,
56 path: String,
57 is_file: bool,
58 is_dir: bool,
59}
60
61#[derive(Debug, Serialize)]
62struct DeleteOutput {
63 success: bool,
64 path: String,
65}
66
67#[derive(Debug, Serialize)]
68struct ListOutput {
69 entries: Vec<ListEntry>,
70 path: String,
71 count: usize,
72}
73
74#[derive(Debug, Serialize)]
75struct ListEntry {
76 name: String,
77 path: String,
78 is_file: bool,
79 is_dir: bool,
80 size: Option<u64>,
81}
82
83#[derive(Debug, Serialize)]
84struct MkdirOutput {
85 success: bool,
86 path: String,
87}
88
89#[derive(Debug, Serialize)]
90struct InfoOutput {
91 path: String,
92 exists: bool,
93 is_file: bool,
94 is_dir: bool,
95 size: Option<u64>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 modified: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 created: Option<String>,
100}
101
102#[async_trait]
103impl Tool for FileTool {
104 fn id(&self) -> &str {
105 "file"
106 }
107
108 fn name(&self) -> &str {
109 "File Operations"
110 }
111
112 fn description(&self) -> &str {
113 "Read, write, and manage files. Operations: read (read file content), write (write content to file), append (append to file), exists (check if path exists), delete (delete file/directory), list (list directory contents), mkdir (create directory), info (get file metadata)."
114 }
115
116 fn input_schema(&self) -> Value {
117 generate_schema::<FileInput>()
118 }
119
120 async fn execute(&self, args: Value) -> ToolResult {
121 let input: FileInput = match serde_json::from_value(args) {
122 Ok(input) => input,
123 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
124 };
125
126 match input.operation.to_lowercase().as_str() {
127 "read" => self.handle_read(&input),
128 "write" => self.handle_write(&input),
129 "append" => self.handle_append(&input),
130 "exists" => self.handle_exists(&input),
131 "delete" => self.handle_delete(&input),
132 "list" => self.handle_list(&input),
133 "mkdir" => self.handle_mkdir(&input),
134 "info" => self.handle_info(&input),
135 _ => ToolResult::error(format!(
136 "Unknown operation: {}. Valid: read, write, append, exists, delete, list, mkdir, info",
137 input.operation
138 )),
139 }
140 }
141}
142
143impl FileTool {
144 fn handle_read(&self, input: &FileInput) -> ToolResult {
145 match fs::read_to_string(&input.path) {
146 Ok(content) => {
147 let output = ReadOutput {
148 size: content.len(),
149 content,
150 path: input.path.clone(),
151 };
152 self.to_result(&output)
153 }
154 Err(e) => ToolResult::error(format!("Read error: {}", e)),
155 }
156 }
157
158 fn handle_write(&self, input: &FileInput) -> ToolResult {
159 let content = input.content.as_deref().unwrap_or("");
160 match fs::write(&input.path, content) {
161 Ok(_) => {
162 let output = WriteOutput {
163 success: true,
164 path: input.path.clone(),
165 bytes_written: content.len(),
166 };
167 self.to_result(&output)
168 }
169 Err(e) => ToolResult::error(format!("Write error: {}", e)),
170 }
171 }
172
173 fn handle_append(&self, input: &FileInput) -> ToolResult {
174 use std::fs::OpenOptions;
175 use std::io::Write;
176
177 let content = input.content.as_deref().unwrap_or("");
178 let file = OpenOptions::new()
179 .create(true)
180 .append(true)
181 .open(&input.path);
182
183 match file {
184 Ok(mut f) => match f.write_all(content.as_bytes()) {
185 Ok(_) => {
186 let output = WriteOutput {
187 success: true,
188 path: input.path.clone(),
189 bytes_written: content.len(),
190 };
191 self.to_result(&output)
192 }
193 Err(e) => ToolResult::error(format!("Append error: {}", e)),
194 },
195 Err(e) => ToolResult::error(format!("File open error: {}", e)),
196 }
197 }
198
199 fn handle_exists(&self, input: &FileInput) -> ToolResult {
200 let path = Path::new(&input.path);
201 let output = ExistsOutput {
202 exists: path.exists(),
203 path: input.path.clone(),
204 is_file: path.is_file(),
205 is_dir: path.is_dir(),
206 };
207 self.to_result(&output)
208 }
209
210 fn handle_delete(&self, input: &FileInput) -> ToolResult {
211 let path = Path::new(&input.path);
212 let result = if path.is_dir() {
213 fs::remove_dir_all(path)
214 } else {
215 fs::remove_file(path)
216 };
217
218 match result {
219 Ok(_) => {
220 let output = DeleteOutput {
221 success: true,
222 path: input.path.clone(),
223 };
224 self.to_result(&output)
225 }
226 Err(e) => ToolResult::error(format!("Delete error: {}", e)),
227 }
228 }
229
230 fn handle_list(&self, input: &FileInput) -> ToolResult {
231 let path = Path::new(&input.path);
232 if !path.is_dir() {
233 return ToolResult::error(format!("Not a directory: {}", input.path));
234 }
235
236 let pattern = input.pattern.as_deref();
237
238 match fs::read_dir(path) {
239 Ok(entries) => {
240 let mut list_entries = Vec::new();
241
242 for entry in entries.flatten() {
243 let file_name = entry.file_name().to_string_lossy().to_string();
244
245 if let Some(pat) = pattern {
246 if !self.matches_pattern(&file_name, pat) {
247 continue;
248 }
249 }
250
251 let metadata = entry.metadata().ok();
252 let entry_path = entry.path();
253
254 list_entries.push(ListEntry {
255 name: file_name,
256 path: entry_path.to_string_lossy().to_string(),
257 is_file: entry_path.is_file(),
258 is_dir: entry_path.is_dir(),
259 size: metadata.map(|m| m.len()),
260 });
261 }
262
263 let output = ListOutput {
264 count: list_entries.len(),
265 entries: list_entries,
266 path: input.path.clone(),
267 };
268 self.to_result(&output)
269 }
270 Err(e) => ToolResult::error(format!("List error: {}", e)),
271 }
272 }
273
274 fn handle_mkdir(&self, input: &FileInput) -> ToolResult {
275 match fs::create_dir_all(&input.path) {
276 Ok(_) => {
277 let output = MkdirOutput {
278 success: true,
279 path: input.path.clone(),
280 };
281 self.to_result(&output)
282 }
283 Err(e) => ToolResult::error(format!("Mkdir error: {}", e)),
284 }
285 }
286
287 fn handle_info(&self, input: &FileInput) -> ToolResult {
288 let path = Path::new(&input.path);
289
290 if !path.exists() {
291 let output = InfoOutput {
292 path: input.path.clone(),
293 exists: false,
294 is_file: false,
295 is_dir: false,
296 size: None,
297 modified: None,
298 created: None,
299 };
300 return self.to_result(&output);
301 }
302
303 let metadata = match fs::metadata(path) {
304 Ok(m) => m,
305 Err(e) => return ToolResult::error(format!("Metadata error: {}", e)),
306 };
307
308 let modified = metadata.modified().ok().map(|t| {
309 let datetime: chrono::DateTime<chrono::Utc> = t.into();
310 datetime.to_rfc3339()
311 });
312
313 let created = metadata.created().ok().map(|t| {
314 let datetime: chrono::DateTime<chrono::Utc> = t.into();
315 datetime.to_rfc3339()
316 });
317
318 let output = InfoOutput {
319 path: input.path.clone(),
320 exists: true,
321 is_file: metadata.is_file(),
322 is_dir: metadata.is_dir(),
323 size: Some(metadata.len()),
324 modified,
325 created,
326 };
327 self.to_result(&output)
328 }
329
330 fn matches_pattern(&self, name: &str, pattern: &str) -> bool {
331 let pattern = pattern.trim();
332 if pattern.is_empty() || pattern == "*" {
333 return true;
334 }
335
336 if pattern.starts_with("*.") {
337 let ext = &pattern[2..];
338 return name.ends_with(&format!(".{}", ext));
339 }
340
341 if pattern.ends_with(".*") {
342 let prefix = &pattern[..pattern.len() - 2];
343 return name.starts_with(prefix);
344 }
345
346 if pattern.starts_with('*') && pattern.ends_with('*') {
347 let middle = &pattern[1..pattern.len() - 1];
348 return name.contains(middle);
349 }
350
351 name == pattern
352 }
353
354 fn to_result<T: Serialize>(&self, output: &T) -> ToolResult {
355 match serde_json::to_string(output) {
356 Ok(json) => ToolResult::ok(json),
357 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use std::fs;
366 use tempfile::tempdir;
367
368 #[tokio::test]
369 async fn test_write_and_read() {
370 let dir = tempdir().unwrap();
371 let file_path = dir.path().join("test.txt");
372 let path_str = file_path.to_str().unwrap();
373 let tool = FileTool::new();
374
375 let result = tool
376 .execute(serde_json::json!({
377 "operation": "write",
378 "path": path_str,
379 "content": "hello world"
380 }))
381 .await;
382 assert!(result.success);
383
384 let result = tool
385 .execute(serde_json::json!({
386 "operation": "read",
387 "path": path_str
388 }))
389 .await;
390 assert!(result.success);
391 assert!(result.output.contains("hello world"));
392 }
393
394 #[tokio::test]
395 async fn test_append() {
396 let dir = tempdir().unwrap();
397 let file_path = dir.path().join("append.txt");
398 let path_str = file_path.to_str().unwrap();
399 let tool = FileTool::new();
400
401 tool.execute(serde_json::json!({
402 "operation": "write",
403 "path": path_str,
404 "content": "line1\n"
405 }))
406 .await;
407
408 tool.execute(serde_json::json!({
409 "operation": "append",
410 "path": path_str,
411 "content": "line2\n"
412 }))
413 .await;
414
415 let content = fs::read_to_string(&file_path).unwrap();
416 assert!(content.contains("line1"));
417 assert!(content.contains("line2"));
418 }
419
420 #[tokio::test]
421 async fn test_exists() {
422 let dir = tempdir().unwrap();
423 let file_path = dir.path().join("exists.txt");
424 let path_str = file_path.to_str().unwrap();
425 let tool = FileTool::new();
426
427 let result = tool
428 .execute(serde_json::json!({
429 "operation": "exists",
430 "path": path_str
431 }))
432 .await;
433 assert!(result.success);
434 assert!(result.output.contains("\"exists\":false"));
435
436 fs::write(&file_path, "test").unwrap();
437
438 let result = tool
439 .execute(serde_json::json!({
440 "operation": "exists",
441 "path": path_str
442 }))
443 .await;
444 assert!(result.success);
445 assert!(result.output.contains("\"exists\":true"));
446 }
447
448 #[tokio::test]
449 async fn test_delete() {
450 let dir = tempdir().unwrap();
451 let file_path = dir.path().join("delete.txt");
452 let path_str = file_path.to_str().unwrap();
453 let tool = FileTool::new();
454
455 fs::write(&file_path, "test").unwrap();
456 assert!(file_path.exists());
457
458 let result = tool
459 .execute(serde_json::json!({
460 "operation": "delete",
461 "path": path_str
462 }))
463 .await;
464 assert!(result.success);
465 assert!(!file_path.exists());
466 }
467
468 #[tokio::test]
469 async fn test_list() {
470 let dir = tempdir().unwrap();
471 let tool = FileTool::new();
472
473 fs::write(dir.path().join("a.txt"), "a").unwrap();
474 fs::write(dir.path().join("b.json"), "b").unwrap();
475 fs::write(dir.path().join("c.txt"), "c").unwrap();
476
477 let result = tool
478 .execute(serde_json::json!({
479 "operation": "list",
480 "path": dir.path().to_str().unwrap()
481 }))
482 .await;
483 assert!(result.success);
484 assert!(result.output.contains("\"count\":3"));
485
486 let result = tool
487 .execute(serde_json::json!({
488 "operation": "list",
489 "path": dir.path().to_str().unwrap(),
490 "pattern": "*.txt"
491 }))
492 .await;
493 assert!(result.success);
494 assert!(result.output.contains("\"count\":2"));
495 }
496
497 #[tokio::test]
498 async fn test_mkdir() {
499 let dir = tempdir().unwrap();
500 let new_dir = dir.path().join("new/nested/dir");
501 let tool = FileTool::new();
502
503 let result = tool
504 .execute(serde_json::json!({
505 "operation": "mkdir",
506 "path": new_dir.to_str().unwrap()
507 }))
508 .await;
509 assert!(result.success);
510 assert!(new_dir.exists());
511 }
512
513 #[tokio::test]
514 async fn test_info() {
515 let dir = tempdir().unwrap();
516 let file_path = dir.path().join("info.txt");
517 let tool = FileTool::new();
518
519 fs::write(&file_path, "test content").unwrap();
520
521 let result = tool
522 .execute(serde_json::json!({
523 "operation": "info",
524 "path": file_path.to_str().unwrap()
525 }))
526 .await;
527 assert!(result.success);
528 assert!(result.output.contains("\"is_file\":true"));
529 assert!(result.output.contains("\"size\":12"));
530 }
531
532 #[tokio::test]
533 async fn test_invalid_operation() {
534 let tool = FileTool::new();
535 let result = tool
536 .execute(serde_json::json!({
537 "operation": "invalid",
538 "path": "/tmp/test"
539 }))
540 .await;
541 assert!(!result.success);
542 }
543}