mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
14 Commits
add-action
...
JayQuimby/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0ba806f35 | ||
|
|
04e9caab8b | ||
|
|
26a1ce2d73 | ||
|
|
87e36570f6 | ||
|
|
ce9bd95009 | ||
|
|
0f76f70a81 | ||
|
|
e830627b4d | ||
|
|
30f084713a | ||
|
|
24b21de8bc | ||
|
|
db608aba83 | ||
|
|
1a882acb1b | ||
|
|
823a631a12 | ||
|
|
cca6ba7f25 | ||
|
|
ed608ee23c |
@@ -2,8 +2,10 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# Import agents after environment variables are loaded
|
||||
from . import monologue_agent # noqa: E402
|
||||
from . import codeact_agent # noqa: E402
|
||||
from . import planner_agent # noqa: E402
|
||||
from . import monologue_agent # noqa: E402
|
||||
from . import codeact_agent # noqa: E402
|
||||
from . import planner_agent # noqa: E402
|
||||
from . import thinkact_agent # noqa: E402
|
||||
|
||||
__all__ = ['monologue_agent', 'codeact_agent', 'planner_agent']
|
||||
__all__ = ['monologue_agent', 'codeact_agent',
|
||||
'planner_agent', 'thinkact_agent']
|
||||
|
||||
@@ -32,46 +32,46 @@ MAX_MONOLOGUE_LENGTH = 20000
|
||||
MAX_OUTPUT_LENGTH = 5000
|
||||
|
||||
INITIAL_THOUGHTS = [
|
||||
"I exist!",
|
||||
"Hmm...looks like I can type in a command line prompt",
|
||||
"Looks like I have a web browser too!",
|
||||
'I exist!',
|
||||
'Hmm...looks like I can type in a command line prompt',
|
||||
'Looks like I have a web browser too!',
|
||||
"Here's what I want to do: $TASK",
|
||||
"How am I going to get there though?",
|
||||
"It seems like I have some kind of short term memory.",
|
||||
"Each of my thoughts seems to be stored in a JSON array.",
|
||||
"It seems whatever I say next will be added as an object to the list.",
|
||||
"But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.",
|
||||
"Fortunately I have long term memory!",
|
||||
"I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!",
|
||||
'How am I going to get there though?',
|
||||
'It seems like I have some kind of short term memory.',
|
||||
'Each of my thoughts seems to be stored in a JSON array.',
|
||||
'It seems whatever I say next will be added as an object to the list.',
|
||||
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
|
||||
'Fortunately I have long term memory!',
|
||||
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
|
||||
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
|
||||
"Let's try it out!",
|
||||
"RECALL what it is I want to do",
|
||||
'RECALL what it is I want to do',
|
||||
"Here's what I want to do: $TASK",
|
||||
"How am I going to get there though?",
|
||||
'How am I going to get there though?',
|
||||
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
|
||||
'RUN echo "hello world"',
|
||||
"hello world",
|
||||
"Cool! I bet I can write files too using the write action.",
|
||||
"WRITE echo \"console.log('hello world')\" > test.js",
|
||||
"",
|
||||
'hello world',
|
||||
'Cool! I bet I can write files too using the write action.',
|
||||
"WRITE 0 1 echo \"console.log('hello world')\" > test.js",
|
||||
'',
|
||||
"I just created test.js. I'll try and run it now.",
|
||||
"RUN node test.js",
|
||||
"hello world",
|
||||
"It works!",
|
||||
'RUN node test.js',
|
||||
'hello world',
|
||||
'It works!',
|
||||
"I'm going to try reading it now using the read action.",
|
||||
"READ test.js",
|
||||
'READ test.js',
|
||||
"console.log('hello world')",
|
||||
"Nice! I can read files too!",
|
||||
"And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument",
|
||||
'Nice! I can read files too!',
|
||||
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
|
||||
"Let's try that...",
|
||||
"BROWSE google.com",
|
||||
'BROWSE google.com',
|
||||
'<form><input type="text"></input><button type="submit"></button></form>',
|
||||
"I can browse the web too!",
|
||||
"And once I have completed my task, I can use the finish action to stop working.",
|
||||
'I can browse the web too!',
|
||||
'And once I have completed my task, I can use the finish action to stop working.',
|
||||
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
|
||||
"Very cool. Now to accomplish my task.",
|
||||
'Very cool. Now to accomplish my task.',
|
||||
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
|
||||
"In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.",
|
||||
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
|
||||
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
|
||||
"It seems like there might be an existing project here. I should probably start by running `ls` to see what's here.",
|
||||
]
|
||||
@@ -106,15 +106,15 @@ class MonologueAgent(Agent):
|
||||
- event (dict): The event that will be added to monologue and memory
|
||||
"""
|
||||
|
||||
if "extras" in event and "screenshot" in event["extras"]:
|
||||
del event["extras"]["screenshot"]
|
||||
if 'extras' in event and 'screenshot' in event['extras']:
|
||||
del event['extras']['screenshot']
|
||||
if (
|
||||
"args" in event
|
||||
and "output" in event["args"]
|
||||
and len(event["args"]["output"]) > MAX_OUTPUT_LENGTH
|
||||
'args' in event
|
||||
and 'output' in event['args']
|
||||
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
|
||||
):
|
||||
event["args"]["output"] = (
|
||||
event["args"]["output"][:MAX_OUTPUT_LENGTH] + "..."
|
||||
event['args']['output'] = (
|
||||
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
|
||||
)
|
||||
|
||||
self.monologue.add_event(event)
|
||||
@@ -125,7 +125,7 @@ class MonologueAgent(Agent):
|
||||
def _initialize(self, task: str):
|
||||
"""
|
||||
Utilizes the INITIAL_THOUGHTS list to give the agent a context for it's capabilities and how to navigate the /workspace.
|
||||
Short circuted to return when already initialized.
|
||||
Short circuited to return when already initialized.
|
||||
|
||||
Parameters:
|
||||
- task (str): The initial goal statement provided by the user
|
||||
@@ -137,51 +137,54 @@ class MonologueAgent(Agent):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if task is None or task == "":
|
||||
raise ValueError("Instruction must be provided")
|
||||
if task is None or task == '':
|
||||
raise ValueError('Instruction must be provided')
|
||||
self.monologue = Monologue()
|
||||
self.memory = LongTermMemory()
|
||||
|
||||
output_type = ""
|
||||
output_type = ''
|
||||
for thought in INITIAL_THOUGHTS:
|
||||
thought = thought.replace("$TASK", task)
|
||||
if output_type != "":
|
||||
observation: Observation = NullObservation(content="")
|
||||
thought = thought.replace('$TASK', task)
|
||||
if output_type != '':
|
||||
observation: Observation = NullObservation(content='')
|
||||
if output_type == ObservationType.RUN:
|
||||
observation = CmdOutputObservation(
|
||||
content=thought, command_id=0, command=""
|
||||
content=thought, command_id=0, command=''
|
||||
)
|
||||
elif output_type == ObservationType.READ:
|
||||
observation = FileReadObservation(content=thought, path="")
|
||||
observation = FileReadObservation(content=thought, path='')
|
||||
elif output_type == ObservationType.RECALL:
|
||||
observation = AgentRecallObservation(content=thought, memories=[])
|
||||
observation = AgentRecallObservation(
|
||||
content=thought, memories=[])
|
||||
elif output_type == ObservationType.BROWSE:
|
||||
observation = BrowserOutputObservation(
|
||||
content=thought, url="", screenshot=""
|
||||
content=thought, url='', screenshot=''
|
||||
)
|
||||
self._add_event(observation.to_dict())
|
||||
output_type = ""
|
||||
output_type = ''
|
||||
else:
|
||||
action: Action = NullAction()
|
||||
if thought.startswith("RUN"):
|
||||
command = thought.split("RUN ")[1]
|
||||
if thought.startswith('RUN'):
|
||||
command = thought.split('RUN ')[1]
|
||||
action = CmdRunAction(command)
|
||||
output_type = ActionType.RUN
|
||||
elif thought.startswith("WRITE"):
|
||||
parts = thought.split("WRITE ")[1].split(" > ")
|
||||
elif thought.startswith('WRITE'):
|
||||
parts = thought.split('WRITE ')[1].split(' > ')
|
||||
path = parts[1]
|
||||
content = parts[0]
|
||||
action = FileWriteAction(path=path, content=content)
|
||||
elif thought.startswith("READ"):
|
||||
path = thought.split("READ ")[1]
|
||||
start, end, content = parts[0].split(' ')
|
||||
start_ind, end_ind = int(start), int(end)
|
||||
action = FileWriteAction(
|
||||
path=path, start=start_ind, end=end_ind, content=content)
|
||||
elif thought.startswith('READ'):
|
||||
path = thought.split('READ ')[1]
|
||||
action = FileReadAction(path=path)
|
||||
output_type = ActionType.READ
|
||||
elif thought.startswith("RECALL"):
|
||||
query = thought.split("RECALL ")[1]
|
||||
elif thought.startswith('RECALL'):
|
||||
query = thought.split('RECALL ')[1]
|
||||
action = AgentRecallAction(query=query)
|
||||
output_type = ActionType.RECALL
|
||||
elif thought.startswith("BROWSE"):
|
||||
url = thought.split("BROWSE ")[1]
|
||||
elif thought.startswith('BROWSE'):
|
||||
url = thought.split('BROWSE ')[1]
|
||||
action = BrowseURLAction(url=url)
|
||||
output_type = ActionType.BROWSE
|
||||
else:
|
||||
@@ -211,9 +214,9 @@ class MonologueAgent(Agent):
|
||||
self.monologue.get_thoughts(),
|
||||
state.background_commands_obs,
|
||||
)
|
||||
messages = [{"content": prompt, "role": "user"}]
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
action_resp = resp["choices"][0]["message"]["content"]
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
action = prompts.parse_action_response(action_resp)
|
||||
self.latest_action = action
|
||||
return action
|
||||
|
||||
4
agenthub/thinkact_agent/__init__.py
Normal file
4
agenthub/thinkact_agent/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .agent import ThinkActAgent
|
||||
|
||||
Agent.register('ThinkActAgent', ThinkActAgent)
|
||||
99
agenthub/thinkact_agent/agent.py
Normal file
99
agenthub/thinkact_agent/agent.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from typing import List
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.state import State
|
||||
from opendevin.action import Action, AgentThinkAction
|
||||
from opendevin.observation import Observation
|
||||
|
||||
from .parser import parse_command
|
||||
|
||||
from .prompts import (
|
||||
SYSTEM_MESSAGE,
|
||||
STEP_PROMPT,
|
||||
MEMORY_FORMAT,
|
||||
NO_ACTION,
|
||||
CONTEXT_PROMPT
|
||||
)
|
||||
|
||||
# TODO: Add state management
|
||||
|
||||
|
||||
class ThinkActAgent(Agent):
|
||||
"""
|
||||
An attempt to recreate swe_agent with output parsing, prompting style, and Application Computer Interface (ACI).
|
||||
|
||||
SWE-agent includes ACI functions like 'goto', 'search_for', 'edit', 'scroll', 'run'
|
||||
"""
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
self.memory_window = 5
|
||||
self.max_retries = 5
|
||||
self.running_memory: List[str] = []
|
||||
|
||||
def _remember(self, action: Action, observation: Observation):
|
||||
"""Agent has a limited memory of the few steps implemented as a queue"""
|
||||
memory = MEMORY_FORMAT(action.to_dict(), observation.to_dict())
|
||||
self.running_memory.append(memory)
|
||||
|
||||
def _think_act(self, messages: List[dict]) -> tuple[Action, str]:
|
||||
resp = self.llm.completion(
|
||||
messages=messages,
|
||||
temperature=0.0
|
||||
)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
return parse_command(action_resp)
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
SWE-Agent step:
|
||||
1. Get context - past actions, custom commands, current step
|
||||
2. Perform think-act - prompt model for action and reasoning
|
||||
3. Catch errors - ensure model takes action (5 attempts max)
|
||||
"""
|
||||
for prev_action, obs in state.updated_info:
|
||||
self._remember(prev_action, obs)
|
||||
|
||||
prompt = STEP_PROMPT(
|
||||
state.plan.main_goal,
|
||||
state.working_dir,
|
||||
state.file_name,
|
||||
state.cur_line
|
||||
)
|
||||
|
||||
context = CONTEXT_PROMPT(
|
||||
self.running_memory,
|
||||
self.memory_window
|
||||
)
|
||||
|
||||
msgs = [
|
||||
{'content': SYSTEM_MESSAGE, 'role': 'user'},
|
||||
{'content': prompt, 'role': 'user'}
|
||||
]
|
||||
|
||||
if len(self.running_memory) > 0:
|
||||
msgs.insert(0, context)
|
||||
|
||||
# print('\n\n\n'.join([m['content'] for m in msgs]))
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
start_msg_len = len(msgs)
|
||||
while not action and len(msgs) < self.max_retries + start_msg_len:
|
||||
error = NO_ACTION(thought)
|
||||
error_msg = {'content': error, 'role': 'user'}
|
||||
msgs.append(error_msg)
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
if not action:
|
||||
action = AgentThinkAction(thought)
|
||||
|
||||
self.latest_action = action
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
return [item for item in self.running_memory if query in item]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Used to reset the agent"""
|
||||
self.running_memory = []
|
||||
super().reset()
|
||||
75
agenthub/thinkact_agent/parser.py
Normal file
75
agenthub/thinkact_agent/parser.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from opendevin.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
BrowseURLAction,
|
||||
)
|
||||
|
||||
# commands: exit, read, write, browse, kill, search_file, search_dir
|
||||
|
||||
|
||||
def get_action_from_string(command_string: str) -> Action | None:
|
||||
"""
|
||||
Parses the command string to find which command the agent wants to run
|
||||
Converts the command into a proper Action and returns
|
||||
"""
|
||||
vars = command_string.split(' ')
|
||||
cmd = vars[0]
|
||||
args = [] if len(vars) == 1 else ' '.join(vars[1:])
|
||||
|
||||
# TODO: add exception handling for improper commands
|
||||
if 'exit' == cmd:
|
||||
return AgentFinishAction()
|
||||
|
||||
elif 'read' == cmd:
|
||||
if len(args) == 1:
|
||||
file = args[0]
|
||||
start = 0
|
||||
elif len(args) == 2:
|
||||
file, start = args[0], int(args[1])
|
||||
elif len(args) > 2:
|
||||
file, start = args[0], int(args[1])
|
||||
else:
|
||||
return None
|
||||
|
||||
return FileReadAction(file, start)
|
||||
|
||||
elif 'write' == cmd:
|
||||
assert len(args) >= 4, 'Not enough arguments for this command'
|
||||
file = args[0]
|
||||
start, end = [int(arg) for arg in args[1:3]]
|
||||
content = ' '.join(args[3:])
|
||||
return FileWriteAction(file, content, start, end)
|
||||
|
||||
elif 'browse' == cmd:
|
||||
return BrowseURLAction(args[0].strip())
|
||||
|
||||
# TODO: need to integrate all of the custom commands
|
||||
|
||||
else:
|
||||
return CmdRunAction(command_string)
|
||||
|
||||
|
||||
def parse_command(input_str: str):
|
||||
"""
|
||||
Parses a given string and separates the command (enclosed in triple backticks) from any accompanying text.
|
||||
|
||||
Args:
|
||||
input_str (str): The input string to be parsed.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the command and the accompanying text (if any).
|
||||
"""
|
||||
input_str = input_str.strip()
|
||||
if '```' in input_str:
|
||||
parts = input_str.split('```')
|
||||
command_str = parts[1].strip()
|
||||
action = get_action_from_string(command_str)
|
||||
if action:
|
||||
ind = 2 if len(parts) > 2 else 1
|
||||
accompanying_text = ''.join(parts[:-ind]).strip()
|
||||
return action, accompanying_text
|
||||
|
||||
return None, input_str # used for retry
|
||||
36
agenthub/thinkact_agent/plans.md
Normal file
36
agenthub/thinkact_agent/plans.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Things that need to be done for SWE-Agent
|
||||
|
||||
## 1. Add in all of the following commands to ACI
|
||||
```
|
||||
'search_for': {
|
||||
'params': '<keywords>',
|
||||
'description': 'Will allow you to search your working directory for files and folders that match your <keyword>.',
|
||||
}
|
||||
|
||||
'edit ': {
|
||||
'params': '<filename>',
|
||||
'description': 'This will allow you to modify files within your working directory. Usage: "edit example.txt" -> opens the example.txt file and shows the first 100 lines',
|
||||
},
|
||||
|
||||
'goto': {
|
||||
'params': '<line_num>',
|
||||
'description': 'This will allow you to go through a file to any line. Usage: "Goto 124" -> returns lines 124-224 within current file',
|
||||
},
|
||||
|
||||
'scroll_up': {
|
||||
'params': '',
|
||||
'description': 'When you are in a file you can see the 100 lines above your current view. Usage: "scroll_up" -> returns the 100 lines above what you were reading',
|
||||
},
|
||||
|
||||
'scroll_down': {
|
||||
'params': '',
|
||||
'description': 'When you are in a file you can see the 100 lines below your current view. Usage: "scroll_down" -> returns the 100 lines below what you were reading',
|
||||
},
|
||||
|
||||
'modify': {
|
||||
'params': '<start_line>:<end_line> "<replacement>"',
|
||||
'description': 'This will make changes to a file by deleting all lines from <start_line> to <end_line> and replacing them with <replacement>',
|
||||
}
|
||||
|
||||
```
|
||||
## 2. Add linter to check code edits before modifying file
|
||||
158
agenthub/thinkact_agent/prompts.py
Normal file
158
agenthub/thinkact_agent/prompts.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from opendevin.parse_commands import parse_command_file
|
||||
|
||||
|
||||
DEFAULT_COMMANDS = {
|
||||
'exit': 'Executed when task is complete',
|
||||
'read <file_name> [<start_line>]': 'shows a given file\'s contents starting from <start_line>, default start is 0',
|
||||
'write <file> <changes> [<start_line>] [<end_line>]': 'modifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>',
|
||||
'browse <url>': 'returns the text version of any url',
|
||||
'<bash_command> <args>': 'Any bash command is valid (cd, ls, rm, grep, dir, mv, wget, git, zip, etc.) with their arguments included',
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_COMMAND_STR = '\n'.join(
|
||||
[k + ' - ' + v for k, v in DEFAULT_COMMANDS.items()])
|
||||
|
||||
|
||||
COMMAND_DOCS = parse_command_file()
|
||||
|
||||
|
||||
CUSTOM_COMMANDS = f"""
|
||||
Apart from the standard bash commands, you can also use the following special commands:
|
||||
{COMMAND_DOCS}
|
||||
"""
|
||||
|
||||
|
||||
COMMAND_SEGMENT = CUSTOM_COMMANDS if COMMAND_DOCS is not None else ''
|
||||
|
||||
|
||||
DOCUMENTATION = f"""
|
||||
Commands:
|
||||
{DEFAULT_COMMAND_STR}
|
||||
{COMMAND_SEGMENT}
|
||||
"""
|
||||
|
||||
|
||||
RESPONSE_FORMAT = '''
|
||||
This is the format of the response you will make in order to solve the current issue.
|
||||
You will be given multiple iterations to complete this task so break it into steps and solve them one by one.
|
||||
|
||||
Your output must contain the following:
|
||||
- First, think about what your next action should be and plan it out. you will have a memory of your thoughts so you can use this to remember things for the next step.
|
||||
- Second, create a piece of code that will execute your next action based on the thoughts you have.
|
||||
- The code MUST be surrounded in triple back ticks EXACTLY like this: ```\n<code>\n```
|
||||
|
||||
This is a template using the format described above
|
||||
Items in <> are suggestions for you, fill them out based on the context of the problem you are solving.
|
||||
Format:
|
||||
Thoughts:
|
||||
<Provide clear and concise thoughts on the next steps to take, highlighting any important details or context that should be remembered.>
|
||||
Action to execute:
|
||||
```
|
||||
<command> <params>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- If you give more than one command as your output then only the last command in your output will be executed.
|
||||
- To execute multiple commands you should write them down in your thoughts so you can remember it on the next step and execute them then.
|
||||
- The only commands you are not capable of executing are interactive commands like `python` or `vim`.
|
||||
- When you have finished the task you should run the `exit` command so the system knows you have finished.
|
||||
- The write command requires proper indentation in the content section ex. `write hw.py def hello():\n print(\'Hello World\')` this is how you would have to format your write command.
|
||||
- The white spaces matter as the code changes will be added to the code so they must have proper syntax.
|
||||
'''
|
||||
|
||||
|
||||
SYSTEM_MESSAGE = f'''
|
||||
SYSTEM INFO:
|
||||
You are an autonomous coding agent, here to provide solutions for coding issues. I have been designed to assist you with a wide range of programming tasks, from code editing and debugging to testing and deployment. I have access to a variety of tools and commands that I can use to help you solve problems efficiently.
|
||||
{DOCUMENTATION}
|
||||
|
||||
{RESPONSE_FORMAT}
|
||||
'''.strip()
|
||||
|
||||
|
||||
GENERAL_GUIDELINES = '''INSTRUCTIONS:
|
||||
Now, you're going to solve this issue on your own. You can use any bash commands or custom commands you wish to complete your task. Edit all the files you need to and run any checks or tests that you want.
|
||||
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
|
||||
When you're satisfied with all of the changes you've made, you can indicate that you are done by running the exit command.
|
||||
Note however that you cannot use any interactive session commands (e.g. python, vim) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
|
||||
|
||||
NOTE ABOUT THE write COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
|
||||
|
||||
IMPORTANT TIPS:
|
||||
1. Reproduce the bug: Always start by trying to replicate the bug that the issue discusses. If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment and run it to ensure you can reproduce the bug. Then, start trying to fix it. When you think you've fixed the bug, re-run the bug reproduction script to make sure that the issue has indeed been resolved.
|
||||
|
||||
If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file, so that you can be sure the script ran fine all the way through.
|
||||
|
||||
2. Try different commands: If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it.
|
||||
|
||||
3. Navigate large files: If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, you would use the 'read' command like this: 'read <file> 583'. This is a much faster way to read through the file.
|
||||
|
||||
4. Handle input files: If the bug reproduction script requires inputting/reading a specific file, such as 'buggy-input.png', and you'd like to understand how to input that file, conduct a search in the existing repository code to see whether someone else has already done that. Do this by running the command: 'search_dir "buggy-input.png"'. If that doesn't work, use the Linux 'find' command.
|
||||
|
||||
5. Understand your context: Always make sure to look at the currently open file and the current working directory. The currently open file might be in a different directory than the working directory.
|
||||
|
||||
6. Verify your edits: When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
|
||||
|
||||
7. Thoroughly test your solution: After making any changes to fix a bug, be sure to thoroughly test your solution to ensure the bug has been resolved. Re-run the bug reproduction script and verify that the issue has been addressed.
|
||||
'''
|
||||
|
||||
|
||||
def NO_ACTION(latest): return f'''
|
||||
You did not include any action to take in your most recent output:
|
||||
|
||||
===== Output ======
|
||||
{latest}
|
||||
==== End Output ===
|
||||
|
||||
Remember these are the custom commands you can use:
|
||||
{DOCUMENTATION}
|
||||
|
||||
Lets try that again, it is very important that you adhere to the output format
|
||||
This time, be sure to use the exact format below, replacing anything in <> with the appropriate value(s):
|
||||
{RESPONSE_FORMAT}
|
||||
|
||||
It is crucial you use the format provided as the output will be parsed automatically.
|
||||
'''
|
||||
|
||||
|
||||
def file_info(dir: str, file: str, line: int):
|
||||
res = 'Here is an overview of your current workspace:\n'
|
||||
res += f'\t- Working Directory: {dir}'
|
||||
if file:
|
||||
res += f'\t- Open File: {file}, Lines {line}:{line+100}'
|
||||
return res
|
||||
|
||||
|
||||
def STEP_PROMPT(task, dir, file, line_num): return f'''
|
||||
{GENERAL_GUIDELINES}
|
||||
|
||||
You are currently trying to complete this task:
|
||||
{task}
|
||||
|
||||
{file_info(dir, file, line_num)}
|
||||
|
||||
Keep all of the guidelines above in mind when you are thinking and making code.
|
||||
Please come up with a thought and action based on your current task and latest steps.
|
||||
'''.strip()
|
||||
|
||||
|
||||
def MEMORY_FORMAT(act, obs): return f'''
|
||||
You performed this action:
|
||||
'{act}'
|
||||
|
||||
The following happened as a result:
|
||||
{obs}
|
||||
'''.strip()
|
||||
|
||||
|
||||
def CONTEXT_PROMPT(memory, window):
|
||||
res = f'These are your past {window} memories:\n'
|
||||
window_size = window if len(memory) > window else len(memory)
|
||||
cur_mems = memory[-window_size:]
|
||||
res += '===== Memories =====\n'
|
||||
for idx, mem in enumerate(cur_mems):
|
||||
res += f'\nMemory {idx}:\n{mem}\n'
|
||||
res += '======= End =======\n'
|
||||
res += 'Use these memories to provide additional context to the problem you are solving.\n'
|
||||
return res
|
||||
@@ -18,9 +18,9 @@ class AgentRecallAction(ExecutableAction):
|
||||
query: str
|
||||
action: str = ActionType.RECALL
|
||||
|
||||
def run(self, controller: "AgentController") -> AgentRecallObservation:
|
||||
def run(self, controller: 'AgentController') -> AgentRecallObservation:
|
||||
return AgentRecallObservation(
|
||||
content="Recalling memories...",
|
||||
content='Recalling memories...',
|
||||
memories=controller.agent.search_memory(self.query),
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ class AgentThinkAction(NotExecutableAction):
|
||||
thought: str
|
||||
action: str = ActionType.THINK
|
||||
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
def run(self, controller: 'AgentController') -> 'Observation':
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@@ -45,9 +45,9 @@ class AgentThinkAction(NotExecutableAction):
|
||||
@dataclass
|
||||
class AgentEchoAction(ExecutableAction):
|
||||
content: str
|
||||
action: str = "echo"
|
||||
action: str = 'echo'
|
||||
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
def run(self, controller: 'AgentController') -> 'Observation':
|
||||
return AgentMessageObservation(self.content)
|
||||
|
||||
@property
|
||||
@@ -58,7 +58,6 @@ class AgentEchoAction(ExecutableAction):
|
||||
@dataclass
|
||||
class AgentSummarizeAction(NotExecutableAction):
|
||||
summary: str
|
||||
|
||||
action: str = ActionType.SUMMARIZE
|
||||
|
||||
@property
|
||||
@@ -70,7 +69,7 @@ class AgentSummarizeAction(NotExecutableAction):
|
||||
class AgentFinishAction(NotExecutableAction):
|
||||
action: str = ActionType.FINISH
|
||||
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
def run(self, controller: 'AgentController') -> 'Observation':
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,49 +1,170 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.observation import FileReadObservation, FileWriteObservation
|
||||
from opendevin.observation import (
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
AgentErrorObservation,
|
||||
Observation
|
||||
)
|
||||
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
from .base import ExecutableAction
|
||||
|
||||
# This is the path where the workspace is mounted in the container
|
||||
# The LLM sometimes returns paths with this prefix, so we need to remove it
|
||||
PATH_PREFIX = "/workspace/"
|
||||
PATH_PREFIX = '/workspace/'
|
||||
|
||||
|
||||
def resolve_path(base_path, file_path):
|
||||
def validate_file_content(file_path: str, content: str) -> Optional[str]:
|
||||
"""
|
||||
Validates the content of a code file by checking for syntax errors.
|
||||
|
||||
Args:
|
||||
file_path (str): The full path to the file being validated.
|
||||
content (str): The content of the file to be validated.
|
||||
|
||||
Returns:
|
||||
str or None: Error message if there are syntax errors, otherwise None.
|
||||
"""
|
||||
file_extension = os.path.splitext(
|
||||
file_path)[1][1:] # Get the file extension without the dot
|
||||
|
||||
# Determine the appropriate validation command based on the file extension
|
||||
validation_commands = {
|
||||
'py': [sys.executable, '-c', 'import sys; compile(sys.stdin.read(), "<string>", "exec")'],
|
||||
'js': ['node', '-c', '-'],
|
||||
'java': ['javac', '-encoding', 'UTF-8', '-Xlint:all', '-'],
|
||||
'cpp': ['g++', '-std=c++11', '-Wall', '-Wextra', '-Werror', '-x', 'c++', '-c', '-'],
|
||||
'c': ['gcc', '-std=c11', '-Wall', '-Wextra', '-Werror', '-x', 'c', '-c', '-']
|
||||
}
|
||||
ignore_types = ['txt', 'md', 'doc', 'pdf']
|
||||
|
||||
if file_extension in ignore_types:
|
||||
return None
|
||||
|
||||
elif file_extension in validation_commands:
|
||||
try:
|
||||
# Run the validation command and capture the output
|
||||
subprocess.run(
|
||||
validation_commands[file_extension],
|
||||
input=content.encode(),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True, check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
return e.stderr.strip()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
# If the file extension is not recognized, return a default error message
|
||||
return f'Unsupported file type: {file_extension}'
|
||||
|
||||
|
||||
def resolve_path(base_path: str, file_path: str) -> str:
|
||||
if file_path.startswith(PATH_PREFIX):
|
||||
file_path = file_path[len(PATH_PREFIX) :]
|
||||
file_path = file_path[len(PATH_PREFIX):]
|
||||
return os.path.join(base_path, file_path)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileReadAction(ExecutableAction):
|
||||
"""
|
||||
Reads a file from a given path up to 100 lines.
|
||||
Default lines 0:100
|
||||
"""
|
||||
path: str
|
||||
start_index: int = 0
|
||||
max_lines: int = 100
|
||||
action: str = ActionType.READ
|
||||
|
||||
def run(self, controller) -> FileReadObservation:
|
||||
path = resolve_path(controller.workdir, self.path)
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return FileReadObservation(path=path, content=file.read())
|
||||
# def run(self, controller) -> FileReadObservation:
|
||||
# path = resolve_path(controller.workdir, self.path)
|
||||
def run(self):
|
||||
path = resolve_path('./workspace', self.path)
|
||||
if not os.path.exists(path):
|
||||
return FileReadObservation(path=path, content='File not found')
|
||||
|
||||
try:
|
||||
all_lines = []
|
||||
with open(path, 'r', encoding='utf-8') as file:
|
||||
for line in file:
|
||||
all_lines.append(line.strip('\n'))
|
||||
total_lines = len(all_lines)
|
||||
if total_lines >= self.max_lines:
|
||||
end_index = self.start_index + self.max_lines - 1 if total_lines - \
|
||||
self.start_index - self.max_lines >= 0 else -1
|
||||
code_slice = all_lines[self.start_index - 1: end_index]
|
||||
else:
|
||||
code_slice = all_lines[:]
|
||||
code_view = '\n'.join(code_slice)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
return FileReadObservation(path=path, content=f'Error reading file: {e}')
|
||||
|
||||
return FileReadObservation(path=path, content=code_view)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Reading file: {self.path}"
|
||||
return f'Reading file: {self.path}'
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileWriteAction(ExecutableAction):
|
||||
path: str
|
||||
content: str
|
||||
start: int
|
||||
end: int
|
||||
action: str = ActionType.WRITE
|
||||
|
||||
def run(self, controller) -> FileWriteObservation:
|
||||
whole_path = resolve_path(controller.workdir, self.path)
|
||||
with open(whole_path, "w", encoding="utf-8") as file:
|
||||
file.write(self.content)
|
||||
return FileWriteObservation(content="", path=self.path)
|
||||
def run(self, controller) -> Observation:
|
||||
full_path = resolve_path(controller.workdir, self.path)
|
||||
parent_dir = os.path.dirname(full_path)
|
||||
Path(parent_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
all_lines = []
|
||||
try:
|
||||
with open(full_path, 'r', encoding='utf-8') as file:
|
||||
for line in file:
|
||||
all_lines.append(line.strip('\n'))
|
||||
except (IOError, UnicodeDecodeError):
|
||||
all_lines = []
|
||||
|
||||
# Split the content into lines
|
||||
new_lines = self.content.split('\n')
|
||||
|
||||
# Check if the start and end indices are valid
|
||||
if self.start < 0 or self.end < 0:
|
||||
return AgentErrorObservation(content=f'Invalid start or end index: {self.start}, {self.end}')
|
||||
elif self.start <= len(all_lines) and self.start + len(new_lines) <= len(all_lines):
|
||||
new_file_lines = all_lines[:self.start-1] + \
|
||||
new_lines + all_lines[len(all_lines)-1:]
|
||||
elif self.start <= len(all_lines) and self.start + len(new_lines) > len(all_lines):
|
||||
new_file_lines = all_lines[:self.start-1] + new_lines
|
||||
elif self.start > len(all_lines):
|
||||
new_file_lines = all_lines + \
|
||||
['' for i in range(len(all_lines), self.start-1)] + new_lines
|
||||
|
||||
new_file_content = '\n'.join(new_file_lines)
|
||||
|
||||
validation_status = validate_file_content(full_path, new_file_content)
|
||||
|
||||
if not validation_status:
|
||||
try:
|
||||
with open(full_path, 'w', encoding='utf-8') as file:
|
||||
file.write(new_file_content)
|
||||
except IOError as e:
|
||||
return AgentErrorObservation(content=f'Error writing file: {e}')
|
||||
return FileWriteObservation(content=self.content, path=self.path)
|
||||
else:
|
||||
return AgentErrorObservation(content=validation_status)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Writing file: {self.path}"
|
||||
return f'Writing file: {self.path}'
|
||||
|
||||
@@ -58,10 +58,10 @@ def print_with_color(text: Any, print_type: str = 'INFO'):
|
||||
}
|
||||
color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR['INFO'])
|
||||
if DISABLE_COLOR_PRINTING:
|
||||
print(f"\n{print_type.upper()}:\n{str(text)}", flush=True)
|
||||
print(f'\n{print_type.upper()}:\n{str(text)}', flush=True)
|
||||
else:
|
||||
print(
|
||||
colored(f"\n{print_type.upper()}:\n", color, attrs=['bold'])
|
||||
colored(f'\n{print_type.upper()}:\n', color, attrs=['bold'])
|
||||
+ colored(str(text), color),
|
||||
flush=True,
|
||||
)
|
||||
@@ -105,7 +105,7 @@ class AgentController:
|
||||
async def start_loop(self, task: str):
|
||||
finished = False
|
||||
plan = Plan(task)
|
||||
self.state = State(plan)
|
||||
self.state = State(plan, self.workdir)
|
||||
for i in range(self.max_iterations):
|
||||
try:
|
||||
finished = await self.step(i)
|
||||
|
||||
@@ -12,41 +12,42 @@ class Command:
|
||||
|
||||
|
||||
def parse_command_file() -> str | None:
|
||||
if not os.path.exists("commands.sh"):
|
||||
if not os.path.exists('evaluation/SWE-bench/commands.sh'):
|
||||
print('Error reading commands, could not find commands.sh')
|
||||
return None
|
||||
content = open("commands.sh", "r").read()
|
||||
lines = content.split("\n")
|
||||
content = open('evaluation/SWE-bench/commands.sh', 'r').read()
|
||||
lines = content.split('\n')
|
||||
commands: list[Command] = []
|
||||
idx = 0
|
||||
docs: list[str] = []
|
||||
while idx < len(lines):
|
||||
line = lines[idx]
|
||||
idx += 1
|
||||
if line.startswith("# "):
|
||||
if line.startswith('# '):
|
||||
docs.append(line[2:])
|
||||
elif line.strip().endswith("() {"):
|
||||
elif line.strip().endswith('() {'):
|
||||
name = line.split()[0][:-2]
|
||||
while lines[idx].strip() != "}":
|
||||
while lines[idx].strip() != '}':
|
||||
idx += 1
|
||||
docstring, signature = None, name
|
||||
docs_dict = yaml.safe_load("\n".join(docs).replace("@yaml", ""))
|
||||
docs_dict = yaml.safe_load('\n'.join(docs).replace('@yaml', ''))
|
||||
if docs_dict is not None:
|
||||
docstring = docs_dict.get("docstring")
|
||||
arguments = docs_dict.get("arguments", None)
|
||||
if "signature" in docs_dict:
|
||||
signature = docs_dict["signature"]
|
||||
docstring = docs_dict.get('docstring')
|
||||
arguments = docs_dict.get('arguments', None)
|
||||
if 'signature' in docs_dict:
|
||||
signature = docs_dict['signature']
|
||||
else:
|
||||
if arguments is not None:
|
||||
for param, settings in arguments.items():
|
||||
if "required" in settings:
|
||||
signature += f" <{param}>"
|
||||
if 'required' in settings:
|
||||
signature += f' <{param}>'
|
||||
else:
|
||||
signature += f" [<{param}>]"
|
||||
signature += f' [<{param}>]'
|
||||
command = Command(name, docstring, signature)
|
||||
commands.append(command)
|
||||
docs = []
|
||||
function_docs = ""
|
||||
function_docs = ''
|
||||
for cmd in commands:
|
||||
if cmd.docstring is not None:
|
||||
function_docs += f"{cmd.signature or cmd.name} - {cmd.docstring}\n"
|
||||
function_docs += f'{cmd.signature or cmd.name} - {cmd.docstring}\n'
|
||||
return function_docs
|
||||
|
||||
@@ -11,10 +11,16 @@ from opendevin.observation import (
|
||||
CmdOutputObservation,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
plan: Plan
|
||||
working_dir: str
|
||||
iteration: int = 0
|
||||
background_commands_obs: List[CmdOutputObservation] = field(default_factory=list)
|
||||
file_name: str = ''
|
||||
cur_line: int = 0
|
||||
background_commands_obs: List[CmdOutputObservation] = field(
|
||||
default_factory=list)
|
||||
history: List[Tuple[Action, Observation]] = field(default_factory=list)
|
||||
updated_info: List[Tuple[Action, Observation]] = field(default_factory=list)
|
||||
updated_info: List[Tuple[Action, Observation]
|
||||
] = field(default_factory=list)
|
||||
|
||||
@@ -76,7 +76,7 @@ def test_browse_url_action_serialization_deserialization():
|
||||
def test_file_read_action_serialization_deserialization():
|
||||
original_action_dict = {
|
||||
'action': 'read',
|
||||
'args': {'path': '/path/to/file.txt'}
|
||||
'args': {'path': '/path/to/file.txt', 'start_index': 0}
|
||||
}
|
||||
serialization_deserialization(original_action_dict, FileReadAction)
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_file_read_action_serialization_deserialization():
|
||||
def test_file_write_action_serialization_deserialization():
|
||||
original_action_dict = {
|
||||
'action': 'write',
|
||||
'args': {'path': '/path/to/file.txt', 'content': 'Hello world'}
|
||||
'args': {'path': '/path/to/file.txt', 'content': 'Hello world', 'start': 0, 'end': 1}
|
||||
}
|
||||
serialization_deserialization(original_action_dict, FileWriteAction)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user