Skip to main content

ai_agent/tools/
read.rs

1use crate::types::*;
2use std::fs;
3
4pub struct FileReadTool;
5
6impl FileReadTool {
7    pub fn new() -> Self {
8        Self
9    }
10
11    pub fn name(&self) -> &str {
12        "FileRead"
13    }
14
15    pub fn description(&self) -> &str {
16        "Read files from filesystem"
17    }
18
19    pub fn input_schema(&self) -> ToolInputSchema {
20        ToolInputSchema {
21            schema_type: "object".to_string(),
22            properties: serde_json::json!({
23                "path": {
24                    "type": "string",
25                    "description": "The file path to read"
26                }
27            }),
28            required: Some(vec!["path".to_string()]),
29        }
30    }
31
32    pub async fn execute(
33        &self,
34        input: serde_json::Value,
35        context: &ToolContext,
36    ) -> Result<ToolResult, crate::error::AgentError> {
37        let path = input["path"]
38            .as_str()
39            .ok_or_else(|| crate::error::AgentError::Tool("path is required".to_string()))?;
40
41        // Resolve relative paths using cwd from context
42        let path_buf = std::path::PathBuf::from(path);
43
44        // If path is absolute and doesn't exist, try to find it relative to cwd
45        let final_path = if path_buf.is_absolute() && !path_buf.exists() {
46            // Extract filename and try relative to cwd
47            if let Some(filename) = path_buf.file_name() {
48                std::path::Path::new(&context.cwd).join(filename)
49            } else {
50                // Fallback: just use cwd for safety
51                std::path::Path::new(&context.cwd).join(path)
52            }
53        } else if path_buf.is_relative() {
54            std::path::Path::new(&context.cwd).join(path)
55        } else {
56            path_buf
57        };
58
59        let content =
60            fs::read_to_string(&final_path).map_err(|e| crate::error::AgentError::Io(e))?;
61
62        Ok(ToolResult {
63            result_type: "text".to_string(),
64            tool_use_id: "".to_string(),
65            content,
66            is_error: None,
67        })
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_file_read_tool_name() {
77        let tool = FileReadTool::new();
78        assert_eq!(tool.name(), "FileRead");
79    }
80
81    #[test]
82    fn test_file_read_tool_description_contains_read() {
83        let tool = FileReadTool::new();
84        assert!(tool.description().to_lowercase().contains("read"));
85    }
86
87    #[test]
88    fn test_file_read_tool_has_path_in_schema() {
89        let tool = FileReadTool::new();
90        let schema = tool.input_schema();
91        assert!(schema.properties.get("path").is_some());
92    }
93
94    #[tokio::test]
95    async fn test_file_read_tool_execute_reads_file() {
96        // Create a temp file to read
97        let temp_dir = std::env::temp_dir();
98        let temp_file = temp_dir.join("test_read_file.txt");
99        std::fs::write(&temp_file, "Hello, World!").unwrap();
100
101        let tool = FileReadTool::new();
102        let input = serde_json::json!({
103            "path": temp_file.to_str().unwrap()
104        });
105        let context = ToolContext::default();
106
107        let result = tool.execute(input, &context).await;
108        assert!(result.is_ok());
109        let tool_result = result.unwrap();
110        assert!(tool_result.content.contains("Hello, World!"));
111
112        // Cleanup
113        std::fs::remove_file(temp_file).ok();
114    }
115
116    #[tokio::test]
117    async fn test_file_read_tool_returns_error_for_nonexistent_file() {
118        let tool = FileReadTool::new();
119        let input = serde_json::json!({
120            "path": "/nonexistent/file/that/does/not/exist.txt"
121        });
122        let context = ToolContext::default();
123
124        let result = tool.execute(input, &context).await;
125        assert!(result.is_err());
126    }
127}