Compare commits

...

14 Commits

Author SHA1 Message Date
Robert Brennan
381143b8fc Merge branch 'main' into rb/experimental-ui 2024-08-09 13:54:03 -04:00
Robert Brennan
b029883d33 refactor animation sync 2024-08-02 13:37:40 -04:00
Robert Brennan
66789bd968 rename gif file 2024-08-02 13:06:34 -04:00
Robert Brennan
90ef238772 plumb prompt_context through to backend 2024-08-02 13:05:15 -04:00
Robert Brennan
9d31f8a63a add prompt_context 2024-08-02 12:49:33 -04:00
Robert Brennan
439599015c mess with styles a bit 2024-08-01 17:31:04 -04:00
Robert Brennan
8b72528e61 gif editor working e2e 2024-08-01 17:22:31 -04:00
Robert Brennan
e52f2a98bb add byte reading to storage 2024-08-01 17:16:06 -04:00
Robert Brennan
81649ed974 add file content endpoint 2024-08-01 16:13:33 -04:00
Robert Brennan
b1a21d3d35 factor out controls 2024-08-01 15:50:31 -04:00
Robert Brennan
48d62d9adb move main css 2024-08-01 13:46:51 -04:00
Robert Brennan
fe41db58e0 add routing 2024-08-01 13:43:40 -04:00
Robert Brennan
d980f57a82 Update pyproject.toml
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2024-08-01 12:05:30 -04:00
Robert Brennan
558806beb1 add pyjwt to pyproject 2024-08-01 11:50:34 -04:00
22 changed files with 308 additions and 66 deletions

View File

@@ -36,11 +36,18 @@ ENABLE_GITHUB = True
# FIXME: We can tweak these two settings to create MicroAgents specialized toward different area
def get_system_message() -> str:
def get_system_message(prompt_context: str | None) -> str:
if not prompt_context:
prompt_context = ''
msg = SYSTEM_PREFIX
if ENABLE_GITHUB:
return f'{SYSTEM_PREFIX}\n{GITHUB_MESSAGE}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
else:
return f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
msg += f'\n{GITHUB_MESSAGE}'
msg += f'\n\n{COMMAND_DOCS}'
if prompt_context:
msg += f'\n\n{prompt_context}'
msg += f'\n\n{SYSTEM_SUFFIX}'
return msg
def get_in_context_example() -> str:
@@ -94,7 +101,6 @@ class CodeActAgent(Agent):
]
runtime_tools: list[RuntimeTool] = [RuntimeTool.BROWSER]
system_message: str = get_system_message()
in_context_example: str = f"Here is an example of how you can interact with the environment for task solving:\n{get_in_context_example()}\n\nNOW, LET'S START!"
action_parser = CodeActResponseParser()
@@ -209,8 +215,9 @@ class CodeActAgent(Agent):
return self.action_parser.parse(response)
def _get_messages(self, state: State) -> list[Message]:
messages: list[Message] = [
Message(role='system', content=[TextContent(text=self.system_message)]),
system_message: str = get_system_message(state.prompt_context)
messages = [
Message(role='system', content=[TextContent(text=system_message)]),
Message(role='user', content=[TextContent(text=self.in_context_example)]),
]

View File

@@ -30,6 +30,7 @@
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.4.0",
"vite": "^5.4.0",
@@ -4430,6 +4431,14 @@
}
}
},
"node_modules/@remix-run/router": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.0.tgz",
"integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz",
@@ -11035,6 +11044,36 @@
}
}
},
"node_modules/react-router": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.0.tgz",
"integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==",
"dependencies": {
"@remix-run/router": "1.19.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.0.tgz",
"integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==",
"dependencies": {
"@remix-run/router": "1.19.0",
"react-router": "6.26.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",

View File

@@ -29,6 +29,7 @@
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.4.0",
"vite": "^5.4.0",

View File

@@ -1,4 +0,0 @@
/* App.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,33 @@
import AgentControlBar from "#/components/AgentControlBar";
import AgentStatusBar from "#/components/AgentStatusBar";
import VolumeIcon from "#/components/VolumeIcon";
import CogTooth from "#/assets/cog-tooth";
interface Props {
setSettingOpen: (isOpen: boolean) => void;
}
function Controls({ setSettingOpen }: Props): JSX.Element {
return (
<div className="flex w-full p-4 bg-neutral-900 items-center shrink-0 justify-between">
<div className="flex items-center gap-4">
<AgentControlBar />
</div>
<AgentStatusBar />
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "8px" }}>
<VolumeIcon />
</div>
<div
className="cursor-pointer hover:opacity-80 transition-all"
onClick={() => setSettingOpen(true)}
>
<CogTooth />
</div>
</div>
</div>
);
}
export default Controls;

View File

@@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--bg-dark: #0c0e10;
--bg-light: #292929;

View File

@@ -1,10 +1,11 @@
// import React from "react";
import * as React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { Provider } from "react-redux";
import { NextUIProvider } from "@nextui-org/react";
import App from "./App";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Main from "#/pages/Main";
import GifEditor from "#/pages/GifEditor";
import reportWebVitals from "./reportWebVitals";
import store from "#/store";
import "#/i18n";
@@ -16,12 +17,18 @@ root.render(
<React.StrictMode>
<Provider store={store}>
<NextUIProvider>
<App />
<Router>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/ui/gif-editor" element={<GifEditor />} />
{/* Add more routes here */}
</Routes>
</Router>
</NextUIProvider>
</Provider>
</React.StrictMode>,
);
//
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals

View File

@@ -0,0 +1,125 @@
import { useDisclosure } from "@nextui-org/react";
import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { Toaster } from "react-hot-toast";
import ChatInterface from "#/components/chat/ChatInterface";
import Errors from "#/components/Errors";
import { Container, Orientation } from "#/components/Resizable";
import Workspace from "#/components/Workspace";
import LoadPreviousSessionModal from "#/components/modals/load-previous-session/LoadPreviousSessionModal";
import SettingsModal from "#/components/modals/settings/SettingsModal";
import Controls from "#/components/Controls";
import Terminal from "#/components/terminal/Terminal";
import Session from "#/services/session";
import { getToken } from "#/services/auth";
import { settingsAreUpToDate } from "#/services/settings";
import { request } from "#/services/api";
// React.StrictMode will cause double rendering, use this to prevent it
let initOnce = false;
const PROMPT_CONTEXT = `
You're current job is to create an animated gif. You MUST do this by writing a python
script called generate_gif.py. This file MUST create a gif file called animation.gif in the
current directory. generate_gif.py may already exist, in which case you should modify it.
Every time you modify the script, you MUST re-run the script to regenerate the gif.
Don't do anything else after you run the script--the user will see the gif automatically.
You should use the Pillow library. If it's not installed, install it with \`python3 -m pip install --upgrade Pillow\`
`
function GifEditor(): JSX.Element {
/* FIXME: all the below is duplicated from Main.tsx, should be refactored */
const {
isOpen: settingsModalIsOpen,
onOpen: onSettingsModalOpen,
onOpenChange: onSettingsModalOpenChange,
} = useDisclosure();
const {
isOpen: loadPreviousSessionModalIsOpen,
onOpen: onLoadPreviousSessionModalOpen,
onOpenChange: onLoadPreviousSessionModalOpenChange,
} = useDisclosure();
useEffect(() => {
if (initOnce) return;
initOnce = true;
if (!settingsAreUpToDate()) {
onSettingsModalOpen();
/*
* FIXME: how should we do sessions with custom UIs?
* } else if (getToken()) {
onLoadPreviousSessionModalOpen();*/
} else {
Session.startNewSession({prompt_context: PROMPT_CONTEXT});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/* FIXME: all the above is duplicated from Main.tsx, should be refactored */
const [imageContent, setImageContent] = useState(null);
const { curAgentState } = useSelector((state: RootState) => state.agent);
async function blobToBase64(blob) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
async function refreshAnimation() {
let blob = null;
try {
blob = await request(`/api/files/animation.gif`, {}, true, "blob");
} catch (err) {
return;
}
const base64 = await blobToBase64(blob);
setImageContent(base64);
}
useEffect(() => {
refreshAnimation();
}, [curAgentState]);
return (
<div className="h-screen w-screen flex flex-col">
<div className="flex grow bg-neutral-900 text-white min-h-0">
<Container
orientation={Orientation.HORIZONTAL}
className="grow h-full min-h-0 min-w-0 px-3 pt-3"
initialSize={500}
firstChild={<ChatInterface />}
firstClassName="min-w-[500px] rounded-xl overflow-hidden border border-neutral-600"
secondChild={
<>
<h1>Gif Editor</h1>
{ imageContent && (
<img src={imageContent} alt="dance" className="max-h-full max-w-full" />
) }
</>
}
secondClassName="grow"
/>
</div>
<Controls setSettingOpen={onSettingsModalOpen} />
<SettingsModal
isOpen={settingsModalIsOpen}
onOpenChange={onSettingsModalOpenChange}
/>
<LoadPreviousSessionModal
isOpen={loadPreviousSessionModalIsOpen}
onOpenChange={onLoadPreviousSessionModalOpenChange}
/>
<Errors />
<Toaster />
</div>
);
}
export default GifEditor;

View File

@@ -1,53 +1,22 @@
import { useDisclosure } from "@nextui-org/react";
import React, { useEffect } from "react";
import { Toaster } from "react-hot-toast";
import CogTooth from "#/assets/cog-tooth";
import ChatInterface from "#/components/chat/ChatInterface";
import Errors from "#/components/Errors";
import { Container, Orientation } from "#/components/Resizable";
import Workspace from "#/components/Workspace";
import LoadPreviousSessionModal from "#/components/modals/load-previous-session/LoadPreviousSessionModal";
import SettingsModal from "#/components/modals/settings/SettingsModal";
import "./App.css";
import AgentControlBar from "./components/AgentControlBar";
import AgentStatusBar from "./components/AgentStatusBar";
import VolumeIcon from "./components/VolumeIcon";
import Terminal from "./components/terminal/Terminal";
import Controls from "#/components/Controls";
import Terminal from "#/components/terminal/Terminal";
import Session from "#/services/session";
import { getToken } from "#/services/auth";
import { settingsAreUpToDate } from "#/services/settings";
interface Props {
setSettingOpen: (isOpen: boolean) => void;
}
function Controls({ setSettingOpen }: Props): JSX.Element {
return (
<div className="flex w-full p-4 bg-neutral-900 items-center shrink-0 justify-between">
<div className="flex items-center gap-4">
<AgentControlBar />
</div>
<AgentStatusBar />
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "8px" }}>
<VolumeIcon />
</div>
<div
className="cursor-pointer hover:opacity-80 transition-all"
onClick={() => setSettingOpen(true)}
>
<CogTooth />
</div>
</div>
</div>
);
}
// React.StrictMode will cause double rendering, use this to prevent it
let initOnce = false;
function App(): JSX.Element {
function Main(): JSX.Element {
const {
isOpen: settingsModalIsOpen,
onOpen: onSettingsModalOpen,
@@ -113,4 +82,4 @@ function App(): JSX.Element {
);
}
export default App;
export default Main;

View File

@@ -7,6 +7,7 @@ export async function request(
url: string,
options: RequestInit = {},
disableToast: boolean = false,
responseType: string = "json",
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
): Promise<any> {
const onFail = (msg: string) => {
@@ -21,7 +22,7 @@ export async function request(
if (!token && needsAuth) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(request(url, options, disableToast));
resolve(request(url, options, disableToast, responseType));
}, WAIT_FOR_AUTH_DELAY_MS);
});
}
@@ -49,9 +50,10 @@ export async function request(
}
try {
return await (response && response.json());
return await (response && response[responseType]());
} catch (e) {
onFail(`Error parsing JSON from ${url}`);
console.log(e);
onFail(`Error parsing data as ${responseType} from ${url}`);
}
return null;
}

View File

@@ -10,6 +10,11 @@ export async function selectFile(file: string): Promise<string> {
return data.code as string;
}
export async function readFile(file: string): Promise<any> {
const data = await request(`/api/files/${file}`, {}, false, "blob");
return data;
}
interface UploadResult {
message: string;
uploadedFiles: string[];

View File

@@ -32,26 +32,27 @@ class Session {
private static _disconnecting = false;
public static restoreOrStartNewSession() {
public static restoreOrStartNewSession(extraArgs: object = {}) {
if (Session.isConnected()) {
Session.disconnect();
}
Session._connect();
Session._connect(extraArgs);
}
public static startNewSession() {
public static startNewSession(extraArgs: object = {}) {
clearToken();
Session.restoreOrStartNewSession();
Session.restoreOrStartNewSession(extraArgs);
}
private static _initializeAgent = () => {
private static _initializeAgent = (extraArgs: object = {}) => {
const settings = getSettings();
const event = { action: ActionType.INIT, args: settings };
const args = { ...settings, ...extraArgs };
const event = { action: ActionType.INIT, args };
const eventString = JSON.stringify(event);
Session.send(eventString);
};
private static _connect(): void {
private static _connect(extraArgs: object = {}): void {
if (Session.isConnected()) return;
Session._connecting = true;
@@ -65,10 +66,10 @@ class Session {
}
}
Session._socket = new WebSocket(wsURL);
Session._setupSocket();
Session._setupSocket(extraArgs);
}
private static _setupSocket(): void {
private static _setupSocket(extraArgs: object = {}): void {
if (!Session._socket) {
throw new Error(
translate(I18nKey.SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE),
@@ -77,7 +78,7 @@ class Session {
Session._socket.onopen = (e) => {
toast.success("ws", translate(I18nKey.SESSION$SERVER_CONNECTED_MESSAGE));
Session._connecting = false;
Session._initializeAgent();
Session._initializeAgent(extraArgs);
Session.callbacks.open?.forEach((callback) => {
callback(e);
});

View File

@@ -51,6 +51,7 @@ class AgentController:
event_stream: EventStream
state: State
confirmation_mode: bool
prompt_context: str
agent_to_llm_config: dict[str, LLMConfig]
agent_task: Optional[asyncio.Task] = None
parent: 'AgentController | None' = None
@@ -65,6 +66,7 @@ class AgentController:
max_budget_per_task: float | None = None,
agent_to_llm_config: dict[str, LLMConfig] | None = None,
sid: str = 'default',
prompt_context: str | None = None,
confirmation_mode: bool = False,
initial_state: State | None = None,
is_delegate: bool = False,
@@ -98,6 +100,7 @@ class AgentController:
# state from the previous session, state from a parent agent, or a fresh state
self.set_initial_state(
state=initial_state,
prompt_context=prompt_context,
max_iterations=max_iterations,
confirmation_mode=confirmation_mode,
)
@@ -435,6 +438,7 @@ class AgentController:
def set_initial_state(
self,
state: State | None,
prompt_context: str | None,
max_iterations: int,
confirmation_mode: bool = False,
):
@@ -443,6 +447,7 @@ class AgentController:
if state is None:
self.state = State(
inputs={},
prompt_context=prompt_context,
max_iterations=max_iterations,
confirmation_mode=confirmation_mode,
)

View File

@@ -90,6 +90,8 @@ class State:
# max number of iterations for the current task
max_iterations: int = 100
confirmation_mode: bool = False
# additional context for the current environment, which should be added to all prompts. E.g. "you're on an ubuntu machine with python 3.11 installed"
prompt_context: str | None = None
history: ShortTermHistory = field(default_factory=ShortTermHistory)
inputs: dict = field(default_factory=dict)
outputs: dict = field(default_factory=dict)

View File

@@ -11,6 +11,9 @@ class E2BFileStore(FileStore):
def read(self, path: str) -> str:
return self.filesystem.read(path)
def read_bytes(self, path: str) -> bytes:
return self.filesystem.read(path).encode()
def list(self, path: str) -> list[str]:
return self.filesystem.list(path)

View File

@@ -23,7 +23,7 @@ from fastapi import (
status,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import HTTPBearer
from fastapi.staticfiles import StaticFiles
@@ -437,6 +437,29 @@ async def select_file(file: str, request: Request):
)
@app.get('/api/files/{file_path:path}')
def get_file(file_path: str, request: Request):
"""Retrieve the content of a specified file."""
try:
content = request.state.session.agent_session.runtime.file_store.read_bytes(
file_path
)
except Exception as e:
logger.error(f'Error opening file {file_path}: {e}', exc_info=False)
error_msg = f'Error opening file: {e}'
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': error_msg},
)
# write the file to a temp file
# FIXME: there's definitely a better way to do this
with open('/tmp/opendevin-temp-file', 'wb') as f:
f.write(content)
return FileResponse('/tmp/opendevin-temp-file')
def sanitize_filename(filename):
"""Sanitize the filename to prevent directory traversal"""
# Remove any directory components

View File

@@ -37,6 +37,7 @@ class AgentSession:
config: AppConfig,
agent: Agent,
confirmation_mode: bool,
prompt_context: str | None,
max_iterations: int,
max_budget_per_task: float | None = None,
agent_to_llm_config: dict[str, LLMConfig] | None = None,
@@ -54,6 +55,7 @@ class AgentSession:
await self._create_controller(
agent,
confirmation_mode,
prompt_context,
max_iterations,
max_budget_per_task=max_budget_per_task,
agent_to_llm_config=agent_to_llm_config,
@@ -89,6 +91,7 @@ class AgentSession:
self,
agent: Agent,
confirmation_mode: bool,
prompt_context: str | None,
max_iterations: int,
max_budget_per_task: float | None = None,
agent_to_llm_config: dict[str, LLMConfig] | None = None,
@@ -105,6 +108,7 @@ class AgentSession:
sid=self.sid,
event_stream=self.event_stream,
agent=agent,
prompt_context=prompt_context,
max_iterations=int(max_iterations),
max_budget_per_task=max_budget_per_task,
agent_to_llm_config=agent_to_llm_config,
@@ -116,7 +120,7 @@ class AgentSession:
try:
agent_state = State.restore_from_session(self.sid, self.file_store)
self.controller.set_initial_state(
agent_state, max_iterations, confirmation_mode
agent_state, prompt_context, max_iterations, confirmation_mode
)
logger.info(f'Restored agent state from session, sid: {self.sid}')
except Exception as e:

View File

@@ -108,6 +108,7 @@ class Session:
config=self.config,
agent=agent,
confirmation_mode=confirmation_mode,
prompt_context=args.get('prompt_context', ''),
max_iterations=max_iterations,
max_budget_per_task=self.config.max_budget_per_task,
agent_to_llm_config=self.config.get_agent_to_llm_config_map(),

View File

@@ -10,6 +10,10 @@ class FileStore:
def read(self, path: str) -> str:
pass
@abstractmethod
def read_bytes(self, path: str) -> bytes:
pass
@abstractmethod
def list(self, path: str) -> list[str]:
pass

View File

@@ -30,6 +30,11 @@ class LocalFileStore(FileStore):
with open(full_path, 'r') as f:
return f.read()
def read_bytes(self, path: str) -> bytes:
full_path = self.get_full_path(path)
with open(full_path, 'rb') as f:
return f.read()
def list(self, path: str) -> list[str]:
full_path = self.get_full_path(path)
files = [os.path.join(path, f) for f in os.listdir(full_path)]

View File

@@ -19,6 +19,9 @@ class InMemoryFileStore(FileStore):
raise FileNotFoundError(path)
return self.files[path]
def read_bytes(self, path: str) -> bytes:
return self.read(path).encode()
def list(self, path: str) -> list[str]:
files = []
for file in self.files:

View File

@@ -18,7 +18,10 @@ class S3FileStore(FileStore):
self.client.put_object(self.bucket, path, contents)
def read(self, path: str) -> str:
return self.client.get_object(self.bucket, path).data.decode('utf-8')
return self.read_bytes(path).decode('utf-8')
def read_bytes(self, path: str) -> bytes:
return self.client.get_object(self.bucket, path).data
def list(self, path: str) -> list[str]:
return [obj.object_name for obj in self.client.list_objects(self.bucket, path)]