Fix aligning settings between fe and be (#863)

* fix: aligning settings between FE and BE.

* apply black formatter and clean useless codes.
This commit is contained in:
Leo
2024-04-08 03:56:14 +08:00
committed by GitHub
parent e878b0c7ee
commit e52bf5ad7b
16 changed files with 302 additions and 264 deletions

View File

@@ -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 = () => {

View File

@@ -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 <div />;
return <InnerSettingModal isOpen={isOpen} onClose={onClose} />;
}
export default SettingModal;

View File

@@ -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;
}

View File

@@ -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<ResConfigurations> {
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<string, string>([
["model", "model"],
["agent", "agent_cls"],
["workspaceDirectory", "directory"],
// TODO: add the values to i18n to support multi languages
const DISPLAY_MAP = new Map<string, string>([
[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));
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
},
});

View File

@@ -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;

View File

@@ -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,

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
from .action import ActionType
from .config import ConfigType
from .observation import ObservationType
__all__ = ["ActionType", "ObservationType"]
__all__ = ['ActionType', 'ObservationType', 'ConfigType']

View File

@@ -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'

View File

@@ -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

View File

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