mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
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:
parent
73693ba416
commit
446eaec1e6
38
Makefile
38
Makefile
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = []
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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}')
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
|
||||
3
opendevin/core/utils/__init__.py
Normal file
3
opendevin/core/utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .singleton import Singleton
|
||||
|
||||
__all__ = ['Singleton']
|
||||
28
opendevin/core/utils/singleton.py
Normal file
28
opendevin/core/utils/singleton.py
Normal 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)
|
||||
@ -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',
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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__':
|
||||
|
||||
@ -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__':
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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}'),
|
||||
|
||||
0
opendevin/runtime/utils/singleton.py
Normal file
0
opendevin/runtime/utils/singleton.py
Normal 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)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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 =======
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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:
|
||||
```
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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'
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
173
tests/unit/test_config.py
Normal 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
|
||||
)
|
||||
@ -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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user