);
diff --git a/frontend/src/components/CodeEditor.tsx b/frontend/src/components/CodeEditor.tsx
index cf4d1d7c967c..b5a157e7d9a6 100644
--- a/frontend/src/components/CodeEditor.tsx
+++ b/frontend/src/components/CodeEditor.tsx
@@ -7,13 +7,22 @@ function CodeEditor(): JSX.Element {
const code = useSelector((state: RootState) => state.code.code);
return (
-
);
}
diff --git a/frontend/src/components/Planner.tsx b/frontend/src/components/Planner.tsx
index 1cb357912cfa..aec2a1801184 100644
--- a/frontend/src/components/Planner.tsx
+++ b/frontend/src/components/Planner.tsx
@@ -2,7 +2,16 @@ import React from "react";
function Planner(): JSX.Element {
return (
-
+
Current Focus: Set up the development environment according to the
project's instructions.
diff --git a/frontend/src/components/Terminal.tsx b/frontend/src/components/Terminal.tsx
index c6d1d9710289..a28fa66cbb2f 100644
--- a/frontend/src/components/Terminal.tsx
+++ b/frontend/src/components/Terminal.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useRef } from "react";
import { IDisposable, Terminal as XtermTerminal } from "@xterm/xterm";
import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css";
-import socket from "../state/socket";
+import socket from "../socket/socket";
class JsonWebsocketAddon {
_socket: WebSocket;
@@ -22,12 +22,12 @@ class JsonWebsocketAddon {
}),
);
this._socket.addEventListener("message", (event) => {
- const { action, args } = JSON.parse(event.data);
+ const { action, args, observation, content } = JSON.parse(event.data);
if (action === "run") {
terminal.writeln(args.command);
}
- if (action === "output") {
- args.output.split("\n").forEach((line: string) => {
+ if (observation === "run") {
+ content.split("\n").forEach((line: string) => {
terminal.writeln(line);
});
terminal.write("\n$ ");
@@ -92,6 +92,7 @@ function Terminal({ hidden }: TerminalProps): JSX.Element {
width: "100%",
height: "100%",
display: hidden ? "none" : "block",
+ padding: "1rem",
}}
/>
);
diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts
index 61423a82e043..5079cf280da4 100644
--- a/frontend/src/services/chatService.ts
+++ b/frontend/src/services/chatService.ts
@@ -1,5 +1,5 @@
import { appendUserMessage } from "../state/chatSlice";
-import socket from "../state/socket";
+import socket from "../socket/socket";
import store from "../store";
export function sendChatMessage(message: string): void {
diff --git a/frontend/src/services/settingsService.ts b/frontend/src/services/settingsService.ts
new file mode 100644
index 000000000000..a47e7dfaccec
--- /dev/null
+++ b/frontend/src/services/settingsService.ts
@@ -0,0 +1,38 @@
+import socket from "../socket/socket";
+import { appendAssistantMessage } from "../state/chatSlice";
+import { setInitialized } from "../state/taskSlice";
+import store from "../store";
+
+export const MODELS = [
+ "gpt-3.5-turbo-1106",
+ "gpt-4-0125-preview",
+ "claude-3-haiku-20240307",
+ "claude-3-sonnet-20240229",
+ "claude-3-sonnet-20240307",
+];
+
+export type Model = (typeof MODELS)[number];
+
+export const AGENTS = ["LangchainsAgent", "CodeActAgent"];
+
+export type Agent = (typeof AGENTS)[number];
+
+function changeSetting(setting: string, value: string): void {
+ const event = { action: "initialize", args: { [setting]: value } };
+ const eventString = JSON.stringify(event);
+ socket.send(eventString);
+ store.dispatch(setInitialized(false));
+ store.dispatch(appendAssistantMessage(`Changed ${setting} to "${value}"`));
+}
+
+export function changeModel(model: Model): void {
+ changeSetting("model", model);
+}
+
+export function changeAgent(agent: Agent): void {
+ changeSetting("agent_cls", agent);
+}
+
+export function changeDirectory(directory: string): void {
+ changeSetting("directory", directory);
+}
diff --git a/frontend/src/socket/actions.ts b/frontend/src/socket/actions.ts
new file mode 100644
index 000000000000..77ae1c7841e1
--- /dev/null
+++ b/frontend/src/socket/actions.ts
@@ -0,0 +1,39 @@
+import store from "../store";
+import { ActionMessage } from "./types/Message";
+import { setScreenshotSrc, setUrl } from "../state/browserSlice";
+import { appendAssistantMessage } from "../state/chatSlice";
+import { setCode } from "../state/codeSlice";
+import { setInitialized } from "../state/taskSlice";
+
+const messageActions = {
+ initialize: () => {
+ store.dispatch(setInitialized(true));
+ store.dispatch(
+ appendAssistantMessage(
+ "Hello, I am OpenDevin, an AI Software Engineer. What would you like me to build you today?",
+ ),
+ );
+ },
+ browse: (message: ActionMessage) => {
+ const { url, screenshotSrc } = message.args;
+ store.dispatch(setUrl(url));
+ store.dispatch(setScreenshotSrc(screenshotSrc));
+ },
+ write: (message: ActionMessage) => {
+ store.dispatch(setCode(message.args.contents));
+ },
+ think: (message: ActionMessage) => {
+ store.dispatch(appendAssistantMessage(message.args.thought));
+ },
+ finish: (message: ActionMessage) => {
+ store.dispatch(appendAssistantMessage(message.message));
+ },
+};
+
+export function handleActionMessage(message: ActionMessage) {
+ if (message.action in messageActions) {
+ const actionFn =
+ messageActions[message.action as keyof typeof messageActions];
+ actionFn(message);
+ }
+}
diff --git a/frontend/src/socket/observations.ts b/frontend/src/socket/observations.ts
new file mode 100644
index 000000000000..3d93859cdde8
--- /dev/null
+++ b/frontend/src/socket/observations.ts
@@ -0,0 +1,7 @@
+import { appendAssistantMessage } from "../state/chatSlice";
+import store from "../store";
+import { ObservationMessage } from "./types/Message";
+
+export function handleObservationMessage(message: ObservationMessage) {
+ store.dispatch(appendAssistantMessage(message.message));
+}
diff --git a/frontend/src/socket/socket.ts b/frontend/src/socket/socket.ts
new file mode 100644
index 000000000000..27f501269187
--- /dev/null
+++ b/frontend/src/socket/socket.ts
@@ -0,0 +1,34 @@
+import store from "../store";
+import { ActionMessage, ObservationMessage } from "./types/Message";
+import { appendError } from "../state/errorsSlice";
+import { handleActionMessage } from "./actions";
+import { handleObservationMessage } from "./observations";
+
+type SocketMessage = ActionMessage | ObservationMessage;
+
+const WS_URL = import.meta.env.VITE_TERMINAL_WS_URL;
+if (!WS_URL) {
+ throw new Error(
+ "The environment variable VITE_TERMINAL_WS_URL is not set. Please set it to the WebSocket URL of the terminal server.",
+ );
+}
+
+const socket = new WebSocket(WS_URL);
+
+socket.addEventListener("message", (event) => {
+ const socketMessage = JSON.parse(event.data) as SocketMessage;
+ if ("action" in socketMessage) {
+ handleActionMessage(socketMessage);
+ } else {
+ handleObservationMessage(socketMessage);
+ }
+});
+socket.addEventListener("error", () => {
+ store.dispatch(
+ appendError(
+ `Failed connection to server. Please ensure the server is reachable at ${WS_URL}.`,
+ ),
+ );
+});
+
+export default socket;
diff --git a/frontend/src/socket/types/ActionType.tsx b/frontend/src/socket/types/ActionType.tsx
new file mode 100644
index 000000000000..fa668549b515
--- /dev/null
+++ b/frontend/src/socket/types/ActionType.tsx
@@ -0,0 +1,34 @@
+enum ActionType {
+ // Initializes the agent. Only sent by client.
+ INIT = "initialize",
+
+ // Starts a new development task. Only sent by the client.
+ START = "start",
+
+ // Reads the contents of a file.
+ READ = "read",
+
+ // Writes the contents to a file.
+ WRITE = "write",
+
+ // Runs a command.
+ RUN = "run",
+
+ // Kills a background command.
+ KILL = "kill",
+
+ // Opens a web page.
+ BROWSE = "browse",
+
+ // Searches long-term memory.
+ RECALL = "recall",
+
+ // Allows the agent to make a plan, set a goal, or record thoughts.
+ THINK = "think",
+
+ // If you're absolutely certain that you've completed your task and have tested your work,
+ // use the finish action to stop working.
+ FINISH = "finish",
+}
+
+export default ActionType;
diff --git a/frontend/src/socket/types/Message.tsx b/frontend/src/socket/types/Message.tsx
new file mode 100644
index 000000000000..1ff375e9050a
--- /dev/null
+++ b/frontend/src/socket/types/Message.tsx
@@ -0,0 +1,24 @@
+export interface ActionMessage {
+ // The action to be taken
+ action: string;
+
+ // The arguments for the action
+ args: Record;
+
+ // A friendly message that can be put in the chat log
+ message: string;
+}
+
+export interface ObservationMessage {
+ // The type of observation
+ observation: string;
+
+ // The observed data
+ content: string;
+
+ // Additional structured data
+ extras: Record;
+
+ // A friendly message that can be put in the chat log
+ message: string;
+}
diff --git a/frontend/src/socket/types/ObservationType.tsx b/frontend/src/socket/types/ObservationType.tsx
new file mode 100644
index 000000000000..b86bd58919d2
--- /dev/null
+++ b/frontend/src/socket/types/ObservationType.tsx
@@ -0,0 +1,18 @@
+enum ObservationType {
+ // The contents of a file
+ READ = "read",
+
+ // The HTML contents of a URL
+ BROWSE = "browse",
+
+ // The output of a command
+ RUN = "run",
+
+ // The result of a search
+ RECALL = "recall",
+
+ // A message from the user
+ CHAT = "chat",
+}
+
+export default ObservationType;
diff --git a/frontend/src/state/socket.ts b/frontend/src/state/socket.ts
deleted file mode 100644
index 7cb0dbaf75c3..000000000000
--- a/frontend/src/state/socket.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import store from "../store";
-import { setScreenshotSrc, setUrl } from "./browserSlice";
-import { appendAssistantMessage } from "./chatSlice";
-import { setCode } from "./codeSlice";
-import { appendError } from "./errorsSlice";
-import { setInitialized } from "./taskSlice";
-
-const MESSAGE_ACTIONS = ["terminal", "planner", "code", "browser"] as const;
-type MessageAction = (typeof MESSAGE_ACTIONS)[number];
-
-type SocketMessage = {
- action: MessageAction;
- message: string;
- args: Record;
-};
-
-const messageActions = {
- initialize: () => {
- store.dispatch(setInitialized(true));
- store.dispatch(
- appendAssistantMessage(
- "Hello, I am OpenDevin, an AI Software Engineer. What would you like me to build you today?",
- ),
- );
- },
- browse: (message: SocketMessage) => {
- const { url, screenshotSrc } = message.args;
- store.dispatch(setUrl(url));
- store.dispatch(setScreenshotSrc(screenshotSrc));
- },
- write: (message: SocketMessage) => {
- store.dispatch(setCode(message.args.contents));
- },
- think: (message: SocketMessage) => {
- store.dispatch(appendAssistantMessage(message.args.thought));
- },
- finish: (message: SocketMessage) => {
- store.dispatch(appendAssistantMessage(message.message));
- },
-};
-
-const WS_URL = import.meta.env.VITE_TERMINAL_WS_URL;
-if (!WS_URL) {
- throw new Error(
- "The environment variable VITE_TERMINAL_WS_URL is not set. Please set it to the WebSocket URL of the terminal server.",
- );
-}
-
-const socket = new WebSocket(WS_URL);
-
-socket.addEventListener("message", (event) => {
- const socketMessage = JSON.parse(event.data) as SocketMessage;
-
- if (socketMessage.action in messageActions) {
- const actionFn =
- messageActions[socketMessage.action as keyof typeof messageActions];
- actionFn(socketMessage);
- } else if (!socketMessage.action) {
- store.dispatch(appendAssistantMessage(socketMessage.message));
- }
-});
-socket.addEventListener("error", () => {
- store.dispatch(
- appendError(
- `Failed connection to server. Please ensure the server is reachable at ${WS_URL}.`,
- ),
- );
-});
-
-export default socket;
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 000000000000..9286443d8f09
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ extend: {},
+ },
+ daisyui: {
+ themes: ["dark"],
+ },
+ plugins: [require('daisyui')],
+}
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index c027c36bf0c4..1041b2b46031 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -7,8 +7,6 @@ export default defineConfig({
base: "",
plugins: [react(), viteTsconfigPaths()],
server: {
- // this ensures that the browser opens upon server start
- open: true,
// this sets a default port to 3000
port: 3000,
},
diff --git a/logo.png b/logo.png
new file mode 100644
index 000000000000..985f6d981466
Binary files /dev/null and b/logo.png differ
diff --git a/opendevin/README.md b/opendevin/README.md
index d9b9c9e7890a..69237e7c7518 100644
--- a/opendevin/README.md
+++ b/opendevin/README.md
@@ -25,6 +25,11 @@ Example screenshot:
docker build -f opendevin/sandbox/Dockerfile -t opendevin/sandbox:v0.1 .
```
+Or you can pull the latest image [here](https://github.com/opendevin/OpenDevin/pkgs/container/sandbox):
+```bash
+docker pull ghcr.io/opendevin/sandbox:v0.1
+```
+
2. Set the `OPENAI_API_KEY`, please find more details [here](https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety). Also, choose the model you want. Default is `gpt-4-0125-preview`
```bash
export OPENAI_API_KEY=xxxxxxx
diff --git a/opendevin/action/__init__.py b/opendevin/action/__init__.py
index 814842cac5eb..9fe6f123541c 100644
--- a/opendevin/action/__init__.py
+++ b/opendevin/action/__init__.py
@@ -2,7 +2,7 @@
from .bash import CmdRunAction, CmdKillAction
from .browse import BrowseURLAction
from .fileop import FileReadAction, FileWriteAction
-from .agent import AgentRecallAction, AgentThinkAction, AgentFinishAction, AgentEchoAction
+from .agent import AgentRecallAction, AgentThinkAction, AgentFinishAction, AgentEchoAction, AgentSummarizeAction
__all__ = [
"Action",
@@ -16,4 +16,5 @@
"AgentThinkAction",
"AgentFinishAction",
"AgentEchoAction",
+ "AgentSummarizeAction",
]
diff --git a/opendevin/action/agent.py b/opendevin/action/agent.py
index c040ec93a86b..13a4d2ec7dc1 100644
--- a/opendevin/action/agent.py
+++ b/opendevin/action/agent.py
@@ -48,6 +48,13 @@ def run(self, controller: "AgentController") -> "Observation":
def message(self) -> str:
return self.content
+@dataclass
+class AgentSummarizeAction(NotExecutableAction):
+ summary: str
+
+ @property
+ def message(self) -> str:
+ return self.summary
@dataclass
class AgentFinishAction(NotExecutableAction):
@@ -59,3 +66,4 @@ def run(self, controller: "AgentController") -> "Observation":
@property
def message(self) -> str:
return "All done! What's next on the agenda?"
+
diff --git a/opendevin/action/fileop.py b/opendevin/action/fileop.py
index 77accb8e77bb..b3ee9a91d338 100644
--- a/opendevin/action/fileop.py
+++ b/opendevin/action/fileop.py
@@ -1,7 +1,7 @@
import os
from dataclasses import dataclass
-from opendevin.observation import Observation
+from opendevin.observation import FileReadObservation, FileWriteObservation
from .base import ExecutableAction
# This is the path where the workspace is mounted in the container
@@ -17,12 +17,13 @@ def resolve_path(base_path, file_path):
@dataclass
class FileReadAction(ExecutableAction):
path: str
- base_path: str = ""
- def run(self, *args, **kwargs) -> Observation:
- path = resolve_path(self.base_path, self.path)
- with open(path, 'r') as file:
- return Observation(file.read())
+ def run(self, controller) -> FileReadObservation:
+ path = resolve_path(controller.workdir, self.path)
+ with open(path, 'r', encoding='utf-8') as file:
+ return FileReadObservation(
+ path=path,
+ content=file.read())
@property
def message(self) -> str:
@@ -33,13 +34,12 @@ def message(self) -> str:
class FileWriteAction(ExecutableAction):
path: str
contents: str
- base_path: str = ""
- def run(self, *args, **kwargs) -> Observation:
- path = resolve_path(self.base_path, self.path)
- with open(path, 'w') as file:
+ def run(self, controller) -> FileWriteObservation:
+ path = resolve_path(controller.workdir, self.path)
+ with open(path, 'w', encoding='utf-8') as file:
file.write(self.contents)
- return Observation(f"File written to {path}")
+ return FileWriteObservation(content="", path=self.path)
@property
def message(self) -> str:
diff --git a/opendevin/agent.py b/opendevin/agent.py
index 916db4dc05e4..efaf239bf207 100644
--- a/opendevin/agent.py
+++ b/opendevin/agent.py
@@ -12,9 +12,6 @@ class Agent(ABC):
executing a specific instruction and allowing human interaction with the
agent during execution.
It tracks the execution status and maintains a history of interactions.
-
- :param instruction: The instruction for the agent to execute.
- :param model_name: The litellm name of the model to use for the agent.
"""
_registry: Dict[str, Type["Agent"]] = {}
@@ -23,7 +20,6 @@ def __init__(
self,
llm: LLM,
):
- self.instruction = ""
self.llm = llm
self._complete = False
@@ -64,7 +60,6 @@ def reset(self) -> None:
to prepare the agent for restarting the instruction or cleaning up before destruction.
"""
- self.instruction = ""
self._complete = False
@classmethod
diff --git a/opendevin/controller/__init__.py b/opendevin/controller/__init__.py
index d46de8e18451..aa4a979c4a5f 100644
--- a/opendevin/controller/__init__.py
+++ b/opendevin/controller/__init__.py
@@ -1,122 +1,7 @@
-import asyncio
-import traceback
-from typing import List, Callable, Tuple, Any, Literal, Mapping
-from termcolor import colored
-
-from opendevin.state import State
-from opendevin.agent import Agent
-from opendevin.action import (
- Action,
- NullAction,
- FileReadAction,
- FileWriteAction,
- AgentFinishAction,
-)
-from opendevin.observation import Observation, NullObservation
-
+from .agent_controller import AgentController
from .command_manager import CommandManager
-ColorType = Literal['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'light_grey', 'dark_grey', 'light_red', 'light_green', 'light_yellow', 'light_blue', 'light_magenta', 'light_cyan', 'white']
-
-def print_with_color(text: Any, print_type: str = "INFO"):
- TYPE_TO_COLOR: Mapping[str, ColorType] = {
- "BACKGROUND LOG": "blue",
- "ACTION": "green",
- "OBSERVATION": "yellow",
- "INFO": "cyan",
- }
- color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR["INFO"])
- print(
- colored(f"\n{print_type.upper()}:\n", color, attrs=["bold"])
- + colored(str(text), color),
- flush=True,
- )
-
-
-class AgentController:
- def __init__(
- self,
- agent: Agent,
- workdir: str,
- max_iterations: int = 100,
- callbacks: List[Callable] = [],
- ):
- self.agent = agent
- self.max_iterations = max_iterations
- self.workdir = workdir
- self.command_manager = CommandManager(workdir)
- self.callbacks = callbacks
- self.state_updated_info: List[Tuple[Action, Observation]] = []
-
- def get_current_state(self) -> State:
- # update observations & actions
- state = State(
- background_commands_obs=self.command_manager.get_background_obs(),
- updated_info=self.state_updated_info,
- )
- self.state_updated_info = []
- return state
-
- def add_observation(self, observation: Observation):
- self.state_updated_info.append((NullAction(), observation))
-
- async def start_loop(self, task_instruction: str):
- finished = False
- self.agent.instruction = task_instruction
- print_with_color(task_instruction, "OBSERVATION")
- for i in range(self.max_iterations):
- try:
- finished = await self.step(i)
- except Exception as e:
- print("Error in loop", e, flush=True)
- traceback.print_exc()
- break
- if finished:
- break
- if not finished:
- print("Exited before finishing", flush=True)
-
- async def step(self, i: int):
- print("\n==============", flush=True)
- print("STEP", i, flush=True)
- log_obs = self.command_manager.get_background_obs()
- for obs in log_obs:
- self.add_observation(obs)
- await self._run_callbacks(obs)
- print_with_color(obs, "BACKGROUND LOG")
-
- state: State = self.get_current_state()
- action: Action = self.agent.step(state)
-
- print_with_color(action, "ACTION")
- await self._run_callbacks(action)
-
- if isinstance(action, AgentFinishAction):
- print_with_color("FINISHED", "INFO")
- return True
- if isinstance(action, (FileReadAction, FileWriteAction)):
- action_cls = action.__class__
- _kwargs = action.__dict__
- _kwargs["base_path"] = self.workdir
- action = action_cls(**_kwargs)
- if action.executable:
- observation: Observation = action.run(self)
- else:
- observation = NullObservation("[Previous action was not executable, no observation produced.]")
- print_with_color(observation, "OBSERVATION")
- self.state_updated_info.append((action, observation))
- await self._run_callbacks(observation)
-
- async def _run_callbacks(self, event):
- if event is None:
- return
- for callback in self.callbacks:
- idx = self.callbacks.index(callback)
- try:
- callback(event)
- except Exception as e:
- print("Callback error:" + str(idx), e, flush=True)
- pass
- await asyncio.sleep(
- 0.001
- ) # Give back control for a tick, so we can await in callbacks
+__all__ = [
+ 'AgentController',
+ 'CommandManager'
+]
diff --git a/opendevin/controller/agent_controller.py b/opendevin/controller/agent_controller.py
new file mode 100644
index 000000000000..c09f672d9f1b
--- /dev/null
+++ b/opendevin/controller/agent_controller.py
@@ -0,0 +1,139 @@
+import asyncio
+import traceback
+from typing import List, Callable, Literal, Mapping, Any
+from termcolor import colored
+
+from opendevin.state import State
+from opendevin.agent import Agent
+from opendevin.action import (
+ Action,
+ NullAction,
+ AgentFinishAction,
+)
+from opendevin.observation import (
+ Observation,
+ AgentErrorObservation,
+ NullObservation
+)
+
+
+from .command_manager import CommandManager
+
+
+ColorType = Literal['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'light_grey', 'dark_grey', 'light_red', 'light_green', 'light_yellow', 'light_blue', 'light_magenta', 'light_cyan', 'white']
+
+def print_with_color(text: Any, print_type: str = "INFO"):
+ TYPE_TO_COLOR: Mapping[str, ColorType] = {
+ "BACKGROUND LOG": "blue",
+ "ACTION": "green",
+ "OBSERVATION": "yellow",
+ "INFO": "cyan",
+ "ERROR": "red"
+ }
+ color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR["INFO"])
+ print(
+ colored(f"\n{print_type.upper()}:\n", color, attrs=["bold"])
+ + colored(str(text), color),
+ flush=True,
+ )
+
+
+class AgentController:
+ def __init__(
+ self,
+ agent: Agent,
+ workdir: str,
+ max_iterations: int = 100,
+ callbacks: List[Callable] = [],
+ ):
+ self.agent = agent
+ self.max_iterations = max_iterations
+ self.workdir = workdir
+ self.command_manager = CommandManager(workdir)
+ self.callbacks = callbacks
+
+ def update_state_for_step(self, i):
+ self.state.iteration = i
+ self.state.background_commands_obs = self.command_manager.get_background_obs()
+
+ def update_state_after_step(self):
+ self.state.updated_info = []
+
+ def add_history(self, action: Action, observation: Observation):
+ if not isinstance(action, Action):
+ raise ValueError("action must be an instance of Action")
+ if not isinstance(observation, Observation):
+ raise ValueError("observation must be an instance of Observation")
+ self.state.history.append((action, observation))
+ self.state.updated_info.append((action, observation))
+
+
+ async def start_loop(self, task: str):
+ finished = False
+ self.state = State(task)
+ for i in range(self.max_iterations):
+ try:
+ finished = await self.step(i)
+ except Exception as e:
+ print("Error in loop", e, flush=True)
+ traceback.print_exc()
+ break
+ if finished:
+ break
+ if not finished:
+ print("Exited before finishing", flush=True)
+
+ async def step(self, i: int):
+ print("\n\n==============", flush=True)
+ print("STEP", i, flush=True)
+ log_obs = self.command_manager.get_background_obs()
+ for obs in log_obs:
+ self.add_history(NullAction(), obs)
+ await self._run_callbacks(obs)
+ print_with_color(obs, "BACKGROUND LOG")
+
+ self.update_state_for_step(i)
+ action: Action = NullAction()
+ observation: Observation = NullObservation("")
+ try:
+ action = self.agent.step(self.state)
+ if action is None:
+ raise ValueError("Agent must return an action")
+ print_with_color(action, "ACTION")
+ except Exception as e:
+ observation = AgentErrorObservation(str(e))
+ print_with_color(observation, "ERROR")
+ traceback.print_exc()
+ self.update_state_after_step()
+
+ await self._run_callbacks(action)
+
+ if isinstance(action, AgentFinishAction):
+ print_with_color(action, "INFO")
+ return True
+
+ if action.executable:
+ try:
+ observation = action.run(self)
+ except Exception as e:
+ observation = AgentErrorObservation(str(e))
+ print_with_color(observation, "ERROR")
+ traceback.print_exc()
+
+ if not isinstance(observation, NullObservation):
+ print_with_color(observation, "OBSERVATION")
+
+ self.add_history(action, observation)
+ await self._run_callbacks(observation)
+
+ async def _run_callbacks(self, event):
+ if event is None:
+ return
+ for callback in self.callbacks:
+ idx = self.callbacks.index(callback)
+ try:
+ callback(event)
+ except Exception as e:
+ print("Callback error:" + str(idx), e, flush=True)
+ pass
+ await asyncio.sleep(0.001) # Give back control for a tick, so we can await in callbacks
diff --git a/opendevin/llm/llm.py b/opendevin/llm/llm.py
index 040f28b69efc..e91f6ae672a2 100644
--- a/opendevin/llm/llm.py
+++ b/opendevin/llm/llm.py
@@ -1,17 +1,38 @@
+import os
+import uuid
+
from litellm import completion as litellm_completion
from functools import partial
-import os
DEFAULT_MODEL = os.getenv("LLM_MODEL", "gpt-4-0125-preview")
DEFAULT_API_KEY = os.getenv("LLM_API_KEY")
+DEFAULT_BASE_URL = os.getenv("LLM_BASE_URL")
+PROMPT_DEBUG_DIR = os.getenv("PROMPT_DEBUG_DIR", "")
class LLM:
- def __init__(self, model=DEFAULT_MODEL, api_key=DEFAULT_API_KEY):
+ def __init__(self, model=DEFAULT_MODEL, api_key=DEFAULT_API_KEY, base_url=DEFAULT_BASE_URL, debug_dir=PROMPT_DEBUG_DIR):
self.model = model if model else DEFAULT_MODEL
self.api_key = api_key if api_key else DEFAULT_API_KEY
+ self.base_url = base_url if base_url else DEFAULT_BASE_URL
+ self._debug_dir = debug_dir if debug_dir else PROMPT_DEBUG_DIR
+ self._debug_idx = 0
+ self._debug_id = uuid.uuid4().hex
- self._completion = partial(litellm_completion, model=self.model, api_key=self.api_key)
+ self._completion = partial(litellm_completion, model=self.model, api_key=self.api_key, base_url=self.base_url)
+ if self._debug_dir:
+ print(f"Logging prompts to {self._debug_dir}/{self._debug_id}")
+ completion_unwrapped = self._completion
+ def wrapper(*args, **kwargs):
+ if "messages" in kwargs:
+ messages = kwargs["messages"]
+ else:
+ messages = args[1]
+ resp = completion_unwrapped(*args, **kwargs)
+ message_back = resp['choices'][0]['message']['content']
+ self.write_debug(messages, message_back)
+ return resp
+ self._completion = wrapper # type: ignore
@property
def completion(self):
@@ -19,3 +40,19 @@ def completion(self):
Decorator for the litellm completion function.
"""
return self._completion
+
+ def write_debug(self, messages, response):
+ if not self._debug_dir:
+ return
+ dir = self._debug_dir + "/" + self._debug_id + "/" + str(self._debug_idx)
+ os.makedirs(dir, exist_ok=True)
+ prompt_out = ""
+ for message in messages:
+ prompt_out += "<" + message["role"] + ">\n"
+ prompt_out += message["content"] + "\n\n"
+ with open(f"{dir}/prompt.md", "w") as f:
+ f.write(prompt_out)
+ with open(f"{dir}/response.md", "w") as f:
+ f.write(response)
+ self._debug_idx += 1
+
diff --git a/opendevin/main.py b/opendevin/main.py
index 7cdb5682b9c1..c9c6ca18b080 100644
--- a/opendevin/main.py
+++ b/opendevin/main.py
@@ -1,3 +1,4 @@
+import os
import asyncio
import argparse
@@ -34,7 +35,7 @@
parser.add_argument(
"-m",
"--model-name",
- default="gpt-4-0125-preview",
+ default=os.getenv("LLM_MODEL") or "gpt-4-0125-preview",
type=str,
help="The (litellm) model name to use",
)
diff --git a/opendevin/mock/README.md b/opendevin/mock/README.md
new file mode 100644
index 000000000000..26bb0d7e64e6
--- /dev/null
+++ b/opendevin/mock/README.md
@@ -0,0 +1,10 @@
+# OpenDevin mock server
+This is a simple mock server to facilitate development in the frontend.
+
+## Start the Server
+```
+python -m pip install -r requirements.txt
+python listen.py
+```
+
+Then open the frontend to connect to the mock server. It will simply reply to every received message.
\ No newline at end of file
diff --git a/opendevin/mock/listen.py b/opendevin/mock/listen.py
new file mode 100644
index 000000000000..05c157ca0ee6
--- /dev/null
+++ b/opendevin/mock/listen.py
@@ -0,0 +1,29 @@
+import uvicorn
+from fastapi import FastAPI, WebSocket
+
+app = FastAPI()
+@app.websocket("/ws")
+async def websocket_endpoint(websocket: WebSocket):
+ await websocket.accept()
+ # send message to mock connection
+ await websocket.send_json({"action": "initialize", "message": "Control loop started."})
+
+ try:
+ while True:
+ # receive message
+ data = await websocket.receive_json()
+ print(f"Received message: {data}")
+
+ # send mock response to client
+ 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("/")
+def read_root():
+ return {"message": "This is a mock server"}
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="127.0.0.1", port=3000)
diff --git a/opendevin/observation.py b/opendevin/observation.py
index 408099453b48..bdc149648ee6 100644
--- a/opendevin/observation.py
+++ b/opendevin/observation.py
@@ -28,7 +28,7 @@ def to_dict(self) -> dict:
@property
def message(self) -> str:
"""Returns a message describing the observation."""
- return "The agent made an observation."
+ return ""
@dataclass
@@ -47,8 +47,31 @@ def error(self) -> bool:
@property
def message(self) -> str:
- return f'The agent observed command "{self.command}" executed with exit code {self.exit_code}.'
+ return f'Command `{self.command}` executed with exit code {self.exit_code}.'
+@dataclass
+class FileReadObservation(Observation):
+ """
+ This data class represents the content of a file.
+ """
+
+ path: str
+
+ @property
+ def message(self) -> str:
+ return f"I read the file {self.path}."
+
+@dataclass
+class FileWriteObservation(Observation):
+ """
+ This data class represents a file write operation
+ """
+
+ path: str
+
+ @property
+ def message(self) -> str:
+ return f"I wrote to the file {self.path}."
@dataclass
class BrowserOutputObservation(Observation):
@@ -62,7 +85,7 @@ class BrowserOutputObservation(Observation):
@property
def message(self) -> str:
- return "The agent observed the browser output at URL."
+ return "Visited " + self.url
@dataclass
@@ -75,7 +98,7 @@ class UserMessageObservation(Observation):
@property
def message(self) -> str:
- return "The agent received a message from the user."
+ return ""
@dataclass
@@ -88,7 +111,7 @@ class AgentMessageObservation(Observation):
@property
def message(self) -> str:
- return "The agent received a message from itself."
+ return ""
@dataclass
@@ -105,6 +128,16 @@ def message(self) -> str:
return "The agent recalled memories."
+@dataclass
+class AgentErrorObservation(Observation):
+ """
+ This data class represents an error encountered by the agent.
+ """
+
+ @property
+ def message(self) -> str:
+ return "Oops. Something went wrong: " + self.content
+
@dataclass
class NullObservation(Observation):
"""
diff --git a/opendevin/sandbox/Dockerfile b/opendevin/sandbox/Dockerfile
index d855985a60a3..6b4a8924cbdd 100644
--- a/opendevin/sandbox/Dockerfile
+++ b/opendevin/sandbox/Dockerfile
@@ -15,6 +15,3 @@ RUN apt-get update && apt-get install -y \
python3-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
-
-# docker build -f opendevin/sandbox/Dockerfile -t opendevin/sandbox:v0.1 .
-# docker push opendevin/sandbox:v0.1
diff --git a/opendevin/sandbox/Makefile b/opendevin/sandbox/Makefile
new file mode 100644
index 000000000000..9d82aed0a99e
--- /dev/null
+++ b/opendevin/sandbox/Makefile
@@ -0,0 +1,22 @@
+DOCKER_BUILD_REGISTRY=ghcr.io
+DOCKER_BUILD_ORG=opendevin
+DOCKER_BUILD_REPO=sandbox
+DOCKER_BUILD_TAG=v0.1
+FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(DOCKER_BUILD_TAG)
+LATEST_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):latest
+
+# normally, for local build testing or development. use cross platform build for sharing images to others.
+build:
+ docker build -f Dockerfile -t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} .
+
+push:
+ docker push ${FULL_IMAGE} ${LATEST_FULL_IMAGE}
+
+test:
+ docker buildx build --platform linux/amd64 \
+ -t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} --load -f Dockerfile .
+
+# cross platform build, you may need to manually stop the buildx(buildkit) container
+all:
+ docker buildx build --platform linux/amd64,linux/arm64 \
+ -t ${FULL_IMAGE} -t ${LATEST_FULL_IMAGE} --push -f Dockerfile .
diff --git a/opendevin/sandbox/sandbox.py b/opendevin/sandbox/sandbox.py
index a904534e87a3..91e45a77ada0 100644
--- a/opendevin/sandbox/sandbox.py
+++ b/opendevin/sandbox/sandbox.py
@@ -1,77 +1,109 @@
+import atexit
import os
+import select
import sys
-import uuid
import time
-import select
-import docker
-from typing import Tuple, Dict, List
+import uuid
from collections import namedtuple
-import atexit
+from typing import Dict, List, Tuple
+
+import docker
+import concurrent.futures
InputType = namedtuple("InputType", ["content"])
OutputType = namedtuple("OutputType", ["content"])
-CONTAINER_IMAGE = os.getenv("SANDBOX_CONTAINER_IMAGE", "opendevin/sandbox:v0.1")
+DIRECTORY_REWRITE = os.getenv(
+ "DIRECTORY_REWRITE", ""
+) # helpful for docker-in-docker scenarios
+CONTAINER_IMAGE = os.getenv("SANDBOX_CONTAINER_IMAGE", "ghcr.io/opendevin/sandbox:v0.1")
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
# How do we make this more flexible?
RUN_AS_DEVIN = os.getenv("RUN_AS_DEVIN", "true").lower() != "false"
+USER_ID = 1000
+if os.getenv("SANDBOX_USER_ID") is not None:
+ USER_ID = int(os.getenv("SANDBOX_USER_ID", ""))
+elif hasattr(os, "getuid"):
+ USER_ID = os.getuid()
+
class BackgroundCommand:
- def __init__(self, id: int, command: str, result):
+ def __init__(self, id: int, command: str, result, pid: int):
self.id = id
self.command = command
self.result = result
+ self.pid = pid
+
+ def parse_docker_exec_output(self, logs: bytes) -> Tuple[bytes, bytes]:
+ res = b""
+ tail = b""
+ i = 0
+ while i < len(logs):
+ prefix = logs[i : i + 8]
+ if len(prefix) < 8:
+ msg_type = prefix[0:1]
+ if msg_type in [b"\x00", b"\x01", b"\x02", b"\x03"]:
+ tail = prefix
+ break
+
+ msg_type = prefix[0:1]
+ padding = prefix[1:4]
+ if (
+ msg_type in [b"\x00", b"\x01", b"\x02", b"\x03"]
+ and padding == b"\x00\x00\x00"
+ ):
+ msg_length = int.from_bytes(prefix[4:8]) # , byteorder='big'
+ res += logs[i + 8 : i + 8 + msg_length]
+ i += 8 + msg_length
+ else:
+ res += logs[i : i + 1]
+ i += 1
+ return res, tail
def read_logs(self) -> str:
# TODO: get an exit code if process is exited
- logs = ""
+ logs = b""
+ last_remains = b""
while True:
- ready_to_read, _, _ = select.select([self.result.output], [], [], .1) # type: ignore[has-type]
+ ready_to_read, _, _ = select.select([self.result.output], [], [], 0.1) # type: ignore[has-type]
if ready_to_read:
- data = self.result.output.read(4096) # type: ignore[has-type]
+ data = self.result.output.read(4096) # type: ignore[has-type]
if not data:
break
- # FIXME: we're occasionally seeing some escape characters like `\x02` and `\x00` in the logs...
- chunk = data.decode('utf-8')
+ chunk, last_remains = self.parse_docker_exec_output(last_remains + data)
logs += chunk
else:
break
- return logs
-
- def kill(self):
- # FIXME: this doesn't actually kill the process!
- self.result.output.close()
+ return (logs + last_remains).decode("utf-8")
-USER_ID = 1000
-if os.getenv("SANDBOX_USER_ID") is not None:
- USER_ID = int(os.getenv("SANDBOX_USER_ID", ""))
-elif hasattr(os, "getuid"):
- USER_ID = os.getuid()
-
class DockerInteractive:
closed = False
cur_background_id = 0
- background_commands : Dict[int, BackgroundCommand] = {}
+ background_commands: Dict[int, BackgroundCommand] = {}
def __init__(
self,
workspace_dir: str | None = None,
container_image: str | None = None,
timeout: int = 120,
- id: str | None = None
+ id: str | None = None,
):
if id is not None:
self.instance_id = id
else:
self.instance_id = str(uuid.uuid4())
if workspace_dir is not None:
- assert os.path.exists(workspace_dir), f"Directory {workspace_dir} does not exist."
+ os.makedirs(workspace_dir, exist_ok=True)
# expand to absolute path
self.workspace_dir = os.path.abspath(workspace_dir)
else:
self.workspace_dir = os.getcwd()
print(f"workspace unspecified, using current directory: {workspace_dir}")
+ if DIRECTORY_REWRITE != "":
+ parts = DIRECTORY_REWRITE.split(":")
+ self.workspace_dir = self.workspace_dir.replace(parts[0], parts[1])
+ print("Rewriting workspace directory to:", self.workspace_dir)
# TODO: this timeout is actually essential - need a better way to set it
# if it is too short, the container may still waiting for previous
@@ -92,19 +124,20 @@ def __init__(
atexit.register(self.cleanup)
def setup_devin_user(self):
- uid = os.getuid()
- exit_code, logs = self.container.exec_run([
- '/bin/bash', '-c',
- f'useradd --shell /bin/bash -u {uid} -o -c \"\" -m devin'
+ exit_code, logs = self.container.exec_run(
+ [
+ "/bin/bash",
+ "-c",
+ f'useradd --shell /bin/bash -u {USER_ID} -o -c "" -m devin',
],
- workdir="/workspace"
+ workdir="/workspace",
)
def get_exec_cmd(self, cmd: str) -> List[str]:
if RUN_AS_DEVIN:
- return ['su', 'devin', '-c', cmd]
+ return ["su", "devin", "-c", cmd]
else:
- return ['/bin/bash', '-c', cmd]
+ return ["/bin/bash", "-c", cmd]
def read_logs(self, id) -> str:
if id not in self.background_commands:
@@ -114,22 +147,54 @@ def read_logs(self, id) -> str:
def execute(self, cmd: str) -> Tuple[int, str]:
# TODO: each execute is not stateful! We need to keep track of the current working directory
- exit_code, logs = self.container.exec_run(self.get_exec_cmd(cmd), workdir="/workspace")
- return exit_code, logs.decode('utf-8')
+ def run_command(container, command):
+ return container.exec_run(command,workdir="/workspace")
+ # Use ThreadPoolExecutor to control command and set timeout
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ future = executor.submit(run_command, self.container, self.get_exec_cmd(cmd))
+ try:
+ exit_code, logs = future.result(timeout=self.timeout)
+ except concurrent.futures.TimeoutError:
+ print("Command timed out, killing process...")
+ pid = self.get_pid(cmd)
+ if pid is not None:
+ self.container.exec_run(
+ f"kill -9 {pid}", workdir="/workspace"
+ )
+ return -1, f"Command: \"{cmd}\" timed out"
+ return exit_code, logs.decode("utf-8")
def execute_in_background(self, cmd: str) -> BackgroundCommand:
- result = self.container.exec_run(self.get_exec_cmd(cmd), socket=True, workdir="/workspace")
+ result = self.container.exec_run(
+ self.get_exec_cmd(cmd), socket=True, workdir="/workspace"
+ )
result.output._sock.setblocking(0)
- bg_cmd = BackgroundCommand(self.cur_background_id, cmd, result)
+ pid = self.get_pid(cmd)
+ bg_cmd = BackgroundCommand(self.cur_background_id, cmd, result, pid)
self.background_commands[bg_cmd.id] = bg_cmd
self.cur_background_id += 1
return bg_cmd
+ def get_pid(self, cmd):
+ exec_result = self.container.exec_run("ps aux")
+ processes = exec_result.output.decode('utf-8').splitlines()
+ cmd = " ".join(self.get_exec_cmd(cmd))
+
+ for process in processes:
+ if cmd in process:
+ pid = process.split()[1] # second column is the pid
+ return pid
+ return None
+
def kill_background(self, id: int) -> BackgroundCommand:
if id not in self.background_commands:
raise ValueError("Invalid background command id")
bg_cmd = self.background_commands[id]
- bg_cmd.kill()
+ if bg_cmd.pid is not None:
+ self.container.exec_run(
+ f"kill -9 {bg_cmd.pid}", workdir="/workspace"
+ )
+ bg_cmd.result.output.close()
self.background_commands.pop(id)
return bg_cmd
@@ -138,7 +203,15 @@ def close(self):
self.closed = True
def stop_docker_container(self):
- docker_client = docker.from_env()
+
+ # Initialize docker client. Throws an exception if Docker is not reachable.
+ try:
+ docker_client = docker.from_env()
+ except docker.errors.DockerException as e:
+ print('Please check Docker is running using `docker ps`.')
+ print(f"Error! {e}", flush=True)
+ raise e
+
try:
container = docker_client.containers.get(self.container_name)
container.stop()
@@ -154,17 +227,26 @@ def stop_docker_container(self):
pass
def restart_docker_container(self):
- self.stop_docker_container()
- docker_client = docker.from_env()
try:
+ self.stop_docker_container()
+ except docker.errors.DockerException as e:
+ print(f"Failed to stop container: {e}")
+ raise e
+
+ try:
+ # Initialize docker client. Throws an exception if Docker is not reachable.
+ docker_client = docker.from_env()
+
+ # start the container
self.container = docker_client.containers.run(
- self.container_image,
- command="tail -f /dev/null",
- network_mode='host',
- working_dir="/workspace",
- name=self.container_name,
- detach=True,
- volumes={self.workspace_dir: {"bind": "/workspace", "mode": "rw"}})
+ self.container_image,
+ command="tail -f /dev/null",
+ network_mode="host",
+ working_dir="/workspace",
+ name=self.container_name,
+ detach=True,
+ volumes={self.workspace_dir: {"bind": "/workspace", "mode": "rw"}},
+ )
except Exception as e:
print(f"Failed to start container: {e}")
raise e
@@ -191,8 +273,10 @@ def cleanup(self):
return
self.container.remove(force=True)
+
if __name__ == "__main__":
import argparse
+
parser = argparse.ArgumentParser(description="Interactive Docker container")
parser.add_argument(
"-d",
@@ -203,12 +287,19 @@ def cleanup(self):
)
args = parser.parse_args()
- docker_interactive = DockerInteractive(
- workspace_dir=args.directory,
- )
+ try:
+ docker_interactive = DockerInteractive(
+ workspace_dir=args.directory,
+ )
+ except Exception as e:
+ print(f"Failed to start Docker container: {e}")
+ sys.exit(1)
+
print("Interactive Docker container started. Type 'exit' or use Ctrl+C to exit.")
- bg_cmd = docker_interactive.execute_in_background("while true; do echo 'dot ' && sleep 1; done")
+ bg_cmd = docker_interactive.execute_in_background(
+ "while true; do echo 'dot ' && sleep 1; done"
+ )
sys.stdout.flush()
try:
diff --git a/opendevin/server/README.md b/opendevin/server/README.md
index 134f7ac3c922..f4cef80a2c7b 100644
--- a/opendevin/server/README.md
+++ b/opendevin/server/README.md
@@ -37,6 +37,63 @@ websocat ws://127.0.0.1:3000/ws
```sh
OPENAI_API_KEY=sk-... # Your OpenAI API Key
-MODEL_NAME=gpt-4-0125-preview # Default model for the agent to use
+LLM_MODEL=gpt-4-0125-preview # Default model for the agent to use
WORKSPACE_DIR=/path/to/your/workspace # Default path to model's workspace
```
+
+## API Schema
+There are two types of messages that can be sent to, or received from, the server:
+* Actions
+* Observations
+
+### Actions
+An action has three parts:
+* `action`: The action to be taken
+* `args`: The arguments for the action
+* `message`: A friendly message that can be put in the chat log
+
+There are several kinds of actions. Their arguments are listed below.
+This list may grow over time.
+* `initialize` - initializes the agent. Only sent by client.
+ * `model` - the name of the model to use
+ * `directory` - the path to the workspace
+ * `agent_cls` - the class of the agent to use
+* `start` - starts a new development task. Only sent by the client.
+ * `task` - the task to start
+* `read` - reads the contents of a file.
+ * `path` - the path of the file to read
+* `write` - writes the contents to a file.
+ * `path` - the path of the file to write
+ * `contents` - the contents to write to the file
+* `run` - runs a command.
+ * `command` - the command to run
+ * `background` - if true, run the command in the background
+* `kill` - kills a background command
+ * `id` - the ID of the background command to kill
+* `browse` - opens a web page.
+ * `url` - the URL to open
+* `recall` - searches long-term memory
+ * `query` - the query to search for
+* `think` - Allows the agent to make a plan, set a goal, or record thoughts
+ * `thought` - the thought to record
+* `finish` - agent signals that the task is completed
+
+### Observations
+An observation has four parts:
+* `observation`: The observation type
+* `content`: A string representing the observed data
+* `extras`: additional structured data
+* `message`: A friendly message that can be put in the chat log
+
+There are several kinds of observations. Their extras are listed below.
+This list may grow over time.
+* `read` - the contents of a file
+ * `path` - the path of the file read
+* `browse` - the HTML contents of a url
+ * `url` - the URL opened
+* `run` - the output of a command
+ * `command` - the command run
+ * `exit_code` - the exit code of the command
+* `recall` - the result of a search
+ * `query` - the query searched for
+* `chat` - a message from the user
diff --git a/opendevin/server/listen.py b/opendevin/server/listen.py
index e14567640f55..9031a326ffc4 100644
--- a/opendevin/server/listen.py
+++ b/opendevin/server/listen.py
@@ -4,7 +4,7 @@
app = FastAPI()
-# This endpoint recieves events from the client (i.e. the browser)
+# This endpoint receives events from the client (i.e. the browser)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
diff --git a/opendevin/server/schema/action.py b/opendevin/server/schema/action.py
new file mode 100644
index 000000000000..d93384ec3075
--- /dev/null
+++ b/opendevin/server/schema/action.py
@@ -0,0 +1,44 @@
+from enum import Enum
+
+
+class ActionType(str, Enum):
+ INIT = "initialize"
+ """Initializes the agent. Only sent by client.
+ """
+
+ START = "start"
+ """Starts a new development task. Only sent by the client.
+ """
+
+ READ = "read"
+ """Reads the contents of a file.
+ """
+
+ WRITE = "write"
+ """Writes the contents to a file.
+ """
+
+ RUN = "run"
+ """Runs a command.
+ """
+
+ KILL = "kill"
+ """Kills a background command.
+ """
+
+ BROWSE = "browse"
+ """Opens a web page.
+ """
+
+ RECALL = "recall"
+ """Searches long-term memory
+ """
+
+ THINK = "think"
+ """Allows the agent to make a plan, set a goal, or record thoughts
+ """
+
+ FINISH = "finish"
+ """If you're absolutely certain that you've completed your task and have tested your work,
+ use the finish action to stop working.
+ """
diff --git a/opendevin/server/schema/observation.py b/opendevin/server/schema/observation.py
new file mode 100644
index 000000000000..74450ab0a714
--- /dev/null
+++ b/opendevin/server/schema/observation.py
@@ -0,0 +1,23 @@
+from enum import Enum
+
+
+class ObservationType(str, Enum):
+ READ = "read"
+ """The contents of a file
+ """
+
+ BROWSE = "browse"
+ """The HTML contents of a URL
+ """
+
+ RUN = "run"
+ """The output of a command
+ """
+
+ RECALL = "recall"
+ """The result of a search
+ """
+
+ CHAT = "chat"
+ """A message from the user
+ """
diff --git a/opendevin/server/session.py b/opendevin/server/session.py
index bbe44b8a5a89..77651753a161 100644
--- a/opendevin/server/session.py
+++ b/opendevin/server/session.py
@@ -1,27 +1,25 @@
-import os
import asyncio
-from typing import Optional, Dict, Type
+import os
+from typing import Dict, Optional, Type
from fastapi import WebSocketDisconnect
from opendevin.action import (
Action,
- CmdRunAction,
- CmdKillAction,
+ AgentFinishAction,
+ AgentRecallAction,
+ AgentThinkAction,
BrowseURLAction,
+ CmdKillAction,
+ CmdRunAction,
FileReadAction,
FileWriteAction,
- AgentRecallAction,
- AgentThinkAction,
- AgentFinishAction,
+ NullAction,
)
from opendevin.agent import Agent
from opendevin.controller import AgentController
from opendevin.llm.llm import LLM
-from opendevin.observation import (
- Observation,
- UserMessageObservation
-)
+from opendevin.observation import Observation, UserMessageObservation
# NOTE: this is a temporary solution - but hopefully we can use Action/Observation throughout the codebase
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
@@ -37,7 +35,7 @@
DEFAULT_WORKSPACE_DIR = os.getenv("WORKSPACE_DIR", os.path.join(os.getcwd(), "workspace"))
-MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4-0125-preview")
+LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4-0125-preview")
def parse_event(data):
if "action" not in data:
@@ -99,7 +97,7 @@ async def start_listening(self):
await self.send_error("No agent started. Please wait a second...")
elif event["action"] == "chat":
- self.controller.add_observation(UserMessageObservation(event["message"]))
+ self.controller.add_history(NullAction(), UserMessageObservation(event["message"]))
else:
# TODO: we only need to implement user message for now
# since even Devin does not support having the user taking other
@@ -114,14 +112,14 @@ async def start_listening(self):
async def create_controller(self, start_event=None):
directory = DEFAULT_WORKSPACE_DIR
- if start_event and "directory" in start_event.args:
- directory = start_event.args["directory"]
+ if start_event and "directory" in start_event["args"]:
+ directory = start_event["args"]["directory"]
agent_cls = "LangchainsAgent"
- if start_event and "agent_cls" in start_event.args:
- agent_cls = start_event.args["agent_cls"]
- model = MODEL_NAME
- if start_event and "model" in start_event.args:
- model = start_event.args["model"]
+ 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"]
if not os.path.exists(directory):
print(f"Workspace directory {directory} does not exist. Creating it...")
os.makedirs(directory)
@@ -129,7 +127,12 @@ async def create_controller(self, start_event=None):
llm = LLM(model)
AgentCls = Agent.get_cls(agent_cls)
self.agent = AgentCls(llm)
- self.controller = AgentController(self.agent, workdir=directory, callbacks=[self.on_agent_event])
+ try:
+ self.controller = AgentController(self.agent, workdir=directory, callbacks=[self.on_agent_event])
+ except Exception:
+ print("Error creating controller.")
+ await self.send_error("Error creating controller. Please check Docker is running using `docker ps`.")
+ return
await self.send({"action": "initialize", "message": "Control loop started."})
async def start_task(self, start_event):
@@ -144,5 +147,33 @@ async def start_task(self, start_event):
self.agent_task = asyncio.create_task(self.controller.start_loop(task), name="agent loop")
def on_agent_event(self, event: Observation | Action):
+ # FIXME: we need better serialization
event_dict = event.to_dict()
+ if "action" in event_dict:
+ if event_dict["action"] == "CmdRunAction":
+ event_dict["action"] = "run"
+ elif event_dict["action"] == "CmdKillAction":
+ event_dict["action"] = "kill"
+ elif event_dict["action"] == "BrowseURLAction":
+ event_dict["action"] = "browse"
+ elif event_dict["action"] == "FileReadAction":
+ event_dict["action"] = "read"
+ elif event_dict["action"] == "FileWriteAction":
+ event_dict["action"] = "write"
+ elif event_dict["action"] == "AgentFinishAction":
+ event_dict["action"] = "finish"
+ elif event_dict["action"] == "AgentRecallAction":
+ event_dict["action"] = "recall"
+ elif event_dict["action"] == "AgentThinkAction":
+ event_dict["action"] = "think"
+ if "observation" in event_dict:
+ if event_dict["observation"] == "UserMessageObservation":
+ event_dict["observation"] = "chat"
+ elif event_dict["observation"] == "AgentMessageObservation":
+ event_dict["observation"] = "chat"
+ elif event_dict["observation"] == "CmdOutputObservation":
+ event_dict["observation"] = "run"
+ elif event_dict["observation"] == "FileReadObservation":
+ event_dict["observation"] = "read"
+
asyncio.create_task(self.send(event_dict), name="send event in callback")
diff --git a/opendevin/state.py b/opendevin/state.py
index d7a6b41ba793..826053a3df35 100644
--- a/opendevin/state.py
+++ b/opendevin/state.py
@@ -1,4 +1,4 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from typing import List, Tuple
from opendevin.action import (
@@ -9,8 +9,10 @@
CmdOutputObservation,
)
-
@dataclass
class State:
- background_commands_obs: List[CmdOutputObservation]
- updated_info: List[Tuple[Action, Observation]]
+ task: str
+ iteration: int = 0
+ background_commands_obs: List[CmdOutputObservation] = field(default_factory=list)
+ history: List[Tuple[Action, Observation]] = field(default_factory=list)
+ updated_info: List[Tuple[Action, Observation]] = field(default_factory=list)
diff --git a/requirements.txt b/requirements.txt
index 3a3f2cda8159..f681ce479bdc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,7 @@ fastapi
uvicorn[standard]
ruff
mypy
+pytest
# for agenthub/lanchangs_agent
langchain
@@ -16,3 +17,6 @@ langchain-community
llama-index
llama-index-vector-stores-chroma
chromadb
+llama-index-embeddings-huggingface
+llama-index-embeddings-azure-openai
+llama-index-embeddings-ollama