Create runtime implementation (#1626)

* first pass at moving runtime

* fix import

* remove github refs

* remove unnecessary import

* remove unnecessary import

* add e2b

* refactor read and write file ops

* remove github test

* rm action

* revert permissions

* regenerate tests

* re-delete file operations

* regenerate integration tests

* Update opendevin/runtime/runtime.py

Co-authored-by: Graham Neubig <neubig@gmail.com>

* fix ref

* add docs

* remove logspam

---------

Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
Robert Brennan 2024-05-09 19:04:49 -04:00 committed by GitHub
parent 446eaec1e6
commit 26d82841d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 550 additions and 840 deletions

View File

@ -14,7 +14,6 @@ from opendevin.events.action import (
CmdRunAction,
FileReadAction,
FileWriteAction,
GitHubPushAction,
MessageAction,
NullAction,
)
@ -70,10 +69,6 @@ INITIAL_THOUGHTS = [
'BROWSE google.com',
'<form><input type="text"></input><button type="submit"></button></form>',
'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())

View File

@ -1,7 +1,5 @@
from .action_manager import ActionManager
from .agent_controller import AgentController
__all__ = [
'AgentController',
'ActionManager'
]

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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()

View File

@ -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."

View File

@ -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:

View File

@ -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'

View File

@ -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:

View File

@ -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}'
)

View File

@ -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}'

View File

@ -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}')

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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,
)

View File

@ -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": {

View File

@ -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": {

View File

@ -1,3 +1,4 @@
```json
{
"action": "write",
"args": {
@ -5,3 +6,4 @@
"content": "#!/bin/bash\n\necho 'hello'"
}
}
```

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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 =======

View File

@ -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.

View File

@ -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.

View File

@ -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:
```

View File

@ -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:
```

View File

@ -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:
```

View File

@ -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:
```

View File

@ -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:
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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

View File

@ -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',