mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
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:
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
18
frontend/src/types/ConfigType.tsx
Normal file
18
frontend/src/types/ConfigType.tsx
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .action import ActionType
|
||||
from .config import ConfigType
|
||||
from .observation import ObservationType
|
||||
|
||||
__all__ = ["ActionType", "ObservationType"]
|
||||
__all__ = ['ActionType', 'ObservationType', 'ConfigType']
|
||||
|
||||
16
opendevin/schema/config.py
Normal file
16
opendevin/schema/config.py
Normal 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'
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user