diff --git a/agenthub/monologue_agent/agent.py b/agenthub/monologue_agent/agent.py index 8088b3ffdd..19b345bfa2 100644 --- a/agenthub/monologue_agent/agent.py +++ b/agenthub/monologue_agent/agent.py @@ -14,7 +14,6 @@ from opendevin.events.action import ( CmdRunAction, FileReadAction, FileWriteAction, - GitHubPushAction, MessageAction, NullAction, ) @@ -70,10 +69,6 @@ INITIAL_THOUGHTS = [ 'BROWSE google.com', '
', 'I can browse the web too!', - 'If I have done some work and I want to push it to github, I can do that also!', - "Let's do it.", - 'PUSH owner/repo branch', - 'The repo was successfully pushed to https://github.com/owner/repo/branch', '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.', @@ -210,11 +205,6 @@ class MonologueAgent(Agent): url = thought.split('BROWSE ')[1] action = BrowseURLAction(url=url) previous_action = ActionType.BROWSE - elif thought.startswith('PUSH'): - owner_repo, branch = thought.split('PUSH ')[1].split(' ') - owner, repo = owner_repo.split('/') - action = GitHubPushAction(owner=owner, repo=repo, branch=branch) - previous_action = ActionType.PUSH else: action = MessageAction(thought) self._add_event(action.to_memory()) diff --git a/opendevin/controller/__init__.py b/opendevin/controller/__init__.py index 005df25448..703cd40a56 100644 --- a/opendevin/controller/__init__.py +++ b/opendevin/controller/__init__.py @@ -1,7 +1,5 @@ -from .action_manager import ActionManager from .agent_controller import AgentController __all__ = [ 'AgentController', - 'ActionManager' ] diff --git a/opendevin/controller/action_manager.py b/opendevin/controller/action_manager.py deleted file mode 100644 index 0cdfc31935..0000000000 --- a/opendevin/controller/action_manager.py +++ /dev/null @@ -1,97 +0,0 @@ -from typing import List - -from opendevin.core.config import config -from opendevin.events.action import ( - Action, -) -from opendevin.events.observation import ( - CmdOutputObservation, - ErrorObservation, - Observation, -) -from opendevin.runtime import ( - DockerExecBox, - DockerSSHBox, - E2BBox, - LocalBox, - Sandbox, -) -from opendevin.runtime.plugins import PluginRequirement - - -class ActionManager: - id: str - sandbox: Sandbox - - def __init__( - self, - sid: str = 'default', - ): - sandbox_type = config.sandbox_type.lower() - if sandbox_type == 'exec': - self.sandbox = DockerExecBox( - sid=(sid or 'default'), timeout=config.sandbox_timeout - ) - elif sandbox_type == 'local': - self.sandbox = LocalBox(timeout=config.sandbox_timeout) - elif sandbox_type == 'ssh': - self.sandbox = DockerSSHBox( - sid=(sid or 'default'), timeout=config.sandbox_timeout - ) - elif sandbox_type == 'e2b': - self.sandbox = E2BBox(timeout=config.sandbox_timeout) - else: - raise ValueError(f'Invalid sandbox type: {sandbox_type}') - - def init_sandbox_plugins(self, plugins: List[PluginRequirement]): - self.sandbox.init_plugins(plugins) - - async def run_action(self, action: Action, agent_controller) -> Observation: - observation = await action.run(agent_controller) - return observation - - def run_command(self, command: str, background=False) -> Observation: - if background: - return self._run_background(command) - else: - return self._run_immediately(command) - - def _run_immediately(self, command: str) -> Observation: - try: - exit_code, output = self.sandbox.execute(command) - return CmdOutputObservation( - command_id=-1, content=output, command=command, exit_code=exit_code - ) - except UnicodeDecodeError: - return ErrorObservation('Command output could not be decoded as utf-8') - - def _run_background(self, command: str) -> CmdOutputObservation: - bg_cmd = self.sandbox.execute_in_background(command) - content = f'Background command started. To stop it, send a `kill` action with id {bg_cmd.pid}' - return CmdOutputObservation( - content=content, - command_id=bg_cmd.pid, - command=command, - exit_code=0, - ) - - def kill_command(self, id: int) -> CmdOutputObservation: - cmd = self.sandbox.kill_background(id) - return CmdOutputObservation( - content=f'Background command with id {id} has been killed.', - command_id=id, - command=cmd.command, - exit_code=0, - ) - - def get_background_obs(self) -> List[CmdOutputObservation]: - obs = [] - for _id, cmd in self.sandbox.background_commands.items(): - output = cmd.read_logs() - if output is not None and output != '': - obs.append( - CmdOutputObservation( - content=output, command_id=_id, command=cmd.command - ) - ) - return obs diff --git a/opendevin/controller/agent_controller.py b/opendevin/controller/agent_controller.py index 50eaa1078f..512db7ed88 100644 --- a/opendevin/controller/agent_controller.py +++ b/opendevin/controller/agent_controller.py @@ -2,7 +2,6 @@ import asyncio from typing import Optional, Type from agenthub.codeact_agent.codeact_agent import CodeActAgent -from opendevin.controller.action_manager import ActionManager from opendevin.controller.agent import Agent from opendevin.controller.state.plan import Plan from opendevin.controller.state.state import State @@ -17,11 +16,12 @@ from opendevin.core.logger import opendevin_logger as logger from opendevin.core.schema import AgentState from opendevin.events.action import ( Action, + AddTaskAction, AgentDelegateAction, AgentFinishAction, - AgentRejectAction, ChangeAgentStateAction, MessageAction, + ModifyTaskAction, NullAction, ) from opendevin.events.event import Event @@ -34,7 +34,8 @@ from opendevin.events.observation import ( ) from opendevin.events.stream import EventSource, EventStream, EventStreamSubscriber from opendevin.runtime import DockerSSHBox -from opendevin.runtime.browser.browser_env import BrowserEnv +from opendevin.runtime.runtime import Runtime +from opendevin.runtime.server.runtime import ServerRuntime MAX_ITERATIONS = config.max_iterations MAX_CHARS = config.llm.max_chars @@ -44,8 +45,7 @@ class AgentController: id: str agent: Agent max_iterations: int - action_manager: ActionManager - browser: BrowserEnv + runtime: Runtime event_stream: EventStream agent_task: Optional[asyncio.Task] = None delegate: 'AgentController | None' = None @@ -76,15 +76,13 @@ class AgentController: EventStreamSubscriber.AGENT_CONTROLLER, self.on_event ) self.max_iterations = max_iterations - self.action_manager = ActionManager(self.id) + self.runtime = ServerRuntime(self.id) self.max_chars = max_chars # Initialize agent-required plugins for sandbox (if any) - self.action_manager.init_sandbox_plugins(agent.sandbox_plugins) - # Initialize browser environment - self.browser = BrowserEnv() + self.runtime.init_sandbox_plugins(agent.sandbox_plugins) if isinstance(agent, CodeActAgent) and not isinstance( - self.action_manager.sandbox, DockerSSHBox + self.runtime.sandbox, DockerSSHBox ): logger.warning( 'CodeActAgent requires DockerSSHBox as sandbox! Using other sandbox that are not stateful (LocalBox, DockerExecBox) will not work properly.' @@ -94,15 +92,15 @@ class AgentController: if self.agent_task is not None: self.agent_task.cancel() self.event_stream.unsubscribe(EventStreamSubscriber.AGENT_CONTROLLER) - self.action_manager.sandbox.close() - self.browser.close() + self.runtime.sandbox.close() + self.runtime.browser.close() await self.set_agent_state_to(AgentState.STOPPED) def update_state_for_step(self, i): if self.state is None: return self.state.iteration = i - self.state.background_commands_obs = self.action_manager.get_background_obs() + self.state.background_commands_obs = self.runtime.get_background_obs() def update_state_after_step(self): if self.state is None: @@ -248,7 +246,7 @@ class AgentController: if self.state.num_of_chars > self.max_chars: raise MaxCharsExceedError(self.state.num_of_chars, self.max_chars) - log_obs = self.action_manager.get_background_obs() + log_obs = self.runtime.get_background_obs() for obs in log_obs: await self.add_history(NullAction(), obs) logger.info(obs, extra={'msg_type': 'BACKGROUND LOG'}) @@ -266,26 +264,26 @@ class AgentController: self.update_state_after_step() - if isinstance(action, MessageAction) and action.wait_for_response: + if isinstance(action, AgentFinishAction): + self.state.outputs = action.outputs # type: ignore[attr-defined] + logger.info(action, extra={'msg_type': 'INFO'}) + return True + elif isinstance(action, MessageAction) and action.wait_for_response: # FIXME: remove this once history is managed outside the agent controller await self.add_history(action, NullObservation('')) await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT) return False - - finished = isinstance(action, AgentFinishAction) or isinstance( - action, AgentRejectAction - ) - if finished: - self.state.outputs = action.outputs # type: ignore[attr-defined] - logger.info(action, extra={'msg_type': 'INFO'}) - return True - - if isinstance(observation, NullObservation): - observation = await self.action_manager.run_action(action, self) + elif isinstance(action, AgentDelegateAction): + await self.start_delegate(action) + elif isinstance(action, AddTaskAction): + self.state.plan.add_subtask(action.parent, action.goal, action.subtasks) + elif isinstance(action, ModifyTaskAction): + self.state.plan.set_subtask_state(action.id, action.state) + elif not isinstance(observation, ErrorObservation): + observation = await self.runtime.run_action(action) if not isinstance(observation, NullObservation): logger.info(observation, extra={'msg_type': 'OBSERVATION'}) - await self.add_history(action, observation) return False diff --git a/opendevin/events/action/__init__.py b/opendevin/events/action/__init__.py index a01fd6855d..df841d7482 100644 --- a/opendevin/events/action/__init__.py +++ b/opendevin/events/action/__init__.py @@ -13,7 +13,6 @@ from .browse import BrowseURLAction from .commands import CmdKillAction, CmdRunAction, IPythonRunCellAction from .empty import NullAction from .files import FileReadAction, FileWriteAction -from .github import GitHubPushAction from .message import MessageAction from .tasks import AddTaskAction, ModifyTaskAction @@ -31,7 +30,6 @@ actions = ( AddTaskAction, ModifyTaskAction, ChangeAgentStateAction, - GitHubPushAction, MessageAction, ) diff --git a/opendevin/events/action/action.py b/opendevin/events/action/action.py index 9824e66a57..f35d4290ad 100644 --- a/opendevin/events/action/action.py +++ b/opendevin/events/action/action.py @@ -1,17 +1,13 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING from opendevin.events.event import Event -from opendevin.events.observation import NullObservation, Observation - -if TYPE_CHECKING: - from opendevin.controller import AgentController @dataclass class Action(Event): - async def run(self, controller: 'AgentController') -> 'Observation': - return NullObservation('') + @property + def runnable(self): + return False def to_memory(self): d = super().to_memory() diff --git a/opendevin/events/action/agent.py b/opendevin/events/action/agent.py index 280e2e62db..c1e2956aff 100644 --- a/opendevin/events/action/agent.py +++ b/opendevin/events/action/agent.py @@ -1,18 +1,10 @@ from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Dict +from typing import Dict from opendevin.core.schema import ActionType -from opendevin.events.observation import ( - AgentRecallObservation, - NullObservation, - Observation, -) from .action import Action -if TYPE_CHECKING: - from opendevin.controller import AgentController - @dataclass class ChangeAgentStateAction(Action): @@ -33,11 +25,9 @@ class AgentRecallAction(Action): thought: str = '' action: str = ActionType.RECALL - async def run(self, controller: 'AgentController') -> AgentRecallObservation: - return AgentRecallObservation( - content='', - memories=controller.agent.search_memory(self.query), - ) + @property + def runnable(self) -> bool: + return True @property def message(self) -> str: @@ -83,10 +73,6 @@ class AgentDelegateAction(Action): thought: str = '' action: str = ActionType.DELEGATE - async def run(self, controller: 'AgentController') -> Observation: - await controller.start_delegate(self) - return NullObservation('') - @property def message(self) -> str: return f"I'm asking {self.agent} for help with this task." diff --git a/opendevin/events/action/browse.py b/opendevin/events/action/browse.py index ebe567d6e3..5ecf2bd998 100644 --- a/opendevin/events/action/browse.py +++ b/opendevin/events/action/browse.py @@ -1,15 +1,9 @@ -import os from dataclasses import dataclass -from typing import TYPE_CHECKING from opendevin.core.schema import ActionType -from opendevin.events.observation import BrowserOutputObservation from .action import Action -if TYPE_CHECKING: - from opendevin.controller import AgentController - @dataclass class BrowseURLAction(Action): @@ -17,32 +11,9 @@ class BrowseURLAction(Action): thought: str = '' action: str = ActionType.BROWSE - async def run(self, controller: 'AgentController') -> BrowserOutputObservation: # type: ignore - asked_url = self.url - if not asked_url.startswith('http'): - asked_url = os.path.abspath(os.curdir) + self.url - try: - # action in BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/functions.py - action_str = f'goto("{asked_url}")' - # obs provided by BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/env.py#L396 - obs = controller.browser.step(action_str) - return BrowserOutputObservation( - content=obs['text_content'], # text content of the page - open_pages_urls=obs['open_pages_urls'], # list of open pages - active_page_index=obs['active_page_index'], # index of the active page - dom_object=obs['dom_object'], # DOM object - axtree_object=obs['axtree_object'], # accessibility tree object - last_browser_action=obs[ - 'last_action' - ], # last browser env action performed - focused_element_bid=obs['focused_element_bid'], # focused element bid - screenshot=obs['screenshot'], # base64-encoded screenshot, png - url=asked_url, - ) - except Exception as e: - return BrowserOutputObservation( - content=str(e), screenshot='', error=True, url=asked_url - ) + @property + def runnable(self) -> bool: + return True @property def message(self) -> str: diff --git a/opendevin/events/action/commands.py b/opendevin/events/action/commands.py index 5cb72760e3..11d0635369 100644 --- a/opendevin/events/action/commands.py +++ b/opendevin/events/action/commands.py @@ -1,19 +1,9 @@ -import os -import pathlib from dataclasses import dataclass -from typing import TYPE_CHECKING -from opendevin.core.config import config from opendevin.core.schema import ActionType from .action import Action -if TYPE_CHECKING: - from opendevin.controller import AgentController - from opendevin.events.observation import CmdOutputObservation, Observation - -from opendevin.events.observation import IPythonRunCellObservation - @dataclass class CmdRunAction(Action): @@ -22,8 +12,8 @@ class CmdRunAction(Action): thought: str = '' action: str = ActionType.RUN - async def run(self, controller: 'AgentController') -> 'Observation': - return controller.action_manager.run_command(self.command, self.background) + def runnable(self) -> bool: + return True @property def message(self) -> str: @@ -43,8 +33,8 @@ class CmdKillAction(Action): thought: str = '' action: str = ActionType.KILL - async def run(self, controller: 'AgentController') -> 'CmdOutputObservation': - return controller.action_manager.kill_command(self.id) + def runnable(self) -> bool: + return True @property def message(self) -> str: @@ -60,25 +50,8 @@ class IPythonRunCellAction(Action): thought: str = '' action: str = ActionType.RUN_IPYTHON - async def run(self, controller: 'AgentController') -> 'IPythonRunCellObservation': - # echo "import math" | execute_cli - # write code to a temporary file and pass it to `execute_cli` via stdin - tmp_filepath = os.path.join( - config.workspace_base, '.tmp', '.ipython_execution_tmp.py' - ) - pathlib.Path(os.path.dirname(tmp_filepath)).mkdir(parents=True, exist_ok=True) - with open(tmp_filepath, 'w') as tmp_file: - tmp_file.write(self.code) - - tmp_filepath_inside_sandbox = os.path.join( - config.workspace_mount_path_in_sandbox, - '.tmp', - '.ipython_execution_tmp.py', - ) - obs = controller.action_manager.run_command( - f'execute_cli < {tmp_filepath_inside_sandbox}', background=False - ) - return IPythonRunCellObservation(content=obs.content, code=self.code) + def runnable(self) -> bool: + return True def __str__(self) -> str: ret = '**IPythonRunCellAction**\n' diff --git a/opendevin/events/action/files.py b/opendevin/events/action/files.py index defd91013f..09993d372e 100644 --- a/opendevin/events/action/files.py +++ b/opendevin/events/action/files.py @@ -1,46 +1,10 @@ -import os from dataclasses import dataclass -from pathlib import Path -from opendevin.core.config import config from opendevin.core.schema import ActionType -from opendevin.events.observation import ( - ErrorObservation, - FileReadObservation, - FileWriteObservation, - Observation, -) -from opendevin.runtime import E2BBox from .action import Action -def resolve_path(file_path, working_directory): - path_in_sandbox = Path(file_path) - - # Apply working directory - if not path_in_sandbox.is_absolute(): - path_in_sandbox = Path(working_directory) / path_in_sandbox - - # Sanitize the path with respect to the root of the full sandbox - # (deny any .. path traversal to parent directories of the sandbox) - abs_path_in_sandbox = path_in_sandbox.resolve() - - # If the path is outside the workspace, deny it - if not abs_path_in_sandbox.is_relative_to(config.workspace_mount_path_in_sandbox): - raise PermissionError(f'File access not permitted: {file_path}') - - # Get path relative to the root of the workspace inside the sandbox - path_in_workspace = abs_path_in_sandbox.relative_to( - Path(config.workspace_mount_path_in_sandbox) - ) - - # Get path relative to host - path_in_host_workspace = Path(config.workspace_base) / path_in_workspace - - return path_in_host_workspace - - @dataclass class FileReadAction(Action): """ @@ -55,46 +19,9 @@ class FileReadAction(Action): thought: str = '' action: str = ActionType.READ - def _read_lines(self, all_lines: list[str]): - if self.end == -1: - if self.start == 0: - return all_lines - else: - return all_lines[self.start :] - else: - num_lines = len(all_lines) - begin = max(0, min(self.start, num_lines - 2)) - end = -1 if self.end > num_lines else max(begin + 1, self.end) - return all_lines[begin:end] - - async def run(self, controller) -> Observation: - if isinstance(controller.action_manager.sandbox, E2BBox): - content = controller.action_manager.sandbox.filesystem.read(self.path) - read_lines = self._read_lines(content.split('\n')) - code_view = ''.join(read_lines) - else: - try: - whole_path = resolve_path( - self.path, controller.action_manager.sandbox.get_working_directory() - ) - self.start = max(self.start, 0) - try: - with open(whole_path, 'r', encoding='utf-8') as file: - read_lines = self._read_lines(file.readlines()) - code_view = ''.join(read_lines) - except FileNotFoundError: - return ErrorObservation(f'File not found: {self.path}') - except UnicodeDecodeError: - return ErrorObservation( - f'File could not be decoded as utf-8: {self.path}' - ) - except IsADirectoryError: - return ErrorObservation( - f'Path is a directory: {self.path}. You can only read files' - ) - except PermissionError: - return ErrorObservation(f'Malformed paths not permitted: {self.path}') - return FileReadObservation(path=self.path, content=code_view) + @property + def runnable(self) -> bool: + return True @property def message(self) -> str: @@ -110,60 +37,9 @@ class FileWriteAction(Action): thought: str = '' action: str = ActionType.WRITE - def _insert_lines(self, to_insert: list[str], original: list[str]): - """ - Insert the new content to the original content based on self.start and self.end - """ - new_lines = [''] if self.start == 0 else original[: self.start] - new_lines += [i + '\n' for i in to_insert] - new_lines += [''] if self.end == -1 else original[self.end :] - return new_lines - - async def run(self, controller) -> Observation: - insert = self.content.split('\n') - - if isinstance(controller.action_manager.sandbox, E2BBox): - files = controller.action_manager.sandbox.filesystem.list(self.path) - if self.path in files: - all_lines = controller.action_manager.sandbox.filesystem.read(self.path) - new_file = self._insert_lines(self.content.split('\n'), all_lines) - controller.action_manager.sandbox.filesystem.write( - self.path, ''.join(new_file) - ) - else: - return ErrorObservation(f'File not found: {self.path}') - else: - try: - whole_path = resolve_path( - self.path, controller.action_manager.sandbox.get_working_directory() - ) - if not os.path.exists(os.path.dirname(whole_path)): - os.makedirs(os.path.dirname(whole_path)) - mode = 'w' if not os.path.exists(whole_path) else 'r+' - try: - with open(whole_path, mode, encoding='utf-8') as file: - if mode != 'w': - all_lines = file.readlines() - new_file = self._insert_lines(insert, all_lines) - else: - new_file = [i + '\n' for i in insert] - - file.seek(0) - file.writelines(new_file) - file.truncate() - except FileNotFoundError: - return ErrorObservation(f'File not found: {self.path}') - except IsADirectoryError: - return ErrorObservation( - f'Path is a directory: {self.path}. You can only write to files' - ) - except UnicodeDecodeError: - return ErrorObservation( - f'File could not be decoded as utf-8: {self.path}' - ) - except PermissionError: - return ErrorObservation(f'Malformed paths not permitted: {self.path}') - return FileWriteObservation(content='', path=self.path) + @property + def runnable(self) -> bool: + return True @property def message(self) -> str: diff --git a/opendevin/events/action/github.py b/opendevin/events/action/github.py deleted file mode 100644 index d7878573a0..0000000000 --- a/opendevin/events/action/github.py +++ /dev/null @@ -1,157 +0,0 @@ -import random -import string -from dataclasses import dataclass -from typing import TYPE_CHECKING - -import requests - -from opendevin.core.config import config -from opendevin.core.schema import ActionType -from opendevin.events.observation import ( - CmdOutputObservation, - ErrorObservation, - Observation, - SuccessObservation, -) - -from .action import Action - -if TYPE_CHECKING: - from opendevin.controller import AgentController - - -@dataclass -class GitHubPushAction(Action): - """This pushes the current branch to github. - - To use this, you need to set the GITHUB_TOKEN environment variable. - The agent will return a message with a URL that you can click to make a pull - request. - - Attributes: - owner: The owner of the source repo - repo: The name of the source repo - branch: The branch to push - action: The action identifier - """ - - owner: str - repo: str - branch: str - action: str = ActionType.PUSH - - async def run(self, controller: 'AgentController') -> Observation: - github_token = config.github_token - if not github_token: - return ErrorObservation('github_token is not set') - - # Create a random short string to use as a temporary remote - random_remote = ''.join( - ['opendevin_temp_'] + random.choices(string.ascii_lowercase, k=5) - ) - - # Set the temporary remote - new_url = f'https://{github_token}@github.com/{self.owner}/{self.repo}.git' - command = f'git remote add {random_remote} {new_url}' - remote_add_result = controller.action_manager.run_command( - command, background=False - ) - if ( - not isinstance(remote_add_result, CmdOutputObservation) - or remote_add_result.exit_code != 0 - ): - return remote_add_result - - # Push the branch to the temporary remote - command = f'git push {random_remote} {self.branch}' - push_result = controller.action_manager.run_command(command, background=False) - - # Delete the temporary remote - command = f'git remote remove {random_remote}' - remote_remove_result = controller.action_manager.run_command( - command, background=False - ) - if ( - not isinstance(remote_remove_result, CmdOutputObservation) - or remote_remove_result.exit_code != 0 - ): - return remote_remove_result - - return push_result - - @property - def message(self) -> str: - return f'Pushing branch {self.branch} to {self.owner}/{self.repo}' - - -@dataclass -class GitHubSendPRAction(Action): - """An action to send a github PR. - - To use this, you need to set the GITHUB_TOKEN environment variable. - - Attributes: - owner: The owner of the source repo - repo: The name of the source repo - title: The title of the PR - head: The branch to send the PR from - head_repo: The repo to send the PR from - base: The branch to send the PR to - body: The body of the PR - """ - - owner: str - repo: str - title: str - head: str - head_repo: str | None - base: str - body: str | None - action: str = ActionType.SEND_PR - - async def run(self, controller: 'AgentController') -> Observation: - github_token = config.github_token - if not github_token: - return ErrorObservation('github_token is not set') - - # API URL to create the pull request - url = f'https://api.github.com/repos/{self.owner}/{self.repo}/pulls' - - # Headers to authenticate and request JSON responses - headers = { - 'Authorization': f'token {github_token}', - 'Accept': 'application/vnd.github.v3+json', - } - - # Data for the pull request - data = { - 'title': self.title, - 'head': self.head, - 'head_repo': self.head_repo, - 'base': self.base, - 'body': self.body, - } - data = {k: v for k, v in data.items() if v is not None} - - # Make the request - response = requests.post(url, headers=headers, json=data) - - # Check for errors - if response.status_code == 201: - return SuccessObservation( - 'Pull request created successfully!\n' - f'Pull request URL:{response.json()["html_url"]}' - ) - else: - return ErrorObservation( - 'Failed to create pull request\n' - f'Status code: {response.status_code}\n' - f'Response: {response.text}' - ) - - @property - def message(self) -> str: - return ( - f'Sending PR from {self.head_repo}:{self.head} to ' - f'{self.owner}:{self.base}' - ) diff --git a/opendevin/events/action/tasks.py b/opendevin/events/action/tasks.py index d3558ecc42..6ca1e0be47 100644 --- a/opendevin/events/action/tasks.py +++ b/opendevin/events/action/tasks.py @@ -1,14 +1,9 @@ from dataclasses import dataclass, field -from typing import TYPE_CHECKING from opendevin.core.schema import ActionType -from opendevin.events.observation import NullObservation from .action import Action -if TYPE_CHECKING: - from opendevin.controller import AgentController - @dataclass class AddTaskAction(Action): @@ -18,11 +13,6 @@ class AddTaskAction(Action): thought: str = '' action: str = ActionType.ADD_TASK - async def run(self, controller: 'AgentController') -> NullObservation: # type: ignore - if controller.state is not None: - controller.state.plan.add_subtask(self.parent, self.goal, self.subtasks) - return NullObservation('') - @property def message(self) -> str: return f'Added task: {self.goal}' @@ -35,11 +25,6 @@ class ModifyTaskAction(Action): thought: str = '' action: str = ActionType.MODIFY_TASK - async def run(self, controller: 'AgentController') -> NullObservation: # type: ignore - if controller.state is not None: - controller.state.plan.set_subtask_state(self.id, self.state) - return NullObservation('') - @property def message(self) -> str: return f'Set task {self.id} to {self.state}' diff --git a/opendevin/runtime/e2b/runtime.py b/opendevin/runtime/e2b/runtime.py new file mode 100644 index 0000000000..180bf418f7 --- /dev/null +++ b/opendevin/runtime/e2b/runtime.py @@ -0,0 +1,44 @@ +from opendevin.events.action import ( + FileReadAction, + FileWriteAction, +) +from opendevin.events.observation import ( + ErrorObservation, + FileReadObservation, + FileWriteObservation, + Observation, +) +from opendevin.runtime.server.files import insert_lines, read_lines +from opendevin.runtime.server.runtime import ServerRuntime + +from .sandbox import E2BSandbox + + +class E2BRuntime(ServerRuntime): + def __init__( + self, + sid: str = 'default', + ): + super().__init__() + if not isinstance(self.sandbox, E2BSandbox): + raise ValueError('E2BRuntime requires an E2BSandbox') + self.filesystem = self.sandbox.filesystem + + async def read(self, action: FileReadAction) -> Observation: + content = self.filesystem.read(action.path) + lines = read_lines(content.split('\n'), action.start, action.end) + code_view = ''.join(lines) + return FileReadObservation(code_view, path=action.path) + + async def write(self, action: FileWriteAction) -> Observation: + files = self.filesystem.list(action.path) + if action.path in files: + all_lines = self.filesystem.read(action.path) + new_file = insert_lines( + action.content.split('\n'), all_lines, action.start, action.end + ) + self.filesystem.write(action.path, ''.join(new_file)) + return FileWriteObservation('', path=action.path) + else: + # FIXME: we should create a new file here + return ErrorObservation(f'File not found: {action.path}') diff --git a/opendevin/runtime/runtime.py b/opendevin/runtime/runtime.py new file mode 100644 index 0000000000..179ddcc5e2 --- /dev/null +++ b/opendevin/runtime/runtime.py @@ -0,0 +1,129 @@ +from abc import abstractmethod +from typing import List + +from opendevin.core.config import config +from opendevin.events.action import ( + ACTION_TYPE_TO_CLASS, + Action, + AgentRecallAction, + BrowseURLAction, + CmdKillAction, + CmdRunAction, + FileReadAction, + FileWriteAction, + IPythonRunCellAction, +) +from opendevin.events.observation import ( + CmdOutputObservation, + ErrorObservation, + NullObservation, + Observation, +) +from opendevin.runtime import ( + DockerExecBox, + DockerSSHBox, + E2BBox, + LocalBox, + Sandbox, +) +from opendevin.runtime.browser.browser_env import BrowserEnv +from opendevin.runtime.plugins import PluginRequirement + + +def create_sandbox(sid: str = 'default', sandbox_type: str = 'exec') -> Sandbox: + if sandbox_type == 'exec': + return DockerExecBox(sid=sid, timeout=config.sandbox_timeout) + elif sandbox_type == 'local': + return LocalBox(timeout=config.sandbox_timeout) + elif sandbox_type == 'ssh': + return DockerSSHBox(sid=sid, timeout=config.sandbox_timeout) + elif sandbox_type == 'e2b': + return E2BBox(timeout=config.sandbox_timeout) + else: + raise ValueError(f'Invalid sandbox type: {sandbox_type}') + + +class Runtime: + """ + The runtime is how the agent interacts with the external environment. + This includes a bash sandbox, a browser, and filesystem interactions. + + sid is the session id, which is used to identify the current user session. + """ + + sid: str + sandbox: Sandbox + + def __init__( + self, + sid: str = 'default', + ): + self.sid = sid + self.sandbox = create_sandbox(sid, config.sandbox_type) + self.browser = BrowserEnv() + + def init_sandbox_plugins(self, plugins: List[PluginRequirement]) -> None: + self.sandbox.init_plugins(plugins) + + async def run_action(self, action: Action) -> Observation: + """ + Run an action and return the resulting observation. + If the action is not runnable in any runtime, a NullObservation is returned. + If the action is not supported by the current runtime, an ErrorObservation is returned. + """ + if not action.runnable: + return NullObservation('') + action_id = action.action # type: ignore[attr-defined] + if action_id not in ACTION_TYPE_TO_CLASS: + return ErrorObservation(f'Action {action_id} does not exist.') + if not hasattr(self, action_id): + return ErrorObservation( + f'Action {action_id} is not supported in the current runtime.' + ) + observation = await getattr(self, action_id)(action) + return observation + + def get_background_obs(self) -> List[CmdOutputObservation]: + """ + Returns all observations that have accumulated in the runtime's background. + Right now, this is just background commands, but could include e.g. asyncronous + events happening in the browser. + """ + obs = [] + for _id, cmd in self.sandbox.background_commands.items(): + output = cmd.read_logs() + if output is not None and output != '': + obs.append( + CmdOutputObservation( + content=output, command_id=_id, command=cmd.command + ) + ) + return obs + + @abstractmethod + async def run(self, action: CmdRunAction) -> Observation: + pass + + @abstractmethod + async def kill(self, action: CmdKillAction) -> Observation: + pass + + @abstractmethod + async def run_ipython(self, action: IPythonRunCellAction) -> Observation: + pass + + @abstractmethod + async def read(self, action: FileReadAction) -> Observation: + pass + + @abstractmethod + async def write(self, action: FileWriteAction) -> Observation: + pass + + @abstractmethod + async def browse(self, action: BrowseURLAction) -> Observation: + pass + + @abstractmethod + async def recall(self, action: AgentRecallAction) -> Observation: + pass diff --git a/opendevin/runtime/server/browse.py b/opendevin/runtime/server/browse.py new file mode 100644 index 0000000000..3b16bea06c --- /dev/null +++ b/opendevin/runtime/server/browse.py @@ -0,0 +1,29 @@ +import os + +from opendevin.events.observation import BrowserOutputObservation + + +async def browse(action, browser) -> BrowserOutputObservation: # type: ignore + asked_url = action.url + if not asked_url.startswith('http'): + asked_url = os.path.abspath(os.curdir) + action.url + try: + # action in BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/functions.py + action_str = f'goto("{asked_url}")' + # obs provided by BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/env.py#L396 + obs = browser.step(action_str) + return BrowserOutputObservation( + content=obs['text_content'], # text content of the page + open_pages_urls=obs['open_pages_urls'], # list of open pages + active_page_index=obs['active_page_index'], # index of the active page + dom_object=obs['dom_object'], # DOM object + axtree_object=obs['axtree_object'], # accessibility tree object + last_browser_action=obs['last_action'], # last browser env action performed + focused_element_bid=obs['focused_element_bid'], # focused element bid + screenshot=obs['screenshot'], # base64-encoded screenshot, png + url=asked_url, + ) + except Exception as e: + return BrowserOutputObservation( + content=str(e), screenshot='', error=True, url=asked_url + ) diff --git a/opendevin/runtime/server/files.py b/opendevin/runtime/server/files.py new file mode 100644 index 0000000000..1bc49a8c6a --- /dev/null +++ b/opendevin/runtime/server/files.py @@ -0,0 +1,116 @@ +import os +from pathlib import Path + +from opendevin.core.config import config +from opendevin.events.observation import ( + ErrorObservation, + FileReadObservation, + FileWriteObservation, + Observation, +) + + +def resolve_path(file_path, working_directory): + path_in_sandbox = Path(file_path) + + # Apply working directory + if not path_in_sandbox.is_absolute(): + path_in_sandbox = Path(working_directory) / path_in_sandbox + + # Sanitize the path with respect to the root of the full sandbox + # (deny any .. path traversal to parent directories of the sandbox) + abs_path_in_sandbox = path_in_sandbox.resolve() + + # If the path is outside the workspace, deny it + if not abs_path_in_sandbox.is_relative_to(config.workspace_mount_path_in_sandbox): + raise PermissionError(f'File access not permitted: {file_path}') + + # Get path relative to the root of the workspace inside the sandbox + path_in_workspace = abs_path_in_sandbox.relative_to( + Path(config.workspace_mount_path_in_sandbox) + ) + + # Get path relative to host + path_in_host_workspace = Path(config.workspace_base) / path_in_workspace + + return path_in_host_workspace + + +def read_lines(all_lines: list[str], start=0, end=-1): + start = max(start, 0) + start = min(start, len(all_lines)) + end = -1 if end == -1 else max(end, 0) + end = min(end, len(all_lines)) + if end == -1: + if start == 0: + return all_lines + else: + return all_lines[start:] + else: + num_lines = len(all_lines) + begin = max(0, min(start, num_lines - 2)) + end = -1 if end > num_lines else max(begin + 1, end) + return all_lines[begin:end] + + +async def read_file(path, workdir, start=0, end=-1) -> Observation: + try: + whole_path = resolve_path(path, workdir) + except PermissionError: + return ErrorObservation(f'Malformed paths not permitted: {path}') + + try: + with open(whole_path, 'r', encoding='utf-8') as file: + lines = read_lines(file.readlines(), start, end) + except FileNotFoundError: + return ErrorObservation(f'File not found: {path}') + except UnicodeDecodeError: + return ErrorObservation(f'File could not be decoded as utf-8: {path}') + except IsADirectoryError: + return ErrorObservation(f'Path is a directory: {path}. You can only read files') + code_view = ''.join(lines) + return FileReadObservation(path=path, content=code_view) + + +def insert_lines( + to_insert: list[str], original: list[str], start: int = 0, end: int = -1 +): + """ + Insert the new content to the original content based on start and end + """ + new_lines = [''] if start == 0 else original[:start] + new_lines += [i + '\n' for i in to_insert] + new_lines += [''] if end == -1 else original[end:] + return new_lines + + +async def write_file(path, workdir, content, start=0, end=-1) -> Observation: + insert = content.split('\n') + + try: + whole_path = resolve_path(path, workdir) + if not os.path.exists(os.path.dirname(whole_path)): + os.makedirs(os.path.dirname(whole_path)) + mode = 'w' if not os.path.exists(whole_path) else 'r+' + try: + with open(whole_path, mode, encoding='utf-8') as file: + if mode != 'w': + all_lines = file.readlines() + new_file = insert_lines(insert, all_lines, start, end) + else: + new_file = [i + '\n' for i in insert] + + file.seek(0) + file.writelines(new_file) + file.truncate() + except FileNotFoundError: + return ErrorObservation(f'File not found: {path}') + except IsADirectoryError: + return ErrorObservation( + f'Path is a directory: {path}. You can only write to files' + ) + except UnicodeDecodeError: + return ErrorObservation(f'File could not be decoded as utf-8: {path}') + except PermissionError: + return ErrorObservation(f'Malformed paths not permitted: {path}') + return FileWriteObservation(content='', path=path) diff --git a/opendevin/runtime/server/runtime.py b/opendevin/runtime/server/runtime.py new file mode 100644 index 0000000000..1254f4899e --- /dev/null +++ b/opendevin/runtime/server/runtime.py @@ -0,0 +1,99 @@ +import os +import pathlib + +from opendevin.core.config import config +from opendevin.events.action import ( + AgentRecallAction, + BrowseURLAction, + CmdKillAction, + CmdRunAction, + FileReadAction, + FileWriteAction, + IPythonRunCellAction, +) +from opendevin.events.observation import ( + CmdOutputObservation, + ErrorObservation, + IPythonRunCellObservation, + NullObservation, + Observation, +) +from opendevin.runtime.runtime import Runtime + +from .browse import browse +from .files import read_file, write_file + + +class ServerRuntime(Runtime): + async def run(self, action: CmdRunAction) -> Observation: + return self._run_command(action.command, background=action.background) + + async def kill(self, action: CmdKillAction) -> Observation: + cmd = self.sandbox.kill_background(action.id) + return CmdOutputObservation( + content=f'Background command with id {action.id} has been killed.', + command_id=action.id, + command=cmd.command, + exit_code=0, + ) + + async def run_ipython(self, action: IPythonRunCellAction) -> Observation: + # echo "import math" | execute_cli + # write code to a temporary file and pass it to `execute_cli` via stdin + tmp_filepath = os.path.join( + config.workspace_base, '.tmp', '.ipython_execution_tmp.py' + ) + pathlib.Path(os.path.dirname(tmp_filepath)).mkdir(parents=True, exist_ok=True) + with open(tmp_filepath, 'w') as tmp_file: + tmp_file.write(action.code) + + tmp_filepath_inside_sandbox = os.path.join( + config.workspace_mount_path_in_sandbox, + '.tmp', + '.ipython_execution_tmp.py', + ) + obs = self._run_command( + f'execute_cli < {tmp_filepath_inside_sandbox}', background=False + ) + return IPythonRunCellObservation(content=obs.content, code=action.code) + + async def read(self, action: FileReadAction) -> Observation: + working_dir = self.sandbox.get_working_directory() + return await read_file(action.path, working_dir, action.start, action.end) + + async def write(self, action: FileWriteAction) -> Observation: + working_dir = self.sandbox.get_working_directory() + return await write_file( + action.path, working_dir, action.content, action.start, action.end + ) + + async def browse(self, action: BrowseURLAction) -> Observation: + return await browse(action, self.browser) + + async def recall(self, action: AgentRecallAction) -> Observation: + return NullObservation('') + + def _run_command(self, command: str, background=False) -> Observation: + if background: + return self._run_background(command) + else: + return self._run_immediately(command) + + def _run_immediately(self, command: str) -> Observation: + try: + exit_code, output = self.sandbox.execute(command) + return CmdOutputObservation( + command_id=-1, content=output, command=command, exit_code=exit_code + ) + except UnicodeDecodeError: + return ErrorObservation('Command output could not be decoded as utf-8') + + def _run_background(self, command: str) -> Observation: + bg_cmd = self.sandbox.execute_in_background(command) + content = f'Background command started. To stop it, send a `kill` action with id {bg_cmd.pid}' + return CmdOutputObservation( + content=content, + command_id=bg_cmd.pid, + command=command, + exit_code=0, + ) diff --git a/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log b/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log index de2a1bf7a6..8ac0724752 100644 --- a/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log +++ b/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log @@ -270,37 +270,6 @@ This is your internal monologue, in JSON format: "wait_for_response": false } }, - { - "action": "message", - "args": { - "content": "If I have done some work and I want to push it to github, I can do that also!", - "wait_for_response": false - } - }, - { - "action": "message", - "args": { - "content": "Let's do it.", - "wait_for_response": false - } - }, - { - "action": "push", - "args": { - "owner": "owner", - "repo": "repo", - "branch": "branch" - } - }, - { - "observation": "run", - "content": "The repo was successfully pushed to https://github.com/owner/repo/branch", - "extras": { - "command_id": 0, - "command": "", - "exit_code": 0 - } - }, { "action": "message", "args": { diff --git a/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log b/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log index fe42cd8849..08cac84825 100644 --- a/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log +++ b/tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log @@ -270,37 +270,6 @@ This is your internal monologue, in JSON format: "wait_for_response": false } }, - { - "action": "message", - "args": { - "content": "If I have done some work and I want to push it to github, I can do that also!", - "wait_for_response": false - } - }, - { - "action": "message", - "args": { - "content": "Let's do it.", - "wait_for_response": false - } - }, - { - "action": "push", - "args": { - "owner": "owner", - "repo": "repo", - "branch": "branch" - } - }, - { - "observation": "run", - "content": "The repo was successfully pushed to https://github.com/owner/repo/branch", - "extras": { - "command_id": 0, - "command": "", - "exit_code": 0 - } - }, { "action": "message", "args": { diff --git a/tests/integration/mock/PlannerAgent/test_write_simple_script/response_001.log b/tests/integration/mock/PlannerAgent/test_write_simple_script/response_001.log index c768a2a63d..c146bbc016 100644 --- a/tests/integration/mock/PlannerAgent/test_write_simple_script/response_001.log +++ b/tests/integration/mock/PlannerAgent/test_write_simple_script/response_001.log @@ -1,3 +1,4 @@ +```json { "action": "write", "args": { @@ -5,3 +6,4 @@ "content": "#!/bin/bash\n\necho 'hello'" } } +``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_003.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_003.log index 094e3f4db4..e1ec916182 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_003.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_003.log @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_004.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_004.log index 20add1573e..6eeb3b1b86 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_004.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_004.log @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -103,7 +103,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_005.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_005.log index af38a778cb..6a160dd8f8 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_005.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_005.log @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -103,7 +103,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -121,7 +121,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_006.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_006.log index 0486a93615..c24a1c5356 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_006.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_006.log @@ -67,7 +67,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -103,7 +103,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -121,7 +121,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_007.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_007.log index 826ff471cb..c24a1c5356 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_007.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_007.log @@ -67,7 +67,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -103,7 +103,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -121,7 +121,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_008.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_008.log index 08ed4d4f92..58458f332d 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_008.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_008.log @@ -67,7 +67,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -103,7 +103,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -115,19 +115,20 @@ extras: Memory 3: Previous Action: -action: write +action: read args: path: hello.sh start: 0 end: -1 thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I have already written the shell script 'hello.sh' to print 'hello' without asking for confirmation. I need to verify the content of the file to ensure that the script is correctly implemented. Action: Output from Action: -observation: write -content: +observation: read +content: echo 'hello' + extras: path: hello.sh ======= End Actions ======= diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_009.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_009.log index 6e08e0646f..bd2529d885 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_009.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_009.log @@ -67,7 +67,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -85,7 +85,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -97,39 +97,38 @@ extras: Memory 2: Previous Action: -action: write +action: read args: path: hello.sh start: 0 end: -1 thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I have already written the shell script 'hello.sh' to print 'hello' without asking for confirmation. I need to verify the content of the file to ensure that the script is correctly implemented. Action: Output from Action: -observation: write -content: +observation: read +content: echo 'hello' + extras: path: hello.sh Memory 3: Previous Action: -action: write +action: run args: - path: hello.sh - start: 0 - end: -1 - thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. - - Action: + command: bash chmod +x hello.sh + background: False + thought: Output from Action: -observation: write -content: +observation: run +content: /usr/bin/chmod: /usr/bin/chmod: cannot execute binary file extras: - path: hello.sh + command_id: -1 + command: bash chmod +x hello.sh + exit_code: 126 ======= End Actions ======= Use these memories to provide additional context to the problem you are solving. Remember that you have already completed these steps so you do not need to perform them again. diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_010.log b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_010.log index 0473367c11..580bcc5eb1 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_010.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/prompt_010.log @@ -67,7 +67,7 @@ args: start: 0 end: -1 thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. + I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: @@ -79,42 +79,6 @@ extras: Memory 1: Previous Action: -action: write -args: - path: hello.sh - start: 0 - end: -1 - thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. - - Action: - -Output from Action: -observation: write -content: -extras: - path: hello.sh - -Memory 2: -Previous Action: -action: write -args: - path: hello.sh - start: 0 - end: -1 - thought: Thoughts: - I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. - - Action: - -Output from Action: -observation: write -content: -extras: - path: hello.sh - -Memory 3: -Previous Action: action: read args: path: hello.sh @@ -131,6 +95,38 @@ content: echo 'hello' extras: path: hello.sh + +Memory 2: +Previous Action: +action: run +args: + command: bash chmod +x hello.sh + background: False + thought: + +Output from Action: +observation: run +content: /usr/bin/chmod: /usr/bin/chmod: cannot execute binary file +extras: + command_id: -1 + command: bash chmod +x hello.sh + exit_code: 126 + +Memory 3: +Previous Action: +action: run +args: + command: chmod +x hello.sh + background: False + thought: + +Output from Action: +observation: run +content: +extras: + command_id: -1 + command: chmod +x hello.sh + exit_code: 0 ======= End Actions ======= Use these memories to provide additional context to the problem you are solving. Remember that you have already completed these steps so you do not need to perform them again. diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_002.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_002.log index c91f1c337c..ddd480a15e 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_002.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_002.log @@ -1,5 +1,5 @@ Thoughts: -I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_003.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_003.log index c91f1c337c..ddd480a15e 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_003.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_003.log @@ -1,5 +1,5 @@ Thoughts: -I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_004.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_004.log index c91f1c337c..ddd480a15e 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_004.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_004.log @@ -1,5 +1,5 @@ Thoughts: -I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_005.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_005.log index c91f1c337c..ddd480a15e 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_005.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_005.log @@ -1,5 +1,5 @@ Thoughts: -I need to add the command to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_006.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_006.log index 3abac05a69..ddd480a15e 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_006.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_006.log @@ -1,5 +1,5 @@ Thoughts: -I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I need to add the code to the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. Action: ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_007.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_007.log index 3abac05a69..9b051dea70 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_007.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_007.log @@ -1,7 +1,7 @@ Thoughts: -I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I have already written the shell script 'hello.sh' to print 'hello' without asking for confirmation. I need to verify the content of the file to ensure that the script is correctly implemented. Action: ``` -edit 0 -1 "echo 'hello'" +read hello.sh ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_008.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_008.log index 3abac05a69..90ceb6e92c 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_008.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_008.log @@ -1,7 +1,7 @@ Thoughts: -I need to modify the 'hello.sh' script to print 'hello' without asking for confirmation. I will use the echo command to achieve this. +I need to ensure that the 'hello.sh' script is executable. This can be done by using the chmod command to add the execute permission to the file. Action: ``` -edit 0 -1 "echo 'hello'" +bash chmod +x hello.sh ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_009.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_009.log index 9b051dea70..d7dd519ffe 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_009.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_009.log @@ -1,7 +1,7 @@ Thoughts: -I have already written the shell script 'hello.sh' to print 'hello' without asking for confirmation. I need to verify the content of the file to ensure that the script is correctly implemented. +I need to make the 'hello.sh' script executable so that it can be run. The previous attempt to make it executable using the 'bash chmod +x hello.sh' command resulted in an error. I will use the 'chmod' command directly to make the script executable. Action: ``` -read hello.sh +chmod +x hello.sh ``` diff --git a/tests/integration/mock/SWEAgent/test_write_simple_script/response_010.log b/tests/integration/mock/SWEAgent/test_write_simple_script/response_010.log index f5d842205a..79d9ecb941 100644 --- a/tests/integration/mock/SWEAgent/test_write_simple_script/response_010.log +++ b/tests/integration/mock/SWEAgent/test_write_simple_script/response_010.log @@ -1,7 +1,7 @@ Thoughts: -I have already written the shell script 'hello.sh' to print 'hello' without asking for confirmation. I have verified the content of the file and it contains the correct command 'echo 'hello''. The script should work as expected. +The shell script 'hello.sh' has been created to print 'hello' without asking for confirmation. The script content is currently set to "echo 'hello'". The next step is to make sure that the script is executable and then test it to ensure that it prints 'hello' without asking for confirmation. Action: ``` -exit +chmod +x hello.sh ``` diff --git a/tests/unit/test_action_github.py b/tests/unit/test_action_github.py deleted file mode 100644 index c34f84c09a..0000000000 --- a/tests/unit/test_action_github.py +++ /dev/null @@ -1,151 +0,0 @@ -from unittest.mock import MagicMock, call, patch - -import pytest - -from agenthub.dummy_agent.agent import DummyAgent -from opendevin.controller.agent_controller import AgentController -from opendevin.core.config import config -from opendevin.events.action.github import GitHubPushAction, GitHubSendPRAction -from opendevin.events.observation.commands import CmdOutputObservation -from opendevin.events.observation.error import ErrorObservation -from opendevin.events.stream import EventStream -from opendevin.llm.llm import LLM - - -@pytest.fixture -def agent_controller(): - # Setup the environment variable - config.sandbox_type = 'local' - llm = LLM() - agent = DummyAgent(llm=llm) - event_stream = EventStream() - controller = AgentController(agent, event_stream) - yield controller - - -@pytest.mark.asyncio -@patch.object(config, 'github_token', 'fake_token') -@patch('random.choices') -@patch('opendevin.controller.action_manager.ActionManager.run_command') -async def test_run_push_successful( - mock_run_command, mock_random_choices, agent_controller -): - # Setup mock for random.choices - mock_random_choices.return_value = ['a', 'b', 'c', 'd', 'e'] - - # Create a CmdOutputObservation instance for successful command execution - successful_output = CmdOutputObservation( - content='', command_id=1, command='', exit_code=0 - ) - - # Setup the mock for run_command to return successful output - mock_run_command.return_value = successful_output - - # Run the method - push_action = GitHubPushAction(owner='owner', repo='repo', branch='branch') - result = await push_action.run(agent_controller) - - # Verify the result is successful - assert isinstance(result, CmdOutputObservation) - assert result.exit_code == 0 - - # Verify that the correct remote commands were sent - expected_calls = [ - call( - 'git remote add opendevin_temp_abcde https://fake_token@github.com/owner/repo.git', - background=False, - ), - call('git push opendevin_temp_abcde branch', background=False), - call('git remote remove opendevin_temp_abcde', background=False), - ] - mock_run_command.assert_has_calls(expected_calls) - - -@pytest.mark.asyncio -@patch('random.choices') -@patch('opendevin.controller.action_manager.ActionManager.run_command') -async def test_run_push_error_missing_token( - mock_run_command, mock_random_choices, agent_controller -): - # Run the method - push_action = GitHubPushAction(owner='owner', repo='repo', branch='branch') - result = await push_action.run(agent_controller) - - # Verify the result is an error due to missing token - assert isinstance(result, ErrorObservation) - assert result.message == 'github_token is not set' - - -@pytest.mark.asyncio -@patch.object(config, 'github_token', 'fake_token') -@patch('requests.post') -async def test_run_pull_request_created_successfully(mock_post, agent_controller): - # Set up the mock for the requests.post call to simulate a successful pull request creation - mock_response = MagicMock() - mock_response.status_code = 201 - mock_response.json.return_value = {'html_url': 'https://github.com/example/pull/1'} - mock_post.return_value = mock_response - - # Run the method - pr_action = GitHubSendPRAction( - owner='owner', - repo='repo', - title='title', - head='head', - head_repo='head_repo', - base='base', - body='body', - ) - result = await pr_action.run(agent_controller) - - # Verify the result is a success observation - assert 'Pull request created successfully' in result.content - assert 'https://github.com/example/pull/1' in result.content - - -@pytest.mark.asyncio -@patch('requests.post') -@patch.object(config, 'github_token', 'fake_token') -async def test_run_pull_request_creation_failed(mock_post, agent_controller): - # Set up the mock for the requests.post call to simulate a failed pull request creation - mock_response = MagicMock() - mock_response.status_code = 400 - mock_response.text = 'Bad Request' - mock_post.return_value = mock_response - - # Run the method - pr_action = GitHubSendPRAction( - owner='owner', - repo='repo', - title='title', - head='head', - head_repo='head_repo', - base='base', - body='body', - ) - result = await pr_action.run(agent_controller) - - # Verify the result is an error observation - assert isinstance(result, ErrorObservation) - assert 'Failed to create pull request' in result.content - assert 'Status code: 400' in result.content - assert 'Bad Request' in result.content - - -@pytest.mark.asyncio -async def test_run_error_missing_token(agent_controller): - # Run the method - pr_action = GitHubSendPRAction( - owner='owner', - repo='repo', - title='title', - head='head', - head_repo='head_repo', - base='base', - body='body', - ) - result = await pr_action.run(agent_controller) - - # Verify the result is an error due to missing token - assert isinstance(result, ErrorObservation) - assert 'github_token is not set' in result.message diff --git a/tests/unit/test_action_serialization.py b/tests/unit/test_action_serialization.py index 2abffcbcad..23c21b3379 100644 --- a/tests/unit/test_action_serialization.py +++ b/tests/unit/test_action_serialization.py @@ -9,7 +9,6 @@ from opendevin.events.action import ( CmdRunAction, FileReadAction, FileWriteAction, - GitHubPushAction, MessageAction, ModifyTaskAction, action_from_dict, @@ -85,14 +84,6 @@ def test_browse_url_action_serialization_deserialization(): serialization_deserialization(original_action_dict, BrowseURLAction) -def test_github_push_action_serialization_deserialization(): - original_action_dict = { - 'action': 'push', - 'args': {'owner': 'myname', 'repo': 'myrepo', 'branch': 'main'}, - } - serialization_deserialization(original_action_dict, GitHubPushAction) - - def test_file_read_action_serialization_deserialization(): original_action_dict = { 'action': 'read',