Compare commits

...

7 Commits

Author SHA1 Message Date
Xingyao Wang
1e398d6362 Merge commit '1d052818ae51856c13e6d468ab79673747440ae5' into xw/diff-edit 2024-09-25 15:02:02 +00:00
RajWorking
8f565c8740 Updated ErrorObservation for EditAction 2024-09-24 15:42:25 +00:00
Xingyao Wang
df28a7a5b9 bump codeact to 1.10 2024-09-24 15:40:05 +00:00
RajWorking
4743eb4c35 Moved create_dataset to be used implicitly by EditAction. 2024-09-24 15:40:05 +00:00
RajWorking
4a6898bbda added new line to regex of diff blocks 2024-09-24 15:40:04 +00:00
RajWorking
a4ddac4f2c minor bug fixes 2024-09-24 15:40:00 +00:00
RajWorking
33422f1a4a [Feat] Added FileEditAction to enable edits using diff format. 2024-09-24 15:39:09 +00:00
18 changed files with 259 additions and 70 deletions

View File

@@ -6,6 +6,7 @@ from openhands.events.action import (
AgentDelegateAction,
AgentFinishAction,
CmdRunAction,
FileEditAction,
IPythonRunCellAction,
MessageAction,
)
@@ -16,6 +17,7 @@ class CodeActResponseParser(ResponseParser):
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
- FileEditAction(diff_block) - Search/Replace block to edit.
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
@@ -28,6 +30,7 @@ class CodeActResponseParser(ResponseParser):
CodeActActionParserCmdRun(),
CodeActActionParserIPythonRunCell(),
CodeActActionParserAgentDelegate(),
CodeActActionParserFileEdit(),
]
self.default_parser = CodeActActionParserMessage()
@@ -39,7 +42,7 @@ class CodeActResponseParser(ResponseParser):
action = response.choices[0].message.content
if action is None:
return ''
for lang in ['bash', 'ipython', 'browse']:
for lang in ['bash', 'ipython', 'edit', 'browse']:
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
action += f'</execute_{lang}>'
return action
@@ -158,6 +161,33 @@ class CodeActActionParserAgentDelegate(ActionParser):
return AgentDelegateAction(agent='BrowsingAgent', inputs={'task': task})
class CodeActActionParserFileEdit(ActionParser):
"""Parser action:
- FileEditAction(diff_block) - Search/Replace block to edit.
"""
def __init__(
self,
):
self.diff_block = None
def check_condition(self, action_str: str) -> bool:
self.diff_block = re.search(
r'<execute_edit>(.*)</execute_edit>', action_str, re.DOTALL
)
return self.diff_block is not None
def parse(self, action_str: str) -> Action:
assert (
self.diff_block is not None
), 'self.diff_block should not be None when parse is called'
thought = action_str.replace(self.diff_block.group(0), '').strip()
return FileEditAction(
diff_block=self.diff_block.group(1).strip(),
thought=thought,
)
class CodeActActionParserMessage(ActionParser):
"""Parser action:
- MessageAction(content) - Message action to run (e.g. ask for clarification)

View File

@@ -13,12 +13,14 @@ from openhands.events.action import (
AgentDelegateAction,
AgentFinishAction,
CmdRunAction,
FileEditAction,
IPythonRunCellAction,
MessageAction,
)
from openhands.events.observation import (
AgentDelegateObservation,
CmdOutputObservation,
FileEditObservation,
IPythonRunCellObservation,
UserRejectObservation,
)
@@ -36,7 +38,7 @@ from openhands.utils.prompt import PromptManager
class CodeActAgent(Agent):
VERSION = '1.9'
VERSION = '1.10'
"""
The Code Act Agent is a minimalist agent.
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
@@ -104,6 +106,8 @@ class CodeActAgent(Agent):
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
elif isinstance(action, AgentDelegateAction):
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
elif isinstance(action, FileEditAction):
return f'{action.thought}\n<execute_edit>\n{action.diff_block}\n</execute_edit>'
elif isinstance(action, MessageAction):
return action.content
elif isinstance(action, AgentFinishAction) and action.source == 'agent':
@@ -113,6 +117,7 @@ class CodeActAgent(Agent):
def get_action_message(self, action: Action) -> Message | None:
if (
isinstance(action, AgentDelegateAction)
or isinstance(action, FileEditAction)
or isinstance(action, CmdRunAction)
or isinstance(action, IPythonRunCellAction)
or isinstance(action, MessageAction)
@@ -153,6 +158,9 @@ class CodeActAgent(Agent):
text = '\n'.join(splitted)
text = truncate_content(text, max_message_chars)
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, FileEditObservation):
text = obs_prefix + truncate_content(obs.content, max_message_chars)
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, AgentDelegateObservation):
text = obs_prefix + truncate_content(
obs.outputs['content'] if 'content' in obs.outputs else '',
@@ -164,7 +172,7 @@ class CodeActAgent(Agent):
text += '\n[Error occurred in processing last action]'
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, UserRejectObservation):
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
text = obs_prefix + truncate_content(obs.content, max_message_chars)
text += '\n[Last action has been rejected by the user]'
return Message(role='user', content=[TextContent(text=text)])
else:
@@ -187,6 +195,7 @@ class CodeActAgent(Agent):
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
- FileEditAction(diff_block) - Search/Replace block to edit.
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
@@ -203,6 +212,7 @@ class CodeActAgent(Agent):
'</execute_ipython>',
'</execute_bash>',
'</execute_browse>',
'</execute_edit>',
],
}

View File

@@ -19,22 +19,44 @@ the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
{% endset %}
{% set EDIT_DIFF_PREFIX %}
The assistant can edit files with <execute_edit> and </execute_edit>. Each change must be described with a SEARCH/REPLACE block.
Every SEARCH section must EXACTLY MATCH the existing file content, character for character, including all comments, docstrings, etc. SEARCH/REPLACE blocks will replace all matching occurrences. Include enough lines to make the SEARCH blocks uniquely match the lines to change.
Keep SEARCH/REPLACE blocks as concise as possible. Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.
To move code within a file, use 2 SEARCH/REPLACE blocks: 1 to delete it from its current location, 1 to insert it in the new location.
If you want to put code in a new file, use a SEARCH/REPLACE block with: a new file path, an empty `SEARCH` section and the new file's contents in the `REPLACE` section.
Every SEARCH/REPLACE block must use this format:
1. The FULL file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE
For example,
<execute_edit>
demo.py
<<<<<<< SEARCH
print("hello")
=======
print("goodbye")
>>>>>>> REPLACE
</execute_edit>
{% endset %}
{% set PIP_INSTALL_PREFIX %}
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
{% endset %}
{% set SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX %}
{% set SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + EDIT_DIFF_PREFIX + PIP_INSTALL_PREFIX %}
{% set COMMAND_DOCS %}
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
{{ agent_skills_docs }}
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
{% endset %}
{% set SYSTEM_SUFFIX %}
Responses should be concise.

View File

@@ -4,23 +4,12 @@
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
Sure! Let me write the Python code for starting a web server and save it to a new Python file `app.py`:
<execute_edit>
app.py
<<<<<<< SEARCH
=======
from flask import Flask
app = Flask(__name__)
@app.route('/')
@@ -29,30 +18,17 @@ def index():
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)"""
app.run(port=5000)
>>>>>>> REPLACE
</execute_edit>
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
I updated the file app.py by
appending:
from flask import Flask
app = Flask(__name__)
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
@@ -177,30 +153,22 @@ USER:
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
<execute_edit>
app.py
<<<<<<< SEARCH
return str(numbers)
=======
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
>>>>>>> REPLACE
</execute_edit>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
I updated the file app.py by
replacing:
return str(numbers)
with:
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
ASSISTANT:
Running the updated file:

View File

@@ -256,6 +256,7 @@ def process_instance(
if __name__ == '__main__':
args = parse_arguments()
dataset = load_dataset('RajMaheshwari/Exercism-Python')
dataset = dataset.shuffle(seed=42)
aider_bench_tests = dataset['train'].to_pandas()
llm_config = None

View File

@@ -24,6 +24,10 @@ class ActionTypeSchema(BaseModel):
"""Writes the content to a file.
"""
EDIT: str = Field(default='edit')
"""Edits the content of a file.
"""
RUN: str = Field(default='run')
"""Runs a command.
"""

View File

@@ -10,6 +10,10 @@ class ObservationTypeSchema(BaseModel):
WRITE: str = Field(default='write')
EDIT: str = Field(default='edit')
"""The edited file
"""
BROWSE: str = Field(default='browse')
"""The HTML content of a URL
"""

View File

@@ -9,7 +9,11 @@ from openhands.events.action.agent import (
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
from openhands.events.action.commands import CmdRunAction, IPythonRunCellAction
from openhands.events.action.empty import NullAction
from openhands.events.action.files import FileReadAction, FileWriteAction
from openhands.events.action.files import (
FileEditAction,
FileReadAction,
FileWriteAction,
)
from openhands.events.action.message import MessageAction
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
@@ -21,6 +25,7 @@ __all__ = [
'BrowseInteractiveAction',
'FileReadAction',
'FileWriteAction',
'FileEditAction',
'AgentFinishAction',
'AgentRejectAction',
'AgentDelegateAction',

View File

@@ -39,3 +39,23 @@ class FileWriteAction(Action):
@property
def message(self) -> str:
return f'Writing file: {self.path}'
@dataclass
class FileEditAction(Action):
diff_block: str
thought: str = ''
action: str = ActionType.EDIT
runnable: ClassVar[bool] = True
security_risk: ActionSecurityRisk | None = None
def __str__(self) -> str:
ret = '**EditFileAction**\n'
if self.thought:
ret += f'THOUGHT: {self.thought}\n'
ret += f'DIFF BLOCK:\n{self.diff_block}\n'
return ret
@property
def message(self) -> str:
return f'Edit Diff block: {self.diff_block}'

View File

@@ -7,7 +7,11 @@ from openhands.events.observation.commands import (
from openhands.events.observation.delegate import AgentDelegateObservation
from openhands.events.observation.empty import NullObservation
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.files import FileReadObservation, FileWriteObservation
from openhands.events.observation.files import (
FileEditObservation,
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
@@ -20,6 +24,7 @@ __all__ = [
'BrowserOutputObservation',
'FileReadObservation',
'FileWriteObservation',
'FileEditObservation',
'ErrorObservation',
'AgentStateChangedObservation',
'AgentDelegateObservation',

View File

@@ -26,3 +26,30 @@ class FileWriteObservation(Observation):
@property
def message(self) -> str:
return f'I wrote to the file {self.path}.'
@dataclass
class FileEditObservation(Observation):
"""This data class represents a file edit operation"""
path: str
search_block: str
replace_block: str
observation: str = ObservationType.EDIT
@property
def message(self) -> str:
if self.search_block:
return (
f'I updated the file {self.path} by \n'
f'replacing:\n {self.search_block}\n'
f'with:\n {self.replace_block}\n'
)
else:
return (
f'I updated the file {self.path} by \n'
f'appending:\n {self.replace_block}\n'
)
def __str__(self) -> str:
return f'**FileEditObservation**\n' f'DIFF BLOCK: {self.content}\n'

View File

@@ -12,7 +12,11 @@ from openhands.events.action.commands import (
IPythonRunCellAction,
)
from openhands.events.action.empty import NullAction
from openhands.events.action.files import FileReadAction, FileWriteAction
from openhands.events.action.files import (
FileEditAction,
FileReadAction,
FileWriteAction,
)
from openhands.events.action.message import MessageAction
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
@@ -24,6 +28,7 @@ actions = (
BrowseInteractiveAction,
FileReadAction,
FileWriteAction,
FileEditAction,
AgentFinishAction,
AgentRejectAction,
AgentDelegateAction,

View File

@@ -7,7 +7,11 @@ from openhands.events.observation.commands import (
from openhands.events.observation.delegate import AgentDelegateObservation
from openhands.events.observation.empty import NullObservation
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.files import FileReadObservation, FileWriteObservation
from openhands.events.observation.files import (
FileEditObservation,
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
@@ -19,6 +23,7 @@ observations = (
BrowserOutputObservation,
FileReadObservation,
FileWriteObservation,
FileEditObservation,
AgentDelegateObservation,
SuccessObservation,
ErrorObservation,

View File

@@ -28,6 +28,7 @@ from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
@@ -35,6 +36,7 @@ from openhands.events.action import (
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileEditObservation,
FileReadObservation,
FileWriteObservation,
IPythonRunCellObservation,
@@ -48,6 +50,11 @@ from openhands.runtime.plugins import (
JupyterPlugin,
Plugin,
)
from openhands.runtime.plugins.agent_skills.file_ops import (
append_file,
create_file,
edit_file_by_replace,
)
from openhands.runtime.utils import split_bash_commands
from openhands.runtime.utils.files import insert_lines, read_lines
@@ -62,6 +69,10 @@ INIT_COMMANDS = [
]
SOFT_TIMEOUT_SECONDS = 5
HEAD = '<<<<<<< SEARCH'
DIVIDER = '======='
TAIL = '>>>>>>> REPLACE'
class RuntimeClient:
"""RuntimeClient is running inside docker sandbox.
@@ -509,6 +520,55 @@ class RuntimeClient:
return ErrorObservation(f'Malformed paths not permitted: {filepath}')
return FileWriteObservation(content='', path=filepath)
async def edit(self, action: FileEditAction) -> Observation:
diff_blocks = re.search(
f'(.*)\n{HEAD}(.*)\n{DIVIDER}(.*)\n{TAIL}', action.diff_block, re.DOTALL
)
if not diff_blocks or len(diff_blocks.groups()) < 3:
found_head = re.search(f'{HEAD}', action.diff_block) is not None
found_divider = re.search(f'{DIVIDER}', action.diff_block) is not None
found_tail = re.search(f'{TAIL}', action.diff_block) is not None
error_msg = 'Could not resolve diff block into search/replace blocks.'
if found_head and (not found_tail):
error_msg = 'The diff block got cut off because it is too long. Try breaking it into smaller SEARCH/REPLACE blocks.'
elif found_head and found_tail and (not found_divider):
error_msg = 'Could not find the divider between SEARCH/REPLACE blocks.'
return ErrorObservation(error_msg)
path = diff_blocks.group(1)
search_block = diff_blocks.group(2)
replace_block = diff_blocks.group(3)
if search_block:
search_block = search_block[1:]
if replace_block:
replace_block = replace_block[1:]
working_dir = self._get_working_directory()
filepath = self._resolve_path(path, working_dir)
if not search_block:
create_file(filename=filepath)
append_file(
file_name=filepath,
content=replace_block,
)
else:
if search_block == replace_block:
return ErrorObservation(
'Search block should not be same as Replace block.'
)
edit_file_by_replace(
file_name=filepath,
to_replace=search_block,
new_content=replace_block,
)
return FileEditObservation(
content=action.diff_block,
path=filepath,
search_block=search_block,
replace_block=replace_block,
)
async def browse(self, action: BrowseURLAction) -> Observation:
return await browse(action, self.browser)

View File

@@ -17,6 +17,7 @@ from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
@@ -451,6 +452,9 @@ class EventStreamRuntime(Runtime):
def write(self, action: FileWriteAction) -> Observation:
return self.run_action(action)
def edit(self, action: FileEditAction) -> Observation:
return self.run_action(action)
def browse(self, action: BrowseURLAction) -> Observation:
return self.run_action(action)

View File

@@ -10,9 +10,18 @@ import_functions(
module=file_reader, function_names=file_reader.__all__, target_globals=globals()
)
__all__ = file_ops.__all__ + file_reader.__all__
__except__ = [
'create_file',
'edit_file_by_replace',
'insert_content_at_line',
'append_file',
] ## DISABLED TEMPORARILY.
DOCUMENTATION = ''
for func_name in __all__:
if func_name in __except__:
continue
func = globals()[func_name]
cur_doc = func.__doc__

View File

@@ -5,3 +5,8 @@ import_functions(
module=file_ops, function_names=file_ops.__all__, target_globals=globals()
)
__all__ = file_ops.__all__
create_file = file_ops.create_file
append_file = file_ops.append_file
edit_file_by_replace = file_ops.edit_file_by_replace
insert_content_at_line = file_ops.insert_content_at_line

View File

@@ -14,6 +14,7 @@ from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
@@ -179,6 +180,10 @@ class Runtime:
def write(self, action: FileWriteAction) -> Observation:
pass
@abstractmethod
def edit(self, action: FileEditAction) -> Observation:
pass
@abstractmethod
def browse(self, action: BrowseURLAction) -> Observation:
pass