CoolFace
Apppublic

baqr/computer_use_ootb

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
edit.py291 linesDownload Raw Back to tools
1from collections import defaultdict2from pathlib import Path3from typing import Literal, get_args4 5from anthropic.types.beta import BetaToolTextEditor20241022Param6 7from .base import BaseAnthropicTool, CLIResult, ToolError, ToolResult8from .run import maybe_truncate, run9 10Command = Literal[11    "view",12    "create",13    "str_replace",14    "insert",15    "undo_edit",16]17SNIPPET_LINES: int = 418 19 20class EditTool(BaseAnthropicTool):21    """22    An filesystem editor tool that allows the agent to view, create, and edit files.23    The tool parameters are defined by Anthropic and are not editable.24    """25 26    api_type: Literal["text_editor_20241022"] = "text_editor_20241022"27    name: Literal["str_replace_editor"] = "str_replace_editor"28 29    _file_history: dict[Path, list[str]]30 31    def __init__(self):32        self._file_history = defaultdict(list)33        super().__init__()34 35    def to_params(self) -> BetaToolTextEditor20241022Param:36        return {37            "name": self.name,38            "type": self.api_type,39        }40 41    async def __call__(42        self,43        *,44        command: Command,45        path: str,46        file_text: str | None = None,47        view_range: list[int] | None = None,48        old_str: str | None = None,49        new_str: str | None = None,50        insert_line: int | None = None,51        **kwargs,52    ):53        _path = Path(path)54        self.validate_path(command, _path)55        if command == "view":56            return await self.view(_path, view_range)57        elif command == "create":58            if not file_text:59                raise ToolError("Parameter `file_text` is required for command: create")60            self.write_file(_path, file_text)61            self._file_history[_path].append(file_text)62            return ToolResult(output=f"File created successfully at: {_path}")63        elif command == "str_replace":64            if not old_str:65                raise ToolError(66                    "Parameter `old_str` is required for command: str_replace"67                )68            return self.str_replace(_path, old_str, new_str)69        elif command == "insert":70            if insert_line is None:71                raise ToolError(72                    "Parameter `insert_line` is required for command: insert"73                )74            if not new_str:75                raise ToolError("Parameter `new_str` is required for command: insert")76            return self.insert(_path, insert_line, new_str)77        elif command == "undo_edit":78            return self.undo_edit(_path)79        raise ToolError(80            f'Unrecognized command {command}. The allowed commands for the {self.name} tool are: {", ".join(get_args(Command))}'81        )82 83    def validate_path(self, command: str, path: Path):84        """85        Check that the path/command combination is valid.86        """87        # Check if its an absolute path88        if not path.is_absolute():89            suggested_path = Path("") / path90            raise ToolError(91                f"The path {path} is not an absolute path, it should start with `/`. Maybe you meant {suggested_path}?"92            )93        # Check if path exists94        if not path.exists() and command != "create":95            raise ToolError(96                f"The path {path} does not exist. Please provide a valid path."97            )98        if path.exists() and command == "create":99            raise ToolError(100                f"File already exists at: {path}. Cannot overwrite files using command `create`."101            )102        # Check if the path points to a directory103        if path.is_dir():104            if command != "view":105                raise ToolError(106                    f"The path {path} is a directory and only the `view` command can be used on directories"107                )108 109    async def view(self, path: Path, view_range: list[int] | None = None):110        """Implement the view command"""111        if path.is_dir():112            if view_range:113                raise ToolError(114                    "The `view_range` parameter is not allowed when `path` points to a directory."115                )116 117            _, stdout, stderr = await run(118                rf"find {path} -maxdepth 2 -not -path '*/\.*'"119            )120            if not stderr:121                stdout = f"Here's the files and directories up to 2 levels deep in {path}, excluding hidden items:\n{stdout}\n"122            return CLIResult(output=stdout, error=stderr)123 124        file_content = self.read_file(path)125        init_line = 1126        if view_range:127            if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range):128                raise ToolError(129                    "Invalid `view_range`. It should be a list of two integers."130                )131            file_lines = file_content.split("\n")132            n_lines_file = len(file_lines)133            init_line, final_line = view_range134            if init_line < 1 or init_line > n_lines_file:135                raise ToolError(136                    f"Invalid `view_range`: {view_range}. It's first element `{init_line}` should be within the range of lines of the file: {[1, n_lines_file]}"137                )138            if final_line > n_lines_file:139                raise ToolError(140                    f"Invalid `view_range`: {view_range}. It's second element `{final_line}` should be smaller than the number of lines in the file: `{n_lines_file}`"141                )142            if final_line != -1 and final_line < init_line:143                raise ToolError(144                    f"Invalid `view_range`: {view_range}. It's second element `{final_line}` should be larger or equal than its first `{init_line}`"145                )146 147            if final_line == -1:148                file_content = "\n".join(file_lines[init_line - 1 :])149            else:150                file_content = "\n".join(file_lines[init_line - 1 : final_line])151 152        return CLIResult(153            output=self._make_output(file_content, str(path), init_line=init_line)154        )155 156    def str_replace(self, path: Path, old_str: str, new_str: str | None):157        """Implement the str_replace command, which replaces old_str with new_str in the file content"""158        # Read the file content159        file_content = self.read_file(path).expandtabs()160        old_str = old_str.expandtabs()161        new_str = new_str.expandtabs() if new_str is not None else ""162 163        # Check if old_str is unique in the file164        occurrences = file_content.count(old_str)165        if occurrences == 0:166            raise ToolError(167                f"No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}."168            )169        elif occurrences > 1:170            file_content_lines = file_content.split("\n")171            lines = [172                idx + 1173                for idx, line in enumerate(file_content_lines)174                if old_str in line175            ]176            raise ToolError(177                f"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {lines}. Please ensure it is unique"178            )179 180        # Replace old_str with new_str181        new_file_content = file_content.replace(old_str, new_str)182 183        # Write the new content to the file184        self.write_file(path, new_file_content)185 186        # Save the content to history187        self._file_history[path].append(file_content)188 189        # Create a snippet of the edited section190        replacement_line = file_content.split(old_str)[0].count("\n")191        start_line = max(0, replacement_line - SNIPPET_LINES)192        end_line = replacement_line + SNIPPET_LINES + new_str.count("\n")193        snippet = "\n".join(new_file_content.split("\n")[start_line : end_line + 1])194 195        # Prepare the success message196        success_msg = f"The file {path} has been edited. "197        success_msg += self._make_output(198            snippet, f"a snippet of {path}", start_line + 1199        )200        success_msg += "Review the changes and make sure they are as expected. Edit the file again if necessary."201 202        return CLIResult(output=success_msg)203 204    def insert(self, path: Path, insert_line: int, new_str: str):205        """Implement the insert command, which inserts new_str at the specified line in the file content."""206        file_text = self.read_file(path).expandtabs()207        new_str = new_str.expandtabs()208        file_text_lines = file_text.split("\n")209        n_lines_file = len(file_text_lines)210 211        if insert_line < 0 or insert_line > n_lines_file:212            raise ToolError(213                f"Invalid `insert_line` parameter: {insert_line}. It should be within the range of lines of the file: {[0, n_lines_file]}"214            )215 216        new_str_lines = new_str.split("\n")217        new_file_text_lines = (218            file_text_lines[:insert_line]219            + new_str_lines220            + file_text_lines[insert_line:]221        )222        snippet_lines = (223            file_text_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]224            + new_str_lines225            + file_text_lines[insert_line : insert_line + SNIPPET_LINES]226        )227 228        new_file_text = "\n".join(new_file_text_lines)229        snippet = "\n".join(snippet_lines)230 231        self.write_file(path, new_file_text)232        self._file_history[path].append(file_text)233 234        success_msg = f"The file {path} has been edited. "235        success_msg += self._make_output(236            snippet,237            "a snippet of the edited file",238            max(1, insert_line - SNIPPET_LINES + 1),239        )240        success_msg += "Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary."241        return CLIResult(output=success_msg)242 243    def undo_edit(self, path: Path):244        """Implement the undo_edit command."""245        if not self._file_history[path]:246            raise ToolError(f"No edit history found for {path}.")247 248        old_text = self._file_history[path].pop()249        self.write_file(path, old_text)250 251        return CLIResult(252            output=f"Last edit to {path} undone successfully. {self._make_output(old_text, str(path))}"253        )254 255    def read_file(self, path: Path):256        """Read the content of a file from a given path; raise a ToolError if an error occurs."""257        try:258            return path.read_text()259        except Exception as e:260            raise ToolError(f"Ran into {e} while trying to read {path}") from None261 262    def write_file(self, path: Path, file: str):263        """Write the content of a file to a given path; raise a ToolError if an error occurs."""264        try:265            path.write_text(file)266        except Exception as e:267            raise ToolError(f"Ran into {e} while trying to write to {path}") from None268 269    def _make_output(270        self,271        file_content: str,272        file_descriptor: str,273        init_line: int = 1,274        expand_tabs: bool = True,275    ):276        """Generate output for the CLI based on the content of a file."""277        file_content = maybe_truncate(file_content)278        if expand_tabs:279            file_content = file_content.expandtabs()280        file_content = "\n".join(281            [282                f"{i + init_line:6}\t{line}"283                for i, line in enumerate(file_content.split("\n"))284            ]285        )286        return (287            f"Here's the result of running `cat -n` on {file_descriptor}:\n"288            + file_content289            + "\n"290        )291