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
+31 -1
View File
@@ -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)
+12 -2
View File
@@ -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>',
], ],
} }
+27 -5
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>. 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.
+26 -58
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. 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:
+1
View 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
+4
View File
@@ -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.
""" """
+4
View File
@@ -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
""" """
+6 -1
View File
@@ -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',
+20
View File
@@ -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}'
+6 -1
View File
@@ -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',
+27
View File
@@ -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'
+6 -1
View File
@@ -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,
+60
View File
@@ -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)
+4
View File
@@ -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
+5
View File
@@ -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