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})