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.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. 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
- )