mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
7 Commits
openhands/
...
xw/diff-ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e398d6362 | ||
|
|
8f565c8740 | ||
|
|
df28a7a5b9 | ||
|
|
4743eb4c35 | ||
|
|
4a6898bbda | ||
|
|
a4ddac4f2c | ||
|
|
33422f1a4a |
@@ -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)
|
||||
|
||||
@@ -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>',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user