Refactor config to dataclasses (#1552)

* mypy is invaluable

* fix config, add test

* Add new-style toml support

* add singleton, small doc fixes

* fix some cases of loading toml, clean up, try to make it clearer

* Add defaults_dict for UI

* allow config to be mutable
error handling
fix toml parsing

* remove debug stuff

* Adapt Makefile

* Add defaults for temperature and top_p

* update to CodeActAgent

* comments

* fix unit tests

* implement groups of llm settings (CLI)

* fix merge issue

* small fix sandboxes, small refactoring

* adapt LLM init to accept overrides at runtime

* reading config is enough

* Encapsulate minimally embeddings initialization

* agent bug fix; fix tests

* fix sandboxes tests

* refactor globals in sandboxes to properties
This commit is contained in:
Engel Nyst 2024-05-09 22:48:29 +02:00 committed by GitHub
parent 73693ba416
commit 446eaec1e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 964 additions and 533 deletions

View File

@ -219,15 +219,24 @@ setup-config:
@echo "$(GREEN)Config.toml setup completed.$(RESET)"
setup-config-prompts:
@read -p "Enter your LLM Model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
@echo "[core]" > $(CONFIG_FILE).tmp
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "workspace_base=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
@echo "" >> $(CONFIG_FILE).tmp
@echo "[llm]" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
llm_model=$${llm_model:-$(DEFAULT_MODEL)}; \
echo "LLM_MODEL=\"$$llm_model\"" > $(CONFIG_FILE).tmp
echo "model=\"$$llm_model\"" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM API key: " llm_api_key; \
echo "LLM_API_KEY=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM api key: " llm_api_key; \
echo "api_key=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM Base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
if [[ ! -z "$$llm_base_url" ]]; then echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@read -p "Enter your LLM base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
if [[ ! -z "$$llm_base_url" ]]; then echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@echo "Enter your LLM Embedding Model"; \
echo "Choices are:"; \
@ -241,22 +250,19 @@ setup-config-prompts:
echo " - stable-code"; \
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "LLM_EMBEDDING_MODEL=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
read -p "Enter the local model URL for the embedding model (will set LLM_EMBEDDING_BASE_URL): " llm_embedding_base_url; \
echo "LLM_EMBEDDING_BASE_URL=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the local model URL for the embedding model (will set llm.embedding_base_url): " llm_embedding_base_url; \
echo "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
read -p "Enter the Azure endpoint URL (will overwrite LLM_BASE_URL): " llm_base_url; \
echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure endpoint URL (will overwrite llm.base_url): " llm_base_url; \
echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
echo "LLM_EMBEDDING_DEPLOYMENT_NAME=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure API Version: " llm_api_version; \
echo "LLM_API_VERSION=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
fi
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "WORKSPACE_BASE=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
# Clean up all caches
clean:

View File

@ -4,10 +4,9 @@ import agenthub.monologue_agent.utils.prompts as prompts
from agenthub.monologue_agent.utils.monologue import Monologue
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.exceptions import AgentNoInstructionError
from opendevin.core.schema import ActionType
from opendevin.core.schema.config import ConfigType
from opendevin.events.action import (
Action,
AgentRecallAction,
@ -29,7 +28,7 @@ from opendevin.events.observation import (
)
from opendevin.llm.llm import LLM
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
if config.agent.memory_enabled:
from agenthub.monologue_agent.utils.memory import LongTermMemory
MAX_TOKEN_COUNT_PADDING = 512
@ -160,7 +159,7 @@ class MonologueAgent(Agent):
raise AgentNoInstructionError()
self.monologue = Monologue()
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
if config.agent.memory_enabled:
self.memory = LongTermMemory()
else:
self.memory = None

View File

@ -13,15 +13,14 @@ from tenacity import (
wait_random_exponential,
)
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema.config import ConfigType
from . import json
num_retries = config.get(ConfigType.LLM_NUM_RETRIES)
retry_min_wait = config.get(ConfigType.LLM_RETRY_MIN_WAIT)
retry_max_wait = config.get(ConfigType.LLM_RETRY_MAX_WAIT)
num_retries = config.llm.num_retries
retry_min_wait = config.llm.retry_min_wait
retry_max_wait = config.llm.retry_max_wait
# llama-index includes a retry decorator around openai.get_embeddings() function
# it is initialized with hard-coded values and errors
@ -31,7 +30,7 @@ retry_max_wait = config.get(ConfigType.LLM_RETRY_MAX_WAIT)
if hasattr(llama_openai.get_embeddings, '__wrapped__'):
original_get_embeddings = llama_openai.get_embeddings.__wrapped__
else:
logger.warning('Cannot set custom retry limits.') # warn
logger.warning('Cannot set custom retry limits.')
num_retries = 1
original_get_embeddings = llama_openai.get_embeddings
@ -59,63 +58,61 @@ def wrapper_get_embeddings(*args, **kwargs):
llama_openai.get_embeddings = wrapper_get_embeddings
embedding_strategy = config.get(ConfigType.LLM_EMBEDDING_MODEL)
# TODO: More embeddings: https://docs.llamaindex.ai/en/stable/examples/embeddings/OpenAI/
# There's probably a more programmatic way to do this.
supported_ollama_embed_models = [
'llama2',
'mxbai-embed-large',
'nomic-embed-text',
'all-minilm',
'stable-code',
]
if embedding_strategy in supported_ollama_embed_models:
from llama_index.embeddings.ollama import OllamaEmbedding
class EmbeddingsLoader:
"""Loader for embedding model initialization."""
embed_model = OllamaEmbedding(
model_name=embedding_strategy,
base_url=config.get(ConfigType.LLM_EMBEDDING_BASE_URL, required=True),
ollama_additional_kwargs={'mirostat': 0},
)
elif embedding_strategy == 'openai':
from llama_index.embeddings.openai import OpenAIEmbedding
@staticmethod
def get_embedding_model(strategy: str):
supported_ollama_embed_models = [
'llama2',
'mxbai-embed-large',
'nomic-embed-text',
'all-minilm',
'stable-code',
]
if strategy in supported_ollama_embed_models:
from llama_index.embeddings.ollama import OllamaEmbedding
embed_model = OpenAIEmbedding(
model='text-embedding-ada-002',
api_key=config.get(ConfigType.LLM_API_KEY, required=True),
)
elif embedding_strategy == 'azureopenai':
# Need to instruct to set these env variables in documentation
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
return OllamaEmbedding(
model_name=strategy,
base_url=config.llm.embedding_base_url,
ollama_additional_kwargs={'mirostat': 0},
)
elif strategy == 'openai':
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = AzureOpenAIEmbedding(
model='text-embedding-ada-002',
deployment_name=config.get(
ConfigType.LLM_EMBEDDING_DEPLOYMENT_NAME, required=True
),
api_key=config.get(ConfigType.LLM_API_KEY, required=True),
azure_endpoint=config.get(ConfigType.LLM_BASE_URL, required=True),
api_version=config.get(ConfigType.LLM_API_VERSION, required=True),
)
elif (embedding_strategy is not None) and (embedding_strategy.lower() == 'none'):
# TODO: this works but is not elegant enough. The incentive is when
# monologue agent is not used, there is no reason we need to initialize an
# embedding model
embed_model = None
else:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
return OpenAIEmbedding(
model='text-embedding-ada-002',
api_key=config.llm.api_key,
)
elif strategy == 'azureopenai':
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
embed_model = HuggingFaceEmbedding(model_name='BAAI/bge-small-en-v1.5')
return AzureOpenAIEmbedding(
model='text-embedding-ada-002',
deployment_name=config.llm.embedding_deployment_name,
api_key=config.llm.api_key,
azure_endpoint=config.llm.base_url,
api_version=config.llm.api_version,
)
elif (strategy is not None) and (strategy.lower() == 'none'):
# TODO: this works but is not elegant enough. The incentive is when
# monologue agent is not used, there is no reason we need to initialize an
# embedding model
return None
else:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
return HuggingFaceEmbedding(model_name='BAAI/bge-small-en-v1.5')
sema = threading.Semaphore(value=config.get(ConfigType.AGENT_MEMORY_MAX_THREADS))
sema = threading.Semaphore(value=config.agent.memory_max_threads)
class LongTermMemory:
"""
Responsible for storing information that the agent can call on later for better insights and context.
Uses chromadb to store and search through memories.
Handles storing information for the agent to access later, using chromadb.
"""
def __init__(self):
@ -125,9 +122,9 @@ class LongTermMemory:
db = chromadb.Client()
self.collection = db.get_or_create_collection(name='memories')
vector_store = ChromaVectorStore(chroma_collection=self.collection)
self.index = VectorStoreIndex.from_vector_store(
vector_store, embed_model=embed_model
)
embedding_strategy = config.llm.embedding_model
embed_model = EmbeddingsLoader.get_embedding_model(embedding_strategy)
self.index = VectorStoreIndex.from_vector_store(vector_store, embed_model)
self.thought_idx = 0
self._add_threads = []

View File

@ -2,9 +2,8 @@ import re
from json import JSONDecodeError
from typing import List
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.exceptions import LLMOutputError
from opendevin.core.schema.config import ConfigType
from opendevin.events.action import (
Action,
action_from_dict,
@ -149,7 +148,7 @@ def get_request_action_prompt(
)
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
user = 'opendevin' if config.get(ConfigType.RUN_AS_DEVIN) else 'root'
user = 'opendevin' if config.run_as_devin else 'root'
return ACTION_PROMPT % {
'task': task,
@ -157,10 +156,8 @@ def get_request_action_prompt(
'background_commands': bg_commands_message,
'hint': hint,
'user': user,
'timeout': config.get(ConfigType.SANDBOX_TIMEOUT),
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.get(
ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX
),
'timeout': config.sandbox_timeout,
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
}

View File

@ -2,7 +2,7 @@ import argparse
import pytest
from opendevin import config
from opendevin.config import config
if __name__ == '__main__':
"""Main entry point of the script.

View File

@ -1,7 +1,6 @@
from typing import List
from opendevin.core import config
from opendevin.core.schema import ConfigType
from opendevin.core.config import config
from opendevin.events.action import (
Action,
)
@ -28,19 +27,19 @@ class ActionManager:
self,
sid: str = 'default',
):
sandbox_type = config.get(ConfigType.SANDBOX_TYPE).lower()
sandbox_type = config.sandbox_type.lower()
if sandbox_type == 'exec':
self.sandbox = DockerExecBox(
sid=(sid or 'default'), timeout=config.get(ConfigType.SANDBOX_TIMEOUT)
sid=(sid or 'default'), timeout=config.sandbox_timeout
)
elif sandbox_type == 'local':
self.sandbox = LocalBox(timeout=config.get(ConfigType.SANDBOX_TIMEOUT))
self.sandbox = LocalBox(timeout=config.sandbox_timeout)
elif sandbox_type == 'ssh':
self.sandbox = DockerSSHBox(
sid=(sid or 'default'), timeout=config.get(ConfigType.SANDBOX_TIMEOUT)
sid=(sid or 'default'), timeout=config.sandbox_timeout
)
elif sandbox_type == 'e2b':
self.sandbox = E2BBox(timeout=config.get(ConfigType.SANDBOX_TIMEOUT))
self.sandbox = E2BBox(timeout=config.sandbox_timeout)
else:
raise ValueError(f'Invalid sandbox type: {sandbox_type}')

View File

@ -6,7 +6,7 @@ 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
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.exceptions import (
AgentMalformedActionError,
AgentNoActionError,
@ -15,7 +15,6 @@ from opendevin.core.exceptions import (
)
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import AgentState
from opendevin.core.schema.config import ConfigType
from opendevin.events.action import (
Action,
AgentDelegateAction,
@ -37,8 +36,8 @@ from opendevin.events.stream import EventSource, EventStream, EventStreamSubscri
from opendevin.runtime import DockerSSHBox
from opendevin.runtime.browser.browser_env import BrowserEnv
MAX_ITERATIONS = config.get(ConfigType.MAX_ITERATIONS)
MAX_CHARS = config.get(ConfigType.MAX_CHARS)
MAX_ITERATIONS = config.max_iterations
MAX_CHARS = config.llm.max_chars
class AgentController:

View File

@ -3,126 +3,323 @@ import logging
import os
import pathlib
import platform
from dataclasses import dataclass, field, fields, is_dataclass
from types import UnionType
from typing import Any, ClassVar, get_args, get_origin
import toml
from dotenv import load_dotenv
from opendevin.core.schema import ConfigType
from opendevin.core.utils import Singleton
logger = logging.getLogger(__name__)
DEFAULT_CONTAINER_IMAGE = 'ghcr.io/opendevin/sandbox'
if os.getenv('OPEN_DEVIN_BUILD_VERSION'):
DEFAULT_CONTAINER_IMAGE += ':' + (os.getenv('OPEN_DEVIN_BUILD_VERSION') or '')
else:
DEFAULT_CONTAINER_IMAGE += ':main'
load_dotenv()
DEFAULT_CONFIG: dict = {
ConfigType.LLM_API_KEY: None,
ConfigType.LLM_BASE_URL: None,
ConfigType.LLM_CUSTOM_LLM_PROVIDER: None,
ConfigType.AWS_ACCESS_KEY_ID: None,
ConfigType.AWS_SECRET_ACCESS_KEY: None,
ConfigType.AWS_REGION_NAME: None,
ConfigType.WORKSPACE_BASE: os.getcwd(),
ConfigType.WORKSPACE_MOUNT_PATH: None,
ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX: '/workspace',
ConfigType.WORKSPACE_MOUNT_REWRITE: None,
ConfigType.CACHE_DIR: '/tmp/cache', # '/tmp/cache' is the default cache directory
ConfigType.LLM_MODEL: 'gpt-3.5-turbo-1106',
ConfigType.SANDBOX_CONTAINER_IMAGE: DEFAULT_CONTAINER_IMAGE,
ConfigType.RUN_AS_DEVIN: True,
ConfigType.LLM_EMBEDDING_MODEL: 'local',
ConfigType.LLM_EMBEDDING_BASE_URL: None,
ConfigType.LLM_EMBEDDING_DEPLOYMENT_NAME: None,
ConfigType.LLM_API_VERSION: None,
ConfigType.LLM_NUM_RETRIES: 5,
ConfigType.LLM_RETRY_MIN_WAIT: 3,
ConfigType.LLM_RETRY_MAX_WAIT: 60,
ConfigType.MAX_ITERATIONS: 100,
ConfigType.LLM_MAX_INPUT_TOKENS: None,
ConfigType.LLM_MAX_OUTPUT_TOKENS: None,
ConfigType.AGENT_MEMORY_MAX_THREADS: 2,
ConfigType.AGENT_MEMORY_ENABLED: False,
ConfigType.LLM_TIMEOUT: None,
ConfigType.LLM_TEMPERATURE: None,
ConfigType.LLM_TOP_P: None,
# GPT-4 pricing is $10 per 1M input tokens. Since tokenization happens on LLM side,
# we cannot easily count number of tokens, but we can count characters.
# Assuming 5 characters per token, 5 million is a reasonable default limit.
ConfigType.MAX_CHARS: 5_000_000,
ConfigType.AGENT: 'CodeActAgent',
ConfigType.E2B_API_KEY: '',
ConfigType.SANDBOX_TYPE: 'ssh', # Can be 'ssh', 'exec', or 'e2b'
ConfigType.USE_HOST_NETWORK: False,
ConfigType.SSH_HOSTNAME: 'localhost',
ConfigType.DISABLE_COLOR: False,
ConfigType.SANDBOX_USER_ID: os.getuid() if hasattr(os, 'getuid') else None,
ConfigType.SANDBOX_TIMEOUT: 120,
ConfigType.GITHUB_TOKEN: None,
ConfigType.SANDBOX_USER_ID: None,
ConfigType.DEBUG: False,
}
config_str = ''
if os.path.exists('config.toml'):
with open('config.toml', 'rb') as f:
config_str = f.read().decode('utf-8')
@dataclass
class LLMConfig(metaclass=Singleton):
model: str = 'gpt-3.5-turbo-1106'
api_key: str | None = None
base_url: str | None = None
api_version: str | None = None
embedding_model: str = 'local'
embedding_base_url: str | None = None
embedding_deployment_name: str | None = None
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
aws_region_name: str | None = None
num_retries: int = 5
retry_min_wait: int = 3
retry_max_wait: int = 60
timeout: int | None = None
max_chars: int = 5_000_000 # fallback for token counting
temperature: float = 0
top_p: float = 0.5
custom_llm_provider: str | None = None
max_input_tokens: int | None = None
max_output_tokens: int | None = None
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_to_bool(value):
if isinstance(value, bool):
return value
return value.lower() in [
'true',
'1',
]
@dataclass
class AgentConfig(metaclass=Singleton):
name: str = 'CodeActAgent'
memory_enabled: bool = False
memory_max_threads: int = 2
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 int_value(value, default, config_key):
# FIXME use a library
@dataclass
class AppConfig(metaclass=Singleton):
llm: LLMConfig = field(default_factory=LLMConfig)
agent: AgentConfig = field(default_factory=AgentConfig)
workspace_base: str = os.getcwd()
workspace_mount_path: str = os.getcwd()
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
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
github_token: str | None = None
debug: bool = False
defaults_dict: ClassVar[dict] = {}
def __post_init__(self):
"""
Post-initialization hook, called when the instance is created with only default values.
"""
AppConfig.defaults_dict = self.defaults_to_dict()
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):
field_value = getattr(self, f.name)
# dataclasses compute their defaults themselves
if is_dataclass(type(field_value)):
dict[f.name] = field_value.defaults_to_dict()
else:
dict[f.name] = get_field_info(f)
return dict
def get_field_info(field):
"""
Extract information about a dataclass field: type, optional, and default.
Args:
field: The field to extract information from.
Returns: A dict with the field's type, whether it's optional, and its default value.
"""
field_type = field.type
optional = False
# for types like str | None, find the non-None type and set optional to True
# this is useful for the frontend to know if a field is optional
# and to show the correct type in the UI
# Note: this only works for UnionTypes with None as one of the types
if get_origin(field_type) is UnionType:
types = get_args(field_type)
non_none_arg = next((t for t in types if t is not type(None)), None)
if non_none_arg is not None:
field_type = non_none_arg
optional = True
# type name in a pretty format
type_name = (
field_type.__name__ if hasattr(field_type, '__name__') else str(field_type)
)
# default is always present
default = field.default
# return a schema with the useful info for frontend
return {'type': type_name.lower(), 'optional': optional, 'default': default}
def load_from_env(config: AppConfig, env_or_toml_dict: dict | os._Environ):
"""Reads the env-style vars and sets config attributes based on env vars or a config.toml dict.
Compatibility with vars like LLM_BASE_URL, AGENT_MEMORY_ENABLED and others.
Args:
config: The AppConfig object to set attributes on.
env_or_toml_dict: The environment variables or a config.toml dict.
"""
def get_optional_type(union_type: UnionType) -> Any:
"""Returns the non-None type from an Union."""
types = get_args(union_type)
return next((t for t in types if t is not type(None)), None)
# helper function to set attributes based on env vars
def set_attr_from_env(sub_config: Any, prefix=''):
"""Set attributes of a config dataclass based on environment variables."""
for field_name, field_type in sub_config.__annotations__.items():
# compute the expected env var name from the prefix and field name
# e.g. LLM_BASE_URL
env_var_name = (prefix + field_name).upper()
if is_dataclass(field_type):
# nested dataclass
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:
setattr(nested_sub_config, 'name', env_or_toml_dict[env_var_name])
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
value = env_or_toml_dict[env_var_name]
try:
# if it's an optional type, get the non-None type
if get_origin(field_type) is UnionType:
field_type = get_optional_type(field_type)
# Attempt to cast the env var to type hinted in the dataclass
cast_value = field_type(value)
setattr(sub_config, field_name, cast_value)
except (ValueError, TypeError):
logger.error(
f'Error setting env var {env_var_name}={value}: check that the value is of the right type'
)
# Start processing from the root of the config object
set_attr_from_env(config)
def load_from_toml(config: AppConfig, toml_file: str = 'config.toml'):
"""Load the config from the toml file. Supports both styles of config vars.
Args:
config: The AppConfig object to update attributes of.
"""
# try to read the config.toml file into the config object
toml_config = {}
try:
return int(value)
except ValueError:
with open(toml_file, 'r', encoding='utf-8') as toml_contents:
toml_config = toml.load(toml_contents)
except FileNotFoundError:
# the file is optional, we don't need to do anything
return
except toml.TomlDecodeError:
logger.warning(
f'Invalid value for {config_key}: {value} not applied. Using default value {default}'
'Cannot parse config from toml, toml values have not been applied.',
exc_info=False,
)
return
# if there was an exception or core is not in the toml, try to use the old-style toml
if 'core' not in toml_config:
# re-use the env loader to set the config from env-style vars
load_from_env(config, toml_config)
return
core_config = toml_config['core']
try:
# set llm config from the toml file
llm_config = config.llm
if 'llm' in toml_config:
llm_config = LLMConfig(**toml_config['llm'])
# set agent config from the toml file
agent_config = config.agent
if 'agent' in toml_config:
agent_config = AgentConfig(**toml_config['agent'])
# update the config object with the new values
config = AppConfig(llm=llm_config, agent=agent_config, **core_config)
except (TypeError, KeyError):
logger.warning(
'Cannot parse config from toml, toml values have not been applied.',
exc_info=False,
)
return default
tomlConfig = toml.loads(config_str)
config = DEFAULT_CONFIG.copy()
for k, v in config.items():
if k in os.environ:
config[k] = os.environ[k]
elif k in tomlConfig:
config[k] = tomlConfig[k]
def finalize_config(config: AppConfig):
"""
More tweaks to the config after it's been loaded.
"""
if k in [
ConfigType.LLM_NUM_RETRIES,
ConfigType.LLM_RETRY_MIN_WAIT,
ConfigType.LLM_RETRY_MAX_WAIT,
]:
config[k] = int_value(config[k], v, config_key=k)
# In local there is no sandbox, the workspace will have the same pwd as the host
if config.sandbox_type == 'local':
config.workspace_mount_path_in_sandbox = config.workspace_mount_path
if k in [
ConfigType.RUN_AS_DEVIN,
ConfigType.USE_HOST_NETWORK,
ConfigType.AGENT_MEMORY_ENABLED,
ConfigType.DISABLE_COLOR,
ConfigType.DEBUG,
]:
config[k] = str_to_bool(config[k])
# In local there is no sandbox, the workspace will have the same pwd as the host
if config[ConfigType.SANDBOX_TYPE] == 'local':
config[ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX] = config[
ConfigType.WORKSPACE_MOUNT_PATH
]
if config.workspace_mount_rewrite: # and not config.workspace_mount_path:
# TODO why do we need to check if workspace_mount_path is None?
base = config.workspace_base or os.getcwd()
parts = config.workspace_mount_rewrite.split(':')
config.workspace_mount_path = base.replace(parts[0], parts[1])
if config.llm.embedding_base_url is None:
config.llm.embedding_base_url = config.llm.base_url
if config.use_host_network and platform.system() == 'Darwin':
logger.warning(
'Please upgrade to Docker Desktop 4.29.0 or later to use host network mode on macOS. '
'See https://github.com/docker/roadmap/issues/238#issuecomment-2044688144 for more information.'
)
# make sure cache dir exists
if config.cache_dir:
pathlib.Path(config.cache_dir).mkdir(parents=True, exist_ok=True)
config = AppConfig()
load_from_toml(config)
load_from_env(config, os.environ)
finalize_config(config)
# Utility function for command line --group argument
def get_llm_config_arg(llm_config_arg: str):
"""
Get a group of llm settings from the config file.
"""
# keep only the name, just in case
llm_config_arg = llm_config_arg.strip('[]')
logger.info(f'Loading llm config from {llm_config_arg}')
# load the toml file
try:
with open('config.toml', 'r', encoding='utf-8') as toml_file:
toml_config = toml.load(toml_file)
except FileNotFoundError:
return None
except toml.TomlDecodeError as e:
logger.error(f'Cannot parse llm group from {llm_config_arg}. Exception: {e}')
return None
# update the llm config with the specified section
if llm_config_arg in toml_config:
return LLMConfig(**toml_config[llm_config_arg])
logger.debug(f'Loading from toml failed for {llm_config_arg}')
return None
# Command line arguments
def get_parser():
"""
Get the parser for the command line arguments.
"""
parser = argparse.ArgumentParser(description='Run an agent with a specific task')
parser.add_argument(
'-d',
@ -142,89 +339,51 @@ def get_parser():
parser.add_argument(
'-c',
'--agent-cls',
default=config.get(ConfigType.AGENT),
default=config.agent.name,
type=str,
help='The agent class to use',
)
parser.add_argument(
'-m',
'--model-name',
default=config.get(ConfigType.LLM_MODEL),
default=config.llm.model,
type=str,
help='The (litellm) model name to use',
)
parser.add_argument(
'-i',
'--max-iterations',
default=config.get(ConfigType.MAX_ITERATIONS),
default=config.max_iterations,
type=int,
help='The maximum number of iterations to run the agent',
)
parser.add_argument(
'-n',
'--max-chars',
default=config.get(ConfigType.MAX_CHARS),
default=config.llm.max_chars,
type=int,
help='The maximum number of characters to send to and receive from LLM per task',
)
parser.add_argument(
'-l',
'--llm-config',
default=None,
type=str,
help='The group of llm settings, e.g. a [llama3] section in the toml file. Overrides model if both are provided.',
)
return parser
def parse_arguments():
"""
Parse the command line arguments.
"""
parser = get_parser()
args, _ = parser.parse_known_args()
if args.directory:
config[ConfigType.WORKSPACE_BASE] = os.path.abspath(args.directory)
print(f'Setting workspace base to {config[ConfigType.WORKSPACE_BASE]}')
config.workspace_base = os.path.abspath(args.directory)
print(f'Setting workspace base to {config.workspace_base}')
return args
args = parse_arguments()
def finalize_config():
if config.get(ConfigType.WORKSPACE_MOUNT_REWRITE) and not config.get(
ConfigType.WORKSPACE_MOUNT_PATH
):
base = config.get(ConfigType.WORKSPACE_BASE) or os.getcwd()
parts = config[ConfigType.WORKSPACE_MOUNT_REWRITE].split(':')
config[ConfigType.WORKSPACE_MOUNT_PATH] = base.replace(parts[0], parts[1])
if config.get(ConfigType.WORKSPACE_MOUNT_PATH) is None:
config[ConfigType.WORKSPACE_MOUNT_PATH] = os.path.abspath(
config[ConfigType.WORKSPACE_BASE]
)
if config.get(ConfigType.LLM_EMBEDDING_BASE_URL) is None:
config[ConfigType.LLM_EMBEDDING_BASE_URL] = config.get(ConfigType.LLM_BASE_URL)
USE_HOST_NETWORK = config[ConfigType.USE_HOST_NETWORK]
if USE_HOST_NETWORK and platform.system() == 'Darwin':
logger.warning(
'Please upgrade to Docker Desktop 4.29.0 or later to use host network mode on macOS. '
'See https://github.com/docker/roadmap/issues/238#issuecomment-2044688144 for more information.'
)
config[ConfigType.USE_HOST_NETWORK] = USE_HOST_NETWORK
if config.get(ConfigType.WORKSPACE_MOUNT_PATH) is None:
config[ConfigType.WORKSPACE_MOUNT_PATH] = config.get(ConfigType.WORKSPACE_BASE)
finalize_config()
def get(key: ConfigType, required: bool = False):
"""
Get a key from the environment variables or config.toml or default configs.
"""
if not isinstance(key, ConfigType):
raise ValueError(f"key '{key}' must be an instance of ConfigType Enum")
value = config.get(key)
if not value and required:
raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.")
return value
_cache_dir = config.get(ConfigType.CACHE_DIR)
if _cache_dir:
pathlib.Path(_cache_dir).mkdir(parents=True, exist_ok=True)

View File

@ -7,10 +7,9 @@ from typing import Literal, Mapping
from termcolor import colored
from opendevin.core import config
from opendevin.core.schema.config import ConfigType
from opendevin.core.config import config
DISABLE_COLOR_PRINTING = config.get(ConfigType.DISABLE_COLOR)
DISABLE_COLOR_PRINTING = config.disable_color
ColorType = Literal[
'red',
@ -91,7 +90,7 @@ def get_file_handler():
timestamp = datetime.now().strftime('%Y-%m-%d')
file_name = f'opendevin_{timestamp}.log'
file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
if config.get(ConfigType.DEBUG):
if config.debug:
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
return file_handler
@ -197,12 +196,12 @@ def get_llm_response_file_handler():
llm_prompt_logger = logging.getLogger('prompt')
llm_prompt_logger.propagate = False
if config.get(ConfigType.DEBUG):
if config.debug:
llm_prompt_logger.setLevel(logging.DEBUG)
llm_prompt_logger.addHandler(get_llm_prompt_file_handler())
llm_response_logger = logging.getLogger('response')
llm_response_logger.propagate = False
if config.get(ConfigType.DEBUG):
if config.debug:
llm_response_logger.setLevel(logging.DEBUG)
llm_response_logger.addHandler(get_llm_response_file_handler())

View File

@ -5,7 +5,7 @@ from typing import Type
import agenthub # noqa F401 (we import this to get the agents registered)
from opendevin.controller import AgentController
from opendevin.controller.agent import Agent
from opendevin.core.config import args
from opendevin.core.config import args, get_llm_config_arg
from opendevin.core.schema import AgentState
from opendevin.events.action import ChangeAgentStateAction, MessageAction
from opendevin.events.event import Event
@ -40,12 +40,31 @@ async def main(task_str: str = ''):
else:
raise ValueError('No task provided. Please specify a task through -t, -f.')
print(
f'Running agent {args.agent_cls} (model: {args.model_name}) with task: "{task}"'
)
llm = LLM(args.model_name)
# only one of model_name or llm_config is required
if args.llm_config:
# --llm_config
# llm_config can contain any of the attributes of LLMConfig
llm_config = get_llm_config_arg(args.llm_config)
if llm_config is None:
raise ValueError(f'Invalid toml file, cannot read {args.llm_config}')
print(
f'Running agent {args.agent_cls} (model: {llm_config.model}, llm_config: {llm_config}) with task: "{task}"'
)
# create LLM instance with the given config
llm = LLM(llm_config=llm_config)
else:
# --model-name model_name
print(
f'Running agent {args.agent_cls} (model: {args.model_name}), with task: "{task}"'
)
llm = LLM(args.model_name)
AgentCls: Type[Agent] = Agent.get_cls(args.agent_cls)
agent = AgentCls(llm=llm)
event_stream = EventStream()
controller = AgentController(
agent=agent,

View File

@ -0,0 +1,3 @@
from .singleton import Singleton
__all__ = ['Singleton']

View File

@ -0,0 +1,28 @@
import dataclasses
class Singleton(type):
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
else:
# allow updates, just update existing instance
# perhaps not the most orthodox way to do it, though it simplifies client code
# useful for pre-defined groups of settings
instance = cls._instances[cls]
for key, value in kwargs.items():
setattr(instance, key, value)
return cls._instances[cls]
@classmethod
def reset(cls):
# used by pytest to reset the state of the singleton instances
for instance_type, instance in cls._instances.items():
print('resetting... ', instance_type)
for field in dataclasses.fields(instance_type):
if dataclasses.is_dataclass(field.type):
setattr(instance, field.name, field.type())
else:
setattr(instance, field.name, field.default)

View File

@ -3,8 +3,8 @@ import pathlib
from dataclasses import dataclass
from typing import TYPE_CHECKING
from opendevin.core import config
from opendevin.core.schema import ActionType, ConfigType
from opendevin.core.config import config
from opendevin.core.schema import ActionType
from .action import Action
@ -64,14 +64,14 @@ class IPythonRunCellAction(Action):
# 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.get(ConfigType.WORKSPACE_BASE), '.tmp', '.ipython_execution_tmp.py'
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.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX),
config.workspace_mount_path_in_sandbox,
'.tmp',
'.ipython_execution_tmp.py',
)

View File

@ -2,9 +2,8 @@ import os
from dataclasses import dataclass
from pathlib import Path
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.schema import ActionType
from opendevin.core.schema.config import ConfigType
from opendevin.events.observation import (
ErrorObservation,
FileReadObservation,
@ -28,20 +27,16 @@ def resolve_path(file_path, working_directory):
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.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)
):
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.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX))
Path(config.workspace_mount_path_in_sandbox)
)
# Get path relative to host
path_in_host_workspace = (
Path(config.get(ConfigType.WORKSPACE_BASE)) / path_in_workspace
)
path_in_host_workspace = Path(config.workspace_base) / path_in_workspace
return path_in_host_workspace

View File

@ -5,9 +5,8 @@ from typing import TYPE_CHECKING
import requests
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.schema import ActionType
from opendevin.core.schema.config import ConfigType
from opendevin.events.observation import (
CmdOutputObservation,
ErrorObservation,
@ -42,9 +41,9 @@ class GitHubPushAction(Action):
action: str = ActionType.PUSH
async def run(self, controller: 'AgentController') -> Observation:
github_token = config.get(ConfigType.GITHUB_TOKEN)
github_token = config.github_token
if not github_token:
return ErrorObservation('GITHUB_TOKEN is not set')
return ErrorObservation('github_token is not set')
# Create a random short string to use as a temporary remote
random_remote = ''.join(
@ -111,9 +110,9 @@ class GitHubSendPRAction(Action):
action: str = ActionType.SEND_PR
async def run(self, controller: 'AgentController') -> Observation:
github_token = config.get(ConfigType.GITHUB_TOKEN)
github_token = config.github_token
if not github_token:
return ErrorObservation('GITHUB_TOKEN is not set')
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'

View File

@ -2,21 +2,20 @@ import os
import boto3
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ConfigType
AWS_ACCESS_KEY_ID = config.get(ConfigType.AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY = config.get(ConfigType.AWS_SECRET_ACCESS_KEY)
AWS_REGION_NAME = config.get(ConfigType.AWS_REGION_NAME)
AWS_ACCESS_KEY_ID = config.llm.aws_access_key_id
AWS_SECRET_ACCESS_KEY = config.llm.aws_secret_access_key
AWS_REGION_NAME = config.llm.aws_region_name
# It needs to be set as an environment variable, if the variable is configured in the Config file.
if AWS_ACCESS_KEY_ID is not None:
os.environ[ConfigType.AWS_ACCESS_KEY_ID] = AWS_ACCESS_KEY_ID
os.environ['AWS_ACCESS_KEY_ID'] = AWS_ACCESS_KEY_ID
if AWS_SECRET_ACCESS_KEY is not None:
os.environ[ConfigType.AWS_SECRET_ACCESS_KEY] = AWS_SECRET_ACCESS_KEY
os.environ['AWS_SECRET_ACCESS_KEY'] = AWS_SECRET_ACCESS_KEY
if AWS_REGION_NAME is not None:
os.environ[ConfigType.AWS_REGION_NAME] = AWS_REGION_NAME
os.environ['AWS_REGION_NAME'] = AWS_REGION_NAME
def list_foundation_models():

View File

@ -15,50 +15,50 @@ from tenacity import (
wait_random_exponential,
)
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import llm_prompt_logger, llm_response_logger
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ConfigType
__all__ = ['LLM', 'completion_cost']
DEFAULT_API_KEY = config.get(ConfigType.LLM_API_KEY)
DEFAULT_BASE_URL = config.get(ConfigType.LLM_BASE_URL)
DEFAULT_MODEL_NAME = config.get(ConfigType.LLM_MODEL)
DEFAULT_API_VERSION = config.get(ConfigType.LLM_API_VERSION)
LLM_NUM_RETRIES = config.get(ConfigType.LLM_NUM_RETRIES)
LLM_RETRY_MIN_WAIT = config.get(ConfigType.LLM_RETRY_MIN_WAIT)
LLM_RETRY_MAX_WAIT = config.get(ConfigType.LLM_RETRY_MAX_WAIT)
LLM_MAX_INPUT_TOKENS = config.get(ConfigType.LLM_MAX_INPUT_TOKENS)
LLM_MAX_OUTPUT_TOKENS = config.get(ConfigType.LLM_MAX_OUTPUT_TOKENS)
LLM_CUSTOM_LLM_PROVIDER = config.get(ConfigType.LLM_CUSTOM_LLM_PROVIDER)
LLM_TIMEOUT = config.get(ConfigType.LLM_TIMEOUT)
LLM_TEMPERATURE = config.get(ConfigType.LLM_TEMPERATURE)
LLM_TOP_P = config.get(ConfigType.LLM_TOP_P)
class LLM:
"""
The LLM class represents a Language Model instance.
Attributes:
model_name (str): The name of the language model.
api_key (str): The API key for accessing the language model.
base_url (str): The base URL for the language model API.
api_version (str): The version of the API to use.
max_input_tokens (int): The maximum number of tokens to send to the LLM per task.
max_output_tokens (int): The maximum number of tokens to receive from the LLM per task.
llm_timeout (int): The maximum time to wait for a response in seconds.
custom_llm_provider (str): A custom LLM provider.
"""
def __init__(
self,
model=DEFAULT_MODEL_NAME,
api_key=DEFAULT_API_KEY,
base_url=DEFAULT_BASE_URL,
api_version=DEFAULT_API_VERSION,
num_retries=LLM_NUM_RETRIES,
retry_min_wait=LLM_RETRY_MIN_WAIT,
retry_max_wait=LLM_RETRY_MAX_WAIT,
max_input_tokens=LLM_MAX_INPUT_TOKENS,
max_output_tokens=LLM_MAX_OUTPUT_TOKENS,
custom_llm_provider=LLM_CUSTOM_LLM_PROVIDER,
llm_timeout=LLM_TIMEOUT,
llm_temperature=LLM_TEMPERATURE,
llm_top_p=LLM_TOP_P,
model=None,
api_key=None,
base_url=None,
api_version=None,
num_retries=None,
retry_min_wait=None,
retry_max_wait=None,
llm_timeout=None,
llm_temperature=None,
llm_top_p=None,
custom_llm_provider=None,
max_input_tokens=None,
max_output_tokens=None,
llm_config=None,
):
"""
Initializes the LLM. If LLMConfig is passed, its values will be the fallback.
Passing simple parameters always overrides config.
Args:
model (str, optional): The name of the language model. Defaults to LLM_MODEL.
api_key (str, optional): The API key for accessing the language model. Defaults to LLM_API_KEY.
@ -73,12 +73,41 @@ class LLM:
llm_timeout (int, optional): The maximum time to wait for a response in seconds. Defaults to LLM_TIMEOUT.
llm_temperature (float, optional): The temperature for LLM sampling. Defaults to LLM_TEMPERATURE.
Attributes:
model_name (str): The name of the language model.
api_key (str): The API key for accessing the language model.
base_url (str): The base URL for the language model API.
api_version (str): The version of the API to use.
"""
if llm_config is None:
llm_config = config.llm
model = model if model is not None else llm_config.model
api_key = api_key if api_key is not None else llm_config.api_key
base_url = base_url if base_url is not None else llm_config.base_url
api_version = api_version if api_version is not None else llm_config.api_version
num_retries = num_retries if num_retries is not None else llm_config.num_retries
retry_min_wait = (
retry_min_wait if retry_min_wait is not None else llm_config.retry_min_wait
)
retry_max_wait = (
retry_max_wait if retry_max_wait is not None else llm_config.retry_max_wait
)
llm_timeout = llm_timeout if llm_timeout is not None else llm_config.timeout
llm_temperature = (
llm_temperature if llm_temperature is not None else llm_config.temperature
)
llm_top_p = llm_top_p if llm_top_p is not None else llm_config.top_p
custom_llm_provider = (
custom_llm_provider
if custom_llm_provider is not None
else llm_config.custom_llm_provider
)
max_input_tokens = (
max_input_tokens
if max_input_tokens is not None
else llm_config.max_input_tokens
)
max_output_tokens = (
max_output_tokens
if max_output_tokens is not None
else llm_config.max_output_tokens
)
logger.info(f'Initializing LLM with model: {model}')
self.model_name = model
self.api_key = api_key

View File

@ -12,28 +12,16 @@ from typing import Dict, List, Tuple
import docker
from opendevin.const.guide_url import TROUBLESHOOTING_URL
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.exceptions import SandboxInvalidBackgroundCommandError
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ConfigType
from opendevin.runtime.docker.process import DockerProcess, Process
from opendevin.runtime.sandbox import Sandbox
# FIXME these are not used, should we remove them?
InputType = namedtuple('InputType', ['content'])
OutputType = namedtuple('OutputType', ['content'])
CONTAINER_IMAGE = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE)
SANDBOX_WORKSPACE_DIR = config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
# How do we make this more flexible?
RUN_AS_DEVIN = config.get(ConfigType.RUN_AS_DEVIN)
USER_ID = 1000
if SANDBOX_USER_ID := config.get(ConfigType.SANDBOX_USER_ID):
USER_ID = int(SANDBOX_USER_ID)
elif hasattr(os, 'getuid'):
USER_ID = os.getuid()
class DockerExecBox(Sandbox):
instance_id: str
@ -72,35 +60,43 @@ class DockerExecBox(Sandbox):
# if it is too long, the user may have to wait for a unnecessary long time
self.timeout = timeout
self.container_image = (
CONTAINER_IMAGE if container_image is None else container_image
config.sandbox_container_image
if container_image is None
else container_image
)
self.container_name = self.container_name_prefix + self.instance_id
logger.info(
'Starting Docker container with image %s, sandbox workspace dir=%s',
self.container_image,
self.sandbox_workspace_dir,
)
# always restart the container, cuz the initial be regarded as a new session
self.restart_docker_container()
if RUN_AS_DEVIN:
if self.run_as_devin:
self.setup_devin_user()
atexit.register(self.close)
super().__init__()
def setup_devin_user(self):
cmds = [
f'useradd --shell /bin/bash -u {USER_ID} -o -c "" -m devin',
f'useradd --shell /bin/bash -u {self.user_id} -o -c "" -m devin',
r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers",
'sudo adduser devin sudo',
]
for cmd in cmds:
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', cmd],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
raise Exception(f'Failed to setup devin user: {logs}')
def get_exec_cmd(self, cmd: str) -> List[str]:
if RUN_AS_DEVIN:
if self.run_as_devin:
return ['su', 'devin', '-c', cmd]
else:
return ['/bin/bash', '-c', cmd]
@ -115,7 +111,7 @@ class DockerExecBox(Sandbox):
# TODO: each execute is not stateful! We need to keep track of the current working directory
def run_command(container, command):
return container.exec_run(
command, workdir=SANDBOX_WORKSPACE_DIR, environment=self._env
command, workdir=self.sandbox_workspace_dir, environment=self._env
)
# Use ThreadPoolExecutor to control command and set timeout
@ -133,7 +129,7 @@ class DockerExecBox(Sandbox):
if pid is not None:
self.container.exec_run(
f'kill -9 {pid}',
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
return -1, f'Command: "{cmd}" timed out'
@ -146,7 +142,7 @@ class DockerExecBox(Sandbox):
# mkdir -p sandbox_dest if it doesn't exist
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', f'mkdir -p {sandbox_dest}'],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
@ -185,7 +181,7 @@ class DockerExecBox(Sandbox):
result = self.container.exec_run(
self.get_exec_cmd(cmd),
socket=True,
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
result.output._sock.setblocking(0)
@ -213,7 +209,7 @@ class DockerExecBox(Sandbox):
if bg_cmd.pid is not None:
self.container.exec_run(
f'kill -9 {bg_cmd.pid}',
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
assert isinstance(bg_cmd, DockerProcess)
@ -256,15 +252,15 @@ class DockerExecBox(Sandbox):
try:
# start the container
mount_dir = config.get(ConfigType.WORKSPACE_MOUNT_PATH)
mount_dir = config.workspace_mount_path
self.container = self.docker_client.containers.run(
self.container_image,
command='tail -f /dev/null',
network_mode='host',
working_dir=SANDBOX_WORKSPACE_DIR,
working_dir=self.sandbox_workspace_dir,
name=self.container_name,
detach=True,
volumes={mount_dir: {'bind': SANDBOX_WORKSPACE_DIR, 'mode': 'rw'}},
volumes={mount_dir: {'bind': self.sandbox_workspace_dir, 'mode': 'rw'}},
)
logger.info('Container started')
except Exception as ex:
@ -298,7 +294,21 @@ class DockerExecBox(Sandbox):
pass
def get_working_directory(self):
return SANDBOX_WORKSPACE_DIR
return self.sandbox_workspace_dir
@property
def user_id(self):
return config.sandbox_user_id
@property
def run_as_devin(self):
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
# How do we make this more flexible?
return config.run_as_devin
@property
def sandbox_workspace_dir(self):
return config.workspace_mount_path_in_sandbox
if __name__ == '__main__':

View File

@ -4,9 +4,8 @@ import subprocess
import sys
from typing import Dict, Tuple
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema.config import ConfigType
from opendevin.runtime.docker.process import DockerProcess, Process
from opendevin.runtime.sandbox import Sandbox
@ -28,7 +27,7 @@ from opendevin.runtime.sandbox import Sandbox
class LocalBox(Sandbox):
def __init__(self, timeout: int = 120):
os.makedirs(config.get(ConfigType.WORKSPACE_BASE), exist_ok=True)
os.makedirs(config.workspace_base, exist_ok=True)
self.timeout = timeout
self.background_commands: Dict[int, Process] = {}
self.cur_background_id = 0
@ -43,7 +42,7 @@ class LocalBox(Sandbox):
text=True,
capture_output=True,
timeout=self.timeout,
cwd=config.get(ConfigType.WORKSPACE_BASE),
cwd=config.workspace_base,
env=self._env,
)
return completed_process.returncode, completed_process.stdout.strip()
@ -56,7 +55,7 @@ class LocalBox(Sandbox):
f'mkdir -p {sandbox_dest}',
shell=True,
text=True,
cwd=config.get(ConfigType.WORKSPACE_BASE),
cwd=config.workspace_base,
env=self._env,
)
if res.returncode != 0:
@ -67,7 +66,7 @@ class LocalBox(Sandbox):
f'cp -r {host_src} {sandbox_dest}',
shell=True,
text=True,
cwd=config.get(ConfigType.WORKSPACE_BASE),
cwd=config.workspace_base,
env=self._env,
)
if res.returncode != 0:
@ -79,7 +78,7 @@ class LocalBox(Sandbox):
f'cp {host_src} {sandbox_dest}',
shell=True,
text=True,
cwd=config.get(ConfigType.WORKSPACE_BASE),
cwd=config.workspace_base,
env=self._env,
)
if res.returncode != 0:
@ -94,7 +93,7 @@ class LocalBox(Sandbox):
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
cwd=config.get(ConfigType.WORKSPACE_BASE),
cwd=config.workspace_base,
)
bg_cmd = DockerProcess(
id=self.cur_background_id, command=cmd, result=process, pid=process.pid
@ -128,7 +127,7 @@ class LocalBox(Sandbox):
self.close()
def get_working_directory(self):
return config.get(ConfigType.WORKSPACE_BASE)
return config.workspace_base
if __name__ == '__main__':

View File

@ -13,10 +13,9 @@ import docker
from pexpect import pxssh
from opendevin.const.guide_url import TROUBLESHOOTING_URL
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.exceptions import SandboxInvalidBackgroundCommandError
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ConfigType
from opendevin.runtime.docker.process import DockerProcess, Process
from opendevin.runtime.plugins import (
JupyterRequirement,
@ -25,26 +24,10 @@ from opendevin.runtime.plugins import (
from opendevin.runtime.sandbox import Sandbox
from opendevin.runtime.utils import find_available_tcp_port
# FIXME: these are not used, can we remove them?
InputType = namedtuple('InputType', ['content'])
OutputType = namedtuple('OutputType', ['content'])
SANDBOX_WORKSPACE_DIR = config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)
CONTAINER_IMAGE = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE)
SSH_HOSTNAME = config.get(ConfigType.SSH_HOSTNAME)
USE_HOST_NETWORK = config.get(ConfigType.USE_HOST_NETWORK)
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
# How do we make this more flexible?
RUN_AS_DEVIN = config.get(ConfigType.RUN_AS_DEVIN)
USER_ID = 1000
if SANDBOX_USER_ID := config.get(ConfigType.SANDBOX_USER_ID):
USER_ID = int(SANDBOX_USER_ID)
elif hasattr(os, 'getuid'):
USER_ID = os.getuid()
class DockerSSHBox(Sandbox):
instance_id: str
@ -67,7 +50,7 @@ class DockerSSHBox(Sandbox):
sid: str | None = None,
):
logger.info(
f'SSHBox is running as {"opendevin" if RUN_AS_DEVIN else "root"} user with USER_ID={USER_ID} in the sandbox'
f'SSHBox is running as {"opendevin" if self.run_as_devin else "root"} user with USER_ID={self.user_id} in the sandbox'
)
# Initialize docker client. Throws an exception if Docker is not reachable.
try:
@ -89,7 +72,9 @@ class DockerSSHBox(Sandbox):
# if it is too long, the user may have to wait for a unnecessary long time
self.timeout = timeout
self.container_image = (
CONTAINER_IMAGE if container_image is None else container_image
config.sandbox_container_image
if container_image is None
else container_image
)
self.container_name = self.container_name_prefix + self.instance_id
@ -115,7 +100,7 @@ class DockerSSHBox(Sandbox):
# TODO(sandbox): add this line in the Dockerfile for next minor version of docker image
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
@ -126,28 +111,28 @@ class DockerSSHBox(Sandbox):
# Check if the opendevin user exists
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', 'id -u opendevin'],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code == 0:
# User exists, delete it
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', 'userdel -r opendevin'],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
raise Exception(f'Failed to remove opendevin user in sandbox: {logs}')
if RUN_AS_DEVIN:
if self.run_as_devin:
# Create the opendevin user
exit_code, logs = self.container.exec_run(
[
'/bin/bash',
'-c',
f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {USER_ID} opendevin',
f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {self.user_id} opendevin',
],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
@ -158,7 +143,7 @@ class DockerSSHBox(Sandbox):
'-c',
f"echo 'opendevin:{self._ssh_password}' | chpasswd",
],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
@ -167,7 +152,7 @@ class DockerSSHBox(Sandbox):
# chown the home directory
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', 'chown opendevin:root /home/opendevin'],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
@ -175,35 +160,39 @@ class DockerSSHBox(Sandbox):
f'Failed to chown home directory for opendevin in sandbox: {logs}'
)
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', f'chown opendevin:root {SANDBOX_WORKSPACE_DIR}'],
workdir=SANDBOX_WORKSPACE_DIR,
[
'/bin/bash',
'-c',
f'chown opendevin:root {self.sandbox_workspace_dir}',
],
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
# This is not a fatal error, just a warning
logger.warning(
f'Failed to chown workspace directory for opendevin in sandbox: {logs}. But this should be fine if the {SANDBOX_WORKSPACE_DIR=} is mounted by the app docker container.'
f'Failed to chown workspace directory for opendevin in sandbox: {logs}. But this should be fine if the {self.sandbox_workspace_dir=} is mounted by the app docker container.'
)
else:
exit_code, logs = self.container.exec_run(
# change password for root
['/bin/bash', '-c', f"echo 'root:{self._ssh_password}' | chpasswd"],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
raise Exception(f'Failed to set password for root in sandbox: {logs}')
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', "echo 'opendevin-sandbox' > /etc/hostname"],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
def start_ssh_session(self):
# start ssh session at the background
self.ssh = pxssh.pxssh()
hostname = SSH_HOSTNAME
if RUN_AS_DEVIN:
hostname = self.ssh_hostname
if self.run_as_devin:
username = 'opendevin'
else:
username = 'root'
@ -218,11 +207,11 @@ class DockerSSHBox(Sandbox):
self.ssh.sendline("bind 'set enable-bracketed-paste off'")
self.ssh.prompt()
# cd to workspace
self.ssh.sendline(f'cd {SANDBOX_WORKSPACE_DIR}')
self.ssh.sendline(f'cd {self.sandbox_workspace_dir}')
self.ssh.prompt()
def get_exec_cmd(self, cmd: str) -> List[str]:
if RUN_AS_DEVIN:
if self.run_as_devin:
return ['su', 'opendevin', '-c', cmd]
else:
return ['/bin/bash', '-c', cmd]
@ -283,7 +272,7 @@ class DockerSSHBox(Sandbox):
# mkdir -p sandbox_dest if it doesn't exist
exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', f'mkdir -p {sandbox_dest}'],
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
if exit_code != 0:
@ -322,7 +311,7 @@ class DockerSSHBox(Sandbox):
result = self.container.exec_run(
self.get_exec_cmd(cmd),
socket=True,
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
result.output._sock.setblocking(0)
@ -350,7 +339,7 @@ class DockerSSHBox(Sandbox):
if bg_cmd.pid is not None:
self.container.exec_run(
f'kill -9 {bg_cmd.pid}',
workdir=SANDBOX_WORKSPACE_DIR,
workdir=self.sandbox_workspace_dir,
environment=self._env,
)
assert isinstance(bg_cmd, DockerProcess)
@ -379,6 +368,30 @@ class DockerSSHBox(Sandbox):
raise Exception('Failed to get working directory')
return result.strip()
@property
def user_id(self):
return config.sandbox_user_id
@property
def sandbox_user_id(self):
return config.sandbox_user_id
@property
def run_as_devin(self):
return config.run_as_devin
@property
def sandbox_workspace_dir(self):
return config.workspace_mount_path_in_sandbox
@property
def ssh_hostname(self):
return config.ssh_hostname
@property
def use_host_network(self):
return config.use_host_network
def is_container_running(self):
try:
container = self.docker_client.containers.get(self.container_name)
@ -399,7 +412,7 @@ class DockerSSHBox(Sandbox):
try:
network_kwargs: Dict[str, Union[str, Dict[str, int]]] = {}
if USE_HOST_NETWORK:
if self.use_host_network:
network_kwargs['network_mode'] = 'host'
else:
# FIXME: This is a temporary workaround for Mac OS
@ -412,7 +425,7 @@ class DockerSSHBox(Sandbox):
)
)
mount_dir = config.get(ConfigType.WORKSPACE_MOUNT_PATH)
mount_dir = config.workspace_mount_path
logger.info(f'Mounting workspace directory: {mount_dir}')
# start the container
self.container = self.docker_client.containers.run(
@ -420,15 +433,15 @@ class DockerSSHBox(Sandbox):
# allow root login
command=f"/usr/sbin/sshd -D -p {self._ssh_port} -o 'PermitRootLogin=yes'",
**network_kwargs,
working_dir=SANDBOX_WORKSPACE_DIR,
working_dir=self.sandbox_workspace_dir,
name=self.container_name,
detach=True,
volumes={
mount_dir: {'bind': SANDBOX_WORKSPACE_DIR, 'mode': 'rw'},
mount_dir: {'bind': self.sandbox_workspace_dir, 'mode': 'rw'},
# mount cache directory to /home/opendevin/.cache for pip cache reuse
config.get(ConfigType.CACHE_DIR): {
config.cache_dir: {
'bind': '/home/opendevin/.cache'
if RUN_AS_DEVIN
if self.run_as_devin
else '/root/.cache',
'mode': 'rw',
},

View File

@ -8,9 +8,8 @@ from e2b.sandbox.exception import (
TimeoutException,
)
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema.config import ConfigType
from opendevin.runtime.e2b.process import E2BProcess
from opendevin.runtime.process import Process
from opendevin.runtime.sandbox import Sandbox
@ -28,7 +27,7 @@ class E2BBox(Sandbox):
timeout: int = 120,
):
self.sandbox = E2BSandbox(
api_key=config.get(ConfigType.E2B_API_KEY),
api_key=config.e2b_api_key,
template=template,
# It's possible to stream stdout and stderr from sandbox and from each process
on_stderr=lambda x: logger.info(f'E2B sandbox stderr: {x}'),

View File

View File

@ -3,7 +3,7 @@ from typing import Optional
from opendevin.const.guide_url import TROUBLESHOOTING_URL
from opendevin.controller import AgentController
from opendevin.controller.agent import Agent
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ActionType, AgentState, ConfigType
from opendevin.events.action import (
@ -91,19 +91,6 @@ class AgentUnit:
action_obj = action_from_dict(action_dict)
await self.event_stream.add_event(action_obj, EventSource.USER)
def get_arg_or_default(self, _args: dict, key: ConfigType) -> str:
"""Gets an argument from the args dictionary or the default value.
Args:
_args: The args dictionary.
key: The key to get.
Returns:
The value of the key or the default value.
"""
return _args.get(key, config.get(key))
async def create_controller(self, start_event: dict):
"""Creates an AgentController instance.
@ -115,12 +102,12 @@ class AgentUnit:
for key, value in start_event.get('args', {}).items()
if value != ''
} # remove empty values, prevent FE from sending empty strings
agent_cls = self.get_arg_or_default(args, ConfigType.AGENT)
model = self.get_arg_or_default(args, ConfigType.LLM_MODEL)
api_key = self.get_arg_or_default(args, ConfigType.LLM_API_KEY)
api_base = config.get(ConfigType.LLM_BASE_URL)
max_iterations = self.get_arg_or_default(args, ConfigType.MAX_ITERATIONS)
max_chars = self.get_arg_or_default(args, ConfigType.MAX_CHARS)
agent_cls = args.get(ConfigType.AGENT, config.agent.name)
model = args.get(ConfigType.LLM_MODEL, config.llm.model)
api_key = args.get(ConfigType.LLM_API_KEY, config.llm.api_key)
api_base = config.llm.base_url
max_iterations = args.get(ConfigType.MAX_ITERATIONS, config.max_iterations)
max_chars = args.get(ConfigType.MAX_CHARS, config.llm.max_chars)
logger.info(f'Creating agent {agent_cls} using LLM {model}')
llm = LLM(model=model, api_key=api_key, base_url=api_base)

View File

@ -12,9 +12,8 @@ from fastapi.staticfiles import StaticFiles
import agenthub # noqa F401 (we import this to get the agents registered)
from opendevin.controller.agent import Agent
from opendevin.core import config
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema.config import ConfigType
from opendevin.llm import bedrock
from opendevin.runtime import files
from opendevin.server.agent import agent_manager
@ -124,16 +123,14 @@ async def del_messages(
@app.get('/api/refresh-files')
def refresh_files():
structure = files.get_folder_structure(
Path(str(config.get(ConfigType.WORKSPACE_BASE)))
)
structure = files.get_folder_structure(Path(str(config.workspace_base)))
return structure.to_dict()
@app.get('/api/select-file')
def select_file(file: str):
try:
workspace_base = config.get(ConfigType.WORKSPACE_BASE)
workspace_base = config.workspace_base
file_path = Path(workspace_base, file)
# The following will check if the file is within the workspace base and throw an exception if not
file_path.resolve().relative_to(Path(workspace_base).resolve())
@ -152,7 +149,7 @@ def select_file(file: str):
@app.post('/api/upload-file')
async def upload_file(file: UploadFile):
try:
workspace_base = config.get(ConfigType.WORKSPACE_BASE)
workspace_base = config.workspace_base
file_path = Path(workspace_base, file.filename)
# The following will check if the file is within the workspace base and throw an exception if not
file_path.resolve().relative_to(Path(workspace_base).resolve())
@ -189,6 +186,11 @@ def get_plan(
return Response(status_code=status.HTTP_204_NO_CONTENT)
@app.get('/api/defaults')
async def appconfig_defaults():
return config.defaults_dict
@app.get('/')
async def docs_redirect():
response = RedirectResponse(url='/index.html')

View File

@ -1 +1,7 @@
{"action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\necho 'hello'"}}
{
"action": "write",
"args": {
"path": "hello.sh",
"content": "#!/bin/bash\n\necho 'hello'"
}
}

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to create a shell script named 'hello.sh' that prints 'hello' when executed. I will use the 'write' command to create the script with the required content.
I need to create a shell script named 'hello.sh' that prints 'hello' without asking for confirmation. I will use the echo command to print 'hello' to the console.
Action:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to create a shell script named 'hello.sh' that prints 'hello' when executed. I will use the 'write' command to create the script with the required content.
I need to create a shell script named 'hello.sh' that prints 'hello' without asking for confirmation. I will use the echo command to print 'hello' to the console.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to create a shell script named 'hello.sh' that prints 'hello' when executed. I will use the 'write' command to create the script with the required content.
I need to create a shell script named 'hello.sh' that prints 'hello' without asking for confirmation. I will use the echo command to print 'hello' to the console.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to create a shell script named 'hello.sh' that prints 'hello' when executed. I will use the 'write' command to create the script with the required content.
I need to create a shell script named 'hello.sh' that prints 'hello' without asking for confirmation. I will use the echo command to print 'hello' to the console.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -121,7 +121,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -121,7 +121,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -121,7 +121,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
@ -121,7 +121,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
@ -121,7 +121,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:

View File

@ -67,7 +67,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
@ -85,7 +85,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
@ -103,7 +103,7 @@ args:
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
@ -115,19 +115,20 @@ extras:
Memory 3:
Previous Action:
action: write
action: read
args:
path: hello.sh
start: 0
end: -1
thought: Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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

@ -1,5 +1,5 @@
Thoughts:
I need to create a shell script named 'hello.sh' that prints 'hello' when executed. I will use the 'write' command to create the script with the required content.
I need to create a shell script named 'hello.sh' that prints 'hello' without asking for confirmation. I will use the echo command to print 'hello' to the console.
Action:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I need to add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
```

View File

@ -1,5 +1,5 @@
Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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:
```

View File

@ -1,7 +1,7 @@
Thoughts:
I have already created the 'hello.sh' file, and now I need to add the code to the file that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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 add the code to the 'hello.sh' script that will print 'hello' when executed. I will use the 'edit' command to add the necessary code to the file.
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.
Action:
```
edit 0 -1 "echo 'hello'"
exit
```

View File

@ -2,27 +2,44 @@ from pathlib import Path
import pytest
from opendevin import config
from opendevin.core.config import config
from opendevin.events.action import files
from opendevin.schema import ConfigType
SANDBOX_PATH_PREFIX = '/workspace'
def test_resolve_path():
assert files.resolve_path('test.txt', '/workspace') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
assert files.resolve_path('subdir/test.txt', '/workspace') == \
Path(config.get(ConfigType.WORKSPACE_BASE)) / 'subdir' / 'test.txt'
assert files.resolve_path(Path(SANDBOX_PATH_PREFIX) / 'test.txt', '/workspace') == \
Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
assert files.resolve_path(Path(SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt',
'/workspace') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'subdir' / 'test.txt'
assert files.resolve_path(Path(SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt',
'/workspace') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
assert (
files.resolve_path('test.txt', '/workspace')
== Path(config.workspace_base) / 'test.txt'
)
assert (
files.resolve_path('subdir/test.txt', '/workspace')
== Path(config.workspace_base) / 'subdir' / 'test.txt'
)
assert (
files.resolve_path(Path(SANDBOX_PATH_PREFIX) / 'test.txt', '/workspace')
== Path(config.workspace_base) / 'test.txt'
)
assert (
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt', '/workspace'
)
== Path(config.workspace_base) / 'subdir' / 'test.txt'
)
assert (
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt', '/workspace'
)
== Path(config.workspace_base) / 'test.txt'
)
with pytest.raises(PermissionError):
files.resolve_path(Path(SANDBOX_PATH_PREFIX) / '..' / 'test.txt', '/workspace')
with pytest.raises(PermissionError):
files.resolve_path(Path('..') / 'test.txt', '/workspace')
with pytest.raises(PermissionError):
files.resolve_path(Path('/') / 'test.txt', '/workspace')
assert files.resolve_path('test.txt', '/workspace/test') == \
Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test' / 'test.txt'
assert (
files.resolve_path('test.txt', '/workspace/test')
== Path(config.workspace_base) / 'test' / 'test.txt'
)

View File

@ -4,8 +4,7 @@ import pytest
from agenthub.dummy_agent.agent import DummyAgent
from opendevin.controller.agent_controller import AgentController
from opendevin.core import config
from opendevin.core.schema.config import ConfigType
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
@ -16,7 +15,7 @@ from opendevin.llm.llm import LLM
@pytest.fixture
def agent_controller():
# Setup the environment variable
config.config[ConfigType.SANDBOX_TYPE] = 'local'
config.sandbox_type = 'local'
llm = LLM()
agent = DummyAgent(llm=llm)
event_stream = EventStream()
@ -25,7 +24,7 @@ def agent_controller():
@pytest.mark.asyncio
@patch.dict(config.config, {'GITHUB_TOKEN': 'fake_token'}, clear=True)
@patch.object(config, 'github_token', 'fake_token')
@patch('random.choices')
@patch('opendevin.controller.action_manager.ActionManager.run_command')
async def test_run_push_successful(
@ -74,11 +73,11 @@ async def test_run_push_error_missing_token(
# Verify the result is an error due to missing token
assert isinstance(result, ErrorObservation)
assert result.message == 'GITHUB_TOKEN is not set'
assert result.message == 'github_token is not set'
@pytest.mark.asyncio
@patch.dict(config.config, {'GITHUB_TOKEN': 'fake_token'}, clear=True)
@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
@ -106,7 +105,7 @@ async def test_run_pull_request_created_successfully(mock_post, agent_controller
@pytest.mark.asyncio
@patch('requests.post')
@patch.dict(config.config, {'GITHUB_TOKEN': 'fake_token'}, clear=True)
@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()
@ -149,4 +148,4 @@ async def test_run_error_missing_token(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
assert 'github_token is not set' in result.message

View File

@ -10,7 +10,8 @@ def test_help_message(capsys):
captured = capsys.readouterr()
expected_help_message = """
usage: pytest [-h] [-d DIRECTORY] [-t TASK] [-f FILE] [-c AGENT_CLS]
[-m MODEL_NAME] [-i MAX_ITERATIONS] [-n MAX_CHARS]
[-m MODEL_NAME] [-i MAX_ITERATIONS] [-n MAX_CHARS]
[-l LLM_CONFIG]
Run an agent with a specific task
@ -20,7 +21,7 @@ options:
The working directory for the agent
-t TASK, --task TASK The task for the agent to perform
-f FILE, --file FILE Path to a file containing the task. Overrides -t if
both are provided.
both are provided.
-c AGENT_CLS, --agent-cls AGENT_CLS
The agent class to use
-m MODEL_NAME, --model-name MODEL_NAME
@ -30,7 +31,11 @@ options:
-n MAX_CHARS, --max-chars MAX_CHARS
The maximum number of characters to send to and
receive from LLM per task
-l LLM_CONFIG, --llm-config LLM_CONFIG
The group of llm settings, e.g. a [llama3] section in
the toml file. Overrides model if both are provided.
"""
actual_lines = captured.out.strip().split('\n')
expected_lines = expected_help_message.strip().split('\n')

173
tests/unit/test_config.py Normal file
View File

@ -0,0 +1,173 @@
import os
import pytest
from opendevin.core.config import (
AgentConfig,
AppConfig,
LLMConfig,
finalize_config,
load_from_env,
load_from_toml,
)
@pytest.fixture
def setup_env():
# Create old-style and new-style TOML files
with open('old_style_config.toml', 'w') as f:
f.write('[default]\nLLM_MODEL="GPT-4"\n')
with open('new_style_config.toml', 'w') as f:
f.write('[app]\nLLM_MODEL="GPT-3"\n')
yield
# Cleanup TOML files after the test
os.remove('old_style_config.toml')
os.remove('new_style_config.toml')
def test_compat_env_to_config(monkeypatch, setup_env):
# Use `monkeypatch` to set environment variables for this specific test
monkeypatch.setenv('WORKSPACE_BASE', '/repos/opendevin/workspace')
monkeypatch.setenv('LLM_API_KEY', 'sk-proj-rgMV0...')
monkeypatch.setenv('LLM_MODEL', 'gpt-3.5-turbo')
monkeypatch.setenv('AGENT_MEMORY_MAX_THREADS', '4')
monkeypatch.setenv('AGENT_MEMORY_ENABLED', 'True')
monkeypatch.setenv('AGENT', 'CodeActAgent')
config = AppConfig()
load_from_env(config, os.environ)
assert config.workspace_base == '/repos/opendevin/workspace'
assert isinstance(config.llm, LLMConfig)
assert config.llm.api_key == 'sk-proj-rgMV0...'
assert config.llm.model == 'gpt-3.5-turbo'
assert isinstance(config.agent, AgentConfig)
assert isinstance(config.agent.memory_max_threads, int)
assert config.agent.memory_max_threads == 4
@pytest.fixture
def temp_toml_file(tmp_path):
# Fixture to create a temporary directory and TOML file for testing
tmp_toml_file = os.path.join(tmp_path, 'config.toml')
yield tmp_toml_file
@pytest.fixture
def default_config(monkeypatch):
# Fixture to provide a default AppConfig instance
AppConfig.reset()
yield AppConfig()
def test_load_from_old_style_env(monkeypatch, default_config):
# Test loading configuration from old-style environment variables using monkeypatch
monkeypatch.setenv('LLM_API_KEY', 'test-api-key')
monkeypatch.setenv('AGENT_MEMORY_ENABLED', 'True')
monkeypatch.setenv('AGENT_NAME', 'PlannerAgent')
monkeypatch.setenv('WORKSPACE_BASE', '/opt/files/workspace')
load_from_env(default_config, os.environ)
assert default_config.llm.api_key == 'test-api-key'
assert default_config.agent.memory_enabled is True
assert default_config.agent.name == 'PlannerAgent'
assert default_config.workspace_base == '/opt/files/workspace'
def test_load_from_new_style_toml(default_config, temp_toml_file):
# Test loading configuration from a new-style TOML file
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
toml_file.write("""
[llm]
model = "test-model"
api_key = "toml-api-key"
[agent]
name = "TestAgent"
memory_enabled = true
[core]
workspace_base = "/opt/files2/workspace"
""")
load_from_toml(default_config, temp_toml_file)
assert default_config.llm.model == 'test-model'
assert default_config.llm.api_key == 'toml-api-key'
assert default_config.agent.name == 'TestAgent'
assert default_config.agent.memory_enabled is True
assert default_config.workspace_base == '/opt/files2/workspace'
def test_env_overrides_toml(monkeypatch, default_config, temp_toml_file):
# Test that environment variables override TOML values using monkeypatch
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
toml_file.write("""
[llm]
model = "test-model"
api_key = "toml-api-key"
[core]
workspace_base = "/opt/files3/workspace"
sandbox_type = "local"
disable_color = true
""")
monkeypatch.setenv('LLM_API_KEY', 'env-api-key')
monkeypatch.setenv('WORKSPACE_BASE', '/opt/files4/workspace')
monkeypatch.setenv('SANDBOX_TYPE', 'ssh')
load_from_toml(default_config, temp_toml_file)
load_from_env(default_config, os.environ)
assert os.environ.get('LLM_MODEL') is None
assert default_config.llm.model == 'test-model'
assert default_config.llm.api_key == 'env-api-key'
assert default_config.workspace_base == '/opt/files4/workspace'
assert default_config.sandbox_type == 'ssh'
assert default_config.disable_color is True
def test_defaults_dict_after_updates(default_config):
# Test that `defaults_dict` retains initial values after updates.
initial_defaults = default_config.defaults_dict
updated_config = AppConfig()
updated_config.llm.api_key = 'updated-api-key'
updated_config.agent.name = 'MonologueAgent'
defaults_after_updates = updated_config.defaults_dict
assert defaults_after_updates['llm']['api_key']['default'] is None
assert defaults_after_updates['agent']['name']['default'] == 'CodeActAgent'
assert defaults_after_updates == initial_defaults
AppConfig.reset()
def test_invalid_toml_format(monkeypatch, temp_toml_file, default_config):
# Invalid TOML format doesn't break the configuration
monkeypatch.setenv('LLM_MODEL', 'gpt-5-turbo-1106')
monkeypatch.delenv('LLM_API_KEY', raising=False)
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
toml_file.write('INVALID TOML CONTENT')
load_from_toml(default_config)
load_from_env(default_config, os.environ)
assert default_config.llm.model == 'gpt-5-turbo-1106'
assert default_config.llm.custom_llm_provider is None
assert default_config.github_token is None
assert default_config.llm.api_key is None
def test_finalize_config(default_config):
# Test finalize config
default_config.sandbox_type = 'local'
finalize_config(default_config)
assert (
default_config.workspace_mount_path_in_sandbox
== default_config.workspace_mount_path
)

View File

@ -5,19 +5,27 @@ from unittest.mock import patch
import pytest
from opendevin.core import config
from opendevin.core.config import AppConfig, config
from opendevin.runtime.docker.exec_box import DockerExecBox
from opendevin.runtime.docker.local_box import LocalBox
from opendevin.runtime.docker.ssh_box import DockerSSHBox
@pytest.fixture
def temp_dir():
def temp_dir(monkeypatch):
# get a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
pathlib.Path().mkdir(parents=True, exist_ok=True)
yield temp_dir
# make sure os.environ is clean
monkeypatch.delenv('RUN_AS_DEVIN', raising=False)
monkeypatch.delenv('SANDBOX_TYPE', raising=False)
monkeypatch.delenv('WORKSPACE_BASE', raising=False)
# make sure config is clean
AppConfig.reset()
def test_env_vars(temp_dir):
os.environ['SANDBOX_ENV_FOOBAR'] = 'BAZ'
@ -33,18 +41,15 @@ def test_env_vars(temp_dir):
def test_ssh_box_run_as_devin(temp_dir):
# get a temporary directory
with patch.dict(
config.config,
{
config.ConfigType.WORKSPACE_BASE: temp_dir,
config.ConfigType.RUN_AS_DEVIN: 'true',
config.ConfigType.SANDBOX_TYPE: 'ssh',
},
clear=True,
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'
):
ssh_box = DockerSSHBox()
# test the ssh box
assert config.workspace_base == temp_dir
exit_code, output = ssh_box.execute('ls -l')
assert exit_code == 0, 'The exit code should be 0.'
assert output.strip() == 'total 0'
@ -69,14 +74,10 @@ def test_ssh_box_run_as_devin(temp_dir):
def test_ssh_box_multi_line_cmd_run_as_devin(temp_dir):
# get a temporary directory
with patch.dict(
config.config,
{
config.ConfigType.WORKSPACE_BASE: temp_dir,
config.ConfigType.RUN_AS_DEVIN: 'true',
config.ConfigType.SANDBOX_TYPE: 'ssh',
},
clear=True,
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'
):
ssh_box = DockerSSHBox()
@ -89,14 +90,10 @@ def test_ssh_box_multi_line_cmd_run_as_devin(temp_dir):
def test_ssh_box_stateful_cmd_run_as_devin(temp_dir):
# get a temporary directory
with patch.dict(
config.config,
{
config.ConfigType.WORKSPACE_BASE: temp_dir,
config.ConfigType.RUN_AS_DEVIN: 'true',
config.ConfigType.SANDBOX_TYPE: 'ssh',
},
clear=True,
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'
):
ssh_box = DockerSSHBox()
@ -116,14 +113,10 @@ def test_ssh_box_stateful_cmd_run_as_devin(temp_dir):
def test_ssh_box_failed_cmd_run_as_devin(temp_dir):
# get a temporary directory
with patch.dict(
config.config,
{
config.ConfigType.WORKSPACE_BASE: temp_dir,
config.ConfigType.RUN_AS_DEVIN: 'true',
config.ConfigType.SANDBOX_TYPE: 'ssh',
},
clear=True,
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'
):
ssh_box = DockerSSHBox()