mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
14 Commits
openhands/
...
rb/experim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
381143b8fc | ||
|
|
b029883d33 | ||
|
|
66789bd968 | ||
|
|
90ef238772 | ||
|
|
9d31f8a63a | ||
|
|
439599015c | ||
|
|
8b72528e61 | ||
|
|
e52f2a98bb | ||
|
|
81649ed974 | ||
|
|
b1a21d3d35 | ||
|
|
48d62d9adb | ||
|
|
fe41db58e0 | ||
|
|
d980f57a82 | ||
|
|
558806beb1 |
@@ -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)]),
|
||||
]
|
||||
|
||||
|
||||
39
frontend/package-lock.json
generated
39
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/* App.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
33
frontend/src/components/Controls.tsx
Normal file
33
frontend/src/components/Controls.tsx
Normal 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;
|
||||
@@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--bg-dark: #0c0e10;
|
||||
--bg-light: #292929;
|
||||
|
||||
@@ -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
|
||||
|
||||
125
frontend/src/pages/GifEditor.tsx
Normal file
125
frontend/src/pages/GifEditor.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user