Add Docker DOOD setup (#1023)

* simplified get

* resolved merge conflicts

* removed default param for get

* add dood setup

* add readme

* better build process

* multi-stage build

* revert makefile

* rm entrypoint.sh

* adjust ssh box for docker

* update readme

* update readme

* fix hostname

* change workspace setting

* add workspace_mount_base

* fixes for workspace dir

* clean up frontend

* refactor dockerfile

* try download.py

* change docker order a bit

* remove workspace_dir from frontend settings

* fix merge issues

* Update opendevin/config.py

* remove relpath logic from server

* rename workspace_mount_base to workspace_base

* remove workspace dir plumbing for now

* delint

* delint

* move workspace base dir

* remove refs to workspace_dir

* factor out constant

* fix local directory usage

* dont require dir

* fix docs

* fix arg parsing for task

* implement WORKSPACE_MOUNT_PATH

* fix workspace dir

* fix ports

* fix merge issues

* add makefile

* revert settingsService

* fix string

* Add address

* Update Dockerfile

* Update local_box.py

* fix lint

* move to port 3000

---------

Co-authored-by: மனோஜ்குமார் பழனிச்சாமி <smartmanoj42857@gmail.com>
Co-authored-by: enyst <engel.nyst@gmail.com>
This commit is contained in:
Robert Brennan
2024-04-15 14:19:02 +02:00
committed by GitHub
parent 8450b47609
commit 342302ceef
22 changed files with 317 additions and 247 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
frontend/node_modules
config.toml
.envrc
.env

66
Development.md Normal file
View File

@@ -0,0 +1,66 @@
# Development Guide
This guide is for people working on OpenDevin and editing the source code.
## Start the server for development
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install)
* [Docker](https://docs.docker.com/engine/install/)(For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
* [Python](https://www.python.org/downloads/) >= 3.11
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
Make sure you have all these dependencies installed before moving on to `make build`.
### 2. Build and Setup The Environment
- **Build the Project:** Begin by building the project, which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run smoothly on your system.
```bash
make build
```
### 3. Configuring the Language Model
OpenDevin supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest.
To configure the LM of your choice, follow these steps:
1. **Using the Makefile: The Effortless Approach**
With a single command, you can have a smooth LM setup for your OpenDevin experience. Simply run:
```bash
make setup-config
```
This command will prompt you to enter the LLM API key and model name, ensuring that OpenDevin is tailored to your specific needs.
**Note on Alternative Models:**
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest. And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers).
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
### 4. Run the Application
- **Run the Application:** Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin without any hassle.
```bash
make run
```
### 5. Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related components or interface enhancements.
```bash
make start-frontend
```
### 6. Help
- **Get Some Help:** Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
```bash
make help
```

View File

@@ -121,76 +121,51 @@ After completing the MVP, the team will focus on research in various areas, incl
Getting started with the OpenDevin project is incredibly easy. Follow these simple steps to set up and run OpenDevin on your system: Getting started with the OpenDevin project is incredibly easy. Follow these simple steps to set up and run OpenDevin on your system:
### 1. Requirements The easiest way to run OpenDevin is inside a Docker container.
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) You can run:
* [Docker](https://docs.docker.com/engine/install/)(For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!) ```bash
* [Python](https://www.python.org/downloads/) >= 3.11 # Your OpenAI API key, or any other LLM API key
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1 export LLM_API_KEY="sk-..."
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
Make sure you have all these dependencies installed before moving on to `make build`. # The directory you want OpenDevin to modify. MUST be an absolute path!
export WORKSPACE_DIR=$(pwd)/workspace
### 2. Build and Setup The Environment docker build -t opendevin-app -f container/Dockerfile .
- **Build the Project:** Begin by building the project, which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run smoothly on your system. docker run \
```bash -e LLM_API_KEY \
make build -e WORKSPACE_MOUNT_PATH=$WORKSPACE_DIR \
``` -v $WORKSPACE_DIR:/opt/workspace_base \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
opendevin-app
```
Replace `$(pwd)/workspace` with the path to the code you want OpenDevin to work with.
### 3. Configuring the Language Model You can find opendevin running at `http://localhost:3000`.
OpenDevin supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest. See [Development.md](Development.md) for instructions on running OpenDevin without Docker.
To configure the LM of your choice, follow these steps: ## 🤖 LLM Backends
OpenDevin can work with any LLM backend.
For a full list of the LM providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).
1. **Using the Makefile: The Effortless Approach** The following environment variables might be necessary for some LLMs:
With a single command, you can have a smooth LM setup for your OpenDevin experience. Simply run: * `LLM_API_KEY`
```bash * `LLM_BASE_URL`
make setup-config * `LLM_EMBEDDING_MODEL`
``` * `LLM_DEPLOYMENT_NAME`
This command will prompt you to enter the LLM API key and model name, ensuring that OpenDevin is tailored to your specific needs. * `LLM_API_VERSION`
**Note on Alternative Models:** **Note on Alternative Models:**
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest. And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417). Some alternative models may prove more challenging to tame than others.
Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest.
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers). And if you've already mastered the art of wielding a model other than OpenAI's GPT,
we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md). There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
We are working on a [guide for running OpenDevin with Azure](./docs/documentation/AZURE_LLM_GUIDE.md).
### 4. Run the Application
- **Run the Application:** Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin without any hassle.
```bash
make run
```
### 5. Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related components or interface enhancements.
```bash
make start-frontend
```
### 6. Help
- **Get Some Help:** Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
```bash
make help
```
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>
## ⭐️ Research Strategy ## ⭐️ Research Strategy
Achieving full replication of production-grade applications with LLMs is a complex endeavor. Our strategy involves: Achieving full replication of production-grade applications with LLMs is a complex endeavor. Our strategy involves:

35
container/Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
FROM node:21.7.2-bookworm-slim as frontend-builder
WORKDIR /app
COPY ./frontend/package.json frontend/package-lock.json ./
RUN npm install
COPY ./frontend ./
RUN npm run build
FROM python:3.12-slim as runtime
WORKDIR /app
ENV PYTHONPATH '/app'
ENV RUN_AS_DEVIN=false
ENV USE_HOST_NETWORK=false
ENV SSH_HOSTNAME=host.docker.internal
ENV WORKSPACE_BASE=/opt/workspace_base
RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& python3 -m pip install poetry --break-system-packages
COPY ./pyproject.toml ./poetry.lock ./
RUN poetry install --without evaluation
COPY ./opendevin ./opendevin
COPY ./agenthub ./agenthub
RUN poetry run python opendevin/download.py # No-op to download assets
COPY --from=frontend-builder /app/dist ./frontend/dist
CMD ["poetry", "run", "uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"]

31
container/Makefile Normal file
View File

@@ -0,0 +1,31 @@
DOCKER_BUILD_REGISTRY=ghcr.io
DOCKER_BUILD_ORG=opendevin
DOCKER_BUILD_REPO=opendevin
DOCKER_BUILD_TAG=v0.2
FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(DOCKER_BUILD_TAG)
LATEST_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):latest
MAJOR_VERSION=$(shell echo $(DOCKER_BUILD_TAG) | cut -d. -f1)
MAJOR_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(MAJOR_VERSION)
MINOR_VERSION=$(shell echo $(DOCKER_BUILD_TAG) | cut -d. -f1,2)
MINOR_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(MINOR_VERSION)
# normally, for local build testing or development. use cross platform build for sharing images to others.
build:
docker build -f Dockerfile -t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} ..
push:
docker push ${FULL_IMAGE} ${LATEST_FULL_IMAGE}
test:
docker buildx build --platform linux/amd64 \
-t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} --load -f Dockerfile ..
# cross platform build, you may need to manually stop the buildx(buildkit) container
all:
docker buildx build --platform linux/amd64,linux/arm64 \
-t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} -t ${MINOR_FULL_IMAGE} --push -f Dockerfile ..
get-full-image:
@echo ${FULL_IMAGE}

View File

@@ -4,7 +4,6 @@ import {
Autocomplete, Autocomplete,
AutocompleteItem, AutocompleteItem,
Button, Button,
Input,
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
@@ -46,9 +45,6 @@ function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
settings[ArgConfigType.LLM_MODEL], settings[ArgConfigType.LLM_MODEL],
); );
const [agent, setAgent] = useState(settings[ArgConfigType.AGENT]); const [agent, setAgent] = useState(settings[ArgConfigType.AGENT]);
const [workspaceDirectory, setWorkspaceDirectory] = useState(
settings[ArgConfigType.WORKSPACE_DIR],
);
const [language, setLanguage] = useState(settings[ArgConfigType.LANGUAGE]); const [language, setLanguage] = useState(settings[ArgConfigType.LANGUAGE]);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -78,7 +74,6 @@ function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
{ {
[ArgConfigType.LLM_MODEL]: model ?? inputModel, [ArgConfigType.LLM_MODEL]: model ?? inputModel,
[ArgConfigType.AGENT]: agent, [ArgConfigType.AGENT]: agent,
[ArgConfigType.WORKSPACE_DIR]: workspaceDirectory,
[ArgConfigType.LANGUAGE]: language, [ArgConfigType.LANGUAGE]: language,
}, },
Object.fromEntries( Object.fromEntries(
@@ -100,18 +95,6 @@ function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
{t(I18nKey.CONFIGURATION$MODAL_TITLE)} {t(I18nKey.CONFIGURATION$MODAL_TITLE)}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<Input
type="text"
label={t(
I18nKey.CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_LABEL,
)}
defaultValue={workspaceDirectory}
placeholder={t(
I18nKey.CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER,
)}
onChange={(e) => setWorkspaceDirectory(e.target.value)}
/>
<Autocomplete <Autocomplete
defaultItems={supportedModels.map((v: string) => ({ defaultItems={supportedModels.map((v: string) => ({
label: v, label: v,

View File

@@ -9,8 +9,6 @@ export const settingsSlice = createSlice({
[ArgConfigType.LLM_MODEL]: [ArgConfigType.LLM_MODEL]:
localStorage.getItem(ArgConfigType.LLM_MODEL) || "", localStorage.getItem(ArgConfigType.LLM_MODEL) || "",
[ArgConfigType.AGENT]: localStorage.getItem(ArgConfigType.AGENT) || "", [ArgConfigType.AGENT]: localStorage.getItem(ArgConfigType.AGENT) || "",
[ArgConfigType.WORKSPACE_DIR]:
localStorage.getItem(ArgConfigType.WORKSPACE_DIR) || "",
[ArgConfigType.LANGUAGE]: [ArgConfigType.LANGUAGE]:
localStorage.getItem(ArgConfigType.LANGUAGE) || "en", localStorage.getItem(ArgConfigType.LANGUAGE) || "en",
} as { [key: string]: string }, } as { [key: string]: string },

View File

@@ -19,7 +19,7 @@ enum ArgConfigType {
const SupportedList: string[] = [ const SupportedList: string[] = [
// ArgConfigType.LLM_API_KEY, // ArgConfigType.LLM_API_KEY,
// ArgConfigType.LLM_BASE_URL, // ArgConfigType.LLM_BASE_URL,
ArgConfigType.WORKSPACE_DIR, // ArgConfigType.WORKSPACE_DIR,
ArgConfigType.LLM_MODEL, ArgConfigType.LLM_MODEL,
// ArgConfigType.SANDBOX_CONTAINER_IMAGE, // ArgConfigType.SANDBOX_CONTAINER_IMAGE,
// ArgConfigType.RUN_AS_DEVIN, // ArgConfigType.RUN_AS_DEVIN,

View File

@@ -24,7 +24,6 @@ export default defineConfig({
"/api": { "/api": {
target: `http://${BACKEND_HOST}/`, target: `http://${BACKEND_HOST}/`,
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, ""),
}, },
"/ws": { "/ws": {
target: `ws://${BACKEND_HOST}/`, target: `ws://${BACKEND_HOST}/`,

View File

@@ -3,18 +3,18 @@ from dataclasses import dataclass
from opendevin.observation import FileReadObservation, FileWriteObservation from opendevin.observation import FileReadObservation, FileWriteObservation
from opendevin.schema import ActionType from opendevin.schema import ActionType
from opendevin import config
from .base import ExecutableAction from .base import ExecutableAction
# This is the path where the workspace is mounted in the container SANDBOX_PATH_PREFIX = '/workspace/'
# The LLM sometimes returns paths with this prefix, so we need to remove it
PATH_PREFIX = '/workspace/'
def resolve_path(base_path, file_path): def resolve_path(file_path):
if file_path.startswith(PATH_PREFIX): if file_path.startswith(SANDBOX_PATH_PREFIX):
file_path = file_path[len(PATH_PREFIX):] # Sometimes LLMs include the absolute path of the file inside the sandbox
return os.path.join(base_path, file_path) file_path = file_path[len(SANDBOX_PATH_PREFIX):]
return os.path.join(config.get('WORKSPACE_BASE'), file_path)
@dataclass @dataclass
@@ -23,7 +23,7 @@ class FileReadAction(ExecutableAction):
action: str = ActionType.READ action: str = ActionType.READ
def run(self, controller) -> FileReadObservation: def run(self, controller) -> FileReadObservation:
path = resolve_path(controller.workdir, self.path) path = resolve_path(self.path)
with open(path, 'r', encoding='utf-8') as file: with open(path, 'r', encoding='utf-8') as file:
return FileReadObservation(path=path, content=file.read()) return FileReadObservation(path=path, content=file.read())
@@ -39,7 +39,7 @@ class FileWriteAction(ExecutableAction):
action: str = ActionType.WRITE action: str = ActionType.WRITE
def run(self, controller) -> FileWriteObservation: def run(self, controller) -> FileWriteObservation:
whole_path = resolve_path(controller.workdir, self.path) whole_path = resolve_path(self.path)
with open(whole_path, 'w', encoding='utf-8') as file: with open(whole_path, 'w', encoding='utf-8') as file:
file.write(self.content) file.write(self.content)
return FileWriteObservation(content='', path=self.path) return FileWriteObservation(content='', path=self.path)

View File

@@ -1,6 +1,7 @@
import copy import copy
import os import os
import argparse
import toml import toml
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -11,7 +12,9 @@ load_dotenv()
DEFAULT_CONFIG: dict = { DEFAULT_CONFIG: dict = {
ConfigType.LLM_API_KEY: None, ConfigType.LLM_API_KEY: None,
ConfigType.LLM_BASE_URL: None, ConfigType.LLM_BASE_URL: None,
ConfigType.WORKSPACE_DIR: os.path.join(os.getcwd(), 'workspace'), ConfigType.WORKSPACE_BASE: os.getcwd(),
ConfigType.WORKSPACE_MOUNT_PATH: None,
ConfigType.WORKSPACE_MOUNT_REWRITE: None,
ConfigType.LLM_MODEL: 'gpt-3.5-turbo-1106', ConfigType.LLM_MODEL: 'gpt-3.5-turbo-1106',
ConfigType.SANDBOX_CONTAINER_IMAGE: 'ghcr.io/opendevin/sandbox', ConfigType.SANDBOX_CONTAINER_IMAGE: 'ghcr.io/opendevin/sandbox',
ConfigType.RUN_AS_DEVIN: 'true', ConfigType.RUN_AS_DEVIN: 'true',
@@ -20,7 +23,6 @@ DEFAULT_CONFIG: dict = {
ConfigType.LLM_API_VERSION: None, ConfigType.LLM_API_VERSION: None,
ConfigType.LLM_NUM_RETRIES: 6, ConfigType.LLM_NUM_RETRIES: 6,
ConfigType.LLM_COOLDOWN_TIME: 1, ConfigType.LLM_COOLDOWN_TIME: 1,
ConfigType.DIRECTORY_REWRITE: '',
ConfigType.MAX_ITERATIONS: 100, ConfigType.MAX_ITERATIONS: 100,
# GPT-4 pricing is $10 per 1M input tokens. Since tokenization happens on LLM side, # 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. # we cannot easily count number of tokens, but we can count characters.
@@ -28,6 +30,8 @@ DEFAULT_CONFIG: dict = {
ConfigType.MAX_CHARS: 5_000_000, ConfigType.MAX_CHARS: 5_000_000,
ConfigType.AGENT: 'MonologueAgent', ConfigType.AGENT: 'MonologueAgent',
ConfigType.SANDBOX_TYPE: 'ssh', ConfigType.SANDBOX_TYPE: 'ssh',
ConfigType.USE_HOST_NETWORK: 'false',
ConfigType.SSH_HOSTNAME: 'localhost',
ConfigType.DISABLE_COLOR: 'false', ConfigType.DISABLE_COLOR: 'false',
} }
@@ -45,13 +49,39 @@ for k, v in config.items():
config[k] = tomlConfig[k] config[k] = tomlConfig[k]
def parse_arguments():
parser = argparse.ArgumentParser(
description='Run an agent with a specific task')
parser.add_argument(
'-d',
'--directory',
type=str,
help='The working directory for the agent',
)
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]}")
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])
finalize_config()
def get(key: str, required: bool = False): def get(key: str, required: bool = False):
""" """
Get a key from the environment variables or config.toml or default configs. Get a key from the environment variables or config.toml or default configs.
""" """
value = os.environ.get(key) value = config.get(key)
if not value:
value = config.get(key)
if not value and required: if not value and required:
raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.") raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.")
return value return value

View File

@@ -73,14 +73,12 @@ class AgentController:
id: str id: str
agent: Agent agent: Agent
max_iterations: int max_iterations: int
workdir: str
command_manager: CommandManager command_manager: CommandManager
callbacks: List[Callable] callbacks: List[Callable]
def __init__( def __init__(
self, self,
agent: Agent, agent: Agent,
workdir: str,
sid: str = '', sid: str = '',
max_iterations: int = MAX_ITERATIONS, max_iterations: int = MAX_ITERATIONS,
max_chars: int = MAX_CHARS, max_chars: int = MAX_CHARS,
@@ -90,10 +88,8 @@ class AgentController:
self.id = sid self.id = sid
self.agent = agent self.agent = agent
self.max_iterations = max_iterations self.max_iterations = max_iterations
self.command_manager = CommandManager(self.id, container_image)
self.max_chars = max_chars self.max_chars = max_chars
self.workdir = workdir
self.command_manager = CommandManager(
self.id, workdir, container_image)
self.callbacks = callbacks self.callbacks = callbacks
def update_state_for_step(self, i): def update_state_for_step(self, i):
@@ -132,7 +128,6 @@ class AgentController:
print('\n\n==============', flush=True) print('\n\n==============', flush=True)
print('STEP', i, flush=True) print('STEP', i, flush=True)
print_with_color(self.state.plan.main_goal, 'PLAN') print_with_color(self.state.plan.main_goal, 'PLAN')
if self.state.num_of_chars > self.max_chars: if self.state.num_of_chars > self.max_chars:
raise MaxCharsExceedError( raise MaxCharsExceedError(
self.state.num_of_chars, self.max_chars) self.state.num_of_chars, self.max_chars)

View File

@@ -8,26 +8,23 @@ from opendevin.schema import ConfigType
class CommandManager: class CommandManager:
id: str id: str
directory: str
shell: Sandbox shell: Sandbox
def __init__( def __init__(
self, self,
sid: str, sid: str,
directory: str,
container_image: str | None = None, container_image: str | None = None,
): ):
self.directory = directory
sandbox_type = config.get(ConfigType.SANDBOX_TYPE).lower() sandbox_type = config.get(ConfigType.SANDBOX_TYPE).lower()
if sandbox_type == 'exec': if sandbox_type == 'exec':
self.shell = DockerExecBox( self.shell = DockerExecBox(
sid=(sid or 'default'), workspace_dir=directory, container_image=container_image sid=(sid or 'default'), container_image=container_image
) )
elif sandbox_type == 'local': elif sandbox_type == 'local':
self.shell = LocalBox(workspace_dir=directory) self.shell = LocalBox()
elif sandbox_type == 'ssh': elif sandbox_type == 'ssh':
self.shell = DockerSSHBox( self.shell = DockerSSHBox(
sid=(sid or 'default'), workspace_dir=directory, container_image=container_image sid=(sid or 'default'), container_image=container_image
) )
else: else:
raise ValueError(f'Invalid sandbox type: {sandbox_type}') raise ValueError(f'Invalid sandbox type: {sandbox_type}')

2
opendevin/download.py Normal file
View File

@@ -0,0 +1,2 @@
# Run this file to trigger a model download
import agenthub # noqa F401 (we import this to get the agents registered)

View File

@@ -29,7 +29,6 @@ def parse_arguments():
parser.add_argument( parser.add_argument(
'-d', '-d',
'--directory', '--directory',
required=True,
type=str, type=str,
help='The working directory for the agent', help='The working directory for the agent',
) )
@@ -70,7 +69,8 @@ def parse_arguments():
type=int, type=int,
help='The maximum number of characters to send to and receive from LLM per task', help='The maximum number of characters to send to and receive from LLM per task',
) )
return parser.parse_args() args, _ = parser.parse_known_args()
return args
async def main(): async def main():
@@ -80,12 +80,11 @@ async def main():
# Determine the task source # Determine the task source
if args.file: if args.file:
task = read_task_from_file(args.file) task = read_task_from_file(args.file)
elif args.task:
task = args.task
elif not sys.stdin.isatty(): elif not sys.stdin.isatty():
task = read_task_from_stdin() task = read_task_from_stdin()
else: else:
task = args.task
if not task:
raise ValueError( raise ValueError(
'No task provided. Please specify a task through -t, -f.') 'No task provided. Please specify a task through -t, -f.')
@@ -96,7 +95,7 @@ async def main():
AgentCls: Type[Agent] = Agent.get_cls(args.agent_cls) AgentCls: Type[Agent] = Agent.get_cls(args.agent_cls)
agent = AgentCls(llm=llm) agent = AgentCls(llm=llm)
controller = AgentController( controller = AgentController(
agent=agent, workdir=args.directory, max_iterations=args.max_iterations, max_chars=args.max_chars agent=agent, max_iterations=args.max_iterations, max_chars=args.max_chars
) )
await controller.start_loop(task) await controller.start_loop(task)

View File

@@ -18,9 +18,8 @@ from opendevin.exceptions import SandboxInvalidBackgroundCommandError
InputType = namedtuple('InputType', ['content']) InputType = namedtuple('InputType', ['content'])
OutputType = namedtuple('OutputType', ['content']) OutputType = namedtuple('OutputType', ['content'])
# helpful for docker-in-docker scenarios
DIRECTORY_REWRITE = config.get(ConfigType.DIRECTORY_REWRITE)
CONTAINER_IMAGE = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE) CONTAINER_IMAGE = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE)
SANDBOX_WORKSPACE_DIR = '/workspace'
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages # FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
# How do we make this more flexible? # How do we make this more flexible?
@@ -45,7 +44,6 @@ class DockerExecBox(Sandbox):
def __init__( def __init__(
self, self,
workspace_dir: str | None = None,
container_image: str | None = None, container_image: str | None = None,
timeout: int = 120, timeout: int = 120,
sid: str | None = None, sid: str | None = None,
@@ -59,21 +57,6 @@ class DockerExecBox(Sandbox):
raise ex raise ex
self.instance_id = sid if sid is not None else str(uuid.uuid4()) self.instance_id = sid if sid is not None else str(uuid.uuid4())
if workspace_dir is not None:
os.makedirs(workspace_dir, exist_ok=True)
# expand to absolute path
self.workspace_dir = os.path.abspath(workspace_dir)
else:
self.workspace_dir = os.getcwd()
logger.info(
'workspace unspecified, using current directory: %s', workspace_dir)
if DIRECTORY_REWRITE != '':
parts = DIRECTORY_REWRITE.split(':')
self.workspace_dir = self.workspace_dir.replace(parts[0], parts[1])
logger.info('Rewriting workspace directory to: %s',
self.workspace_dir)
else:
logger.info('Using workspace directory: %s', self.workspace_dir)
# TODO: this timeout is actually essential - need a better way to set it # TODO: this timeout is actually essential - need a better way to set it
# if it is too short, the container may still waiting for previous # if it is too short, the container may still waiting for previous
@@ -98,7 +81,7 @@ class DockerExecBox(Sandbox):
] ]
for cmd in cmds: for cmd in cmds:
exit_code, logs = self.container.exec_run( exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', cmd], workdir='/workspace' ['/bin/bash', '-c', cmd], workdir=SANDBOX_WORKSPACE_DIR
) )
if exit_code != 0: if exit_code != 0:
raise Exception(f'Failed to setup devin user: {logs}') raise Exception(f'Failed to setup devin user: {logs}')
@@ -118,7 +101,7 @@ class DockerExecBox(Sandbox):
def execute(self, cmd: str) -> Tuple[int, str]: def execute(self, cmd: str) -> Tuple[int, str]:
# TODO: each execute is not stateful! We need to keep track of the current working directory # TODO: each execute is not stateful! We need to keep track of the current working directory
def run_command(container, command): def run_command(container, command):
return container.exec_run(command, workdir='/workspace') return container.exec_run(command, workdir=SANDBOX_WORKSPACE_DIR)
# Use ThreadPoolExecutor to control command and set timeout # Use ThreadPoolExecutor to control command and set timeout
with concurrent.futures.ThreadPoolExecutor() as executor: with concurrent.futures.ThreadPoolExecutor() as executor:
@@ -133,13 +116,13 @@ class DockerExecBox(Sandbox):
pid = self.get_pid(cmd) pid = self.get_pid(cmd)
if pid is not None: if pid is not None:
self.container.exec_run( self.container.exec_run(
f'kill -9 {pid}', workdir='/workspace') f'kill -9 {pid}', workdir=SANDBOX_WORKSPACE_DIR)
return -1, f'Command: "{cmd}" timed out' return -1, f'Command: "{cmd}" timed out'
return exit_code, logs.decode('utf-8') return exit_code, logs.decode('utf-8')
def execute_in_background(self, cmd: str) -> BackgroundCommand: def execute_in_background(self, cmd: str) -> BackgroundCommand:
result = self.container.exec_run( result = self.container.exec_run(
self.get_exec_cmd(cmd), socket=True, workdir='/workspace' self.get_exec_cmd(cmd), socket=True, workdir=SANDBOX_WORKSPACE_DIR
) )
result.output._sock.setblocking(0) result.output._sock.setblocking(0)
pid = self.get_pid(cmd) pid = self.get_pid(cmd)
@@ -165,7 +148,7 @@ class DockerExecBox(Sandbox):
bg_cmd = self.background_commands[id] bg_cmd = self.background_commands[id]
if bg_cmd.pid is not None: if bg_cmd.pid is not None:
self.container.exec_run( self.container.exec_run(
f'kill -9 {bg_cmd.pid}', workdir='/workspace') f'kill -9 {bg_cmd.pid}', workdir=SANDBOX_WORKSPACE_DIR)
bg_cmd.result.output.close() bg_cmd.result.output.close()
self.background_commands.pop(id) self.background_commands.pop(id)
return bg_cmd return bg_cmd
@@ -206,15 +189,16 @@ class DockerExecBox(Sandbox):
try: try:
# start the container # start the container
mount_dir = config.get('WORKSPACE_MOUNT_PATH')
self.container = self.docker_client.containers.run( self.container = self.docker_client.containers.run(
self.container_image, self.container_image,
command='tail -f /dev/null', command='tail -f /dev/null',
network_mode='host', network_mode='host',
working_dir='/workspace', working_dir=SANDBOX_WORKSPACE_DIR,
name=self.container_name, name=self.container_name,
detach=True, detach=True,
volumes={self.workspace_dir: { volumes={mount_dir: {
'bind': '/workspace', 'mode': 'rw'}}, 'bind': SANDBOX_WORKSPACE_DIR, 'mode': 'rw'}},
) )
logger.info('Container started') logger.info('Container started')
except Exception as ex: except Exception as ex:
@@ -231,7 +215,8 @@ class DockerExecBox(Sandbox):
break break
time.sleep(1) time.sleep(1)
elapsed += 1 elapsed += 1
self.container = self.docker_client.containers.get(self.container_name) self.container = self.docker_client.containers.get(
self.container_name)
if elapsed > self.timeout: if elapsed > self.timeout:
break break
if self.container.status != 'running': if self.container.status != 'running':
@@ -249,23 +234,8 @@ class DockerExecBox(Sandbox):
if __name__ == '__main__': if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description='Interactive Docker container')
parser.add_argument(
'-d',
'--directory',
type=str,
default=None,
help='The directory to mount as the workspace in the Docker container.',
)
args = parser.parse_args()
try: try:
exec_box = DockerExecBox( exec_box = DockerExecBox()
workspace_dir=args.directory,
)
except Exception as e: except Exception as e:
logger.exception('Failed to start Docker container: %s', e) logger.exception('Failed to start Docker container: %s', e)
sys.exit(1) sys.exit(1)

View File

@@ -1,9 +1,8 @@
import subprocess import subprocess
import atexit import atexit
import os from typing import Tuple, Dict
from typing import Tuple, Dict, Optional
from opendevin.sandbox.sandbox import Sandbox, BackgroundCommand from opendevin.sandbox.sandbox import Sandbox, BackgroundCommand
from opendevin import config
# =============================================================================== # ===============================================================================
# ** WARNING ** # ** WARNING **
@@ -20,9 +19,9 @@ from opendevin.sandbox.sandbox import Sandbox, BackgroundCommand
# DO NOT USE THIS SANDBOX IN A PRODUCTION ENVIRONMENT # DO NOT USE THIS SANDBOX IN A PRODUCTION ENVIRONMENT
# =============================================================================== # ===============================================================================
class LocalBox(Sandbox): class LocalBox(Sandbox):
def __init__(self, workspace_dir: Optional[str] = None, timeout: int = 120): def __init__(self, timeout: int = 120):
self.workspace_dir = workspace_dir or os.getcwd()
self.timeout = timeout self.timeout = timeout
self.background_commands: Dict[int, BackgroundCommand] = {} self.background_commands: Dict[int, BackgroundCommand] = {}
self.cur_background_id = 0 self.cur_background_id = 0
@@ -32,7 +31,7 @@ class LocalBox(Sandbox):
try: try:
completed_process = subprocess.run( completed_process = subprocess.run(
cmd, shell=True, text=True, capture_output=True, cmd, shell=True, text=True, capture_output=True,
timeout=self.timeout, cwd=self.workspace_dir timeout=self.timeout, cwd=config.get('WORKSPACE_BASE')
) )
return completed_process.returncode, completed_process.stdout return completed_process.returncode, completed_process.stdout
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
@@ -41,7 +40,7 @@ class LocalBox(Sandbox):
def execute_in_background(self, cmd: str) -> BackgroundCommand: def execute_in_background(self, cmd: str) -> BackgroundCommand:
process = subprocess.Popen( process = subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, cwd=self.workspace_dir text=True, cwd=config.get('WORKSPACE_BASE')
) )
bg_cmd = BackgroundCommand( bg_cmd = BackgroundCommand(
id=self.cur_background_id, command=cmd, result=process, pid=process.pid id=self.cur_background_id, command=cmd, result=process, pid=process.pid

View File

@@ -20,10 +20,17 @@ from opendevin.exceptions import SandboxInvalidBackgroundCommandError
InputType = namedtuple('InputType', ['content']) InputType = namedtuple('InputType', ['content'])
OutputType = namedtuple('OutputType', ['content']) OutputType = namedtuple('OutputType', ['content'])
# helpful for docker-in-docker scenarios SANDBOX_WORKSPACE_DIR = '/workspace'
DIRECTORY_REWRITE = config.get(ConfigType.DIRECTORY_REWRITE)
CONTAINER_IMAGE = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE) CONTAINER_IMAGE = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE)
SSH_HOSTNAME = config.get(ConfigType.SSH_HOSTNAME)
USE_HOST_NETWORK = platform.system() == 'Linux'
if config.get(ConfigType.USE_HOST_NETWORK) is not None:
USE_HOST_NETWORK = config.get(
ConfigType.USE_HOST_NETWORK).lower() != 'false'
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages # FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
# How do we make this more flexible? # How do we make this more flexible?
RUN_AS_DEVIN = config.get('RUN_AS_DEVIN').lower() != 'false' RUN_AS_DEVIN = config.get('RUN_AS_DEVIN').lower() != 'false'
@@ -50,7 +57,6 @@ class DockerSSHBox(Sandbox):
def __init__( def __init__(
self, self,
workspace_dir: str | None = None,
container_image: str | None = None, container_image: str | None = None,
timeout: int = 120, timeout: int = 120,
sid: str | None = None, sid: str | None = None,
@@ -64,21 +70,6 @@ class DockerSSHBox(Sandbox):
raise ex raise ex
self.instance_id = sid if sid is not None else str(uuid.uuid4()) self.instance_id = sid if sid is not None else str(uuid.uuid4())
if workspace_dir is not None:
os.makedirs(workspace_dir, exist_ok=True)
# expand to absolute path
self.workspace_dir = os.path.abspath(workspace_dir)
else:
self.workspace_dir = os.getcwd()
logger.info(
'workspace unspecified, using current directory: %s', workspace_dir)
if DIRECTORY_REWRITE != '':
parts = DIRECTORY_REWRITE.split(':')
self.workspace_dir = self.workspace_dir.replace(parts[0], parts[1])
logger.info('Rewriting workspace directory to: %s',
self.workspace_dir)
else:
logger.info('Using workspace directory: %s', self.workspace_dir)
# TODO: this timeout is actually essential - need a better way to set it # TODO: this timeout is actually essential - need a better way to set it
# if it is too short, the container may still waiting for previous # if it is too short, the container may still waiting for previous
@@ -106,7 +97,7 @@ class DockerSSHBox(Sandbox):
exit_code, logs = self.container.exec_run( exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', ['/bin/bash', '-c',
r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"], r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"],
workdir='/workspace', workdir=SANDBOX_WORKSPACE_DIR,
) )
if exit_code != 0: if exit_code != 0:
raise Exception( raise Exception(
@@ -115,54 +106,54 @@ class DockerSSHBox(Sandbox):
# Check if the opendevin user exists # Check if the opendevin user exists
exit_code, logs = self.container.exec_run( exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', 'id -u opendevin'], ['/bin/bash', '-c', 'id -u opendevin'],
workdir='/workspace', workdir=SANDBOX_WORKSPACE_DIR,
) )
if exit_code == 0: if exit_code == 0:
# User exists, delete it # User exists, delete it
exit_code, logs = self.container.exec_run( exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', 'userdel -r opendevin'], ['/bin/bash', '-c', 'userdel -r opendevin'],
workdir='/workspace', workdir=SANDBOX_WORKSPACE_DIR,
) )
if exit_code != 0: if exit_code != 0:
raise Exception( raise Exception(
f'Failed to remove opendevin user in sandbox: {logs}') f'Failed to remove opendevin user in sandbox: {logs}')
# Create the opendevin user if RUN_AS_DEVIN:
exit_code, logs = self.container.exec_run( # Create the opendevin user
['/bin/bash', '-c', exit_code, logs = self.container.exec_run(
f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {USER_ID} opendevin'], ['/bin/bash', '-c',
workdir='/workspace', f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {USER_ID} opendevin'],
) workdir=SANDBOX_WORKSPACE_DIR,
if exit_code != 0: )
raise Exception( if exit_code != 0:
f'Failed to create opendevin user in sandbox: {logs}') raise Exception(
exit_code, logs = self.container.exec_run( f'Failed to create opendevin user in sandbox: {logs}')
['/bin/bash', '-c', exit_code, logs = self.container.exec_run(
f"echo 'opendevin:{self._ssh_password}' | chpasswd"], ['/bin/bash', '-c',
workdir='/workspace', f"echo 'opendevin:{self._ssh_password}' | chpasswd"],
) workdir=SANDBOX_WORKSPACE_DIR,
if exit_code != 0: )
raise Exception(f'Failed to set password in sandbox: {logs}') if exit_code != 0:
raise Exception(f'Failed to set password in sandbox: {logs}')
if not RUN_AS_DEVIN: else:
exit_code, logs = self.container.exec_run( exit_code, logs = self.container.exec_run(
# change password for root # change password for root
['/bin/bash', '-c', ['/bin/bash', '-c',
f"echo 'root:{self._ssh_password}' | chpasswd"], f"echo 'root:{self._ssh_password}' | chpasswd"],
workdir='/workspace', workdir=SANDBOX_WORKSPACE_DIR,
) )
if exit_code != 0: if exit_code != 0:
raise Exception( raise Exception(
f'Failed to set password for root in sandbox: {logs}') f'Failed to set password for root in sandbox: {logs}')
exit_code, logs = self.container.exec_run( exit_code, logs = self.container.exec_run(
['/bin/bash', '-c', "echo 'opendevin-sandbox' > /etc/hostname"], ['/bin/bash', '-c', "echo 'opendevin-sandbox' > /etc/hostname"],
workdir='/workspace', workdir=SANDBOX_WORKSPACE_DIR,
) )
def start_ssh_session(self): def start_ssh_session(self):
# start ssh session at the background # start ssh session at the background
self.ssh = pxssh.pxssh() self.ssh = pxssh.pxssh()
hostname = 'localhost' hostname = SSH_HOSTNAME
if RUN_AS_DEVIN: if RUN_AS_DEVIN:
username = 'opendevin' username = 'opendevin'
else: else:
@@ -217,7 +208,7 @@ class DockerSSHBox(Sandbox):
def execute_in_background(self, cmd: str) -> BackgroundCommand: def execute_in_background(self, cmd: str) -> BackgroundCommand:
result = self.container.exec_run( result = self.container.exec_run(
self.get_exec_cmd(cmd), socket=True, workdir='/workspace' self.get_exec_cmd(cmd), socket=True, workdir=SANDBOX_WORKSPACE_DIR
) )
result.output._sock.setblocking(0) result.output._sock.setblocking(0)
pid = self.get_pid(cmd) pid = self.get_pid(cmd)
@@ -243,7 +234,7 @@ class DockerSSHBox(Sandbox):
bg_cmd = self.background_commands[id] bg_cmd = self.background_commands[id]
if bg_cmd.pid is not None: if bg_cmd.pid is not None:
self.container.exec_run( self.container.exec_run(
f'kill -9 {bg_cmd.pid}', workdir='/workspace') f'kill -9 {bg_cmd.pid}', workdir=SANDBOX_WORKSPACE_DIR)
bg_cmd.result.output.close() bg_cmd.result.output.close()
self.background_commands.pop(id) self.background_commands.pop(id)
return bg_cmd return bg_cmd
@@ -284,9 +275,9 @@ class DockerSSHBox(Sandbox):
try: try:
network_kwargs: Dict[str, Union[str, Dict[str, int]]] = {} network_kwargs: Dict[str, Union[str, Dict[str, int]]] = {}
if platform.system() == 'Linux': if USE_HOST_NETWORK:
network_kwargs['network_mode'] = 'host' network_kwargs['network_mode'] = 'host'
elif platform.system() == 'Darwin': else:
# FIXME: This is a temporary workaround for Mac OS # FIXME: This is a temporary workaround for Mac OS
network_kwargs['ports'] = {'2222/tcp': self._ssh_port} network_kwargs['ports'] = {'2222/tcp': self._ssh_port}
logger.warning( logger.warning(
@@ -296,18 +287,24 @@ class DockerSSHBox(Sandbox):
) )
) )
mount_dir = config.get('WORKSPACE_MOUNT_PATH')
print('Mounting workspace directory: ', mount_dir)
# start the container # start the container
self.container = self.docker_client.containers.run( self.container = self.docker_client.containers.run(
self.container_image, self.container_image,
# allow root login # allow root login
command="/usr/sbin/sshd -D -p 2222 -o 'PermitRootLogin=yes'", command="/usr/sbin/sshd -D -p 2222 -o 'PermitRootLogin=yes'",
**network_kwargs, **network_kwargs,
working_dir='/workspace', working_dir=SANDBOX_WORKSPACE_DIR,
name=self.container_name, name=self.container_name,
hostname='opendevin_sandbox', hostname='opendevin_sandbox',
detach=True, detach=True,
volumes={self.workspace_dir: { volumes={
'bind': '/workspace', 'mode': 'rw'}}, mount_dir: {
'bind': SANDBOX_WORKSPACE_DIR,
'mode': 'rw'
},
},
) )
logger.info('Container started') logger.info('Container started')
except Exception as ex: except Exception as ex:
@@ -345,23 +342,9 @@ class DockerSSHBox(Sandbox):
if __name__ == '__main__': if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description='Interactive Docker container')
parser.add_argument(
'-d',
'--directory',
type=str,
default=None,
help='The directory to mount as the workspace in the Docker container.',
)
args = parser.parse_args()
try: try:
ssh_box = DockerSSHBox( ssh_box = DockerSSHBox()
workspace_dir=args.directory,
)
except Exception as e: except Exception as e:
logger.exception('Failed to start Docker container: %s', e) logger.exception('Failed to start Docker container: %s', e)
sys.exit(1) sys.exit(1)

View File

@@ -4,7 +4,9 @@ from enum import Enum
class ConfigType(str, Enum): class ConfigType(str, Enum):
LLM_API_KEY = 'LLM_API_KEY' LLM_API_KEY = 'LLM_API_KEY'
LLM_BASE_URL = 'LLM_BASE_URL' LLM_BASE_URL = 'LLM_BASE_URL'
WORKSPACE_DIR = 'WORKSPACE_DIR' WORKSPACE_BASE = 'WORKSPACE_BASE'
WORKSPACE_MOUNT_PATH = 'WORKSPACE_MOUNT_PATH'
WORKSPACE_MOUNT_REWRITE = 'WORKSPACE_MOUNT_REWRITE'
LLM_MODEL = 'LLM_MODEL' LLM_MODEL = 'LLM_MODEL'
SANDBOX_CONTAINER_IMAGE = 'SANDBOX_CONTAINER_IMAGE' SANDBOX_CONTAINER_IMAGE = 'SANDBOX_CONTAINER_IMAGE'
RUN_AS_DEVIN = 'RUN_AS_DEVIN' RUN_AS_DEVIN = 'RUN_AS_DEVIN'
@@ -13,9 +15,10 @@ class ConfigType(str, Enum):
LLM_API_VERSION = 'LLM_API_VERSION' LLM_API_VERSION = 'LLM_API_VERSION'
LLM_NUM_RETRIES = 'LLM_NUM_RETRIES' LLM_NUM_RETRIES = 'LLM_NUM_RETRIES'
LLM_COOLDOWN_TIME = 'LLM_COOLDOWN_TIME' LLM_COOLDOWN_TIME = 'LLM_COOLDOWN_TIME'
DIRECTORY_REWRITE = 'DIRECTORY_REWRITE'
MAX_ITERATIONS = 'MAX_ITERATIONS' MAX_ITERATIONS = 'MAX_ITERATIONS'
MAX_CHARS = 'MAX_CHARS' MAX_CHARS = 'MAX_CHARS'
AGENT = 'AGENT' AGENT = 'AGENT'
SANDBOX_TYPE = 'SANDBOX_TYPE' SANDBOX_TYPE = 'SANDBOX_TYPE'
USE_HOST_NETWORK = 'USE_HOST_NETWORK'
SSH_HOSTNAME = 'SSH_HOSTNAME'
DISABLE_COLOR = 'DISABLE_COLOR' DISABLE_COLOR = 'DISABLE_COLOR'

View File

@@ -25,7 +25,7 @@ websocat ws://127.0.0.1:3000/ws
```sh ```sh
LLM_API_KEY=sk-... # Your OpenAI API Key LLM_API_KEY=sk-... # Your OpenAI API Key
LLM_MODEL=gpt-3.5-turbo-1106 # Default model for the agent to use LLM_MODEL=gpt-3.5-turbo-1106 # Default model for the agent to use
WORKSPACE_DIR=/path/to/your/workspace # Default path to model's workspace WORKSPACE_BASE=/path/to/your/workspace # Default path to model's workspace
``` ```
## API Schema ## API Schema

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
import os
from typing import Optional from typing import Optional
from opendevin import config from opendevin import config
@@ -105,7 +104,6 @@ class AgentUnit:
for key, value in start_event.get('args', {}).items() for key, value in start_event.get('args', {}).items()
if value != '' if value != ''
} # remove empty values, prevent FE from sending empty strings } # remove empty values, prevent FE from sending empty strings
directory = self.get_arg_or_default(args, ConfigType.WORKSPACE_DIR)
agent_cls = self.get_arg_or_default(args, ConfigType.AGENT) agent_cls = self.get_arg_or_default(args, ConfigType.AGENT)
model = self.get_arg_or_default(args, ConfigType.LLM_MODEL) model = self.get_arg_or_default(args, ConfigType.LLM_MODEL)
api_key = config.get(ConfigType.LLM_API_KEY) api_key = config.get(ConfigType.LLM_API_KEY)
@@ -115,18 +113,11 @@ class AgentUnit:
args, ConfigType.MAX_ITERATIONS) args, ConfigType.MAX_ITERATIONS)
max_chars = self.get_arg_or_default(args, ConfigType.MAX_CHARS) max_chars = self.get_arg_or_default(args, ConfigType.MAX_CHARS)
if not os.path.exists(directory):
logger.info(
'Workspace directory %s does not exist. Creating it...', directory
)
os.makedirs(directory)
directory = os.path.relpath(directory, os.getcwd())
llm = LLM(model=model, api_key=api_key, base_url=api_base) llm = LLM(model=model, api_key=api_key, base_url=api_base)
try: try:
self.controller = AgentController( self.controller = AgentController(
sid=self.sid, sid=self.sid,
agent=Agent.get_cls(agent_cls)(llm), agent=Agent.get_cls(agent_cls)(llm),
workdir=directory,
max_iterations=int(max_iterations), max_iterations=int(max_iterations),
max_chars=int(max_chars), max_chars=int(max_chars),
container_image=container_image, container_image=container_image,

View File

@@ -5,6 +5,8 @@ import litellm
from fastapi import Depends, FastAPI, WebSocket from fastapi import Depends, FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from starlette import status from starlette import status
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
@@ -41,7 +43,7 @@ async def websocket_endpoint(websocket: WebSocket):
await session_manager.loop_recv(sid, agent_manager.dispatch) await session_manager.loop_recv(sid, agent_manager.dispatch)
@app.get('/litellm-models') @app.get('/api/litellm-models')
async def get_litellm_models(): async def get_litellm_models():
""" """
Get all models supported by LiteLLM. Get all models supported by LiteLLM.
@@ -49,7 +51,7 @@ async def get_litellm_models():
return list(set(litellm.model_list + list(litellm.model_cost.keys()))) return list(set(litellm.model_list + list(litellm.model_cost.keys())))
@app.get('/litellm-agents') @app.get('/api/litellm-agents')
async def get_litellm_agents(): async def get_litellm_agents():
""" """
Get all agents supported by LiteLLM. Get all agents supported by LiteLLM.
@@ -57,7 +59,7 @@ async def get_litellm_agents():
return Agent.list_agents() return Agent.list_agents()
@app.get('/auth') @app.get('/api/auth')
async def get_token( async def get_token(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme), credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
): ):
@@ -72,7 +74,7 @@ async def get_token(
) )
@app.get('/messages') @app.get('/api/messages')
async def get_messages( async def get_messages(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme), credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
): ):
@@ -87,7 +89,7 @@ async def get_messages(
) )
@app.get('/messages/total') @app.get('/api/messages/total')
async def get_message_total( async def get_message_total(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme), credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
): ):
@@ -110,20 +112,28 @@ async def del_messages(
) )
@app.get('/configurations') @app.get('/api/configurations')
def read_default_model(): def read_default_model():
return config.get_fe_config() return config.get_fe_config()
@app.get('/refresh-files') @app.get('/api/refresh-files')
def refresh_files(): def refresh_files():
structure = files.get_folder_structure( structure = files.get_folder_structure(
Path(str(config.get('WORKSPACE_DIR')))) Path(str(config.get('WORKSPACE_BASE'))))
return structure.to_dict() return structure.to_dict()
@app.get('/select-file') @app.get('/api/select-file')
def select_file(file: str): def select_file(file: str):
with open(Path(Path(str(config.get('WORKSPACE_DIR'))), file), 'r') as selected_file: with open(Path(Path(str(config.get('WORKSPACE_BASE'))), file), 'r') as selected_file:
content = selected_file.read() content = selected_file.read()
return {'code': content} return {'code': content}
@app.get('/')
async def docs_redirect():
response = RedirectResponse(url='/index.html')
return response
app.mount('/', StaticFiles(directory='./frontend/dist'), name='dist')