Skip to content

Commit 9605106

Browse files
authored
feat: append_file incl. all tests [agentskills] (#2346)
* new skill: append_file incl. all tests * more tests needed caring * file_name for append_file/edit_file; updated tests
1 parent a5f5bc3 commit 9605106

File tree

20 files changed

+512
-140
lines changed

20 files changed

+512
-140
lines changed

agenthub/codeact_agent/prompt.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
COMMAND_DOCS = (
66
'\nApart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:\n'
77
f'{_AGENT_SKILLS_DOCS}'
8-
"Please note that THE `edit_file` FUNCTION REQUIRES PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
8+
"Please note that THE `edit_file` and `append_file` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
99
)
1010

1111
# ======= SYSTEM MESSAGE =======
@@ -74,7 +74,7 @@ def index():
7474
7575
if __name__ == '__main__':
7676
app.run(port=5000)\"\"\"
77-
edit_file(start=1, end=1, content=EDITED_CODE)
77+
edit_file('app.py', start=1, end=1, content=EDITED_CODE)
7878
</execute_ipython>
7979
8080
USER:
@@ -213,7 +213,7 @@ def index():
213213
ASSISTANT:
214214
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
215215
<execute_ipython>
216-
edit_file(start=7, end=7, content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'")
216+
edit_file('app.py', start=7, end=7, content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'")
217217
</execute_ipython>
218218
219219
USER:

opendevin/runtime/plugins/agent_skills/agentskills.py

Lines changed: 165 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
- search_dir(search_term, dir_path='./'): Searches for a term in all files in the specified directory.
1313
- search_file(search_term, file_path=None): Searches for a term in the specified file or the currently open file.
1414
- find_file(file_name, dir_path='./'): Finds all files with the given name in the specified directory.
15-
- edit_file(start, end, content): Replaces lines in a file with the given content.
15+
- edit_file(file_name, start, end, content): Replaces lines in a file with the given content.
16+
- append_file(file_name, content): Appends given content to a file.
1617
"""
1718

1819
import base64
1920
import functools
2021
import os
2122
import subprocess
23+
import tempfile
2224
from inspect import signature
2325
from typing import Optional
2426

@@ -66,12 +68,13 @@ def wrapper(*args, **kwargs):
6668
return wrapper
6769

6870

69-
def _lint_file(file_path: str) -> Optional[str]:
71+
def _lint_file(file_path: str) -> tuple[Optional[str], Optional[int]]:
7072
"""
71-
Lint the file at the given path.
73+
Lint the file at the given path and return a tuple with a boolean indicating if there are errors,
74+
and the line number of the first error, if any.
7275
7376
Returns:
74-
Optional[str]: A string containing the linting report if the file failed to lint, None otherwise.
77+
tuple[str, Optional[int]]: (lint_error, first_error_line_number)
7578
"""
7679

7780
if file_path.endswith('.py'):
@@ -91,13 +94,28 @@ def _lint_file(file_path: str) -> Optional[str]:
9194
)
9295
if result.returncode == 0:
9396
# Linting successful. No issues found.
94-
return None
95-
else:
96-
ret = 'ERRORS:\n'
97-
ret += result.stdout.decode().strip()
98-
return ret.rstrip('\n')
97+
return None, None
98+
99+
# Extract the line number from the first error message
100+
error_message = result.stdout.decode().strip()
101+
lint_error = 'ERRORS:\n' + error_message
102+
first_error_line = None
103+
for line in error_message.split('\n'):
104+
if line.strip():
105+
# The format of the error message is: <filename>:<line>:<column>: <error code> <error message>
106+
parts = line.split(':')
107+
if len(parts) >= 2:
108+
try:
109+
first_error_line = int(parts[1])
110+
break
111+
except ValueError:
112+
# Not a valid line number, continue to the next line
113+
continue
114+
115+
return lint_error, first_error_line
116+
99117
# Not a python file, skip linting
100-
return None
118+
return None, None
101119

102120

103121
def _print_window(CURRENT_FILE, CURRENT_LINE, WINDOW, return_str=False):
@@ -247,25 +265,26 @@ def create_file(filename: str) -> None:
247265

248266

249267
@update_pwd_decorator
250-
def edit_file(start: int, end: int, content: str) -> None:
268+
def edit_file(file_name: str, start: int, end: int, content: str) -> None:
251269
"""Edit a file.
252270
253-
It replaces lines `start` through `end` (inclusive) with the given text `content` in the open file. Remember, the file must be open before editing.
271+
Replaces in given file `file_name` the lines `start` through `end` (inclusive) with the given text `content`.
254272
255273
Args:
274+
file_name: str: The name of the file to edit.
256275
start: int: The start line number. Must satisfy start >= 1.
257276
end: int: The end line number. Must satisfy start <= end <= number of lines in the file.
258277
content: str: The content to replace the lines with.
259278
"""
260279
global CURRENT_FILE, CURRENT_LINE, WINDOW
261-
if not CURRENT_FILE or not os.path.isfile(CURRENT_FILE):
262-
raise FileNotFoundError('No file open. Use the open_file function first.')
280+
if not os.path.isfile(file_name):
281+
raise FileNotFoundError(f'File {file_name} not found.')
263282

264283
# Load the file
265-
with open(CURRENT_FILE, 'r') as file:
284+
with open(file_name, 'r') as file:
266285
lines = file.readlines()
267286

268-
ERROR_MSG = f'[Error editing opened file {CURRENT_FILE}. Please confirm the opened file is correct.]'
287+
ERROR_MSG = f'[Error editing file {file_name}. Please confirm the file is correct.]'
269288
ERROR_MSG_SUFFIX = (
270289
'Your changes have NOT been applied. Please fix your edit command and try again.\n'
271290
'You either need to 1) Open the correct file and try again or 2) Specify the correct start/end line arguments.\n'
@@ -296,24 +315,29 @@ def edit_file(start: int, end: int, content: str) -> None:
296315
return
297316

298317
edited_content = content + '\n'
299-
n_edited_lines = len(edited_content.split('\n'))
300318
new_lines = lines[: start - 1] + [edited_content] + lines[end:]
301319

302320
# directly write edited lines to the file
303-
with open(CURRENT_FILE, 'w') as file:
321+
with open(file_name, 'w') as file:
304322
file.writelines(new_lines)
305323

324+
# set current line to the center of the edited lines
325+
CURRENT_LINE = (start + end) // 2
326+
first_error_line = None
327+
306328
# Handle linting
307329
if ENABLE_AUTO_LINT:
308330
# BACKUP the original file
309331
original_file_backup_path = os.path.join(
310-
os.path.dirname(CURRENT_FILE), f'.backup.{os.path.basename(CURRENT_FILE)}'
332+
os.path.dirname(file_name), f'.backup.{os.path.basename(file_name)}'
311333
)
312334
with open(original_file_backup_path, 'w') as f:
313335
f.writelines(lines)
314336

315-
lint_error = _lint_file(CURRENT_FILE)
316-
if lint_error:
337+
lint_error, first_error_line = _lint_file(file_name)
338+
if lint_error is not None:
339+
if first_error_line is not None:
340+
CURRENT_LINE = int(first_error_line)
317341
# only change any literal strings here in combination with unit tests!
318342
print(
319343
'[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]'
@@ -322,8 +346,8 @@ def edit_file(start: int, end: int, content: str) -> None:
322346

323347
print('[This is how your edit would have looked if applied]')
324348
print('-------------------------------------------------')
325-
cur_line = (n_edited_lines // 2) + start
326-
_print_window(CURRENT_FILE, cur_line, 10)
349+
cur_line = first_error_line
350+
_print_window(file_name, cur_line, 10)
327351
print('-------------------------------------------------\n')
328352

329353
print('[This is the original code before your edit]')
@@ -339,25 +363,137 @@ def edit_file(start: int, end: int, content: str) -> None:
339363

340364
# recover the original file
341365
with open(original_file_backup_path, 'r') as fin, open(
342-
CURRENT_FILE, 'w'
366+
file_name, 'w'
343367
) as fout:
344368
fout.write(fin.read())
345369
os.remove(original_file_backup_path)
346370
return
347371

348372
os.remove(original_file_backup_path)
349373

350-
with open(CURRENT_FILE, 'r') as file:
374+
# Update the file information and print the updated content
375+
with open(file_name, 'r') as file:
351376
n_total_lines = len(file.readlines())
352-
# set current line to the center of the edited lines
353-
CURRENT_LINE = (start + end) // 2
377+
if first_error_line is not None and int(first_error_line) > 0:
378+
CURRENT_LINE = first_error_line
379+
else:
380+
CURRENT_LINE = n_total_lines
354381
print(
355-
f'[File: {os.path.abspath(CURRENT_FILE)} ({n_total_lines} lines total after edit)]'
382+
f'[File: {os.path.abspath(file_name)} ({n_total_lines} lines total after edit)]'
356383
)
384+
CURRENT_FILE = file_name
357385
_print_window(CURRENT_FILE, CURRENT_LINE, WINDOW)
358386
print(MSG_FILE_UPDATED)
359387

360388

389+
@update_pwd_decorator
390+
def append_file(file_name: str, content: str) -> None:
391+
"""Append content to the given file.
392+
393+
It appends text `content` to the end of the specified file.
394+
395+
Args:
396+
file_name: str: The name of the file to append to.
397+
content: str: The content to append to the file.
398+
"""
399+
global CURRENT_FILE, CURRENT_LINE, WINDOW
400+
if not os.path.isfile(file_name):
401+
raise FileNotFoundError(f'File {file_name} not found.')
402+
403+
# Use a temporary file to write changes
404+
temp_file_path = ''
405+
first_error_line = None
406+
try:
407+
# Create a temporary file
408+
with tempfile.NamedTemporaryFile('w', delete=False) as temp_file:
409+
temp_file_path = temp_file.name
410+
411+
# Read the original file and check if empty and for a trailing newline
412+
with open(file_name, 'r') as original_file:
413+
lines = original_file.readlines()
414+
415+
if lines and not (len(lines) == 1 and lines[0].strip() == ''):
416+
if not lines[-1].endswith('\n'):
417+
lines[-1] += '\n'
418+
content = ''.join(lines) + content
419+
else:
420+
content = content
421+
422+
if not content.endswith('\n'):
423+
content += '\n'
424+
425+
# Append the new content with a trailing newline
426+
temp_file.write(content)
427+
428+
# Replace the original file with the temporary file atomically
429+
os.replace(temp_file_path, file_name)
430+
431+
# Handle linting
432+
if ENABLE_AUTO_LINT:
433+
# BACKUP the original file
434+
original_file_backup_path = os.path.join(
435+
os.path.dirname(file_name),
436+
f'.backup.{os.path.basename(file_name)}',
437+
)
438+
with open(original_file_backup_path, 'w') as f:
439+
f.writelines(lines)
440+
441+
lint_error, first_error_line = _lint_file(file_name)
442+
if lint_error is not None:
443+
if first_error_line is not None:
444+
CURRENT_LINE = int(first_error_line)
445+
print(
446+
'[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]'
447+
)
448+
print(lint_error)
449+
450+
print('[This is how your edit would have looked if applied]')
451+
print('-------------------------------------------------')
452+
_print_window(file_name, CURRENT_LINE, 10)
453+
print('-------------------------------------------------\n')
454+
455+
print('[This is the original code before your edit]')
456+
print('-------------------------------------------------')
457+
_print_window(original_file_backup_path, CURRENT_LINE, 10)
458+
print('-------------------------------------------------')
459+
460+
print(
461+
'Your changes have NOT been applied. Please fix your edit command and try again.\n'
462+
'You need to correct your added code.\n'
463+
'DO NOT re-run the same failed edit command. Running it again will lead to the same error.'
464+
)
465+
466+
# recover the original file
467+
with open(original_file_backup_path, 'r') as fin, open(
468+
file_name, 'w'
469+
) as fout:
470+
fout.write(fin.read())
471+
os.remove(original_file_backup_path)
472+
return
473+
474+
except Exception as e:
475+
# Clean up the temporary file if an error occurs
476+
if temp_file_path and os.path.exists(temp_file_path):
477+
os.remove(temp_file_path)
478+
raise e
479+
480+
# Update the file information and print the updated content
481+
with open(file_name, 'r', encoding='utf-8') as file:
482+
n_total_lines = len(file.readlines())
483+
if first_error_line is not None and int(first_error_line) > 0:
484+
CURRENT_LINE = first_error_line
485+
else:
486+
CURRENT_LINE = n_total_lines
487+
print(
488+
f'[File: {os.path.abspath(file_name)} ({n_total_lines} lines total after edit)]'
489+
)
490+
CURRENT_FILE = file_name
491+
_print_window(CURRENT_FILE, CURRENT_LINE, WINDOW)
492+
print(
493+
'[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]'
494+
)
495+
496+
361497
@update_pwd_decorator
362498
def search_dir(search_term: str, dir_path: str = './') -> None:
363499
"""Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
@@ -672,6 +808,7 @@ def parse_pptx(file_path: str) -> None:
672808
'scroll_down',
673809
'scroll_up',
674810
'create_file',
811+
'append_file',
675812
'edit_file',
676813
'search_dir',
677814
'search_file',

tests/integration/mock/CodeActAgent/test_browse_internet/prompt_001.log

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ create_file(filename: str) -> None:
5151
Args:
5252
filename: str: The name of the file to create.
5353

54-
edit_file(start: int, end: int, content: str) -> None:
54+
append_file(file_name: str, content: str) -> None:
55+
Append content to the given file.
56+
It appends text `content` to the end of the specified file.
57+
Args:
58+
file_name: str: The name of the file to append to.
59+
content: str: The content to append to the file.
60+
61+
edit_file(file_name: str, start: int, end: int, content: str) -> None:
5562
Edit a file.
56-
It replaces lines `start` through `end` (inclusive) with the given text `content` in the open file. Remember, the file must be open before editing.
63+
Replaces in given file `file_name` the lines `start` through `end` (inclusive) with the given text `content`.
5764
Args:
65+
file_name: str: The name of the file to edit.
5866
start: int: The start line number. Must satisfy start >= 1.
5967
end: int: The end line number. Must satisfy start <= end <= number of lines in the file.
6068
content: str: The content to replace the lines with.
@@ -97,7 +105,7 @@ parse_pptx(file_path: str) -> None:
97105
Args:
98106
file_path: str: The path to the file to open.
99107

100-
Please note that THE `edit_file` FUNCTION REQUIRES PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
108+
Please note that THE `edit_file` and `append_file` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
101109

102110
Responses should be concise.
103111
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
@@ -138,7 +146,7 @@ def index():
138146

139147
if __name__ == '__main__':
140148
app.run(port=5000)"""
141-
edit_file(start=1, end=1, content=EDITED_CODE)
149+
edit_file('app.py', start=1, end=1, content=EDITED_CODE)
142150
</execute_ipython>
143151

144152
USER:
@@ -277,7 +285,7 @@ USER:
277285
ASSISTANT:
278286
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
279287
<execute_ipython>
280-
edit_file(start=7, end=7, content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'")
288+
edit_file('app.py', start=7, end=7, content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'")
281289
</execute_ipython>
282290

283291
USER:

0 commit comments

Comments
 (0)