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 "Read"
13 }
14
15 pub fn description(&self) -> &str {
16 "Read files from filesystem"
17 }
18
19 pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
20 "Read".to_string()
21 }
22
23 pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
24 input.and_then(|inp| inp["file_path"].as_str().map(String::from))
25 }
26
27 pub fn render_tool_result_message(
28 &self,
29 content: &serde_json::Value,
30 ) -> Option<String> {
31 let text = content["content"].as_str()?;
32 let line_count = text.lines().count();
33 Some(format!("{} {} {}", line_count, if line_count == 1 { "line" } else { "lines" }, "read"))
34 }
35
36 pub fn input_schema(&self) -> ToolInputSchema {
37 ToolInputSchema {
38 schema_type: "object".to_string(),
39 properties: serde_json::json!({
40 "file_path": {
41 "type": "string",
42 "description": "The absolute path to the file to read"
43 }
44 }),
45 required: Some(vec!["file_path".to_string()]),
46 }
47 }
48
49 pub async fn execute(
50 &self,
51 input: serde_json::Value,
52 context: &ToolContext,
53 ) -> Result<ToolResult, crate::error::AgentError> {
54 let path = input["file_path"]
55 .as_str()
56 .ok_or_else(|| crate::error::AgentError::Tool("file_path is required".to_string()))?;
57
58 let path_buf = std::path::PathBuf::from(path);
60
61 let final_path = if path_buf.is_absolute() && !path_buf.exists() {
63 if let Some(filename) = path_buf.file_name() {
64 std::path::Path::new(&context.cwd).join(filename)
65 } else {
66 std::path::Path::new(&context.cwd).join(path)
67 }
68 } else if path_buf.is_relative() {
69 std::path::Path::new(&context.cwd).join(path)
70 } else {
71 path_buf
72 };
73
74 let content =
75 fs::read_to_string(&final_path).map_err(|e| crate::error::AgentError::Io(e))?;
76
77 let ext = final_path
79 .extension()
80 .and_then(|e| e.to_str())
81 .unwrap_or("");
82
83 let model = std::env::var("AI_MODEL")
86 .ok()
87 .unwrap_or_else(|| crate::utils::model::get_main_loop_model());
88 if let Err(e) = crate::services::validate_content_tokens(
89 &content, ext, None, None, None, &model,
90 )
91 .await
92 {
93 return Err(crate::error::AgentError::Tool(e.to_string()));
94 }
95
96 Ok(ToolResult {
97 result_type: "text".to_string(),
98 tool_use_id: "".to_string(),
99 content,
100 is_error: None,
101 was_persisted: None,
102 })
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_file_read_tool_name() {
112 let tool = FileReadTool::new();
113 assert_eq!(tool.name(), "Read");
114 }
115
116 #[test]
117 fn test_file_read_tool_description_contains_read() {
118 let tool = FileReadTool::new();
119 assert!(tool.description().to_lowercase().contains("read"));
120 }
121
122 #[test]
123 fn test_file_read_tool_has_path_in_schema() {
124 let tool = FileReadTool::new();
125 let schema = tool.input_schema();
126 assert!(schema.properties.get("file_path").is_some());
127 }
128
129 #[tokio::test]
130 async fn test_file_read_tool_execute_reads_file() {
131 let temp_dir = std::env::temp_dir();
133 let temp_file = temp_dir.join("test_read_file.txt");
134 std::fs::write(&temp_file, "Hello, World!").unwrap();
135
136 let tool = FileReadTool::new();
137 let input = serde_json::json!({
138 "file_path": temp_file.to_str().unwrap()
139 });
140 let context = ToolContext::default();
141
142 let result = tool.execute(input, &context).await;
143 assert!(result.is_ok());
144 let tool_result = result.unwrap();
145 assert!(tool_result.content.contains("Hello, World!"));
146
147 std::fs::remove_file(temp_file).ok();
149 }
150
151 #[tokio::test]
152 async fn test_file_read_tool_returns_error_for_nonexistent_file() {
153 let tool = FileReadTool::new();
154 let input = serde_json::json!({
155 "file_path": "/nonexistent/file/that/does/not/exist.txt"
156 });
157 let context = ToolContext::default();
158
159 let result = tool.execute(input, &context).await;
160 assert!(result.is_err());
161 }
162}