openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -1,683 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import shutil
|
|
4
|
-
import tempfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import get_args
|
|
7
|
-
|
|
8
|
-
from binaryornot.check import is_binary
|
|
9
|
-
|
|
10
|
-
from openhands.sdk.utils.truncate import maybe_truncate
|
|
11
|
-
from openhands.tools.str_replace_editor.definition import (
|
|
12
|
-
CommandLiteral,
|
|
13
|
-
StrReplaceEditorObservation,
|
|
14
|
-
)
|
|
15
|
-
from openhands.tools.str_replace_editor.exceptions import (
|
|
16
|
-
EditorToolParameterInvalidError,
|
|
17
|
-
EditorToolParameterMissingError,
|
|
18
|
-
FileValidationError,
|
|
19
|
-
ToolError,
|
|
20
|
-
)
|
|
21
|
-
from openhands.tools.str_replace_editor.utils.config import SNIPPET_CONTEXT_WINDOW
|
|
22
|
-
from openhands.tools.str_replace_editor.utils.constants import (
|
|
23
|
-
BINARY_FILE_CONTENT_TRUNCATED_NOTICE,
|
|
24
|
-
DIRECTORY_CONTENT_TRUNCATED_NOTICE,
|
|
25
|
-
MAX_RESPONSE_LEN_CHAR,
|
|
26
|
-
TEXT_FILE_CONTENT_TRUNCATED_NOTICE,
|
|
27
|
-
)
|
|
28
|
-
from openhands.tools.str_replace_editor.utils.encoding import (
|
|
29
|
-
EncodingManager,
|
|
30
|
-
with_encoding,
|
|
31
|
-
)
|
|
32
|
-
from openhands.tools.str_replace_editor.utils.history import FileHistoryManager
|
|
33
|
-
from openhands.tools.str_replace_editor.utils.shell import run_shell_cmd
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class FileEditor:
|
|
37
|
-
"""
|
|
38
|
-
An filesystem editor tool that allows the agent to
|
|
39
|
-
- view
|
|
40
|
-
- create
|
|
41
|
-
- navigate
|
|
42
|
-
- edit files
|
|
43
|
-
The tool parameters are defined by Anthropic and are not editable.
|
|
44
|
-
|
|
45
|
-
Original implementation: https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/edit.py
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
MAX_FILE_SIZE_MB = 10 # Maximum file size in MB
|
|
49
|
-
|
|
50
|
-
def __init__(
|
|
51
|
-
self,
|
|
52
|
-
max_file_size_mb: int | None = None,
|
|
53
|
-
workspace_root: str | None = None,
|
|
54
|
-
):
|
|
55
|
-
"""Initialize the editor.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
max_file_size_mb: Maximum file size in MB. If None, uses the default
|
|
59
|
-
MAX_FILE_SIZE_MB.
|
|
60
|
-
workspace_root: Root directory that serves as the current working
|
|
61
|
-
directory for relative path suggestions. Must be an absolute path.
|
|
62
|
-
If None, no path suggestions will be provided for relative paths.
|
|
63
|
-
"""
|
|
64
|
-
self._history_manager = FileHistoryManager(max_history_per_file=10)
|
|
65
|
-
self._max_file_size = (
|
|
66
|
-
(max_file_size_mb or self.MAX_FILE_SIZE_MB) * 1024 * 1024
|
|
67
|
-
) # Convert to bytes
|
|
68
|
-
|
|
69
|
-
# Initialize encoding manager
|
|
70
|
-
self._encoding_manager = EncodingManager()
|
|
71
|
-
|
|
72
|
-
# Set cwd (current working directory) if workspace_root is provided
|
|
73
|
-
if workspace_root is not None:
|
|
74
|
-
workspace_path = Path(workspace_root)
|
|
75
|
-
# Ensure workspace_root is an absolute path
|
|
76
|
-
if not workspace_path.is_absolute():
|
|
77
|
-
raise ValueError(
|
|
78
|
-
f"workspace_root must be an absolute path, got: {workspace_root}"
|
|
79
|
-
)
|
|
80
|
-
self._cwd = workspace_path
|
|
81
|
-
else:
|
|
82
|
-
self._cwd = None # type: ignore
|
|
83
|
-
|
|
84
|
-
def __call__(
|
|
85
|
-
self,
|
|
86
|
-
*,
|
|
87
|
-
command: CommandLiteral,
|
|
88
|
-
path: str,
|
|
89
|
-
file_text: str | None = None,
|
|
90
|
-
view_range: list[int] | None = None,
|
|
91
|
-
old_str: str | None = None,
|
|
92
|
-
new_str: str | None = None,
|
|
93
|
-
insert_line: int | None = None,
|
|
94
|
-
enable_linting: bool = False,
|
|
95
|
-
) -> StrReplaceEditorObservation:
|
|
96
|
-
_path = Path(path)
|
|
97
|
-
self.validate_path(command, _path)
|
|
98
|
-
if command == "view":
|
|
99
|
-
return self.view(_path, view_range)
|
|
100
|
-
elif command == "create":
|
|
101
|
-
if file_text is None:
|
|
102
|
-
raise EditorToolParameterMissingError(command, "file_text")
|
|
103
|
-
self.write_file(_path, file_text)
|
|
104
|
-
self._history_manager.add_history(_path, file_text)
|
|
105
|
-
return StrReplaceEditorObservation(
|
|
106
|
-
path=str(_path),
|
|
107
|
-
new_content=file_text,
|
|
108
|
-
prev_exist=False,
|
|
109
|
-
output=f"File created successfully at: {_path}",
|
|
110
|
-
)
|
|
111
|
-
elif command == "str_replace":
|
|
112
|
-
if old_str is None:
|
|
113
|
-
raise EditorToolParameterMissingError(command, "old_str")
|
|
114
|
-
if new_str == old_str:
|
|
115
|
-
raise EditorToolParameterInvalidError(
|
|
116
|
-
"new_str",
|
|
117
|
-
new_str,
|
|
118
|
-
"No replacement was performed. `new_str` and `old_str` must be "
|
|
119
|
-
"different.",
|
|
120
|
-
)
|
|
121
|
-
return self.str_replace(_path, old_str, new_str)
|
|
122
|
-
elif command == "insert":
|
|
123
|
-
if insert_line is None:
|
|
124
|
-
raise EditorToolParameterMissingError(command, "insert_line")
|
|
125
|
-
if new_str is None:
|
|
126
|
-
raise EditorToolParameterMissingError(command, "new_str")
|
|
127
|
-
return self.insert(_path, insert_line, new_str)
|
|
128
|
-
elif command == "undo_edit":
|
|
129
|
-
return self.undo_edit(_path)
|
|
130
|
-
|
|
131
|
-
raise ToolError(
|
|
132
|
-
f"Unrecognized command {command}. The allowed commands for "
|
|
133
|
-
f"{self.__class__.__name__} tool are: {', '.join(get_args(CommandLiteral))}"
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
@with_encoding
|
|
137
|
-
def _count_lines(self, path: Path, encoding: str = "utf-8") -> int:
|
|
138
|
-
"""
|
|
139
|
-
Count the number of lines in a file safely.
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
path: Path to the file
|
|
143
|
-
encoding: The encoding to use when reading the file (auto-detected by
|
|
144
|
-
decorator)
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
The number of lines in the file
|
|
148
|
-
"""
|
|
149
|
-
with open(path, encoding=encoding) as f:
|
|
150
|
-
return sum(1 for _ in f)
|
|
151
|
-
|
|
152
|
-
@with_encoding
|
|
153
|
-
def str_replace(
|
|
154
|
-
self,
|
|
155
|
-
path: Path,
|
|
156
|
-
old_str: str,
|
|
157
|
-
new_str: str | None,
|
|
158
|
-
) -> StrReplaceEditorObservation:
|
|
159
|
-
"""
|
|
160
|
-
Implement the str_replace command, which replaces old_str with new_str in
|
|
161
|
-
the file content.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
path: Path to the file
|
|
165
|
-
old_str: String to replace
|
|
166
|
-
new_str: Replacement string
|
|
167
|
-
enable_linting: Whether to run linting on the changes
|
|
168
|
-
encoding: The encoding to use (auto-detected by decorator)
|
|
169
|
-
"""
|
|
170
|
-
self.validate_file(path)
|
|
171
|
-
new_str = new_str or ""
|
|
172
|
-
|
|
173
|
-
# Read the entire file first to handle both single-line and multi-line
|
|
174
|
-
# replacements
|
|
175
|
-
file_content = self.read_file(path)
|
|
176
|
-
|
|
177
|
-
# Find all occurrences using regex
|
|
178
|
-
# Escape special regex characters in old_str to match it literally
|
|
179
|
-
pattern = re.escape(old_str)
|
|
180
|
-
occurrences = [
|
|
181
|
-
(
|
|
182
|
-
file_content.count("\n", 0, match.start()) + 1, # line number
|
|
183
|
-
match.group(), # matched text
|
|
184
|
-
match.start(), # start position
|
|
185
|
-
)
|
|
186
|
-
for match in re.finditer(pattern, file_content)
|
|
187
|
-
]
|
|
188
|
-
|
|
189
|
-
if not occurrences:
|
|
190
|
-
# We found no occurrences, possibly because of extra white spaces at
|
|
191
|
-
# either the front or back of the string.
|
|
192
|
-
# Remove the white spaces and try again.
|
|
193
|
-
old_str = old_str.strip()
|
|
194
|
-
new_str = new_str.strip()
|
|
195
|
-
pattern = re.escape(old_str)
|
|
196
|
-
occurrences = [
|
|
197
|
-
(
|
|
198
|
-
file_content.count("\n", 0, match.start()) + 1, # line number
|
|
199
|
-
match.group(), # matched text
|
|
200
|
-
match.start(), # start position
|
|
201
|
-
)
|
|
202
|
-
for match in re.finditer(pattern, file_content)
|
|
203
|
-
]
|
|
204
|
-
if not occurrences:
|
|
205
|
-
raise ToolError(
|
|
206
|
-
f"No replacement was performed, old_str `{old_str}` did not "
|
|
207
|
-
f"appear verbatim in {path}."
|
|
208
|
-
)
|
|
209
|
-
if len(occurrences) > 1:
|
|
210
|
-
line_numbers = sorted(set(line for line, _, _ in occurrences))
|
|
211
|
-
raise ToolError(
|
|
212
|
-
f"No replacement was performed. Multiple occurrences of old_str "
|
|
213
|
-
f"`{old_str}` in lines {line_numbers}. Please ensure it is unique."
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
# We found exactly one occurrence
|
|
217
|
-
replacement_line, matched_text, idx = occurrences[0]
|
|
218
|
-
|
|
219
|
-
# Create new content by replacing just the matched text
|
|
220
|
-
new_file_content = (
|
|
221
|
-
file_content[:idx] + new_str + file_content[idx + len(matched_text) :]
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# Write the new content to the file
|
|
225
|
-
self.write_file(path, new_file_content)
|
|
226
|
-
|
|
227
|
-
# Save the content to history
|
|
228
|
-
self._history_manager.add_history(path, file_content)
|
|
229
|
-
|
|
230
|
-
# Create a snippet of the edited section
|
|
231
|
-
start_line = max(0, replacement_line - SNIPPET_CONTEXT_WINDOW)
|
|
232
|
-
end_line = replacement_line + SNIPPET_CONTEXT_WINDOW + new_str.count("\n")
|
|
233
|
-
|
|
234
|
-
# Read just the snippet range
|
|
235
|
-
snippet = self.read_file(path, start_line=start_line + 1, end_line=end_line)
|
|
236
|
-
|
|
237
|
-
# Prepare the success message
|
|
238
|
-
success_message = f"The file {path} has been edited. "
|
|
239
|
-
success_message += self._make_output(
|
|
240
|
-
snippet, f"a snippet of {path}", start_line + 1
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
success_message += (
|
|
244
|
-
"Review the changes and make sure they are as expected. Edit the "
|
|
245
|
-
"file again if necessary."
|
|
246
|
-
)
|
|
247
|
-
return StrReplaceEditorObservation(
|
|
248
|
-
output=success_message,
|
|
249
|
-
prev_exist=True,
|
|
250
|
-
path=str(path),
|
|
251
|
-
old_content=file_content,
|
|
252
|
-
new_content=new_file_content,
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
def view(
|
|
256
|
-
self, path: Path, view_range: list[int] | None = None
|
|
257
|
-
) -> StrReplaceEditorObservation:
|
|
258
|
-
"""
|
|
259
|
-
View the contents of a file or a directory.
|
|
260
|
-
"""
|
|
261
|
-
if path.is_dir():
|
|
262
|
-
if view_range:
|
|
263
|
-
raise EditorToolParameterInvalidError(
|
|
264
|
-
"view_range",
|
|
265
|
-
view_range,
|
|
266
|
-
"The `view_range` parameter is not allowed when `path` points to "
|
|
267
|
-
"a directory.",
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
# First count hidden files/dirs in current directory only
|
|
271
|
-
# -mindepth 1 excludes . and .. automatically
|
|
272
|
-
_, hidden_stdout, _ = run_shell_cmd(
|
|
273
|
-
rf"find -L {path} -mindepth 1 -maxdepth 1 -name '.*'"
|
|
274
|
-
)
|
|
275
|
-
hidden_count = (
|
|
276
|
-
len(hidden_stdout.strip().split("\n")) if hidden_stdout.strip() else 0
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
# Then get files/dirs up to 2 levels deep, excluding hidden entries at
|
|
280
|
-
# both depth 1 and 2
|
|
281
|
-
_, stdout, stderr = run_shell_cmd(
|
|
282
|
-
rf"find -L {path} -maxdepth 2 -not \( -path '{path}/\.*' -o "
|
|
283
|
-
rf"-path '{path}/*/\.*' \) | sort",
|
|
284
|
-
truncate_notice=DIRECTORY_CONTENT_TRUNCATED_NOTICE,
|
|
285
|
-
)
|
|
286
|
-
if not stderr:
|
|
287
|
-
# Add trailing slashes to directories
|
|
288
|
-
paths = stdout.strip().split("\n") if stdout.strip() else []
|
|
289
|
-
formatted_paths = []
|
|
290
|
-
for p in paths:
|
|
291
|
-
if Path(p).is_dir():
|
|
292
|
-
formatted_paths.append(f"{p}/")
|
|
293
|
-
else:
|
|
294
|
-
formatted_paths.append(p)
|
|
295
|
-
|
|
296
|
-
msg = [
|
|
297
|
-
f"Here's the files and directories up to 2 levels deep in {path}, "
|
|
298
|
-
"excluding hidden items:\n" + "\n".join(formatted_paths)
|
|
299
|
-
]
|
|
300
|
-
if hidden_count > 0:
|
|
301
|
-
msg.append(
|
|
302
|
-
f"\n{hidden_count} hidden files/directories in this directory "
|
|
303
|
-
f"are excluded. You can use 'ls -la {path}' to see them."
|
|
304
|
-
)
|
|
305
|
-
stdout = "\n".join(msg)
|
|
306
|
-
return StrReplaceEditorObservation(
|
|
307
|
-
output=stdout,
|
|
308
|
-
error=stderr,
|
|
309
|
-
path=str(path),
|
|
310
|
-
prev_exist=True,
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
# Validate file and count lines
|
|
314
|
-
self.validate_file(path)
|
|
315
|
-
num_lines = self._count_lines(path)
|
|
316
|
-
|
|
317
|
-
start_line = 1
|
|
318
|
-
if not view_range:
|
|
319
|
-
file_content = self.read_file(path)
|
|
320
|
-
output = self._make_output(file_content, str(path), start_line)
|
|
321
|
-
|
|
322
|
-
return StrReplaceEditorObservation(
|
|
323
|
-
output=output,
|
|
324
|
-
path=str(path),
|
|
325
|
-
prev_exist=True,
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range):
|
|
329
|
-
raise EditorToolParameterInvalidError(
|
|
330
|
-
"view_range",
|
|
331
|
-
view_range,
|
|
332
|
-
"It should be a list of two integers.",
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
start_line, end_line = view_range
|
|
336
|
-
if start_line < 1 or start_line > num_lines:
|
|
337
|
-
raise EditorToolParameterInvalidError(
|
|
338
|
-
"view_range",
|
|
339
|
-
view_range,
|
|
340
|
-
f"Its first element `{start_line}` should be within the range of "
|
|
341
|
-
f"lines of the file: {[1, num_lines]}.",
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
# Normalize end_line and provide a warning if it exceeds file length
|
|
345
|
-
warning_message: str | None = None
|
|
346
|
-
if end_line == -1:
|
|
347
|
-
end_line = num_lines
|
|
348
|
-
elif end_line > num_lines:
|
|
349
|
-
warning_message = (
|
|
350
|
-
f"We only show up to {num_lines} since there're only {num_lines} "
|
|
351
|
-
"lines in this file."
|
|
352
|
-
)
|
|
353
|
-
end_line = num_lines
|
|
354
|
-
|
|
355
|
-
if end_line < start_line:
|
|
356
|
-
raise EditorToolParameterInvalidError(
|
|
357
|
-
"view_range",
|
|
358
|
-
view_range,
|
|
359
|
-
f"Its second element `{end_line}` should be greater than or equal "
|
|
360
|
-
f"to the first element `{start_line}`.",
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
file_content = self.read_file(path, start_line=start_line, end_line=end_line)
|
|
364
|
-
|
|
365
|
-
# Get the detected encoding
|
|
366
|
-
output = self._make_output(
|
|
367
|
-
"\n".join(file_content.splitlines()), str(path), start_line
|
|
368
|
-
) # Remove extra newlines
|
|
369
|
-
|
|
370
|
-
# Prepend warning if we truncated the end_line
|
|
371
|
-
if warning_message:
|
|
372
|
-
output = f"NOTE: {warning_message}\n{output}"
|
|
373
|
-
|
|
374
|
-
return StrReplaceEditorObservation(
|
|
375
|
-
path=str(path),
|
|
376
|
-
output=output,
|
|
377
|
-
prev_exist=True,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
@with_encoding
|
|
381
|
-
def write_file(self, path: Path, file_text: str, encoding: str = "utf-8") -> None:
|
|
382
|
-
"""
|
|
383
|
-
Write the content of a file to a given path; raise a ToolError if an
|
|
384
|
-
error occurs.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
path: Path to the file to write
|
|
388
|
-
file_text: Content to write to the file
|
|
389
|
-
encoding: The encoding to use when writing the file (auto-detected by
|
|
390
|
-
decorator)
|
|
391
|
-
"""
|
|
392
|
-
self.validate_file(path)
|
|
393
|
-
try:
|
|
394
|
-
# Use open with encoding instead of path.write_text
|
|
395
|
-
with open(path, "w", encoding=encoding) as f:
|
|
396
|
-
f.write(file_text)
|
|
397
|
-
except Exception as e:
|
|
398
|
-
raise ToolError(f"Ran into {e} while trying to write to {path}") from None
|
|
399
|
-
|
|
400
|
-
@with_encoding
|
|
401
|
-
def insert(
|
|
402
|
-
self,
|
|
403
|
-
path: Path,
|
|
404
|
-
insert_line: int,
|
|
405
|
-
new_str: str,
|
|
406
|
-
encoding: str = "utf-8",
|
|
407
|
-
) -> StrReplaceEditorObservation:
|
|
408
|
-
"""
|
|
409
|
-
Implement the insert command, which inserts new_str at the specified line
|
|
410
|
-
in the file content.
|
|
411
|
-
|
|
412
|
-
Args:
|
|
413
|
-
path: Path to the file
|
|
414
|
-
insert_line: Line number where to insert the new content
|
|
415
|
-
new_str: Content to insert
|
|
416
|
-
enable_linting: Whether to run linting on the changes
|
|
417
|
-
encoding: The encoding to use (auto-detected by decorator)
|
|
418
|
-
"""
|
|
419
|
-
# Validate file and count lines
|
|
420
|
-
self.validate_file(path)
|
|
421
|
-
num_lines = self._count_lines(path)
|
|
422
|
-
|
|
423
|
-
if insert_line < 0 or insert_line > num_lines:
|
|
424
|
-
raise EditorToolParameterInvalidError(
|
|
425
|
-
"insert_line",
|
|
426
|
-
insert_line,
|
|
427
|
-
f"It should be within the range of allowed values: {[0, num_lines]}",
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
new_str_lines = new_str.split("\n")
|
|
431
|
-
|
|
432
|
-
# Create temporary file for the new content
|
|
433
|
-
with tempfile.NamedTemporaryFile(
|
|
434
|
-
mode="w", encoding=encoding, delete=False
|
|
435
|
-
) as temp_file:
|
|
436
|
-
# Copy lines before insert point and save them for history
|
|
437
|
-
history_lines = []
|
|
438
|
-
with open(path, "r", encoding=encoding) as f:
|
|
439
|
-
for i, line in enumerate(f, 1):
|
|
440
|
-
if i > insert_line:
|
|
441
|
-
break
|
|
442
|
-
temp_file.write(line)
|
|
443
|
-
history_lines.append(line)
|
|
444
|
-
|
|
445
|
-
# Insert new content
|
|
446
|
-
for line in new_str_lines:
|
|
447
|
-
temp_file.write(line + "\n")
|
|
448
|
-
|
|
449
|
-
# Copy remaining lines and save them for history
|
|
450
|
-
with open(path, "r", encoding=encoding) as f:
|
|
451
|
-
for i, line in enumerate(f, 1):
|
|
452
|
-
if i <= insert_line:
|
|
453
|
-
continue
|
|
454
|
-
temp_file.write(line)
|
|
455
|
-
history_lines.append(line)
|
|
456
|
-
|
|
457
|
-
# Move temporary file to original location
|
|
458
|
-
shutil.move(temp_file.name, path)
|
|
459
|
-
|
|
460
|
-
# Read just the snippet range
|
|
461
|
-
start_line = max(0, insert_line - SNIPPET_CONTEXT_WINDOW)
|
|
462
|
-
end_line = min(
|
|
463
|
-
num_lines + len(new_str_lines),
|
|
464
|
-
insert_line + SNIPPET_CONTEXT_WINDOW + len(new_str_lines),
|
|
465
|
-
)
|
|
466
|
-
snippet = self.read_file(path, start_line=start_line + 1, end_line=end_line)
|
|
467
|
-
|
|
468
|
-
# Save history - we already have the lines in memory
|
|
469
|
-
file_text = "".join(history_lines)
|
|
470
|
-
self._history_manager.add_history(path, file_text)
|
|
471
|
-
|
|
472
|
-
# Read new content for result
|
|
473
|
-
new_file_text = self.read_file(path)
|
|
474
|
-
|
|
475
|
-
success_message = f"The file {path} has been edited. "
|
|
476
|
-
success_message += self._make_output(
|
|
477
|
-
snippet,
|
|
478
|
-
"a snippet of the edited file",
|
|
479
|
-
max(1, insert_line - SNIPPET_CONTEXT_WINDOW + 1),
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
success_message += (
|
|
483
|
-
"Review the changes and make sure they are as expected (correct "
|
|
484
|
-
"indentation, no duplicate lines, etc). Edit the file again if necessary."
|
|
485
|
-
)
|
|
486
|
-
return StrReplaceEditorObservation(
|
|
487
|
-
output=success_message,
|
|
488
|
-
prev_exist=True,
|
|
489
|
-
path=str(path),
|
|
490
|
-
old_content=file_text,
|
|
491
|
-
new_content=new_file_text,
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
def validate_path(self, command: CommandLiteral, path: Path) -> None:
|
|
495
|
-
"""
|
|
496
|
-
Check that the path/command combination is valid.
|
|
497
|
-
|
|
498
|
-
Validates:
|
|
499
|
-
1. Path is absolute
|
|
500
|
-
2. Path and command are compatible
|
|
501
|
-
"""
|
|
502
|
-
# Check if its an absolute path
|
|
503
|
-
if not path.is_absolute():
|
|
504
|
-
suggestion_message = (
|
|
505
|
-
"The path should be an absolute path, starting with `/`."
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
# Only suggest the absolute path if cwd is provided and the path exists
|
|
509
|
-
if self._cwd is not None:
|
|
510
|
-
suggested_path = self._cwd / path
|
|
511
|
-
if suggested_path.exists():
|
|
512
|
-
suggestion_message += f" Maybe you meant {suggested_path}?"
|
|
513
|
-
|
|
514
|
-
raise EditorToolParameterInvalidError(
|
|
515
|
-
"path",
|
|
516
|
-
path,
|
|
517
|
-
suggestion_message,
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
# Check if path and command are compatible
|
|
521
|
-
if command == "create" and path.exists():
|
|
522
|
-
raise EditorToolParameterInvalidError(
|
|
523
|
-
"path",
|
|
524
|
-
path,
|
|
525
|
-
f"File already exists at: {path}. Cannot overwrite files using "
|
|
526
|
-
"command `create`.",
|
|
527
|
-
)
|
|
528
|
-
if command != "create" and not path.exists():
|
|
529
|
-
raise EditorToolParameterInvalidError(
|
|
530
|
-
"path",
|
|
531
|
-
path,
|
|
532
|
-
f"The path {path} does not exist. Please provide a valid path.",
|
|
533
|
-
)
|
|
534
|
-
if command != "view":
|
|
535
|
-
if path.is_dir():
|
|
536
|
-
raise EditorToolParameterInvalidError(
|
|
537
|
-
"path",
|
|
538
|
-
path,
|
|
539
|
-
f"The path {path} is a directory and only the `view` command can "
|
|
540
|
-
"be used on directories.",
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
def undo_edit(self, path: Path) -> StrReplaceEditorObservation:
|
|
544
|
-
"""
|
|
545
|
-
Implement the undo_edit command.
|
|
546
|
-
"""
|
|
547
|
-
current_text = self.read_file(path)
|
|
548
|
-
old_text = self._history_manager.pop_last_history(path)
|
|
549
|
-
if old_text is None:
|
|
550
|
-
raise ToolError(f"No edit history found for {path}.")
|
|
551
|
-
|
|
552
|
-
self.write_file(path, old_text)
|
|
553
|
-
|
|
554
|
-
return StrReplaceEditorObservation(
|
|
555
|
-
output=(
|
|
556
|
-
f"Last edit to {path} undone successfully. "
|
|
557
|
-
f"{self._make_output(old_text, str(path))}"
|
|
558
|
-
),
|
|
559
|
-
path=str(path),
|
|
560
|
-
prev_exist=True,
|
|
561
|
-
old_content=current_text,
|
|
562
|
-
new_content=old_text,
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
def validate_file(self, path: Path) -> None:
|
|
566
|
-
"""
|
|
567
|
-
Validate a file for reading or editing operations.
|
|
568
|
-
|
|
569
|
-
Args:
|
|
570
|
-
path: Path to the file to validate
|
|
571
|
-
|
|
572
|
-
Raises:
|
|
573
|
-
FileValidationError: If the file fails validation
|
|
574
|
-
"""
|
|
575
|
-
# Skip validation for directories or non-existent files (for create command)
|
|
576
|
-
if not path.exists() or not path.is_file():
|
|
577
|
-
return
|
|
578
|
-
|
|
579
|
-
# Check file size
|
|
580
|
-
file_size = os.path.getsize(path)
|
|
581
|
-
max_size = self._max_file_size
|
|
582
|
-
if file_size > max_size:
|
|
583
|
-
raise FileValidationError(
|
|
584
|
-
path=str(path),
|
|
585
|
-
reason=(
|
|
586
|
-
f"File is too large ({file_size / 1024 / 1024:.1f}MB). "
|
|
587
|
-
f"Maximum allowed size is {int(max_size / 1024 / 1024)}MB."
|
|
588
|
-
),
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
# Check file type
|
|
592
|
-
if is_binary(str(path)):
|
|
593
|
-
raise FileValidationError(
|
|
594
|
-
path=str(path),
|
|
595
|
-
reason=(
|
|
596
|
-
"File appears to be binary and this file type cannot be read "
|
|
597
|
-
"or edited by this tool."
|
|
598
|
-
),
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
@with_encoding
|
|
602
|
-
def read_file(
|
|
603
|
-
self,
|
|
604
|
-
path: Path,
|
|
605
|
-
start_line: int | None = None,
|
|
606
|
-
end_line: int | None = None,
|
|
607
|
-
encoding: str = "utf-8", # Default will be overridden by decorator
|
|
608
|
-
) -> str:
|
|
609
|
-
"""
|
|
610
|
-
Read the content of a file from a given path; raise a ToolError if an
|
|
611
|
-
error occurs.
|
|
612
|
-
|
|
613
|
-
Args:
|
|
614
|
-
path: Path to the file to read
|
|
615
|
-
start_line: Optional start line number (1-based). If provided with
|
|
616
|
-
end_line, only reads that range.
|
|
617
|
-
end_line: Optional end line number (1-based). Must be provided with
|
|
618
|
-
start_line.
|
|
619
|
-
encoding: The encoding to use when reading the file (auto-detected by
|
|
620
|
-
decorator)
|
|
621
|
-
"""
|
|
622
|
-
self.validate_file(path)
|
|
623
|
-
try:
|
|
624
|
-
if start_line is not None and end_line is not None:
|
|
625
|
-
# Read only the specified line range
|
|
626
|
-
lines = []
|
|
627
|
-
with open(path, "r", encoding=encoding) as f:
|
|
628
|
-
for i, line in enumerate(f, 1):
|
|
629
|
-
if i > end_line:
|
|
630
|
-
break
|
|
631
|
-
if i >= start_line:
|
|
632
|
-
lines.append(line)
|
|
633
|
-
return "".join(lines)
|
|
634
|
-
elif start_line is not None or end_line is not None:
|
|
635
|
-
raise ValueError(
|
|
636
|
-
"Both start_line and end_line must be provided together"
|
|
637
|
-
)
|
|
638
|
-
else:
|
|
639
|
-
# Use line-by-line reading to avoid loading entire file into memory
|
|
640
|
-
with open(path, "r", encoding=encoding) as f:
|
|
641
|
-
return "".join(f)
|
|
642
|
-
except Exception as e:
|
|
643
|
-
raise ToolError(f"Ran into {e} while trying to read {path}") from None
|
|
644
|
-
|
|
645
|
-
def _make_output(
|
|
646
|
-
self,
|
|
647
|
-
snippet_content: str,
|
|
648
|
-
snippet_description: str,
|
|
649
|
-
start_line: int = 1,
|
|
650
|
-
is_converted_markdown: bool = False,
|
|
651
|
-
) -> str:
|
|
652
|
-
"""
|
|
653
|
-
Generate output for the CLI based on the content of a code snippet.
|
|
654
|
-
"""
|
|
655
|
-
# If the content is converted from Markdown, we don't need line numbers
|
|
656
|
-
if is_converted_markdown:
|
|
657
|
-
snippet_content = maybe_truncate(
|
|
658
|
-
snippet_content,
|
|
659
|
-
truncate_after=MAX_RESPONSE_LEN_CHAR,
|
|
660
|
-
truncate_notice=BINARY_FILE_CONTENT_TRUNCATED_NOTICE,
|
|
661
|
-
)
|
|
662
|
-
return (
|
|
663
|
-
f"Here's the content of the file {snippet_description} displayed in "
|
|
664
|
-
"Markdown format:\n" + snippet_content + "\n"
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
snippet_content = maybe_truncate(
|
|
668
|
-
snippet_content,
|
|
669
|
-
truncate_after=MAX_RESPONSE_LEN_CHAR,
|
|
670
|
-
truncate_notice=TEXT_FILE_CONTENT_TRUNCATED_NOTICE,
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
snippet_content = "\n".join(
|
|
674
|
-
[
|
|
675
|
-
f"{i + start_line:6}\t{line}"
|
|
676
|
-
for i, line in enumerate(snippet_content.split("\n"))
|
|
677
|
-
]
|
|
678
|
-
)
|
|
679
|
-
return (
|
|
680
|
-
f"Here's the result of running `cat -n` on {snippet_description}:\n"
|
|
681
|
-
+ snippet_content
|
|
682
|
-
+ "\n"
|
|
683
|
-
)
|