Skip to content

[discarded] Add append_file to CodeActAgent, incl. tests; some markdown fixes #2207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
2 changes: 1 addition & 1 deletion agenthub/codeact_agent/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
COMMAND_DOCS = (
'\nApart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:\n'
f'{_AGENT_SKILLS_DOCS}'
"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."
"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."
)

# ======= SYSTEM MESSAGE =======
Expand Down
5 changes: 3 additions & 2 deletions opendevin/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# OpenDevin Shared Abstraction and Components

This is a Python package that contains all the shared abstraction (e.g., Agent) and components (e.g., sandbox, web browser, search API, selenium).
This is a Python package that contains all the shared abstraction (e.g. Agent) and components (e.g. sandbox, web browser, search API, selenium).

See the [main README](../README.md) for instructions on how to run OpenDevin from the command line.

## Sandbox Image

```bash
docker build -f opendevin/sandbox/docker/Dockerfile -t opendevin/sandbox:v0.1 .
```
Expand All @@ -22,4 +23,4 @@ It will map `./workspace` into the docker container with the folder permission c

Example screenshot:

<img width="868" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/8dedcdee-437a-4469-870f-be29ca2b7c32">
![image](https://github.com/OpenDevin/OpenDevin/assets/38853559/8dedcdee-437a-4469-870f-be29ca2b7c32)
9 changes: 5 additions & 4 deletions opendevin/runtime/plugins/agent_skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ This folder implements a skill/tool set `agentskills` for OpenDevin.
It is intended to be used by the agent **inside sandbox**.
The skill set will be exposed as a `pip` package that can be installed as a plugin inside the sandbox.

The skill set can contains a bunch of wrapped tools for agent ([many examples here](https://github.com/OpenDevin/OpenDevin/pull/1914)), for example:
- Audio/Video to text (these are a temporary solution, and we should switch to multimodal models when they are sufficiently cheap
The skill set can contains a bunch of wrapped tools for agents ([many examples here](https://github.com/OpenDevin/OpenDevin/pull/1914)), for example:

- Audio/Video to text (these are a temporary solutions, and we should switch to multimodal models when they are sufficiently cheap
- PDF to text
- etc.

# Inclusion Criteria
## Inclusion Criteria

We are walking a fine line here.
We DON't want to *wrap* every possible python packages and re-teach agent their usage (e.g., LLM already knows `pandas` pretty well, so we don't really need create a skill that reads `csv` - it can just use `pandas`).
Expand All @@ -19,7 +20,7 @@ We ONLY want to add a new skill, when:
- Such skill is not easily achievable for LLM to write code directly (e.g., edit code and replace certain line)
- It involves calling an external model (e.g., you need to call a speech to text model, editor model for speculative editing)

# Intended functionality
## Intended functionality

- Tool/skill usage (through `IPythonRunAction`)

Expand Down
92 changes: 91 additions & 1 deletion opendevin/runtime/plugins/agent_skills/agentskills.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- search_file(search_term, file_path=None): Searches for a term in the specified file or the currently open file.
- find_file(file_name, dir_path='./'): Finds all files with the given name in the specified directory.
- edit_file(start, end, content): Replaces lines in a file with the given content.
- append_file(content): Appends given content to a file.
"""

import base64
Expand Down Expand Up @@ -232,7 +233,6 @@ def create_file(filename: str) -> None:
Args:
filename: str: The name of the file to create.
"""
global CURRENT_FILE, CURRENT_LINE
if os.path.exists(filename):
raise FileExistsError(f"File '{filename}' already exists.")

Expand Down Expand Up @@ -356,6 +356,95 @@ def edit_file(start: int, end: int, content: str) -> None:
)


@update_pwd_decorator
def append_file(content: str) -> None:
"""Append content to the open file.

It appends text `content` to the end of the open file. Remember, the file must be open before editing.

Args:
content: str: The content to append to the file.
"""
global CURRENT_FILE, CURRENT_LINE, WINDOW
if not CURRENT_FILE or not os.path.isfile(CURRENT_FILE):
raise FileNotFoundError('No file open. Use the open_file function first.')

# Load the file
with open(CURRENT_FILE, 'r') as file:
lines = file.readlines()

# Check if the first line is solely a line break and overwrite it
if lines and lines[0].strip() == '':
lines[0] = content + '\n'
else:
# Add a leading newline if the file is not empty
if lines and not lines[-1].endswith('\n'):
lines[-1] += '\n'

edited_content = content + '\n'
n_edited_lines = len(edited_content.split('\n'))
new_lines = lines + [edited_content]

# directly write edited lines to the file
with open(CURRENT_FILE, 'w') as file:
file.writelines(new_lines)

# Handle linting
if ENABLE_AUTO_LINT:
# BACKUP the original file
original_file_backup_path = os.path.join(
os.path.dirname(CURRENT_FILE), f'.backup.{os.path.basename(CURRENT_FILE)}'
)
with open(original_file_backup_path, 'w') as f:
f.writelines(lines)

lint_error = _lint_file(CURRENT_FILE)
if lint_error:
print(
'[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]'
)
print(lint_error)

print('[This is how your edit would have looked if applied]')
print('-------------------------------------------------')
cur_line = len(lines) + (n_edited_lines // 2)
_print_window(CURRENT_FILE, cur_line, 10)
print('-------------------------------------------------\n')

print('[This is the original code before your edit]')
print('-------------------------------------------------')
_print_window(original_file_backup_path, cur_line, 10)
print('-------------------------------------------------')

print(
'Your changes have NOT been applied. Please fix your edit command and try again.\n'
'You need to correct your added code.\n'
'DO NOT re-run the same failed edit command. Running it again will lead to the same error.'
)

# recover the original file
with open(original_file_backup_path, 'r') as fin, open(
CURRENT_FILE, 'w'
) as fout:
fout.write(fin.read())
os.remove(original_file_backup_path)
return

os.remove(original_file_backup_path)

with open(CURRENT_FILE, 'r') as file:
n_total_lines = len(file.readlines())
# set current line to the center of the appended lines
CURRENT_LINE = len(lines) + (n_edited_lines // 2)
print(
f'[File: {os.path.abspath(CURRENT_FILE)} ({n_total_lines} lines total after edit)]'
)
_print_window(CURRENT_FILE, CURRENT_LINE, WINDOW)
print(
'[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]'
)


@update_pwd_decorator
def search_dir(search_term: str, dir_path: str = './') -> None:
"""Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
Expand Down Expand Up @@ -670,6 +759,7 @@ def parse_pptx(file_path: str) -> None:
'scroll_down',
'scroll_up',
'create_file',
'append_file',
'edit_file',
'search_dir',
'search_file',
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/test_agent_skill.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import contextlib
import io
import os
import sys

import docx
import pytest

from opendevin.runtime.plugins.agent_skills.agentskills import (
append_file,
create_file,
edit_file,
find_file,
Expand Down Expand Up @@ -274,6 +276,60 @@ def test_scroll_down_edge(tmp_path):
assert result.split('\n') == expected.split('\n')


def test_append_file(tmp_path):
temp_file_path = tmp_path / 'a1.txt'
content = 'Line 1\nLine 2'
temp_file_path.write_text(content)
try:
open_file(str(temp_file_path))
append_file(content='APPENDED TEXT')

with open(temp_file_path, 'r') as file:
lines = file.readlines()
assert len(lines) == 3
assert lines[0].rstrip() == 'Line 1'
assert lines[1].rstrip() == 'Line 2'
assert lines[2].rstrip() == 'APPENDED TEXT'
finally:
os.remove(temp_file_path)


def test_append_file_from_scratch(tmp_path):
temp_file_path = tmp_path / 'a2.txt'
create_file(str(temp_file_path))
try:
open_file(str(temp_file_path))
append_file(content='APPENDED TEXT')

with open(temp_file_path, 'r') as file:
lines = file.readlines()
assert len(lines) == 2
assert lines[0].rstrip() == 'APPENDED TEXT'
finally:
os.remove(temp_file_path)


def test_append_file_from_scratch_multiline(tmp_path):
temp_file_path = tmp_path / 'a3.txt'
create_file(str(temp_file_path))
try:
open_file(temp_file_path)
append_file(content='First line\nSecond line')

with open(temp_file_path, 'r') as file:
lines = file.readlines()
assert len(lines) == 4
assert lines[0].rstrip() == 'First line'
assert lines[1].rstrip() == 'Second line'
finally:
os.remove(temp_file_path)


def test_append_file_not_opened():
with pytest.raises(FileNotFoundError):
append_file(content='APPEND TEXT')


def test_edit_file(tmp_path):
temp_file_path = tmp_path / 'a.txt'
content = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5'
Expand Down