diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 9fb4043e39..d5cfccfc36 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -7,11 +7,11 @@ import Errors from "./components/Errors";
import SettingModal from "./components/SettingModal";
import Terminal from "./components/Terminal";
import Workspace from "./components/Workspace";
-import store, { RootState } from "./store";
-import { setInitialized } from "./state/globalSlice";
import { fetchMsgTotal } from "./services/session";
import LoadMessageModal from "./components/LoadMessageModal";
-import { ResFetchMsgTotal } from "./types/ResponseType";
+import { ResConfigurations, ResFetchMsgTotal } from "./types/ResponseType";
+import { fetchConfigurations, saveSettings } from "./services/settingsService";
+import { RootState } from "./store";
interface Props {
setSettingOpen: (isOpen: boolean) => void;
@@ -30,22 +30,38 @@ function LeftNav({ setSettingOpen }: Props): JSX.Element {
);
}
+// React.StrictMode will cause double rendering, use this to prevent it
+let initOnce = false;
+
function App(): JSX.Element {
- const { initialized } = useSelector((state: RootState) => state.global);
const [settingOpen, setSettingOpen] = useState(false);
const [loadMsgWarning, setLoadMsgWarning] = useState(false);
+ const settings = useSelector((state: RootState) => state.settings);
useEffect(() => {
- if (!initialized) {
- fetchMsgTotal()
- .then((data: ResFetchMsgTotal) => {
- if (data.msg_total > 0) {
- setLoadMsgWarning(true);
- }
- store.dispatch(setInitialized(true));
- })
- .catch();
- }
+ if (initOnce) return;
+ initOnce = true;
+ // only fetch configurations in the first time
+ fetchConfigurations()
+ .then((data: ResConfigurations) => {
+ saveSettings(
+ Object.fromEntries(
+ Object.entries(data).map(([key, value]) => [key, String(value)]),
+ ),
+ Object.fromEntries(
+ Object.entries(settings).map(([key, value]) => [key, value]),
+ ),
+ true,
+ );
+ })
+ .catch();
+ fetchMsgTotal()
+ .then((data: ResFetchMsgTotal) => {
+ if (data.msg_total > 0) {
+ setLoadMsgWarning(true);
+ }
+ })
+ .catch();
}, []);
const handleCloseModal = () => {
diff --git a/frontend/src/components/SettingModal.tsx b/frontend/src/components/SettingModal.tsx
index 4eed24fc1d..60e7261366 100644
--- a/frontend/src/components/SettingModal.tsx
+++ b/frontend/src/components/SettingModal.tsx
@@ -1,31 +1,31 @@
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
- Modal,
- ModalContent,
- ModalHeader,
- ModalBody,
- ModalFooter,
- Input,
- Button,
Autocomplete,
AutocompleteItem,
+ Button,
+ Input,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
Select,
SelectItem,
} from "@nextui-org/react";
import { KeyboardEvent } from "@react-types/shared/src/events";
import { useTranslation } from "react-i18next";
import {
- INITIAL_AGENTS,
- fetchModels,
fetchAgents,
+ fetchModels,
+ INITIAL_AGENTS,
INITIAL_MODELS,
saveSettings,
- getInitialModel,
} from "../services/settingsService";
import { RootState } from "../store";
import { I18nKey } from "../i18n/declaration";
import { AvailableLanguages } from "../i18n";
+import ArgConfigType from "../types/ConfigType";
interface Props {
isOpen: boolean;
@@ -39,21 +39,17 @@ const cachedAgents = JSON.parse(
localStorage.getItem("supportedAgents") || "[]",
);
-function SettingModal({ isOpen, onClose }: Props): JSX.Element {
- const defModel = useSelector((state: RootState) => state.settings.model);
- const [model, setModel] = useState(defModel);
- const defAgent = useSelector((state: RootState) => state.settings.agent);
- const [agent, setAgent] = useState(defAgent);
- const defWorkspaceDirectory = useSelector(
- (state: RootState) => state.settings.workspaceDirectory,
+function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
+ const settings = useSelector((state: RootState) => state.settings);
+ const [model, setModel] = useState(settings[ArgConfigType.LLM_MODEL]);
+ const [inputModel, setInputModel] = useState(
+ settings[ArgConfigType.LLM_MODEL],
);
+ const [agent, setAgent] = useState(settings[ArgConfigType.AGENT]);
const [workspaceDirectory, setWorkspaceDirectory] = useState(
- defWorkspaceDirectory,
+ settings[ArgConfigType.WORKSPACE_DIR],
);
- const defLanguage = useSelector(
- (state: RootState) => state.settings.language,
- );
- const [language, setLanguage] = useState(defLanguage);
+ const [language, setLanguage] = useState(settings[ArgConfigType.LANGUAGE]);
const { t } = useTranslation();
@@ -65,12 +61,6 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
);
useEffect(() => {
- getInitialModel()
- .then((initialModel) => {
- setModel(initialModel);
- })
- .catch();
-
fetchModels().then((fetchedModels) => {
const sortedModels = fetchedModels.sort(); // Sorting the models alphabetically
setSupportedModels(sortedModels);
@@ -85,10 +75,16 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
const handleSaveCfg = () => {
saveSettings(
- { model, agent, workspaceDirectory, language },
- model !== defModel &&
- agent !== defAgent &&
- workspaceDirectory !== defWorkspaceDirectory,
+ {
+ [ArgConfigType.LLM_MODEL]: model ?? inputModel,
+ [ArgConfigType.AGENT]: agent,
+ [ArgConfigType.WORKSPACE_DIR]: workspaceDirectory,
+ [ArgConfigType.LANGUAGE]: language,
+ },
+ Object.fromEntries(
+ Object.entries(settings).map(([key, value]) => [key, value]),
+ ),
+ false,
);
onClose();
};
@@ -127,9 +123,10 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
onSelectionChange={(key) => {
setModel(key as string);
}}
+ onInputChange={(e) => setInputModel(e)}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
- defaultInputValue={model}
+ defaultInputValue={inputModel}
allowsCustomValue
>
{(item: { label: string; value: string }) => (
@@ -187,4 +184,10 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
);
}
+function SettingModal({ isOpen, onClose }: Props): JSX.Element {
+ // Do not render the modal if it is not open, prevents reading empty from localStorage after initialization
+ if (!isOpen) return
;
+ return ;
+}
+
export default SettingModal;
diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts
index dbdb3e50e3..4b09339396 100644
--- a/frontend/src/i18n/index.ts
+++ b/frontend/src/i18n/index.ts
@@ -2,6 +2,7 @@ import i18n from "i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
+import ArgConfigType from "../types/ConfigType";
export const AvailableLanguages = [
{ label: "English", value: "en" },
@@ -21,14 +22,14 @@ i18n
// assume all detected languages are available
const detectLanguage = i18n.language;
// cannot trust browser language setting
- const settingLanguage = localStorage.getItem("language");
+ const settingLanguage = localStorage.getItem(ArgConfigType.LANGUAGE);
// if setting is not initialized, but detected language is available, use detected language and update language setting
if (
!settingLanguage &&
AvailableLanguages.some((lang) => detectLanguage === lang.value)
) {
- localStorage.setItem("language", detectLanguage);
+ localStorage.setItem(ArgConfigType.LANGUAGE, detectLanguage);
i18n.changeLanguage(detectLanguage);
return;
}
diff --git a/frontend/src/services/settingsService.ts b/frontend/src/services/settingsService.ts
index 3d1bbfc247..6bf4dc9661 100644
--- a/frontend/src/services/settingsService.ts
+++ b/frontend/src/services/settingsService.ts
@@ -3,20 +3,20 @@ import { setInitialized } from "../state/taskSlice";
import store from "../store";
import ActionType from "../types/ActionType";
import Socket from "./socket";
-import {
- setAgent,
- setLanguage,
- setModel,
- setWorkspaceDirectory,
-} from "../state/settingsSlice";
+import { setByKey } from "../state/settingsSlice";
+import { ResConfigurations } from "../types/ResponseType";
+import ArgConfigType from "../types/ConfigType";
-export async function getInitialModel() {
- if (localStorage.getItem("model")) {
- return localStorage.getItem("model");
+export async function fetchConfigurations(): Promise {
+ const headers = new Headers({
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ });
+ const response = await fetch(`/api/configurations`, { headers });
+ if (response.status !== 200) {
+ throw new Error("Get configurations failed.");
}
-
- const res = await fetch("/api/default-model");
- return res.json();
+ return (await response.json()) as ResConfigurations;
}
export async function fetchModels() {
@@ -43,36 +43,53 @@ export const INITIAL_AGENTS = ["MonologueAgent", "CodeActAgent"];
export type Agent = (typeof INITIAL_AGENTS)[number];
-// Map Redux settings to socket event arguments
-const SETTINGS_MAP = new Map([
- ["model", "model"],
- ["agent", "agent_cls"],
- ["workspaceDirectory", "directory"],
+// TODO: add the values to i18n to support multi languages
+const DISPLAY_MAP = new Map([
+ [ArgConfigType.LLM_MODEL, "model"],
+ [ArgConfigType.AGENT, "agent"],
+ [ArgConfigType.WORKSPACE_DIR, "directory"],
+ [ArgConfigType.LANGUAGE, "language"],
]);
// Send settings to the server
export function saveSettings(
- reduxSettings: { [id: string]: string },
- needToSend: boolean = false,
+ newSettings: { [key: string]: string },
+ oldSettings: { [key: string]: string },
+ isInit: boolean = false,
): void {
- if (needToSend) {
- const socketSettings = Object.fromEntries(
- Object.entries(reduxSettings).map(([setting, value]) => [
- SETTINGS_MAP.get(setting) || setting,
- value,
- ]),
- );
- const event = { action: ActionType.INIT, args: socketSettings };
+ let needToSend = false;
+ const updatedSettings: { [key: string]: string } = {};
+ const mergedSettings = { ...oldSettings, ...newSettings };
+ Object.keys(newSettings).forEach((key) => {
+ if (
+ Object.hasOwnProperty.call(oldSettings, key) &&
+ oldSettings[key] !== String(newSettings[key])
+ ) {
+ if (isInit && oldSettings[key] !== "") {
+ mergedSettings[key] = oldSettings[key];
+ return;
+ }
+ needToSend = true;
+ updatedSettings[key] = String(newSettings[key]);
+ mergedSettings[key] = String(newSettings[key]);
+ } else {
+ mergedSettings[key] = oldSettings[key];
+ }
+ });
+
+ if (needToSend || isInit) {
+ const event = { action: ActionType.INIT, args: mergedSettings };
const eventString = JSON.stringify(event);
store.dispatch(setInitialized(false));
Socket.send(eventString);
}
- for (const [setting, value] of Object.entries(reduxSettings)) {
- localStorage.setItem(setting, value);
- store.dispatch(appendAssistantMessage(`Set ${setting} to "${value}"`));
+
+ for (const [key, value] of Object.entries(updatedSettings)) {
+ if (DISPLAY_MAP.has(key)) {
+ store.dispatch(setByKey({ key, value }));
+ store.dispatch(
+ appendAssistantMessage(`Set ${DISPLAY_MAP.get(key)} to "${value}"`),
+ );
+ }
}
- store.dispatch(setModel(reduxSettings.model));
- store.dispatch(setAgent(reduxSettings.agent));
- store.dispatch(setWorkspaceDirectory(reduxSettings.workspaceDirectory));
- store.dispatch(setLanguage(reduxSettings.language));
}
diff --git a/frontend/src/services/socket.ts b/frontend/src/services/socket.ts
index f2dc5098d5..5755309533 100644
--- a/frontend/src/services/socket.ts
+++ b/frontend/src/services/socket.ts
@@ -2,7 +2,6 @@ import store from "../store";
import { appendError, removeError } from "../state/errorsSlice";
import { handleAssistantMessage } from "./actions";
import { getToken } from "./auth";
-import ActionType from "../types/ActionType";
class Socket {
private static _socket: WebSocket | null = null;
@@ -26,22 +25,7 @@ class Socket {
const WS_URL = `ws://${window.location.host}/ws?token=${token}`;
Socket._socket = new WebSocket(WS_URL);
- Socket._socket.onopen = () => {
- const model = localStorage.getItem("model") || "gpt-3.5-turbo-1106";
- const agent = localStorage.getItem("agent") || "MonologueAgent";
- const workspaceDirectory =
- localStorage.getItem("workspaceDirectory") || "./workspace";
- Socket._socket?.send(
- JSON.stringify({
- action: ActionType.INIT,
- args: {
- model,
- agent_cls: agent,
- directory: workspaceDirectory,
- },
- }),
- );
- };
+ Socket._socket.onopen = () => {};
Socket._socket.onmessage = (e) => {
handleAssistantMessage(e.data);
diff --git a/frontend/src/state/globalSlice.ts b/frontend/src/state/globalSlice.ts
deleted file mode 100644
index 6be8cde206..0000000000
--- a/frontend/src/state/globalSlice.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-
-export const globalSlice = createSlice({
- name: "global",
- initialState: {
- initialized: false,
- },
- reducers: {
- setInitialized: (state, action) => {
- state.initialized = action.payload;
- },
- },
-});
-
-export const { setInitialized } = globalSlice.actions;
-
-export default globalSlice.reducer;
diff --git a/frontend/src/state/settingsSlice.ts b/frontend/src/state/settingsSlice.ts
index 03b4c08198..482aa93248 100644
--- a/frontend/src/state/settingsSlice.ts
+++ b/frontend/src/state/settingsSlice.ts
@@ -1,37 +1,31 @@
import { createSlice } from "@reduxjs/toolkit";
import i18next from "i18next";
+import ArgConfigType from "../types/ConfigType";
export const settingsSlice = createSlice({
name: "settings",
initialState: {
- model: localStorage.getItem("model") || "gpt-3.5-turbo-1106",
- agent: localStorage.getItem("agent") || "MonologueAgent",
- workspaceDirectory:
- localStorage.getItem("workspaceDirectory") || "./workspace",
- language: localStorage.getItem("language") || "en",
- },
+ [ArgConfigType.LLM_MODEL]:
+ localStorage.getItem(ArgConfigType.LLM_MODEL) || "",
+ [ArgConfigType.AGENT]: localStorage.getItem(ArgConfigType.AGENT) || "",
+ [ArgConfigType.WORKSPACE_DIR]:
+ localStorage.getItem(ArgConfigType.WORKSPACE_DIR) || "",
+ [ArgConfigType.LANGUAGE]:
+ localStorage.getItem(ArgConfigType.LANGUAGE) || "en",
+ } as { [key: string]: string },
reducers: {
- setModel: (state, action) => {
- localStorage.setItem("model", action.payload);
- state.model = action.payload;
- },
- setAgent: (state, action) => {
- localStorage.setItem("agent", action.payload);
- state.agent = action.payload;
- },
- setWorkspaceDirectory: (state, action) => {
- localStorage.setItem("workspaceDirectory", action.payload);
- state.workspaceDirectory = action.payload;
- },
- setLanguage: (state, action) => {
- localStorage.setItem("language", action.payload);
- state.language = action.payload;
- i18next.changeLanguage(action.payload);
+ setByKey: (state, action) => {
+ const { key, value } = action.payload;
+ state[key] = value;
+ localStorage.setItem(key, value);
+ // language is a special case for now.
+ if (key === ArgConfigType.LANGUAGE) {
+ i18next.changeLanguage(value);
+ }
},
},
});
-export const { setModel, setAgent, setWorkspaceDirectory, setLanguage } =
- settingsSlice.actions;
+export const { setByKey } = settingsSlice.actions;
export default settingsSlice.reducer;
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index 21ab5336ec..b0a54a7467 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -5,7 +5,6 @@ import codeReducer from "./state/codeSlice";
import commandReducer from "./state/commandSlice";
import taskReducer from "./state/taskSlice";
import errorsReducer from "./state/errorsSlice";
-import globalReducer from "./state/globalSlice";
import settingsReducer from "./state/settingsSlice";
const store = configureStore({
@@ -16,7 +15,6 @@ const store = configureStore({
cmd: commandReducer,
task: taskReducer,
errors: errorsReducer,
- global: globalReducer,
settings: settingsReducer,
},
});
diff --git a/frontend/src/types/ConfigType.tsx b/frontend/src/types/ConfigType.tsx
new file mode 100644
index 0000000000..448b62ae02
--- /dev/null
+++ b/frontend/src/types/ConfigType.tsx
@@ -0,0 +1,18 @@
+enum ArgConfigType {
+ LLM_API_KEY = "LLM_API_KEY",
+ LLM_BASE_URL = "LLM_BASE_URL",
+ WORKSPACE_DIR = "WORKSPACE_DIR",
+ LLM_MODEL = "LLM_MODEL",
+ SANDBOX_CONTAINER_IMAGE = "SANDBOX_CONTAINER_IMAGE",
+ RUN_AS_DEVIN = "RUN_AS_DEVIN",
+ LLM_EMBEDDING_MODEL = "LLM_EMBEDDING_MODEL",
+ LLM_NUM_RETRIES = "LLM_NUM_RETRIES",
+ LLM_COOLDOWN_TIME = "LLM_COOLDOWN_TIME",
+ DIRECTORY_REWRITE = "DIRECTORY_REWRITE",
+ MAX_ITERATIONS = "MAX_ITERATIONS",
+ AGENT = "AGENT",
+
+ LANGUAGE = "LANGUAGE",
+}
+
+export default ArgConfigType;
diff --git a/frontend/src/types/ResponseType.tsx b/frontend/src/types/ResponseType.tsx
index 883840cab5..73b6c9a7e4 100644
--- a/frontend/src/types/ResponseType.tsx
+++ b/frontend/src/types/ResponseType.tsx
@@ -1,5 +1,9 @@
import { ActionMessage, ObservationMessage } from "./Message";
+interface ResConfigurations {
+ [key: string]: string | boolean | number;
+}
+
interface ResFetchToken {
token: string;
}
@@ -25,6 +29,7 @@ interface ResDelMsg {
type SocketMessage = ActionMessage | ObservationMessage;
export {
+ type ResConfigurations,
type ResFetchToken,
type ResFetchMsgTotal,
type ResFetchMsg,
diff --git a/opendevin/config.py b/opendevin/config.py
index 643ef607d3..bde1959b1a 100644
--- a/opendevin/config.py
+++ b/opendevin/config.py
@@ -1,22 +1,26 @@
+import copy
import os
-import toml
+import toml
from dotenv import load_dotenv
+from opendevin.schema import ConfigType
+
load_dotenv()
-DEFAULT_CONFIG = {
- 'LLM_API_KEY': None,
- 'LLM_BASE_URL': None,
- 'WORKSPACE_DIR': os.path.join(os.getcwd(), 'workspace'),
- 'LLM_MODEL': 'gpt-3.5-turbo-1106',
- 'SANDBOX_CONTAINER_IMAGE': 'ghcr.io/opendevin/sandbox',
- 'RUN_AS_DEVIN': 'false',
- 'LLM_EMBEDDING_MODEL': 'local',
- 'LLM_NUM_RETRIES': 6,
- 'LLM_COOLDOWN_TIME': 1,
- 'DIRECTORY_REWRITE': '',
- 'MAX_ITERATIONS': 100,
+DEFAULT_CONFIG: dict = {
+ ConfigType.LLM_API_KEY: None,
+ ConfigType.LLM_BASE_URL: None,
+ ConfigType.WORKSPACE_DIR: os.path.join(os.getcwd(), 'workspace'),
+ ConfigType.LLM_MODEL: 'gpt-3.5-turbo-1106',
+ ConfigType.SANDBOX_CONTAINER_IMAGE: 'ghcr.io/opendevin/sandbox',
+ ConfigType.RUN_AS_DEVIN: 'false',
+ ConfigType.LLM_EMBEDDING_MODEL: 'local',
+ ConfigType.LLM_NUM_RETRIES: 6,
+ ConfigType.LLM_COOLDOWN_TIME: 1,
+ ConfigType.DIRECTORY_REWRITE: '',
+ ConfigType.MAX_ITERATIONS: 100,
+ ConfigType.AGENT: 'MonologueAgent',
}
config_str = ''
@@ -26,11 +30,11 @@ if os.path.exists('config.toml'):
tomlConfig = toml.loads(config_str)
config = DEFAULT_CONFIG.copy()
-for key, value in config.items():
- if key in os.environ:
- config[key] = os.environ[key]
- elif key in tomlConfig:
- config[key] = tomlConfig[key]
+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 _get(key: str, default):
@@ -69,3 +73,10 @@ def get(key: str):
Get a key from the config, please make sure it exists.
"""
return config.get(key)
+
+
+def get_all() -> dict:
+ """
+ Get all the configuration values by performing a deep copy.
+ """
+ return copy.deepcopy(config)
diff --git a/opendevin/mock/listen.py b/opendevin/mock/listen.py
index ca722d1c72..47493a7d8f 100644
--- a/opendevin/mock/listen.py
+++ b/opendevin/mock/listen.py
@@ -1,16 +1,17 @@
import uvicorn
from fastapi import FastAPI, WebSocket
+
from opendevin.schema import ActionType
app = FastAPI()
-@app.websocket("/ws")
+@app.websocket('/ws')
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
# send message to mock connection
await websocket.send_json(
- {"action": ActionType.INIT, "message": "Control loop started."}
+ {'action': ActionType.INIT, 'message': 'Control loop started.'}
)
try:
@@ -20,41 +21,36 @@ async def websocket_endpoint(websocket: WebSocket):
print(f"Received message: {data}")
# send mock response to client
- response = {"message": f"receive {data}"}
+ response = {'message': f"receive {data}"}
await websocket.send_json(response)
print(f"Sent message: {response}")
except Exception as e:
print(f"WebSocket Error: {e}")
-@app.get("/")
+@app.get('/')
def read_root():
- return {"message": "This is a mock server"}
+ return {'message': 'This is a mock server'}
-@app.get("/litellm-models")
+@app.get('/litellm-models')
def read_llm_models():
return [
- "gpt-4",
- "gpt-4-turbo-preview",
- "gpt-4-0314",
- "gpt-4-0613",
+ 'gpt-4',
+ 'gpt-4-turbo-preview',
+ 'gpt-4-0314',
+ 'gpt-4-0613',
]
-@app.get("/litellm-agents")
+@app.get('/litellm-agents')
def read_llm_agents():
return [
- "MonologueAgent",
- "CodeActAgent",
- "PlannerAgent",
+ 'MonologueAgent',
+ 'CodeActAgent',
+ 'PlannerAgent',
]
-@app.get("/default-model")
-def read_default_model():
- return "gpt-4"
-
-
-if __name__ == "__main__":
- uvicorn.run(app, host="127.0.0.1", port=3000)
+if __name__ == '__main__':
+ uvicorn.run(app, host='127.0.0.1', port=3000)
diff --git a/opendevin/schema/__init__.py b/opendevin/schema/__init__.py
index 9bfd2a29a9..16c69a9c63 100644
--- a/opendevin/schema/__init__.py
+++ b/opendevin/schema/__init__.py
@@ -1,4 +1,5 @@
from .action import ActionType
+from .config import ConfigType
from .observation import ObservationType
-__all__ = ["ActionType", "ObservationType"]
+__all__ = ['ActionType', 'ObservationType', 'ConfigType']
diff --git a/opendevin/schema/config.py b/opendevin/schema/config.py
new file mode 100644
index 0000000000..badb081ca1
--- /dev/null
+++ b/opendevin/schema/config.py
@@ -0,0 +1,16 @@
+from enum import Enum
+
+
+class ConfigType(str, Enum):
+ LLM_API_KEY = 'LLM_API_KEY'
+ LLM_BASE_URL = 'LLM_BASE_URL'
+ WORKSPACE_DIR = 'WORKSPACE_DIR'
+ LLM_MODEL = 'LLM_MODEL'
+ SANDBOX_CONTAINER_IMAGE = 'SANDBOX_CONTAINER_IMAGE'
+ RUN_AS_DEVIN = 'RUN_AS_DEVIN'
+ LLM_EMBEDDING_MODEL = 'LLM_EMBEDDING_MODEL'
+ LLM_NUM_RETRIES = 'LLM_NUM_RETRIES'
+ LLM_COOLDOWN_TIME = 'LLM_COOLDOWN_TIME'
+ DIRECTORY_REWRITE = 'DIRECTORY_REWRITE'
+ MAX_ITERATIONS = 'MAX_ITERATIONS'
+ AGENT = 'AGENT'
diff --git a/opendevin/server/agent/manager.py b/opendevin/server/agent/manager.py
index 2dbbe272ad..68a6ee9e7d 100644
--- a/opendevin/server/agent/manager.py
+++ b/opendevin/server/agent/manager.py
@@ -10,17 +10,10 @@ from opendevin.action import (
from opendevin.agent import Agent
from opendevin.controller import AgentController
from opendevin.llm.llm import LLM
-from opendevin.observation import NullObservation, Observation, UserMessageObservation
-from opendevin.server.session import session_manager
-from opendevin.schema import ActionType
from opendevin.logging import opendevin_logger as logger
-
-DEFAULT_API_KEY = config.get("LLM_API_KEY")
-DEFAULT_BASE_URL = config.get("LLM_BASE_URL")
-DEFAULT_WORKSPACE_DIR = config.get("WORKSPACE_DIR")
-LLM_MODEL = config.get("LLM_MODEL")
-CONTAINER_IMAGE = config.get("SANDBOX_CONTAINER_IMAGE")
-MAX_ITERATIONS = config.get("MAX_ITERATIONS")
+from opendevin.observation import NullObservation, Observation, UserMessageObservation
+from opendevin.schema import ActionType, ConfigType
+from opendevin.server.session import session_manager
class AgentManager:
@@ -68,7 +61,7 @@ class AgentManager:
async def dispatch(self, action: str | None, data: dict):
"""Dispatches actions to the agent from the client."""
if action is None:
- await self.send_error("Invalid action")
+ await self.send_error('Invalid action')
return
if action == ActionType.INIT:
@@ -77,52 +70,52 @@ class AgentManager:
await self.start_task(data)
else:
if self.controller is None:
- await self.send_error("No agent started. Please wait a second...")
+ await self.send_error('No agent started. Please wait a second...')
elif action == ActionType.CHAT:
self.controller.add_history(
- NullAction(), UserMessageObservation(data["message"])
+ NullAction(), UserMessageObservation(data['message'])
)
else:
await self.send_error("I didn't recognize this action:" + action)
- async def create_controller(self, start_event=None):
+ 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.
Args:
start_event: The start event data (optional).
"""
- directory = DEFAULT_WORKSPACE_DIR
- if start_event and "directory" in start_event["args"]:
- directory = start_event["args"]["directory"]
- agent_cls = "MonologueAgent"
- if start_event and "agent_cls" in start_event["args"]:
- agent_cls = start_event["args"]["agent_cls"]
- model = LLM_MODEL
- if start_event and "model" in start_event["args"]:
- model = start_event["args"]["model"]
- api_key = DEFAULT_API_KEY
- if start_event and "api_key" in start_event["args"]:
- api_key = start_event["args"]["api_key"]
- api_base = DEFAULT_BASE_URL
- if start_event and "api_base" in start_event["args"]:
- api_base = start_event["args"]["api_base"]
- container_image = CONTAINER_IMAGE
- if start_event and "container_image" in start_event["args"]:
- container_image = start_event["args"]["container_image"]
- max_iterations = MAX_ITERATIONS
- if start_event and "max_iterations" in start_event["args"]:
- max_iterations = start_event["args"]["max_iterations"]
-
- # double check preventing error occurs
- if directory == "":
- directory = DEFAULT_WORKSPACE_DIR
- if agent_cls == "":
- agent_cls = "MonologueAgent"
- if model == "":
- model = LLM_MODEL
+ args = {
+ key: value
+ for key, value in start_event.get('args', {}).items()
+ if value != ''
+ } # 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)
+ model = self.get_arg_or_default(args, ConfigType.LLM_MODEL)
+ api_key = self.get_arg_or_default(args, ConfigType.LLM_API_KEY)
+ api_base = self.get_arg_or_default(args, ConfigType.LLM_BASE_URL)
+ container_image = self.get_arg_or_default(
+ args, ConfigType.SANDBOX_CONTAINER_IMAGE
+ )
+ max_iterations = self.get_arg_or_default(
+ args, ConfigType.MAX_ITERATIONS)
if not os.path.exists(directory):
- logger.info("Workspace directory %s does not exist. Creating it...", 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)
@@ -133,17 +126,17 @@ class AgentManager:
id=self.sid,
agent=self.agent,
workdir=directory,
- max_iterations=max_iterations,
+ max_iterations=int(max_iterations),
container_image=container_image,
callbacks=[self.on_agent_event],
)
except Exception:
- logger.exception("Error creating controller.")
+ logger.exception('Error creating controller.')
await self.send_error(
- "Error creating controller. Please check Docker is running using `docker ps`."
+ 'Error creating controller. Please check Docker is running using `docker ps`.'
)
return
- await self.send({"action": ActionType.INIT, "message": "Control loop started."})
+ await self.send({'action': ActionType.INIT, 'message': 'Control loop started.'})
async def start_task(self, start_event):
"""Starts a task for the agent.
@@ -151,20 +144,20 @@ class AgentManager:
Args:
start_event: The start event data.
"""
- if "task" not in start_event["args"]:
- await self.send_error("No task specified")
+ if 'task' not in start_event['args']:
+ await self.send_error('No task specified')
return
- await self.send_message("Starting new task...")
- task = start_event["args"]["task"]
+ await self.send_message('Starting new task...')
+ task = start_event['args']['task']
if self.controller is None:
- await self.send_error("No agent started. Please wait a second...")
+ await self.send_error('No agent started. Please wait a second...')
return
try:
self.agent_task = await asyncio.create_task(
- self.controller.start_loop(task), name="agent loop"
+ self.controller.start_loop(task), name='agent loop'
)
except Exception:
- await self.send_error("Error during task loop.")
+ await self.send_error('Error during task loop.')
def on_agent_event(self, event: Observation | Action):
"""Callback function for agent events.
@@ -177,7 +170,8 @@ class AgentManager:
if isinstance(event, NullObservation):
return
event_dict = event.to_dict()
- asyncio.create_task(self.send(event_dict), name="send event in callback")
+ asyncio.create_task(self.send(event_dict),
+ name='send event in callback')
def disconnect(self):
self.websocket = None
diff --git a/opendevin/server/listen.py b/opendevin/server/listen.py
index 6b812614d8..06a98f0ea5 100644
--- a/opendevin/server/listen.py
+++ b/opendevin/server/listen.py
@@ -19,21 +19,21 @@ from opendevin.server.session import message_stack, session_manager
app = FastAPI()
app.add_middleware(
CORSMiddleware,
- allow_origins=["http://localhost:3001"],
+ allow_origins=['http://localhost:3001'],
allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
+ allow_methods=['*'],
+ allow_headers=['*'],
)
security_scheme = HTTPBearer()
# This endpoint receives events from the client (i.e. the browser)
-@app.websocket("/ws")
+@app.websocket('/ws')
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
- sid = get_sid_from_token(websocket.query_params.get("token") or "")
- if sid == "":
+ sid = get_sid_from_token(websocket.query_params.get('token') or '')
+ if sid == '':
return
session_manager.add_session(sid, websocket)
# TODO: actually the agent_manager is created for each websocket connection, even if the session id is the same,
@@ -42,7 +42,7 @@ async def websocket_endpoint(websocket: WebSocket):
await session_manager.loop_recv(sid, agent_manager.dispatch)
-@app.get("/litellm-models")
+@app.get('/litellm-models')
async def get_litellm_models():
"""
Get all models supported by LiteLLM.
@@ -50,7 +50,7 @@ async def get_litellm_models():
return list(set(litellm.model_list + list(litellm.model_cost.keys())))
-@app.get("/litellm-agents")
+@app.get('/litellm-agents')
async def get_litellm_agents():
"""
Get all agents supported by LiteLLM.
@@ -58,7 +58,7 @@ async def get_litellm_agents():
return Agent.listAgents()
-@app.get("/auth")
+@app.get('/auth')
async def get_token(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
):
@@ -66,40 +66,40 @@ async def get_token(
Get token for authentication when starts a websocket connection.
"""
sid = get_sid_from_token(credentials.credentials) or str(uuid.uuid4())
- token = sign_token({"sid": sid})
+ token = sign_token({'sid': sid})
return JSONResponse(
status_code=status.HTTP_200_OK,
- content={"token": token},
+ content={'token': token},
)
-@app.get("/messages")
+@app.get('/messages')
async def get_messages(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
):
data = []
sid = get_sid_from_token(credentials.credentials)
- if sid != "":
+ if sid != '':
data = message_stack.get_messages(sid)
return JSONResponse(
status_code=status.HTTP_200_OK,
- content={"messages": data},
+ content={'messages': data},
)
-@app.get("/messages/total")
+@app.get('/messages/total')
async def get_message_total(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
):
sid = get_sid_from_token(credentials.credentials)
return JSONResponse(
status_code=status.HTTP_200_OK,
- content={"msg_total": message_stack.get_message_total(sid)},
+ content={'msg_total': message_stack.get_message_total(sid)},
)
-@app.delete("/messages")
+@app.delete('/messages')
async def del_messages(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
):
@@ -107,23 +107,24 @@ async def del_messages(
message_stack.del_messages(sid)
return JSONResponse(
status_code=status.HTTP_200_OK,
- content={"ok": True},
+ content={'ok': True},
)
-@app.get("/default-model")
+@app.get('/configurations')
def read_default_model():
- return config.get_or_error("LLM_MODEL")
+ return config.get_all()
-@app.get("/refresh-files")
+@app.get('/refresh-files')
def refresh_files():
- structure = files.get_folder_structure(Path(str(config.get("WORKSPACE_DIR"))))
+ structure = files.get_folder_structure(
+ Path(str(config.get('WORKSPACE_DIR'))))
return json.dumps(structure.to_dict())
-@app.get("/select-file")
+@app.get('/select-file')
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_DIR'))), file), 'r') as selected_file:
content = selected_file.read()
- return json.dumps({"code": content})
+ return json.dumps({'code': content})