agents/tools/file_tools.py (198 lines of code) (raw):

"""File operation tools for reading and writing files.""" import asyncio import glob import os from pathlib import Path from .base import Tool class FileReadTool(Tool): """Tool for reading files and listing directories.""" def __init__(self): super().__init__( name="file_read", description=""" Read files or list directory contents. Operations: - read: Read the contents of a file - list: List files in a directory """, input_schema={ "type": "object", "properties": { "operation": { "type": "string", "enum": ["read", "list"], "description": "File operation to perform", }, "path": { "type": "string", "description": "File path for read or directory path", }, "max_lines": { "type": "integer", "description": "Maximum lines to read (0 means no limit)", }, "pattern": { "type": "string", "description": "File pattern to match", }, }, "required": ["operation", "path"], }, ) async def execute( self, operation: str, path: str, max_lines: int = 0, pattern: str = "*", ) -> str: """Execute a file read operation. Args: operation: The operation to perform (read or list) path: The file or directory path max_lines: Maximum lines to read (for read operation, 0 means no limit) pattern: File pattern to match (for list operation) Returns: Result of the operation as string """ if operation == "read": return await self._read_file(path, max_lines) elif operation == "list": return await self._list_files(path, pattern) else: return f"Error: Unsupported operation '{operation}'" async def _read_file(self, path: str, max_lines: int = 0) -> str: """Read a file from disk. Args: path: Path to the file to read max_lines: Maximum number of lines to read (0 means read entire file) """ try: file_path = Path(path) if not file_path.exists(): return f"Error: File not found at {path}" if not file_path.is_file(): return f"Error: {path} is not a file" def read_sync(): with open(file_path, encoding="utf-8", errors="replace") as f: if max_lines > 0: lines = [] for i, line in enumerate(f): if i >= max_lines: break lines.append(line) return "".join(lines) return f.read() return await asyncio.to_thread(read_sync) except Exception as e: return f"Error reading {path}: {str(e)}" async def _list_files(self, directory: str, pattern: str = "*") -> str: """List files in a directory.""" try: dir_path = Path(directory) if not dir_path.exists(): return f"Error: Directory not found at {directory}" if not dir_path.is_dir(): return f"Error: {directory} is not a directory" def list_sync(): search_pattern = f"{directory}/{pattern}" files = glob.glob(search_pattern) if not files: return f"No files found matching {directory}/{pattern}" file_list = [] for file_path in sorted(files): path_obj = Path(file_path) rel_path = str(file_path).replace(str(dir_path) + "/", "") if path_obj.is_dir(): file_list.append(f"📁 {rel_path}/") else: file_list.append(f"📄 {rel_path}") return "\n".join(file_list) return await asyncio.to_thread(list_sync) except Exception as e: return f"Error listing files in {directory}: {str(e)}" class FileWriteTool(Tool): """Tool for writing and editing files.""" def __init__(self): super().__init__( name="file_write", description=""" Write or edit files. Operations: - write: Create or completely replace a file - edit: Make targeted changes to parts of a file """, input_schema={ "type": "object", "properties": { "operation": { "type": "string", "enum": ["write", "edit"], "description": "File operation to perform", }, "path": { "type": "string", "description": "File path to write to or edit", }, "content": { "type": "string", "description": "Content to write", }, "old_text": { "type": "string", "description": "Text to replace (for edit operation)", }, "new_text": { "type": "string", "description": "Replacement text (for edit operation)", }, }, "required": ["operation", "path"], }, ) async def execute( self, operation: str, path: str, content: str = "", old_text: str = "", new_text: str = "", ) -> str: """Execute a file write operation. Args: operation: The operation to perform (write or edit) path: The file path content: Content to write (for write operation) old_text: Text to replace (for edit operation) new_text: Replacement text (for edit operation) Returns: Result of the operation as string """ if operation == "write": if not content: return "Error: content parameter is required" return await self._write_file(path, content) elif operation == "edit": if not old_text or not new_text: return ( "Error: both old_text and new_text parameters " "are required for edit operation" ) return await self._edit_file(path, old_text, new_text) else: return f"Error: Unsupported operation '{operation}'" async def _write_file(self, path: str, content: str) -> str: """Write content to a file.""" try: file_path = Path(path) os.makedirs(file_path.parent, exist_ok=True) def write_sync(): with open(file_path, "w", encoding="utf-8") as f: f.write(content) return ( f"Successfully wrote {len(content)} " f"characters to {path}" ) return await asyncio.to_thread(write_sync) except Exception as e: return f"Error writing to {path}: {str(e)}" async def _edit_file(self, path: str, old_text: str, new_text: str) -> str: """Make targeted changes to a file.""" try: file_path = Path(path) if not file_path.exists(): return f"Error: File not found at {path}" if not file_path.is_file(): return f"Error: {path} is not a file" def edit_sync(): try: with open( file_path, encoding="utf-8", errors="replace" ) as f: content = f.read() if old_text not in content: return ( f"Error: The specified text was not " f"found in {path}" ) # Count occurrences to warn about multiple matches count = content.count(old_text) if count > 1: # Edit with warning about multiple occurrences new_content = content.replace(old_text, new_text) with open(file_path, "w", encoding="utf-8") as f: f.write(new_content) return ( f"Warning: Found {count} occurrences. " f"All were replaced in {path}" ) else: # One occurrence, straightforward replacement new_content = content.replace(old_text, new_text) with open(file_path, "w", encoding="utf-8") as f: f.write(new_content) return f"Successfully edited {path}" except UnicodeDecodeError: return f"Error: {path} appears to be a binary file" return await asyncio.to_thread(edit_sync) except Exception as e: return f"Error editing {path}: {str(e)}"