mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e398d6362 | |||
| 8f565c8740 | |||
| df28a7a5b9 | |||
| 4743eb4c35 | |||
| 4a6898bbda | |||
| a4ddac4f2c | |||
| 33422f1a4a |
@@ -6,6 +6,7 @@ from openhands.events.action import (
|
|||||||
AgentDelegateAction,
|
AgentDelegateAction,
|
||||||
AgentFinishAction,
|
AgentFinishAction,
|
||||||
CmdRunAction,
|
CmdRunAction,
|
||||||
|
FileEditAction,
|
||||||
IPythonRunCellAction,
|
IPythonRunCellAction,
|
||||||
MessageAction,
|
MessageAction,
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,7 @@ class CodeActResponseParser(ResponseParser):
|
|||||||
- CmdRunAction(command) - bash command to run
|
- CmdRunAction(command) - bash command to run
|
||||||
- IPythonRunCellAction(code) - IPython code to run
|
- IPythonRunCellAction(code) - IPython code to run
|
||||||
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
|
- 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)
|
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||||
- AgentFinishAction() - end the interaction
|
- AgentFinishAction() - end the interaction
|
||||||
"""
|
"""
|
||||||
@@ -28,6 +30,7 @@ class CodeActResponseParser(ResponseParser):
|
|||||||
CodeActActionParserCmdRun(),
|
CodeActActionParserCmdRun(),
|
||||||
CodeActActionParserIPythonRunCell(),
|
CodeActActionParserIPythonRunCell(),
|
||||||
CodeActActionParserAgentDelegate(),
|
CodeActActionParserAgentDelegate(),
|
||||||
|
CodeActActionParserFileEdit(),
|
||||||
]
|
]
|
||||||
self.default_parser = CodeActActionParserMessage()
|
self.default_parser = CodeActActionParserMessage()
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ class CodeActResponseParser(ResponseParser):
|
|||||||
action = response.choices[0].message.content
|
action = response.choices[0].message.content
|
||||||
if action is None:
|
if action is None:
|
||||||
return ''
|
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:
|
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
|
||||||
action += f'</execute_{lang}>'
|
action += f'</execute_{lang}>'
|
||||||
return action
|
return action
|
||||||
@@ -158,6 +161,33 @@ class CodeActActionParserAgentDelegate(ActionParser):
|
|||||||
return AgentDelegateAction(agent='BrowsingAgent', inputs={'task': task})
|
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):
|
class CodeActActionParserMessage(ActionParser):
|
||||||
"""Parser action:
|
"""Parser action:
|
||||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ from openhands.events.action import (
|
|||||||
AgentDelegateAction,
|
AgentDelegateAction,
|
||||||
AgentFinishAction,
|
AgentFinishAction,
|
||||||
CmdRunAction,
|
CmdRunAction,
|
||||||
|
FileEditAction,
|
||||||
IPythonRunCellAction,
|
IPythonRunCellAction,
|
||||||
MessageAction,
|
MessageAction,
|
||||||
)
|
)
|
||||||
from openhands.events.observation import (
|
from openhands.events.observation import (
|
||||||
AgentDelegateObservation,
|
AgentDelegateObservation,
|
||||||
CmdOutputObservation,
|
CmdOutputObservation,
|
||||||
|
FileEditObservation,
|
||||||
IPythonRunCellObservation,
|
IPythonRunCellObservation,
|
||||||
UserRejectObservation,
|
UserRejectObservation,
|
||||||
)
|
)
|
||||||
@@ -36,7 +38,7 @@ from openhands.utils.prompt import PromptManager
|
|||||||
|
|
||||||
|
|
||||||
class CodeActAgent(Agent):
|
class CodeActAgent(Agent):
|
||||||
VERSION = '1.9'
|
VERSION = '1.10'
|
||||||
"""
|
"""
|
||||||
The Code Act Agent is a minimalist agent.
|
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.
|
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>'
|
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
|
||||||
elif isinstance(action, AgentDelegateAction):
|
elif isinstance(action, AgentDelegateAction):
|
||||||
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
|
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):
|
elif isinstance(action, MessageAction):
|
||||||
return action.content
|
return action.content
|
||||||
elif isinstance(action, AgentFinishAction) and action.source == 'agent':
|
elif isinstance(action, AgentFinishAction) and action.source == 'agent':
|
||||||
@@ -113,6 +117,7 @@ class CodeActAgent(Agent):
|
|||||||
def get_action_message(self, action: Action) -> Message | None:
|
def get_action_message(self, action: Action) -> Message | None:
|
||||||
if (
|
if (
|
||||||
isinstance(action, AgentDelegateAction)
|
isinstance(action, AgentDelegateAction)
|
||||||
|
or isinstance(action, FileEditAction)
|
||||||
or isinstance(action, CmdRunAction)
|
or isinstance(action, CmdRunAction)
|
||||||
or isinstance(action, IPythonRunCellAction)
|
or isinstance(action, IPythonRunCellAction)
|
||||||
or isinstance(action, MessageAction)
|
or isinstance(action, MessageAction)
|
||||||
@@ -153,6 +158,9 @@ class CodeActAgent(Agent):
|
|||||||
text = '\n'.join(splitted)
|
text = '\n'.join(splitted)
|
||||||
text = truncate_content(text, max_message_chars)
|
text = truncate_content(text, max_message_chars)
|
||||||
return Message(role='user', content=[TextContent(text=text)])
|
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):
|
elif isinstance(obs, AgentDelegateObservation):
|
||||||
text = obs_prefix + truncate_content(
|
text = obs_prefix + truncate_content(
|
||||||
obs.outputs['content'] if 'content' in obs.outputs else '',
|
obs.outputs['content'] if 'content' in obs.outputs else '',
|
||||||
@@ -164,7 +172,7 @@ class CodeActAgent(Agent):
|
|||||||
text += '\n[Error occurred in processing last action]'
|
text += '\n[Error occurred in processing last action]'
|
||||||
return Message(role='user', content=[TextContent(text=text)])
|
return Message(role='user', content=[TextContent(text=text)])
|
||||||
elif isinstance(obs, UserRejectObservation):
|
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]'
|
text += '\n[Last action has been rejected by the user]'
|
||||||
return Message(role='user', content=[TextContent(text=text)])
|
return Message(role='user', content=[TextContent(text=text)])
|
||||||
else:
|
else:
|
||||||
@@ -187,6 +195,7 @@ class CodeActAgent(Agent):
|
|||||||
- CmdRunAction(command) - bash command to run
|
- CmdRunAction(command) - bash command to run
|
||||||
- IPythonRunCellAction(code) - IPython code to run
|
- IPythonRunCellAction(code) - IPython code to run
|
||||||
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
|
- 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)
|
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||||
- AgentFinishAction() - end the interaction
|
- AgentFinishAction() - end the interaction
|
||||||
"""
|
"""
|
||||||
@@ -203,6 +212,7 @@ class CodeActAgent(Agent):
|
|||||||
'</execute_ipython>',
|
'</execute_ipython>',
|
||||||
'</execute_bash>',
|
'</execute_bash>',
|
||||||
'</execute_browse>',
|
'</execute_browse>',
|
||||||
|
'</execute_edit>',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>.
|
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>.
|
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>.
|
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 %}
|
{% endset %}
|
||||||
{% set PIP_INSTALL_PREFIX %}
|
{% 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.
|
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 %}
|
{% 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 %}
|
{% set COMMAND_DOCS %}
|
||||||
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
|
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
|
||||||
{{ agent_skills_docs }}
|
{{ agent_skills_docs }}
|
||||||
IMPORTANT:
|
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!
|
- `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.
|
- 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!
|
- 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 %}
|
{% endset %}
|
||||||
{% set SYSTEM_SUFFIX %}
|
{% set SYSTEM_SUFFIX %}
|
||||||
Responses should be concise.
|
Responses should be concise.
|
||||||
|
|||||||
@@ -4,23 +4,12 @@
|
|||||||
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
|
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
|
||||||
|
|
||||||
ASSISTANT:
|
ASSISTANT:
|
||||||
Sure! Let me create a Python file `app.py`:
|
Sure! Let me write the Python code for starting a web server and save it to a new Python file `app.py`:
|
||||||
<execute_ipython>
|
<execute_edit>
|
||||||
create_file('app.py')
|
app.py
|
||||||
</execute_ipython>
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
USER:
|
from flask import Flask
|
||||||
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
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -29,30 +18,17 @@ def index():
|
|||||||
return str(numbers)
|
return str(numbers)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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:
|
USER:
|
||||||
OBSERVATION:
|
OBSERVATION:
|
||||||
(this is the beginning of the file)
|
I updated the file app.py by
|
||||||
1|from flask import Flask
|
appending:
|
||||||
2|app = Flask(__name__)
|
from flask import Flask
|
||||||
3|
|
app = Flask(__name__)
|
||||||
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.]
|
|
||||||
|
|
||||||
ASSISTANT:
|
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:
|
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:
|
ASSISTANT:
|
||||||
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
|
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>
|
<execute_edit>
|
||||||
edit_file_by_replace(
|
app.py
|
||||||
'app.py',
|
<<<<<<< SEARCH
|
||||||
to_replace=" return str(numbers)",
|
return str(numbers)
|
||||||
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
|
=======
|
||||||
)
|
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||||
</execute_ipython>
|
>>>>>>> REPLACE
|
||||||
|
</execute_edit>
|
||||||
|
|
||||||
USER:
|
USER:
|
||||||
Observation:
|
Observation:
|
||||||
[File: /workspace/app.py (10 lines total after edit)]
|
I updated the file app.py by
|
||||||
(this is the beginning of the file)
|
replacing:
|
||||||
1|from flask import Flask
|
return str(numbers)
|
||||||
2|app = Flask(__name__)
|
with:
|
||||||
3|
|
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||||
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.]
|
|
||||||
|
|
||||||
ASSISTANT:
|
ASSISTANT:
|
||||||
Running the updated file:
|
Running the updated file:
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ def process_instance(
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
dataset = load_dataset('RajMaheshwari/Exercism-Python')
|
dataset = load_dataset('RajMaheshwari/Exercism-Python')
|
||||||
|
dataset = dataset.shuffle(seed=42)
|
||||||
aider_bench_tests = dataset['train'].to_pandas()
|
aider_bench_tests = dataset['train'].to_pandas()
|
||||||
|
|
||||||
llm_config = None
|
llm_config = None
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ class ActionTypeSchema(BaseModel):
|
|||||||
"""Writes the content to a file.
|
"""Writes the content to a file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
EDIT: str = Field(default='edit')
|
||||||
|
"""Edits the content of a file.
|
||||||
|
"""
|
||||||
|
|
||||||
RUN: str = Field(default='run')
|
RUN: str = Field(default='run')
|
||||||
"""Runs a command.
|
"""Runs a command.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ class ObservationTypeSchema(BaseModel):
|
|||||||
|
|
||||||
WRITE: str = Field(default='write')
|
WRITE: str = Field(default='write')
|
||||||
|
|
||||||
|
EDIT: str = Field(default='edit')
|
||||||
|
"""The edited file
|
||||||
|
"""
|
||||||
|
|
||||||
BROWSE: str = Field(default='browse')
|
BROWSE: str = Field(default='browse')
|
||||||
"""The HTML content of a URL
|
"""The HTML content of a URL
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ from openhands.events.action.agent import (
|
|||||||
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
|
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
|
||||||
from openhands.events.action.commands import CmdRunAction, IPythonRunCellAction
|
from openhands.events.action.commands import CmdRunAction, IPythonRunCellAction
|
||||||
from openhands.events.action.empty import NullAction
|
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.message import MessageAction
|
||||||
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
|
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
|
||||||
|
|
||||||
@@ -21,6 +25,7 @@ __all__ = [
|
|||||||
'BrowseInteractiveAction',
|
'BrowseInteractiveAction',
|
||||||
'FileReadAction',
|
'FileReadAction',
|
||||||
'FileWriteAction',
|
'FileWriteAction',
|
||||||
|
'FileEditAction',
|
||||||
'AgentFinishAction',
|
'AgentFinishAction',
|
||||||
'AgentRejectAction',
|
'AgentRejectAction',
|
||||||
'AgentDelegateAction',
|
'AgentDelegateAction',
|
||||||
|
|||||||
@@ -39,3 +39,23 @@ class FileWriteAction(Action):
|
|||||||
@property
|
@property
|
||||||
def message(self) -> str:
|
def message(self) -> str:
|
||||||
return f'Writing file: {self.path}'
|
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}'
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ from openhands.events.observation.commands import (
|
|||||||
from openhands.events.observation.delegate import AgentDelegateObservation
|
from openhands.events.observation.delegate import AgentDelegateObservation
|
||||||
from openhands.events.observation.empty import NullObservation
|
from openhands.events.observation.empty import NullObservation
|
||||||
from openhands.events.observation.error import ErrorObservation
|
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.observation import Observation
|
||||||
from openhands.events.observation.reject import UserRejectObservation
|
from openhands.events.observation.reject import UserRejectObservation
|
||||||
from openhands.events.observation.success import SuccessObservation
|
from openhands.events.observation.success import SuccessObservation
|
||||||
@@ -20,6 +24,7 @@ __all__ = [
|
|||||||
'BrowserOutputObservation',
|
'BrowserOutputObservation',
|
||||||
'FileReadObservation',
|
'FileReadObservation',
|
||||||
'FileWriteObservation',
|
'FileWriteObservation',
|
||||||
|
'FileEditObservation',
|
||||||
'ErrorObservation',
|
'ErrorObservation',
|
||||||
'AgentStateChangedObservation',
|
'AgentStateChangedObservation',
|
||||||
'AgentDelegateObservation',
|
'AgentDelegateObservation',
|
||||||
|
|||||||
@@ -26,3 +26,30 @@ class FileWriteObservation(Observation):
|
|||||||
@property
|
@property
|
||||||
def message(self) -> str:
|
def message(self) -> str:
|
||||||
return f'I wrote to the file {self.path}.'
|
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'
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ from openhands.events.action.commands import (
|
|||||||
IPythonRunCellAction,
|
IPythonRunCellAction,
|
||||||
)
|
)
|
||||||
from openhands.events.action.empty import NullAction
|
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.message import MessageAction
|
||||||
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
|
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ actions = (
|
|||||||
BrowseInteractiveAction,
|
BrowseInteractiveAction,
|
||||||
FileReadAction,
|
FileReadAction,
|
||||||
FileWriteAction,
|
FileWriteAction,
|
||||||
|
FileEditAction,
|
||||||
AgentFinishAction,
|
AgentFinishAction,
|
||||||
AgentRejectAction,
|
AgentRejectAction,
|
||||||
AgentDelegateAction,
|
AgentDelegateAction,
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ from openhands.events.observation.commands import (
|
|||||||
from openhands.events.observation.delegate import AgentDelegateObservation
|
from openhands.events.observation.delegate import AgentDelegateObservation
|
||||||
from openhands.events.observation.empty import NullObservation
|
from openhands.events.observation.empty import NullObservation
|
||||||
from openhands.events.observation.error import ErrorObservation
|
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.observation import Observation
|
||||||
from openhands.events.observation.reject import UserRejectObservation
|
from openhands.events.observation.reject import UserRejectObservation
|
||||||
from openhands.events.observation.success import SuccessObservation
|
from openhands.events.observation.success import SuccessObservation
|
||||||
@@ -19,6 +23,7 @@ observations = (
|
|||||||
BrowserOutputObservation,
|
BrowserOutputObservation,
|
||||||
FileReadObservation,
|
FileReadObservation,
|
||||||
FileWriteObservation,
|
FileWriteObservation,
|
||||||
|
FileEditObservation,
|
||||||
AgentDelegateObservation,
|
AgentDelegateObservation,
|
||||||
SuccessObservation,
|
SuccessObservation,
|
||||||
ErrorObservation,
|
ErrorObservation,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from openhands.events.action import (
|
|||||||
BrowseInteractiveAction,
|
BrowseInteractiveAction,
|
||||||
BrowseURLAction,
|
BrowseURLAction,
|
||||||
CmdRunAction,
|
CmdRunAction,
|
||||||
|
FileEditAction,
|
||||||
FileReadAction,
|
FileReadAction,
|
||||||
FileWriteAction,
|
FileWriteAction,
|
||||||
IPythonRunCellAction,
|
IPythonRunCellAction,
|
||||||
@@ -35,6 +36,7 @@ from openhands.events.action import (
|
|||||||
from openhands.events.observation import (
|
from openhands.events.observation import (
|
||||||
CmdOutputObservation,
|
CmdOutputObservation,
|
||||||
ErrorObservation,
|
ErrorObservation,
|
||||||
|
FileEditObservation,
|
||||||
FileReadObservation,
|
FileReadObservation,
|
||||||
FileWriteObservation,
|
FileWriteObservation,
|
||||||
IPythonRunCellObservation,
|
IPythonRunCellObservation,
|
||||||
@@ -48,6 +50,11 @@ from openhands.runtime.plugins import (
|
|||||||
JupyterPlugin,
|
JupyterPlugin,
|
||||||
Plugin,
|
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 import split_bash_commands
|
||||||
from openhands.runtime.utils.files import insert_lines, read_lines
|
from openhands.runtime.utils.files import insert_lines, read_lines
|
||||||
|
|
||||||
@@ -62,6 +69,10 @@ INIT_COMMANDS = [
|
|||||||
]
|
]
|
||||||
SOFT_TIMEOUT_SECONDS = 5
|
SOFT_TIMEOUT_SECONDS = 5
|
||||||
|
|
||||||
|
HEAD = '<<<<<<< SEARCH'
|
||||||
|
DIVIDER = '======='
|
||||||
|
TAIL = '>>>>>>> REPLACE'
|
||||||
|
|
||||||
|
|
||||||
class RuntimeClient:
|
class RuntimeClient:
|
||||||
"""RuntimeClient is running inside docker sandbox.
|
"""RuntimeClient is running inside docker sandbox.
|
||||||
@@ -509,6 +520,55 @@ class RuntimeClient:
|
|||||||
return ErrorObservation(f'Malformed paths not permitted: {filepath}')
|
return ErrorObservation(f'Malformed paths not permitted: {filepath}')
|
||||||
return FileWriteObservation(content='', path=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:
|
async def browse(self, action: BrowseURLAction) -> Observation:
|
||||||
return await browse(action, self.browser)
|
return await browse(action, self.browser)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from openhands.events.action import (
|
|||||||
BrowseInteractiveAction,
|
BrowseInteractiveAction,
|
||||||
BrowseURLAction,
|
BrowseURLAction,
|
||||||
CmdRunAction,
|
CmdRunAction,
|
||||||
|
FileEditAction,
|
||||||
FileReadAction,
|
FileReadAction,
|
||||||
FileWriteAction,
|
FileWriteAction,
|
||||||
IPythonRunCellAction,
|
IPythonRunCellAction,
|
||||||
@@ -451,6 +452,9 @@ class EventStreamRuntime(Runtime):
|
|||||||
def write(self, action: FileWriteAction) -> Observation:
|
def write(self, action: FileWriteAction) -> Observation:
|
||||||
return self.run_action(action)
|
return self.run_action(action)
|
||||||
|
|
||||||
|
def edit(self, action: FileEditAction) -> Observation:
|
||||||
|
return self.run_action(action)
|
||||||
|
|
||||||
def browse(self, action: BrowseURLAction) -> Observation:
|
def browse(self, action: BrowseURLAction) -> Observation:
|
||||||
return self.run_action(action)
|
return self.run_action(action)
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,18 @@ import_functions(
|
|||||||
module=file_reader, function_names=file_reader.__all__, target_globals=globals()
|
module=file_reader, function_names=file_reader.__all__, target_globals=globals()
|
||||||
)
|
)
|
||||||
__all__ = file_ops.__all__ + file_reader.__all__
|
__all__ = file_ops.__all__ + file_reader.__all__
|
||||||
|
__except__ = [
|
||||||
|
'create_file',
|
||||||
|
'edit_file_by_replace',
|
||||||
|
'insert_content_at_line',
|
||||||
|
'append_file',
|
||||||
|
] ## DISABLED TEMPORARILY.
|
||||||
|
|
||||||
DOCUMENTATION = ''
|
DOCUMENTATION = ''
|
||||||
for func_name in __all__:
|
for func_name in __all__:
|
||||||
|
if func_name in __except__:
|
||||||
|
continue
|
||||||
|
|
||||||
func = globals()[func_name]
|
func = globals()[func_name]
|
||||||
|
|
||||||
cur_doc = func.__doc__
|
cur_doc = func.__doc__
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ import_functions(
|
|||||||
module=file_ops, function_names=file_ops.__all__, target_globals=globals()
|
module=file_ops, function_names=file_ops.__all__, target_globals=globals()
|
||||||
)
|
)
|
||||||
__all__ = file_ops.__all__
|
__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
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from openhands.events.action import (
|
|||||||
BrowseInteractiveAction,
|
BrowseInteractiveAction,
|
||||||
BrowseURLAction,
|
BrowseURLAction,
|
||||||
CmdRunAction,
|
CmdRunAction,
|
||||||
|
FileEditAction,
|
||||||
FileReadAction,
|
FileReadAction,
|
||||||
FileWriteAction,
|
FileWriteAction,
|
||||||
IPythonRunCellAction,
|
IPythonRunCellAction,
|
||||||
@@ -179,6 +180,10 @@ class Runtime:
|
|||||||
def write(self, action: FileWriteAction) -> Observation:
|
def write(self, action: FileWriteAction) -> Observation:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def edit(self, action: FileEditAction) -> Observation:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def browse(self, action: BrowseURLAction) -> Observation:
|
def browse(self, action: BrowseURLAction) -> Observation:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user