Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored sandbox config #2455

Merged
merged 44 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1bf5f07
Refactored sandbox config and added fastboot
SmartManoj Jun 15, 2024
2f67131
added tests
SmartManoj Jun 16, 2024
7079b64
fixed tests
SmartManoj Jun 16, 2024
90702e4
fixed tests
SmartManoj Jun 16, 2024
a35a6e5
intimate user about breaking change
SmartManoj Jun 21, 2024
de57c58
remove default config from eval
SmartManoj Jun 25, 2024
55729c4
check for lowercase env
SmartManoj Jun 25, 2024
ae02fcd
Merge branch 'main' into fastboot
SmartManoj Jun 25, 2024
75e2677
add test
SmartManoj Jun 25, 2024
09bfcef
Revert Migration
SmartManoj Jun 25, 2024
139b1a3
Merge branch 'fastboot' of https://github.com/SmartManoj/Kevin into f…
SmartManoj Jun 25, 2024
a4d18ad
migrate old sandbox configs
SmartManoj Jun 25, 2024
1f340a8
resolve merge conflict
SmartManoj Jun 25, 2024
8b84541
revert migration 2
SmartManoj Jun 25, 2024
522d6e4
Merge branch 'main' into fastboot
SmartManoj Jun 28, 2024
4d9cad2
Revert "remove default config from eval"
SmartManoj Jun 29, 2024
9335342
Merge branch 'main' into fastboot
SmartManoj Jun 29, 2024
d00a426
change type to box_type
SmartManoj Jul 1, 2024
964304b
Merge branch 'main' into fastboot
SmartManoj Jul 1, 2024
000b80f
fix var name
SmartManoj Jul 1, 2024
ea991e8
linted
SmartManoj Jul 1, 2024
bfa6046
Merge branch 'main' into fastboot
SmartManoj Jul 1, 2024
9d172ed
lint
SmartManoj Jul 1, 2024
5dfb93c
Merge branch 'fastboot' of https://github.com/SmartManoj/Kevin into f…
SmartManoj Jul 1, 2024
c605641
lint comments
SmartManoj Jul 1, 2024
6b3e5d3
Merge branch 'main' into fastboot
SmartManoj Jul 1, 2024
f7e8f24
fix tests
SmartManoj Jul 1, 2024
461d655
fix tests
SmartManoj Jul 2, 2024
dab8f00
Merge branch 'main' into fastboot
SmartManoj Jul 2, 2024
6b9dd3a
fix typo
SmartManoj Jul 3, 2024
fd95205
Merge branch 'main' into fastboot
SmartManoj Jul 3, 2024
d67518c
Merge branch 'main' of github.com:OpenDevin/OpenDevin into fastboot
enyst Jul 4, 2024
61fd15f
fix box_type, remove fast_boot
enyst Jul 4, 2024
0f2e7ab
Merge branch 'main' of github.com:OpenDevin/OpenDevin into fastboot
enyst Jul 4, 2024
f039c7c
add tests for sandbox config
enyst Jul 4, 2024
a645759
fix test
enyst Jul 4, 2024
0a9fb94
update eval docs
enyst Jul 4, 2024
2c1b402
small removal comments
enyst Jul 4, 2024
a35e5b7
adapt toml template
enyst Jul 4, 2024
243f451
old fields shouldn't be in the app dataclass
enyst Jul 5, 2024
2a45d60
Merge branch 'main' of github.com:OpenDevin/OpenDevin into fastboot
enyst Jul 5, 2024
8ec0223
fix old keys in app config
enyst Jul 5, 2024
4c68841
clean up exec box
enyst Jul 5, 2024
84c2d27
Merge branch 'main' of github.com:OpenDevin/OpenDevin into fastboot
enyst Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions agenthub/monologue_agent/utils/prompts.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from opendevin.core.config import config
from opendevin.core.utils import json
from opendevin.events.observation import (
CmdOutputObservation,
)
from opendevin.events.action import (
Action,
)

from opendevin.events.observation import (
CmdOutputObservation,
)
from opendevin.events.serialization.action import action_from_dict

ACTION_PROMPT = """
You're a thoughtful robot. Your main task is this:
%(task)s
Expand Down Expand Up @@ -206,7 +206,7 @@ def get_request_action_prompt(
'background_commands': bg_commands_message,
'hint': hint,
'user': user,
'timeout': config.sandbox_timeout,
'timeout': config.sandbox.timeout,
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
}

Expand Down Expand Up @@ -242,4 +242,4 @@ def parse_summary_response(response: str) -> list[dict]:
- list[dict]: The list of summaries output by the model
"""
parsed = json.loads(response)
return parsed['new_monologue']
return parsed['new_monologue']
93 changes: 77 additions & 16 deletions opendevin/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,56 @@ def defaults_to_dict(self) -> dict:
return dict


@dataclass
class SandboxConfig(metaclass=Singleton):
"""
Configuration for the sandbox.

Attributes:
type: The type of sandbox to use. Options are: ssh, exec, e2b, local.
container_image: The container image to use for the sandbox.
user_id: The user ID for the sandbox.
timeout: The timeout for the sandbox.
initialize_plugins: Whether to initialize the plugins.
fast_boot: Whether to fast boot the sandbox.


"""

type: str = 'ssh'
container_image: str = 'ghcr.io/opendevin/sandbox' + (
f':{os.getenv("OPEN_DEVIN_BUILD_VERSION")}'
if os.getenv('OPEN_DEVIN_BUILD_VERSION')
else ':main'
)
user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
timeout: int = 120
initialize_plugins: bool = True
fast_boot: bool = False

def defaults_to_dict(self) -> dict:
"""
Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional.
"""
dict = {}
for f in fields(self):
dict[f.name] = get_field_info(f)
return dict

def __str__(self):
attr_str = []
for f in fields(self):
attr_name = f.name
attr_value = getattr(self, f.name)

attr_str.append(f'{attr_name}={repr(attr_value)}')

return f"SandboxConfig({', '.join(attr_str)})"

def __repr__(self):
return self.__str__()


class UndefinedString(str, Enum):
UNDEFINED = 'UNDEFINED'

Expand All @@ -135,6 +185,7 @@ class AppConfig(metaclass=Singleton):
Attributes:
llm: The LLM configuration.
agent: The agent configuration.
sandbox: The sandbox configuration.
runtime: The runtime environment.
file_store: The file store to use.
file_store_path: The path to the file store.
Expand All @@ -143,23 +194,20 @@ class AppConfig(metaclass=Singleton):
workspace_mount_path_in_sandbox: The path to mount the workspace in the sandbox. Defaults to /workspace.
workspace_mount_rewrite: The path to rewrite the workspace mount path to.
cache_dir: The path to the cache directory. Defaults to /tmp/cache.
sandbox_container_image: The container image to use for the sandbox.
run_as_devin: Whether to run as devin.
max_iterations: The maximum number of iterations.
max_budget_per_task: The maximum budget allowed per task, beyond which the agent will stop.
e2b_api_key: The E2B API key.
sandbox_type: The type of sandbox to use. Options are: ssh, exec, e2b, local.
use_host_network: Whether to use the host network.
ssh_hostname: The SSH hostname.
disable_color: Whether to disable color. For terminals that don't support color.
sandbox_user_id: The user ID for the sandbox.
sandbox_timeout: The timeout for the sandbox.
debug: Whether to enable debugging.
enable_auto_lint: Whether to enable auto linting. This is False by default, for regular runs of the app. For evaluation, please set this to True.
"""

llm: LLMConfig = field(default_factory=LLMConfig)
agent: AgentConfig = field(default_factory=AgentConfig)
sandbox: SandboxConfig = field(default_factory=SandboxConfig)
runtime: str = 'server'
file_store: str = 'memory'
file_store_path: str = '/tmp/file_store'
Expand All @@ -170,22 +218,13 @@ class AppConfig(metaclass=Singleton):
workspace_mount_path_in_sandbox: str = '/workspace'
workspace_mount_rewrite: str | None = None
cache_dir: str = '/tmp/cache'
sandbox_container_image: str = 'ghcr.io/opendevin/sandbox' + (
f':{os.getenv("OPEN_DEVIN_BUILD_VERSION")}'
if os.getenv('OPEN_DEVIN_BUILD_VERSION')
else ':main'
)
run_as_devin: bool = True
max_iterations: int = 100
max_budget_per_task: float | None = None
e2b_api_key: str = ''
sandbox_type: str = 'ssh' # Can be 'ssh', 'exec', or 'e2b'
use_host_network: bool = False
ssh_hostname: str = 'localhost'
disable_color: bool = False
sandbox_user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
sandbox_timeout: int = 120
initialize_plugins: bool = True
persist_sandbox: bool = False
ssh_port: int = 63710
ssh_password: str | None = None
Expand Down Expand Up @@ -297,9 +336,21 @@ def set_attr_from_env(sub_config: Any, prefix=''):
nested_sub_config = getattr(sub_config, field_name)

# the agent field: the env var for agent.name is just 'AGENT'
if field_name == 'agent' and 'AGENT' in env_or_toml_dict:
if field_name == 'agent' and env_var_name in env_or_toml_dict:
setattr(nested_sub_config, 'name', env_or_toml_dict[env_var_name])

old_configs = ['INITIALIZE_PLUGINS']
if field_name == 'sandbox':
for old_config in old_configs:
if (
old_config.lower() in nested_sub_config.__annotations__
and old_config in env_or_toml_dict
):
suggested_name = f'{field_name}_{old_config}'.upper()
logger.error(
f'Please migrate {old_config} config to {suggested_name} = {env_or_toml_dict[old_config]}'
)
exit(1)
set_attr_from_env(nested_sub_config, prefix=field_name + '_')
elif env_var_name in env_or_toml_dict:
# convert the env var to the correct type and set it
Expand Down Expand Up @@ -366,8 +417,18 @@ def load_from_toml(config: AppConfig, toml_file: str = 'config.toml'):
if 'agent' in toml_config:
agent_config = AgentConfig(**toml_config['agent'])

# set sandbox config from the toml file
sandbox_config = config.sandbox
if 'sandbox' in toml_config:
sandbox_config = SandboxConfig(**toml_config['sandbox'])

# update the config object with the new values
config = AppConfig(llm=llm_config, agent=agent_config, **core_config)
config = AppConfig(
llm=llm_config,
agent=agent_config,
sandbox=sandbox_config,
**core_config,
)
except (TypeError, KeyError):
logger.warning(
'Cannot parse config from toml, toml values have not been applied.',
Expand All @@ -386,7 +447,7 @@ def finalize_config(config: AppConfig):
config.workspace_base = os.path.abspath(config.workspace_base)

# In local there is no sandbox, the workspace will have the same pwd as the host
if config.sandbox_type == 'local':
if config.sandbox.type == 'local':
config.workspace_mount_path_in_sandbox = config.workspace_mount_path

if config.workspace_mount_rewrite: # and not config.workspace_mount_path:
Expand Down
6 changes: 3 additions & 3 deletions opendevin/runtime/docker/exec_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class DockerExecBox(Sandbox):
def __init__(
self,
container_image: str | None = None,
timeout: int = config.sandbox_timeout,
timeout: int = config.sandbox.timeout,
sid: str | None = None,
):
# Initialize docker client. Throws an exception if Docker is not reachable.
Expand All @@ -130,7 +130,7 @@ def __init__(
# if it is too long, the user may have to wait for a unnecessary long time
self.timeout = timeout
self.container_image = (
config.sandbox_container_image
config.sandbox.container_image
if container_image is None
else container_image
)
Expand Down Expand Up @@ -360,7 +360,7 @@ def get_working_directory(self):

@property
def user_id(self):
return config.sandbox_user_id
return config.sandbox.user_id

@property
def run_as_devin(self):
Expand Down
2 changes: 1 addition & 1 deletion opendevin/runtime/docker/local_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@


class LocalBox(Sandbox):
def __init__(self, timeout: int = config.sandbox_timeout):
def __init__(self, timeout: int = config.sandbox.timeout):
os.makedirs(config.workspace_base, exist_ok=True)
self.timeout = timeout
self.background_commands: dict[int, Process] = {}
Expand Down
13 changes: 5 additions & 8 deletions opendevin/runtime/docker/ssh_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class DockerSSHBox(Sandbox):
def __init__(
self,
container_image: str | None = None,
timeout: int = config.sandbox_timeout,
timeout: int = config.sandbox.timeout,
sid: str | None = None,
):
logger.info(
Expand All @@ -226,7 +226,7 @@ def __init__(
self.instance_id = (sid or '') + str(uuid.uuid4())

self.timeout = timeout
self.container_image = container_image or config.sandbox_container_image
self.container_image = container_image or config.sandbox.container_image
self.container_name = self.container_name_prefix + self.instance_id

# set up random user password
Expand Down Expand Up @@ -636,11 +636,7 @@ def get_working_directory(self):

@property
def user_id(self):
return config.sandbox_user_id

@property
def sandbox_user_id(self):
return config.sandbox_user_id
return config.sandbox.user_id

@property
def run_as_devin(self):
Expand Down Expand Up @@ -748,7 +744,8 @@ def close(self):
try:
if container.name.startswith(self.container_name):
if config.persist_sandbox:
container.stop()
if not config.sandbox.fast_boot:
container.stop()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand this, sorry. Does this mean that fast_boot is the same with not initialize_plugins?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this work with non-persistent sandboxes (the default)?
wouldn't a new session create a new container anyways and leave the old "abandonded" here?
@enyst

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it wouldn't work well, but the lines above limit it to persist_sandbox.

Copy link
Contributor Author

@SmartManoj SmartManoj Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the first time, if we set initialize_plugins, here "not initialize_plugins" will fail and sandbox will be stopped -> (effect) jupyter server will also be stopped. (then we need to initialize_plugins again) -> loop

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if initialize_plugins and fast_boot are both true, or are both false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of fast_boot is to not stop the container for a persistent sandbox.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if initialize_plugins and fast_boot are set to true; it will initialize_plugins and don't stop the container.
if both are set to false, if the plugin is already initialized, and if the sandbox was not stopped before, it will work. But for the next run, the jupyter server will be stopped as the sandbox was stopped. So, it won't work.

else:
# only remove the container we created
# otherwise all other containers with the same prefix will be removed
Expand Down
2 changes: 1 addition & 1 deletion opendevin/runtime/e2b/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class E2BBox(Sandbox):
def __init__(
self,
template: str = 'open-devin',
timeout: int = config.sandbox_timeout,
timeout: int = config.sandbox.timeout,
):
self.sandbox = E2BSandbox(
api_key=config.e2b_api_key,
Expand Down
2 changes: 1 addition & 1 deletion opendevin/runtime/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(
):
self.sid = sid
if sandbox is None:
self.sandbox = create_sandbox(sid, config.sandbox_type)
self.sandbox = create_sandbox(sid, config.sandbox.type)
self._is_external_sandbox = False
else:
self.sandbox = sandbox
Expand Down
2 changes: 1 addition & 1 deletion opendevin/runtime/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, **kwargs):
self.add_to_env(sandbox_key, os.environ[key])
if config.enable_auto_lint:
self.add_to_env('ENABLE_AUTO_LINT', 'true')
self.initialize_plugins: bool = config.initialize_plugins
self.initialize_plugins: bool = config.sandbox.initialize_plugins

def add_to_env(self, key: str, value: str):
self._env[key] = value
Expand Down
10 changes: 7 additions & 3 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def test_compat_env_to_config(monkeypatch, setup_env):
monkeypatch.setenv('AGENT_MEMORY_MAX_THREADS', '4')
monkeypatch.setenv('AGENT_MEMORY_ENABLED', 'True')
monkeypatch.setenv('AGENT', 'CodeActAgent')
monkeypatch.setenv('SANDBOX_TYPE', 'local')

config = AppConfig()
load_from_env(config, os.environ)
Expand All @@ -62,6 +63,9 @@ def test_compat_env_to_config(monkeypatch, setup_env):
assert isinstance(config.agent, AgentConfig)
assert isinstance(config.agent.memory_max_threads, int)
assert config.agent.memory_max_threads == 4
assert config.agent.memory_enabled is True
assert config.agent.name == 'CodeActAgent'
assert config.sandbox.type == 'local'


def test_load_from_old_style_env(monkeypatch, default_config):
Expand Down Expand Up @@ -160,7 +164,7 @@ def test_env_overrides_toml(monkeypatch, default_config, temp_toml_file):
assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
assert default_config.workspace_mount_path == 'UNDEFINED'

assert default_config.sandbox_type == 'ssh'
assert default_config.sandbox.type == 'ssh'
assert default_config.disable_color is True

finalize_config(default_config)
Expand Down Expand Up @@ -210,7 +214,7 @@ def test_invalid_toml_format(monkeypatch, temp_toml_file, default_config):
def test_finalize_config(default_config):
# Test finalize config
assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
default_config.sandbox_type = 'local'
default_config.sandbox.type = 'local'
finalize_config(default_config)

assert (
Expand All @@ -233,7 +237,7 @@ def test_workspace_mount_path_default(default_config):

def test_workspace_mount_path_in_sandbox_local(default_config):
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
default_config.sandbox_type = 'local'
default_config.sandbox.type = 'local'
finalize_config(default_config)
assert (
default_config.workspace_mount_path_in_sandbox
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_ipython.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def test_sandbox_jupyter_plugin_backticks(temp_dir):
with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
config, 'workspace_mount_path', new=temp_dir
), patch.object(config, 'run_as_devin', new='true'), patch.object(
config, 'sandbox_type', new='ssh'
config.sandbox, 'type', new='ssh'
):
for box in [DockerSSHBox()]:
box.init_plugins([JupyterRequirement])
Expand Down
Loading