mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Unify linter behaviour across CI and pre-commit-hook (#1071)
* CI: Add autopep8 linter Currently, we have autopep8 as part of pre-commit-hook. To ensure consistent behaviour, we should have it in CI as well. Moreover, pre-commit-hook contains a double-quote-string-fixer hook which changes all double quotes to single quotes, but I do observe some PRs with massive changes that do the opposite way. I suspect that these authors 1) disable or circumvent the pre-commit-hook, and 2) have other linters such as black in their IDE, which automatically change all single quotes to double quotes. This has caused a lot of unnecessary diff, made review really hard, and led to a lot of conflicts. * Use -diff for autopep8 * autopep8: Freeze version in CI * Ultimate fix * Remove pep8 long line disable workaround * Fix lint.yml * Fix all files under opendevin and agenthub
This commit is contained in:
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -32,11 +32,7 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Create mypy cache directory
|
||||
run: mkdir -p .mypy_cache
|
||||
- name: Install dependencies
|
||||
run: pip install ruff mypy==1.9.0 types-PyYAML types-toml
|
||||
- name: Run mypy
|
||||
run: python -m mypy --install-types --non-interactive --config-file dev_config/python/mypy.ini opendevin/ agenthub/
|
||||
- name: Run ruff
|
||||
run: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --files opendevin/**/* agenthub/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
@@ -2,8 +2,8 @@ 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
|
||||
|
||||
__all__ = ['monologue_agent', 'codeact_agent', 'planner_agent']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CodeAct-based Agent Framework
|
||||
|
||||
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
|
||||
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
|
||||
|
||||
**NOTE: This agent is still highly experimental and under active development to reach the capability described in the original paper & [repo](https://github.com/xingyaoww/code-act).**
|
||||
|
||||
@@ -18,6 +18,6 @@ Example: prompts `gpt-4-0125-preview` to write a flask server, install `flask` l
|
||||
|
||||
<img width="957" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/68ad10c1-744a-4e9d-bb29-0f163d665a0a">
|
||||
|
||||
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
|
||||
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
|
||||
|
||||
**TODO**: This should be fixable by either (1) including a complete in-context example like [this](https://github.com/xingyaoww/mint-bench/blob/main/mint/tasks/in_context_examples/reasoning/with_tool.txt), OR (2) collect some interaction data like this and fine-tune a model (like [this](https://github.com/xingyaoww/code-act), a more complex route).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .codeact_agent import CodeActAgent
|
||||
|
||||
Agent.register("CodeActAgent", CodeActAgent)
|
||||
Agent.register('CodeActAgent', CodeActAgent)
|
||||
|
||||
@@ -117,13 +117,11 @@ class CodeActAgent(Agent):
|
||||
{'role': 'user', 'content': obs.content})
|
||||
elif isinstance(obs, CmdOutputObservation):
|
||||
content = 'OBSERVATION:\n' + obs.content
|
||||
# FIXME: autopep8 and mypy are fighting each other on this line
|
||||
# autopep8: off
|
||||
content += f"\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]"
|
||||
content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
|
||||
self.messages.append({'role': 'user', 'content': content})
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unknown observation type: {obs.__class__}"
|
||||
f'Unknown observation type: {obs.__class__}'
|
||||
)
|
||||
response = self.llm.completion(
|
||||
messages=self.messages,
|
||||
|
||||
@@ -6,4 +6,3 @@ There's a lot of low-hanging fruit for this agent:
|
||||
* Improve memory condensing--condense earlier memories more aggressively
|
||||
* Limit the time that `run` can wait (in case agent runs an interactive command and it's hanging)
|
||||
* Figure out how to run background processes, e.g. `node server.js` to start a server
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .agent import MonologueAgent
|
||||
|
||||
Agent.register("MonologueAgent", MonologueAgent)
|
||||
Agent.register('MonologueAgent', MonologueAgent)
|
||||
|
||||
@@ -51,7 +51,7 @@ class Monologue:
|
||||
try:
|
||||
total_length += len(json.dumps(t))
|
||||
except TypeError as e:
|
||||
print(f"Error serializing thought: {e}")
|
||||
print(f'Error serializing thought: {e}')
|
||||
return total_length
|
||||
|
||||
def condense(self, llm: LLM):
|
||||
@@ -73,4 +73,4 @@ class Monologue:
|
||||
self.thoughts = prompts.parse_summary_response(summary_resp)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise RuntimeError(f"Error condensing thoughts: {e}")
|
||||
raise RuntimeError(f'Error condensing thoughts: {e}')
|
||||
|
||||
@@ -93,13 +93,14 @@ def get_summarize_monologue_prompt(thoughts: List[dict]):
|
||||
"""
|
||||
Gets the prompt for summarizing the monologue
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
- str: A formatted string with the current monologue within the prompt
|
||||
"""
|
||||
return MONOLOGUE_SUMMARY_PROMPT % {
|
||||
'monologue': json.dumps({'old_monologue': thoughts}, indent=2),
|
||||
}
|
||||
|
||||
|
||||
def get_request_action_prompt(
|
||||
task: str,
|
||||
thoughts: List[dict],
|
||||
@@ -120,23 +121,23 @@ def get_request_action_prompt(
|
||||
hint = ''
|
||||
if len(thoughts) > 0:
|
||||
latest_thought = thoughts[-1]
|
||||
if "action" in latest_thought:
|
||||
if latest_thought["action"] == "think":
|
||||
if latest_thought["args"]["thought"].startswith("OK so my task is"):
|
||||
if 'action' in latest_thought:
|
||||
if latest_thought['action'] == 'think':
|
||||
if latest_thought['args']['thought'].startswith('OK so my task is'):
|
||||
hint = "You're just getting started! What should you do first?"
|
||||
else:
|
||||
hint = "You've been thinking a lot lately. Maybe it's time to take action?"
|
||||
elif latest_thought["action"] == "error":
|
||||
hint = "Looks like that last command failed. Maybe you need to fix it, or try something else."
|
||||
elif latest_thought['action'] == 'error':
|
||||
hint = 'Looks like that last command failed. Maybe you need to fix it, or try something else.'
|
||||
|
||||
bg_commands_message = ""
|
||||
bg_commands_message = ''
|
||||
if len(background_commands_obs) > 0:
|
||||
bg_commands_message = "The following commands are running in the background:"
|
||||
bg_commands_message = 'The following commands are running in the background:'
|
||||
for command_obs in background_commands_obs:
|
||||
bg_commands_message += (
|
||||
f"\n`{command_obs.command_id}`: {command_obs.command}"
|
||||
f'\n`{command_obs.command_id}`: {command_obs.command}'
|
||||
)
|
||||
bg_commands_message += "\nYou can end any process by sending a `kill` action with the numerical `id` above."
|
||||
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
|
||||
|
||||
return ACTION_PROMPT % {
|
||||
'task': task,
|
||||
@@ -145,6 +146,7 @@ def get_request_action_prompt(
|
||||
'hint': hint,
|
||||
}
|
||||
|
||||
|
||||
def parse_action_response(response: str) -> Action:
|
||||
"""
|
||||
Parses a string to find an action within it
|
||||
@@ -162,17 +164,18 @@ def parse_action_response(response: str) -> Action:
|
||||
response_json_matches = re.finditer(
|
||||
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
|
||||
response) # Find all response-looking strings
|
||||
|
||||
def rank(match):
|
||||
return len(match[2]) if match[1] == "think" else 130 # Crudely rank multiple responses by length
|
||||
return len(match[2]) if match[1] == 'think' else 130 # Crudely rank multiple responses by length
|
||||
try:
|
||||
action_dict = json.loads(max(response_json_matches, key=rank)[0]) # Use the highest ranked response
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"Output from the LLM isn't properly formatted. The model may be misconfigured."
|
||||
) from e
|
||||
if "content" in action_dict:
|
||||
if 'content' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict["contents"] = action_dict.pop("content")
|
||||
action_dict['contents'] = action_dict.pop('content')
|
||||
return action_from_dict(action_dict)
|
||||
|
||||
|
||||
@@ -187,4 +190,4 @@ def parse_summary_response(response: str) -> List[dict]:
|
||||
- List[dict]: The list of summaries output by the model
|
||||
"""
|
||||
parsed = json.loads(response)
|
||||
return parsed["new_monologue"]
|
||||
return parsed['new_monologue']
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from opendevin.agent import Agent
|
||||
from .agent import PlannerAgent
|
||||
|
||||
Agent.register("PlannerAgent", PlannerAgent)
|
||||
Agent.register('PlannerAgent', PlannerAgent)
|
||||
|
||||
@@ -155,14 +155,14 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
if not isinstance(observation, NullObservation):
|
||||
observation_dict = observation.to_dict()
|
||||
if (
|
||||
"extras" in observation_dict
|
||||
and "screenshot" in observation_dict["extras"]
|
||||
'extras' in observation_dict
|
||||
and 'screenshot' in observation_dict['extras']
|
||||
):
|
||||
del observation_dict["extras"]["screenshot"]
|
||||
del observation_dict['extras']['screenshot']
|
||||
history_dicts.append(observation_dict)
|
||||
history_str = json.dumps(history_dicts, indent=2)
|
||||
|
||||
hint = ""
|
||||
hint = ''
|
||||
current_task = plan.get_current_task()
|
||||
if current_task is not None:
|
||||
plan_status = f"You're currently working on this task:\n{current_task.goal}."
|
||||
@@ -172,39 +172,39 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
|
||||
hint = plan_status
|
||||
|
||||
latest_action_id = latest_action.to_dict()["action"]
|
||||
latest_action_id = latest_action.to_dict()['action']
|
||||
|
||||
if current_task is not None:
|
||||
if latest_action_id == "":
|
||||
if latest_action_id == '':
|
||||
hint = "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with."
|
||||
elif latest_action_id == ActionType.RUN:
|
||||
hint = "You should think about the command you just ran, what output it gave, and how that affects your plan."
|
||||
hint = 'You should think about the command you just ran, what output it gave, and how that affects your plan.'
|
||||
elif latest_action_id == ActionType.READ:
|
||||
hint = "You should think about the file you just read, what you learned from it, and how that affects your plan."
|
||||
hint = 'You should think about the file you just read, what you learned from it, and how that affects your plan.'
|
||||
elif latest_action_id == ActionType.WRITE:
|
||||
hint = "You just changed a file. You should think about how it affects your plan."
|
||||
hint = 'You just changed a file. You should think about how it affects your plan.'
|
||||
elif latest_action_id == ActionType.BROWSE:
|
||||
hint = "You should think about the page you just visited, and what you learned from it."
|
||||
hint = 'You should think about the page you just visited, and what you learned from it.'
|
||||
elif latest_action_id == ActionType.THINK:
|
||||
hint = "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action."
|
||||
elif latest_action_id == ActionType.RECALL:
|
||||
hint = "You should think about the information you just recalled, and how it should affect your plan."
|
||||
hint = 'You should think about the information you just recalled, and how it should affect your plan.'
|
||||
elif latest_action_id == ActionType.ADD_TASK:
|
||||
hint = "You should think about the next action to take."
|
||||
hint = 'You should think about the next action to take.'
|
||||
elif latest_action_id == ActionType.MODIFY_TASK:
|
||||
hint = "You should think about the next action to take."
|
||||
hint = 'You should think about the next action to take.'
|
||||
elif latest_action_id == ActionType.SUMMARIZE:
|
||||
hint = ""
|
||||
hint = ''
|
||||
elif latest_action_id == ActionType.FINISH:
|
||||
hint = ""
|
||||
hint = ''
|
||||
|
||||
print_with_color("HINT:\n" + hint, "INFO")
|
||||
print_with_color('HINT:\n' + hint, 'INFO')
|
||||
return prompt % {
|
||||
"task": plan.main_goal,
|
||||
"plan": plan_str,
|
||||
"history": history_str,
|
||||
"hint": hint,
|
||||
"plan_status": plan_status,
|
||||
'task': plan.main_goal,
|
||||
'plan': plan_str,
|
||||
'history': history_str,
|
||||
'hint': hint,
|
||||
'plan_status': plan_status,
|
||||
}
|
||||
|
||||
|
||||
@@ -218,12 +218,12 @@ def parse_response(response: str) -> Action:
|
||||
Returns:
|
||||
- Action: A valid next action to perform from model output
|
||||
"""
|
||||
json_start = response.find("{")
|
||||
json_end = response.rfind("}") + 1
|
||||
json_start = response.find('{')
|
||||
json_end = response.rfind('}') + 1
|
||||
response = response[json_start:json_end]
|
||||
action_dict = json.loads(response)
|
||||
if "contents" in action_dict:
|
||||
if 'contents' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict["content"] = action_dict.pop("contents")
|
||||
action_dict['content'] = action_dict.pop('contents')
|
||||
action = action_from_dict(action_dict)
|
||||
return action
|
||||
|
||||
@@ -23,4 +23,3 @@ It will map `./workspace` into the docker container with the folder permission c
|
||||
Example screenshot:
|
||||
|
||||
<img width="868" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/8dedcdee-437a-4469-870f-be29ca2b7c32">
|
||||
|
||||
|
||||
@@ -29,30 +29,30 @@ ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in ac
|
||||
|
||||
def action_from_dict(action: dict) -> Action:
|
||||
action = action.copy()
|
||||
if "action" not in action:
|
||||
if 'action' not in action:
|
||||
raise KeyError(f"'action' key is not found in {action=}")
|
||||
action_class = ACTION_TYPE_TO_CLASS.get(action["action"])
|
||||
action_class = ACTION_TYPE_TO_CLASS.get(action['action'])
|
||||
if action_class is None:
|
||||
raise KeyError(
|
||||
f"'{action['action']=}' is not defined. Available actions: {ACTION_TYPE_TO_CLASS.keys()}"
|
||||
)
|
||||
args = action.get("args", {})
|
||||
args = action.get('args', {})
|
||||
return action_class(**args)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Action",
|
||||
"NullAction",
|
||||
"CmdRunAction",
|
||||
"CmdKillAction",
|
||||
"BrowseURLAction",
|
||||
"FileReadAction",
|
||||
"FileWriteAction",
|
||||
"AgentRecallAction",
|
||||
"AgentThinkAction",
|
||||
"AgentFinishAction",
|
||||
"AgentEchoAction",
|
||||
"AgentSummarizeAction",
|
||||
"AddTaskAction",
|
||||
"ModifyTaskAction",
|
||||
'Action',
|
||||
'NullAction',
|
||||
'CmdRunAction',
|
||||
'CmdKillAction',
|
||||
'BrowseURLAction',
|
||||
'FileReadAction',
|
||||
'FileWriteAction',
|
||||
'AgentRecallAction',
|
||||
'AgentThinkAction',
|
||||
'AgentFinishAction',
|
||||
'AgentEchoAction',
|
||||
'AgentSummarizeAction',
|
||||
'AddTaskAction',
|
||||
'ModifyTaskAction',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -70,7 +70,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
|
||||
|
||||
@@ -9,16 +9,16 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class Action:
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
def run(self, controller: 'AgentController') -> 'Observation':
|
||||
raise NotImplementedError
|
||||
|
||||
def to_dict(self):
|
||||
d = asdict(self)
|
||||
try:
|
||||
v = d.pop("action")
|
||||
v = d.pop('action')
|
||||
except KeyError:
|
||||
raise NotImplementedError(f"{self=} does not have action attribute set")
|
||||
return {"action": v, "args": d, "message": self.message}
|
||||
raise NotImplementedError(f'{self=} does not have action attribute set')
|
||||
return {'action': v, 'args': d, 'message': self.message}
|
||||
|
||||
@property
|
||||
def executable(self) -> bool:
|
||||
@@ -53,4 +53,4 @@ class NullAction(NotExecutableAction):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "No action"
|
||||
return 'No action'
|
||||
|
||||
@@ -15,12 +15,12 @@ class CmdRunAction(ExecutableAction):
|
||||
background: bool = False
|
||||
action: str = ActionType.RUN
|
||||
|
||||
def run(self, controller: "AgentController") -> "CmdOutputObservation":
|
||||
def run(self, controller: 'AgentController') -> 'CmdOutputObservation':
|
||||
return controller.command_manager.run_command(self.command, self.background)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Running command: {self.command}"
|
||||
return f'Running command: {self.command}'
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -28,9 +28,9 @@ class CmdKillAction(ExecutableAction):
|
||||
id: int
|
||||
action: str = ActionType.KILL
|
||||
|
||||
def run(self, controller: "AgentController") -> "CmdOutputObservation":
|
||||
def run(self, controller: 'AgentController') -> 'CmdOutputObservation':
|
||||
return controller.command_manager.kill_command(self.id)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Killing command: {self.id}"
|
||||
return f'Killing command: {self.id}'
|
||||
|
||||
@@ -17,9 +17,9 @@ class BrowseURLAction(ExecutableAction):
|
||||
url: str
|
||||
action: str = ActionType.BROWSE
|
||||
|
||||
async def run(self, controller: "AgentController") -> BrowserOutputObservation: # type: ignore
|
||||
async def run(self, controller: 'AgentController') -> BrowserOutputObservation: # type: ignore
|
||||
asked_url = self.url
|
||||
if not asked_url.startswith("http"):
|
||||
if not asked_url.startswith('http'):
|
||||
asked_url = os.path.abspath(os.curdir) + self.url
|
||||
try:
|
||||
async with async_playwright() as p:
|
||||
@@ -27,11 +27,11 @@ class BrowseURLAction(ExecutableAction):
|
||||
page = await browser.new_page()
|
||||
response = await page.goto(asked_url)
|
||||
# content = await page.content()
|
||||
inner_text = await page.evaluate("() => document.body.innerText")
|
||||
inner_text = await page.evaluate('() => document.body.innerText')
|
||||
screenshot_bytes = await page.screenshot(full_page=True)
|
||||
await browser.close()
|
||||
|
||||
screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
|
||||
screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8')
|
||||
return BrowserOutputObservation(
|
||||
content=inner_text, # HTML content of the page
|
||||
screenshot=screenshot_base64, # Base64-encoded screenshot
|
||||
@@ -41,12 +41,11 @@ class BrowseURLAction(ExecutableAction):
|
||||
except Exception as e:
|
||||
return BrowserOutputObservation(
|
||||
content=str(e),
|
||||
screenshot="",
|
||||
screenshot='',
|
||||
error=True,
|
||||
url=asked_url
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Browsing URL: {self.url}"
|
||||
return f'Browsing URL: {self.url}'
|
||||
|
||||
@@ -8,12 +8,12 @@ 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):
|
||||
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)
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ class FileReadAction(ExecutableAction):
|
||||
|
||||
def run(self, controller) -> FileReadObservation:
|
||||
path = resolve_path(controller.workdir, self.path)
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
with open(path, 'r', encoding='utf-8') as file:
|
||||
return FileReadObservation(path=path, content=file.read())
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Reading file: {self.path}"
|
||||
return f'Reading file: {self.path}'
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -40,10 +40,10 @@ class FileWriteAction(ExecutableAction):
|
||||
|
||||
def run(self, controller) -> FileWriteObservation:
|
||||
whole_path = resolve_path(controller.workdir, self.path)
|
||||
with open(whole_path, "w", encoding="utf-8") as file:
|
||||
with open(whole_path, 'w', encoding='utf-8') as file:
|
||||
file.write(self.content)
|
||||
return FileWriteObservation(content="", path=self.path)
|
||||
return FileWriteObservation(content='', path=self.path)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Writing file: {self.path}"
|
||||
return f'Writing file: {self.path}'
|
||||
|
||||
@@ -13,7 +13,7 @@ class AddTaskAction(NotExecutableAction):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Added task: {self.goal}"
|
||||
return f'Added task: {self.goal}'
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -24,4 +24,4 @@ class ModifyTaskAction(NotExecutableAction):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Set task {self.id} to {self.state}"
|
||||
return f'Set task {self.id} to {self.state}'
|
||||
|
||||
@@ -206,7 +206,7 @@ class AgentController:
|
||||
try:
|
||||
callback(event)
|
||||
except Exception as e:
|
||||
logger.exception(f"Callback error: {e}, idx: {idx}")
|
||||
logger.exception(f'Callback error: {e}, idx: {idx}')
|
||||
await asyncio.sleep(
|
||||
0.001
|
||||
) # Give back control for a tick, so we can await in callbacks
|
||||
|
||||
@@ -46,8 +46,6 @@ class CommandManager:
|
||||
|
||||
def _run_background(self, command: str) -> CmdOutputObservation:
|
||||
bg_cmd = self.shell.execute_in_background(command)
|
||||
# FIXME: autopep8 and mypy are fighting each other on this line
|
||||
# autopep8: off
|
||||
content = f'Background command started. To stop it, send a `kill` action with id {bg_cmd.id}'
|
||||
return CmdOutputObservation(
|
||||
content=content,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
class MaxCharsExceedError(Exception):
|
||||
def __init__(self, num_of_chars=None, max_chars_limit=None):
|
||||
if num_of_chars is not None and max_chars_limit is not None:
|
||||
# FIXME: autopep8 and mypy are fighting each other on this line
|
||||
# autopep8: off
|
||||
message = f"Number of characters {num_of_chars} exceeds MAX_CHARS limit: {max_chars_limit}"
|
||||
message = f'Number of characters {num_of_chars} exceeds MAX_CHARS limit: {max_chars_limit}'
|
||||
else:
|
||||
message = 'Number of characters exceeds MAX_CHARS limit'
|
||||
super().__init__(message)
|
||||
|
||||
@@ -4,9 +4,9 @@ from typing import Any, Dict, List
|
||||
|
||||
class WorkspaceFile:
|
||||
name: str
|
||||
children: List["WorkspaceFile"]
|
||||
children: List['WorkspaceFile']
|
||||
|
||||
def __init__(self, name: str, children: List["WorkspaceFile"]):
|
||||
def __init__(self, name: str, children: List['WorkspaceFile']):
|
||||
self.name = name
|
||||
self.children = children
|
||||
|
||||
@@ -17,8 +17,8 @@ class WorkspaceFile:
|
||||
The dictionary representation of the File object.
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"children": [child.to_dict() for child in self.children],
|
||||
'name': self.name,
|
||||
'children': [child.to_dict() for child in self.children],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def get_file_handler():
|
||||
log_dir = os.path.join(os.getcwd(), 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||
file_name = f"opendevin_{timestamp}.log"
|
||||
file_name = f'opendevin_{timestamp}.log'
|
||||
file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
@@ -98,7 +98,8 @@ class LlmFileHandler(logging.FileHandler):
|
||||
self.log_directory = os.path.join(
|
||||
os.getcwd(), 'logs', 'llm', self.session)
|
||||
os.makedirs(self.log_directory, exist_ok=True)
|
||||
self.baseFilename = os.path.join(self.log_directory, f"{self.filename}_{self.message_counter:03}.log")
|
||||
self.baseFilename = os.path.join(
|
||||
self.log_directory, f'{self.filename}_{self.message_counter:03}.log')
|
||||
super().__init__(self.baseFilename, mode, encoding, delay)
|
||||
|
||||
def emit(self, record):
|
||||
@@ -108,7 +109,8 @@ class LlmFileHandler(logging.FileHandler):
|
||||
Args:
|
||||
record (logging.LogRecord): The log record to emit.
|
||||
"""
|
||||
self.baseFilename = os.path.join(self.log_directory, f"{self.filename}_{self.message_counter:03}.log")
|
||||
self.baseFilename = os.path.join(
|
||||
self.log_directory, f'{self.filename}_{self.message_counter:03}.log')
|
||||
self.stream = self._open()
|
||||
super().emit(record)
|
||||
self.stream.close
|
||||
|
||||
@@ -89,8 +89,6 @@ async def main():
|
||||
raise ValueError(
|
||||
'No task provided. Please specify a task through -t, -f.')
|
||||
|
||||
# FIXME: autopep8 and mypy are fighting each other on this line
|
||||
# autopep8: off
|
||||
print(
|
||||
f'Running agent {args.agent_cls} (model: {args.model_name}, directory: {args.directory}) with task: "{task}"'
|
||||
)
|
||||
|
||||
@@ -18,14 +18,14 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
while True:
|
||||
# receive message
|
||||
data = await websocket.receive_json()
|
||||
print(f"Received message: {data}")
|
||||
print(f'Received message: {data}')
|
||||
|
||||
# send mock response to client
|
||||
response = {'message': f"receive {data}"}
|
||||
response = {'message': f'receive {data}'}
|
||||
await websocket.send_json(response)
|
||||
print(f"Sent message: {response}")
|
||||
print(f'Sent message: {response}')
|
||||
except Exception as e:
|
||||
print(f"WebSocket Error: {e}")
|
||||
print(f'WebSocket Error: {e}')
|
||||
|
||||
|
||||
@app.get('/')
|
||||
|
||||
@@ -17,30 +17,32 @@ observations = (
|
||||
AgentErrorObservation,
|
||||
)
|
||||
|
||||
OBSERVATION_TYPE_TO_CLASS = {observation_class.observation:observation_class for observation_class in observations} # type: ignore[attr-defined]
|
||||
OBSERVATION_TYPE_TO_CLASS = {observation_class.observation: observation_class for observation_class in observations} # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def observation_from_dict(observation: dict) -> Observation:
|
||||
observation = observation.copy()
|
||||
if "observation" not in observation:
|
||||
if 'observation' not in observation:
|
||||
raise KeyError(f"'observation' key is not found in {observation=}")
|
||||
observation_class = OBSERVATION_TYPE_TO_CLASS.get(observation["observation"])
|
||||
observation_class = OBSERVATION_TYPE_TO_CLASS.get(observation['observation'])
|
||||
if observation_class is None:
|
||||
raise KeyError(f"'{observation['observation']=}' is not defined. Available observations: {OBSERVATION_TYPE_TO_CLASS.keys()}")
|
||||
observation.pop("observation")
|
||||
observation.pop("message", None)
|
||||
content = observation.pop("content", "")
|
||||
extras = observation.pop("extras", {})
|
||||
observation.pop('observation')
|
||||
observation.pop('message', None)
|
||||
content = observation.pop('content', '')
|
||||
extras = observation.pop('extras', {})
|
||||
return observation_class(content=content, **extras)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Observation",
|
||||
"NullObservation",
|
||||
"CmdOutputObservation",
|
||||
"BrowserOutputObservation",
|
||||
"FileReadObservation",
|
||||
"FileWriteObservation",
|
||||
"UserMessageObservation",
|
||||
"AgentMessageObservation",
|
||||
"AgentRecallObservation",
|
||||
"AgentErrorObservation",
|
||||
'Observation',
|
||||
'NullObservation',
|
||||
'CmdOutputObservation',
|
||||
'BrowserOutputObservation',
|
||||
'FileReadObservation',
|
||||
'FileWriteObservation',
|
||||
'UserMessageObservation',
|
||||
'AgentMessageObservation',
|
||||
'AgentRecallObservation',
|
||||
'AgentErrorObservation',
|
||||
]
|
||||
|
||||
@@ -17,19 +17,19 @@ class Observation:
|
||||
def to_dict(self) -> dict:
|
||||
"""Converts the observation to a dictionary."""
|
||||
extras = copy.deepcopy(self.__dict__)
|
||||
content = extras.pop("content", "")
|
||||
observation = extras.pop("observation", "")
|
||||
content = extras.pop('content', '')
|
||||
observation = extras.pop('observation', '')
|
||||
return {
|
||||
"observation": observation,
|
||||
"content": content,
|
||||
"extras": extras,
|
||||
"message": self.message,
|
||||
'observation': observation,
|
||||
'content': content,
|
||||
'extras': extras,
|
||||
'message': self.message,
|
||||
}
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
"""Returns a message describing the observation."""
|
||||
return ""
|
||||
return ''
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,4 +43,4 @@ class NullObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return ""
|
||||
return ''
|
||||
|
||||
@@ -18,4 +18,4 @@ class BrowserOutputObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "Visited " + self.url
|
||||
return 'Visited ' + self.url
|
||||
|
||||
@@ -14,4 +14,4 @@ class AgentErrorObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "Oops. Something went wrong: " + self.content
|
||||
return 'Oops. Something went wrong: ' + self.content
|
||||
|
||||
@@ -15,7 +15,7 @@ class FileReadObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"I read the file {self.path}."
|
||||
return f'I read the file {self.path}.'
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -29,4 +29,4 @@ class FileWriteObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"I wrote to the file {self.path}."
|
||||
return f'I wrote to the file {self.path}.'
|
||||
|
||||
@@ -10,12 +10,12 @@ class UserMessageObservation(Observation):
|
||||
This data class represents a message sent by the user.
|
||||
"""
|
||||
|
||||
role: str = "user"
|
||||
role: str = 'user'
|
||||
observation: str = ObservationType.MESSAGE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return ""
|
||||
return ''
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -24,9 +24,9 @@ class AgentMessageObservation(Observation):
|
||||
This data class represents a message sent by the agent.
|
||||
"""
|
||||
|
||||
role: str = "assistant"
|
||||
role: str = 'assistant'
|
||||
observation: str = ObservationType.MESSAGE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return ""
|
||||
return ''
|
||||
|
||||
@@ -12,9 +12,9 @@ class AgentRecallObservation(Observation):
|
||||
"""
|
||||
|
||||
memories: List[str]
|
||||
role: str = "assistant"
|
||||
role: str = 'assistant'
|
||||
observation: str = ObservationType.RECALL
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "The agent recalled memories."
|
||||
return 'The agent recalled memories.'
|
||||
|
||||
@@ -21,4 +21,4 @@ class CmdOutputObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Command `{self.command}` executed with exit code {self.exit_code}."
|
||||
return f'Command `{self.command}` executed with exit code {self.exit_code}.'
|
||||
|
||||
@@ -12,41 +12,41 @@ class Command:
|
||||
|
||||
|
||||
def parse_command_file() -> str | None:
|
||||
if not os.path.exists("commands.sh"):
|
||||
if not os.path.exists('commands.sh'):
|
||||
return None
|
||||
content = open("commands.sh", "r").read()
|
||||
lines = content.split("\n")
|
||||
content = open('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
|
||||
|
||||
@@ -167,11 +167,10 @@ class DockerSSHBox(Sandbox):
|
||||
else:
|
||||
username = 'root'
|
||||
logger.info(
|
||||
# FIXME: mypy and autopep8 fight each other on this line
|
||||
# autopep8: off
|
||||
f"Connecting to {username}@{hostname} via ssh. If you encounter any issues, you can try `ssh -v -p {self._ssh_port} {username}@{hostname}` with the password '{self._ssh_password}' and report the issue on GitHub."
|
||||
)
|
||||
self.ssh.login(hostname, username, self._ssh_password, port=self._ssh_port)
|
||||
self.ssh.login(hostname, username, self._ssh_password,
|
||||
port=self._ssh_port)
|
||||
|
||||
# Fix: https://github.com/pexpect/pexpect/issues/669
|
||||
self.ssh.sendline("bind 'set enable-bracketed-paste off'")
|
||||
|
||||
@@ -2,53 +2,53 @@ from enum import Enum
|
||||
|
||||
|
||||
class ActionType(str, Enum):
|
||||
INIT = "initialize"
|
||||
INIT = 'initialize'
|
||||
"""Initializes the agent. Only sent by client.
|
||||
"""
|
||||
|
||||
START = "start"
|
||||
START = 'start'
|
||||
"""Starts a new development task. Only sent by the client.
|
||||
"""
|
||||
|
||||
READ = "read"
|
||||
READ = 'read'
|
||||
"""Reads the content of a file.
|
||||
"""
|
||||
|
||||
WRITE = "write"
|
||||
WRITE = 'write'
|
||||
"""Writes the content to a file.
|
||||
"""
|
||||
|
||||
RUN = "run"
|
||||
RUN = 'run'
|
||||
"""Runs a command.
|
||||
"""
|
||||
|
||||
KILL = "kill"
|
||||
KILL = 'kill'
|
||||
"""Kills a background command.
|
||||
"""
|
||||
|
||||
BROWSE = "browse"
|
||||
BROWSE = 'browse'
|
||||
"""Opens a web page.
|
||||
"""
|
||||
|
||||
RECALL = "recall"
|
||||
RECALL = 'recall'
|
||||
"""Searches long-term memory
|
||||
"""
|
||||
|
||||
THINK = "think"
|
||||
THINK = 'think'
|
||||
"""Allows the agent to make a plan, set a goal, or record thoughts
|
||||
"""
|
||||
|
||||
FINISH = "finish"
|
||||
"""If you're absolutely certain that you've completed your task and have tested your work,
|
||||
FINISH = 'finish'
|
||||
"""If you're absolutely certain that you've completed your task and have tested your work,
|
||||
use the finish action to stop working.
|
||||
"""
|
||||
|
||||
CHAT = "chat"
|
||||
CHAT = 'chat'
|
||||
|
||||
SUMMARIZE = "summarize"
|
||||
SUMMARIZE = 'summarize'
|
||||
|
||||
ADD_TASK = "add_task"
|
||||
ADD_TASK = 'add_task'
|
||||
|
||||
MODIFY_TASK = "modify_task"
|
||||
MODIFY_TASK = 'modify_task'
|
||||
|
||||
NULL = "null"
|
||||
NULL = 'null'
|
||||
|
||||
@@ -2,30 +2,30 @@ from enum import Enum
|
||||
|
||||
|
||||
class ObservationType(str, Enum):
|
||||
READ = "read"
|
||||
READ = 'read'
|
||||
"""The content of a file
|
||||
"""
|
||||
|
||||
WRITE = "write"
|
||||
WRITE = 'write'
|
||||
|
||||
BROWSE = "browse"
|
||||
BROWSE = 'browse'
|
||||
"""The HTML content of a URL
|
||||
"""
|
||||
|
||||
RUN = "run"
|
||||
RUN = 'run'
|
||||
"""The output of a command
|
||||
"""
|
||||
|
||||
RECALL = "recall"
|
||||
RECALL = 'recall'
|
||||
"""The result of a search
|
||||
"""
|
||||
|
||||
CHAT = "chat"
|
||||
CHAT = 'chat'
|
||||
"""A message from the user
|
||||
"""
|
||||
|
||||
MESSAGE = "message"
|
||||
MESSAGE = 'message'
|
||||
|
||||
ERROR = "error"
|
||||
ERROR = 'error'
|
||||
|
||||
NULL = "null"
|
||||
NULL = 'null'
|
||||
|
||||
@@ -35,7 +35,7 @@ class AgentManager:
|
||||
await self.sid_to_agent[sid].dispatch(action, data)
|
||||
|
||||
def handle_signal(self, signum, _):
|
||||
print(f"Received signal {signum}, exiting...")
|
||||
print(f'Received signal {signum}, exiting...')
|
||||
self.close()
|
||||
exit(0)
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .auth import get_sid_from_token, sign_token
|
||||
|
||||
__all__ = ["get_sid_from_token", "sign_token"]
|
||||
__all__ = ['get_sid_from_token', 'sign_token']
|
||||
|
||||
@@ -29,7 +29,7 @@ class SessionManager:
|
||||
self._sessions[sid].update_connection(ws_conn)
|
||||
|
||||
async def loop_recv(self, sid: str, dispatch: Callable):
|
||||
print(f"Starting loop_recv for sid: {sid}")
|
||||
print(f'Starting loop_recv for sid: {sid}')
|
||||
"""Starts listening for messages from the client."""
|
||||
if sid not in self._sessions:
|
||||
return
|
||||
@@ -39,7 +39,7 @@ class SessionManager:
|
||||
self._save_sessions()
|
||||
|
||||
def handle_signal(self, signum, _):
|
||||
print(f"Received signal {signum}, exiting...")
|
||||
print(f'Received signal {signum}, exiting...')
|
||||
self.close()
|
||||
exit(0)
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ pytest = "*"
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
torch = "*"
|
||||
|
||||
[tool.autopep8]
|
||||
ignore = [ "E501" ]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
Reference in New Issue
Block a user