mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
style: Action and Observation use schema for unifying (#494)
* style: Action and Observation use schema for unifying * merge from upstream/main * merge from upstream/main
This commit is contained in:
@@ -2,6 +2,7 @@ from typing import List
|
||||
from opendevin.agent import Agent
|
||||
from opendevin.state import State
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.schema import ActionType, ObservationType
|
||||
|
||||
from opendevin.action import (
|
||||
Action,
|
||||
@@ -98,7 +99,7 @@ class MonologueAgent(Agent):
|
||||
|
||||
def _add_event(self, event: dict):
|
||||
"""
|
||||
Adds a new event to the agent's monologue and memory.
|
||||
Adds a new event to the agent's monologue and memory.
|
||||
Monologue automatically condenses when it gets too large.
|
||||
|
||||
Parameters:
|
||||
@@ -107,8 +108,14 @@ class MonologueAgent(Agent):
|
||||
|
||||
if "extras" in event and "screenshot" in event["extras"]:
|
||||
del event["extras"]["screenshot"]
|
||||
if 'args' in event and 'output' in event['args'] and len(event['args']['output']) > MAX_OUTPUT_LENGTH:
|
||||
event['args']['output'] = event['args']['output'][:MAX_OUTPUT_LENGTH] + "..."
|
||||
if (
|
||||
"args" in event
|
||||
and "output" in event["args"]
|
||||
and len(event["args"]["output"]) > MAX_OUTPUT_LENGTH
|
||||
):
|
||||
event["args"]["output"] = (
|
||||
event["args"]["output"][:MAX_OUTPUT_LENGTH] + "..."
|
||||
)
|
||||
|
||||
self.monologue.add_event(event)
|
||||
self.memory.add_event(event)
|
||||
@@ -121,7 +128,7 @@ class MonologueAgent(Agent):
|
||||
Short circuted to return when already initialized.
|
||||
|
||||
Parameters:
|
||||
- task (str): The initial goal statement provided by the user
|
||||
- task (str): The initial goal statement provided by the user
|
||||
|
||||
Raises:
|
||||
- ValueError: If task is not provided
|
||||
@@ -140,14 +147,18 @@ class MonologueAgent(Agent):
|
||||
thought = thought.replace("$TASK", task)
|
||||
if output_type != "":
|
||||
observation: Observation = NullObservation(content="")
|
||||
if output_type == "run":
|
||||
observation = CmdOutputObservation(content=thought, command_id=0, command="")
|
||||
elif output_type == "read":
|
||||
if output_type == ObservationType.RUN:
|
||||
observation = CmdOutputObservation(
|
||||
content=thought, command_id=0, command=""
|
||||
)
|
||||
elif output_type == ObservationType.READ:
|
||||
observation = FileReadObservation(content=thought, path="")
|
||||
elif output_type == "recall":
|
||||
elif output_type == ObservationType.RECALL:
|
||||
observation = AgentRecallObservation(content=thought, memories=[])
|
||||
elif output_type == "browse":
|
||||
observation = BrowserOutputObservation(content=thought, url="", screenshot="")
|
||||
elif output_type == ObservationType.BROWSE:
|
||||
observation = BrowserOutputObservation(
|
||||
content=thought, url="", screenshot=""
|
||||
)
|
||||
self._add_event(observation.to_dict())
|
||||
output_type = ""
|
||||
else:
|
||||
@@ -155,7 +166,7 @@ class MonologueAgent(Agent):
|
||||
if thought.startswith("RUN"):
|
||||
command = thought.split("RUN ")[1]
|
||||
action = CmdRunAction(command)
|
||||
output_type = "run"
|
||||
output_type = ActionType.RUN
|
||||
elif thought.startswith("WRITE"):
|
||||
parts = thought.split("WRITE ")[1].split(" > ")
|
||||
path = parts[1]
|
||||
@@ -164,15 +175,15 @@ class MonologueAgent(Agent):
|
||||
elif thought.startswith("READ"):
|
||||
path = thought.split("READ ")[1]
|
||||
action = FileReadAction(path=path)
|
||||
output_type = "read"
|
||||
output_type = ActionType.READ
|
||||
elif thought.startswith("RECALL"):
|
||||
query = thought.split("RECALL ")[1]
|
||||
action = AgentRecallAction(query=query)
|
||||
output_type = "recall"
|
||||
output_type = ActionType.RECALL
|
||||
elif thought.startswith("BROWSE"):
|
||||
url = thought.split("BROWSE ")[1]
|
||||
action = BrowseURLAction(url=url)
|
||||
output_type = "browse"
|
||||
output_type = ActionType.BROWSE
|
||||
else:
|
||||
action = AgentThinkAction(thought=thought)
|
||||
self._add_event(action.to_dict())
|
||||
@@ -200,9 +211,9 @@ class MonologueAgent(Agent):
|
||||
self.monologue.get_thoughts(),
|
||||
state.background_commands_obs,
|
||||
)
|
||||
messages = [{"content": prompt,"role": "user"}]
|
||||
messages = [{"content": prompt, "role": "user"}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
action_resp = resp["choices"][0]["message"]["content"]
|
||||
action = prompts.parse_action_response(action_resp)
|
||||
self.latest_action = action
|
||||
return action
|
||||
@@ -211,7 +222,7 @@ class MonologueAgent(Agent):
|
||||
"""
|
||||
Uses VectorIndexRetriever to find related memories within the long term memory.
|
||||
Uses search to produce top 10 results.
|
||||
|
||||
|
||||
Parameters:
|
||||
- query (str): The query that we want to find related memories for
|
||||
|
||||
@@ -219,4 +230,3 @@ class MonologueAgent(Agent):
|
||||
- List[str]: A list of top 10 text results that matched the query
|
||||
"""
|
||||
return self.memory.search(query)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from opendevin.controller.agent_controller import print_with_color
|
||||
from opendevin.plan import Plan
|
||||
from opendevin.action import Action, action_from_dict
|
||||
from opendevin.observation import Observation
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
from opendevin.action import (
|
||||
NullAction,
|
||||
@@ -26,17 +27,17 @@ from opendevin.observation import (
|
||||
)
|
||||
|
||||
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
|
||||
"run": CmdRunAction,
|
||||
"kill": CmdKillAction,
|
||||
"browse": BrowseURLAction,
|
||||
"read": FileReadAction,
|
||||
"write": FileWriteAction,
|
||||
"recall": AgentRecallAction,
|
||||
"think": AgentThinkAction,
|
||||
"summarize": AgentSummarizeAction,
|
||||
"finish": AgentFinishAction,
|
||||
"add_task": AddTaskAction,
|
||||
"modify_task": ModifyTaskAction,
|
||||
ActionType.RUN: CmdRunAction,
|
||||
ActionType.KILL: CmdKillAction,
|
||||
ActionType.BROWSE: BrowseURLAction,
|
||||
ActionType.READ: FileReadAction,
|
||||
ActionType.WRITE: FileWriteAction,
|
||||
ActionType.RECALL: AgentRecallAction,
|
||||
ActionType.THINK: AgentThinkAction,
|
||||
ActionType.SUMMARIZE: AgentSummarizeAction,
|
||||
ActionType.FINISH: AgentFinishAction,
|
||||
ActionType.ADD_TASK: AddTaskAction,
|
||||
ActionType.MODIFY_TASK: ModifyTaskAction,
|
||||
}
|
||||
|
||||
HISTORY_SIZE = 10
|
||||
@@ -129,9 +130,10 @@ What is your next thought or action? Again, you must reply with JSON, and only w
|
||||
%(hint)s
|
||||
"""
|
||||
|
||||
|
||||
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
"""
|
||||
Gets the prompt for the planner agent.
|
||||
Gets the prompt for the planner agent.
|
||||
Formatted with the most recent action-observation pairs, current task, and hint based on last action
|
||||
|
||||
Parameters:
|
||||
@@ -152,7 +154,10 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
latest_action = action
|
||||
if not isinstance(observation, NullObservation):
|
||||
observation_dict = observation.to_dict()
|
||||
if "extras" in observation_dict and "screenshot" in observation_dict["extras"]:
|
||||
if (
|
||||
"extras" in observation_dict
|
||||
and "screenshot" in observation_dict["extras"]
|
||||
):
|
||||
del observation_dict["extras"]["screenshot"]
|
||||
history_dicts.append(observation_dict)
|
||||
history_str = json.dumps(history_dicts, indent=2)
|
||||
@@ -167,41 +172,42 @@ def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
|
||||
hint = plan_status
|
||||
|
||||
latest_action_id = latest_action.to_dict()['action']
|
||||
latest_action_id = latest_action.to_dict()["action"]
|
||||
|
||||
if current_task is not None:
|
||||
if latest_action_id == "":
|
||||
hint = "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with."
|
||||
elif latest_action_id == "run":
|
||||
elif latest_action_id == ActionType.RUN:
|
||||
hint = "You should think about the command you just ran, what output it gave, and how that affects your plan."
|
||||
elif latest_action_id == "read":
|
||||
elif latest_action_id == ActionType.READ:
|
||||
hint = "You should think about the file you just read, what you learned from it, and how that affects your plan."
|
||||
elif latest_action_id == "write":
|
||||
elif latest_action_id == ActionType.WRITE:
|
||||
hint = "You just changed a file. You should think about how it affects your plan."
|
||||
elif latest_action_id == "browse":
|
||||
elif latest_action_id == ActionType.BROWSE:
|
||||
hint = "You should think about the page you just visited, and what you learned from it."
|
||||
elif latest_action_id == "think":
|
||||
elif latest_action_id == ActionType.THINK:
|
||||
hint = "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action."
|
||||
elif latest_action_id == "recall":
|
||||
elif latest_action_id == ActionType.RECALL:
|
||||
hint = "You should think about the information you just recalled, and how it should affect your plan."
|
||||
elif latest_action_id == "add_task":
|
||||
elif latest_action_id == ActionType.ADD_TASK:
|
||||
hint = "You should think about the next action to take."
|
||||
elif latest_action_id == "modify_task":
|
||||
elif latest_action_id == ActionType.MODIFY_TASK:
|
||||
hint = "You should think about the next action to take."
|
||||
elif latest_action_id == "summarize":
|
||||
elif latest_action_id == ActionType.SUMMARIZE:
|
||||
hint = ""
|
||||
elif latest_action_id == "finish":
|
||||
elif latest_action_id == ActionType.FINISH:
|
||||
hint = ""
|
||||
|
||||
print_with_color("HINT:\n" + hint, "INFO")
|
||||
return prompt % {
|
||||
'task': plan.main_goal,
|
||||
'plan': plan_str,
|
||||
'history': history_str,
|
||||
'hint': hint,
|
||||
'plan_status': plan_status,
|
||||
"task": plan.main_goal,
|
||||
"plan": plan_str,
|
||||
"history": history_str,
|
||||
"hint": hint,
|
||||
"plan_status": plan_status,
|
||||
}
|
||||
|
||||
|
||||
def parse_response(response: str) -> Action:
|
||||
"""
|
||||
Parses the model output to find a valid action to take
|
||||
@@ -216,9 +222,8 @@ def parse_response(response: str) -> Action:
|
||||
json_end = response.rfind("}") + 1
|
||||
response = response[json_start:json_end]
|
||||
action_dict = json.loads(response)
|
||||
if 'contents' in action_dict:
|
||||
if "contents" in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict['content'] = action_dict.pop('contents')
|
||||
action_dict["content"] = action_dict.pop("contents")
|
||||
action = action_from_dict(action_dict)
|
||||
return action
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { IDisposable, Terminal as XtermTerminal } from "@xterm/xterm";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import ActionType from "../types/ActionType";
|
||||
import ObservationType from "../types/ObservationType";
|
||||
import Socket from "../services/socket";
|
||||
import { RootState } from "../store";
|
||||
|
||||
@@ -22,10 +24,10 @@ class JsonWebsocketAddon {
|
||||
);
|
||||
Socket.addEventListener("message", (event) => {
|
||||
const { action, args, observation, content } = JSON.parse(event.data);
|
||||
if (action === "run") {
|
||||
if (action === ActionType.RUN) {
|
||||
terminal.writeln(args.command);
|
||||
}
|
||||
if (observation === "run") {
|
||||
if (observation === ObservationType.RUN) {
|
||||
content.split("\n").forEach((line: string) => {
|
||||
terminal.writeln(line);
|
||||
});
|
||||
|
||||
@@ -7,11 +7,12 @@ import store from "../store";
|
||||
import { ActionMessage } from "../types/Message";
|
||||
import { SocketMessage } from "../types/ResponseType";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
import ActionType from "../types/ActionType";
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
const messageActions = {
|
||||
initialize: () => {
|
||||
[ActionType.INIT]: () => {
|
||||
store.dispatch(setInitialized(true));
|
||||
if (isInitialized) {
|
||||
return;
|
||||
@@ -23,23 +24,23 @@ const messageActions = {
|
||||
);
|
||||
isInitialized = true;
|
||||
},
|
||||
browse: (message: ActionMessage) => {
|
||||
[ActionType.BROWSE]: (message: ActionMessage) => {
|
||||
const { url, screenshotSrc } = message.args;
|
||||
store.dispatch(setUrl(url));
|
||||
store.dispatch(setScreenshotSrc(screenshotSrc));
|
||||
},
|
||||
write: (message: ActionMessage) => {
|
||||
[ActionType.WRITE]: (message: ActionMessage) => {
|
||||
const { path, content } = message.args;
|
||||
store.dispatch(updatePath(path));
|
||||
store.dispatch(setCode(content));
|
||||
},
|
||||
think: (message: ActionMessage) => {
|
||||
[ActionType.THINK]: (message: ActionMessage) => {
|
||||
store.dispatch(appendAssistantMessage(message.args.thought));
|
||||
},
|
||||
finish: (message: ActionMessage) => {
|
||||
[ActionType.FINISH]: (message: ActionMessage) => {
|
||||
store.dispatch(appendAssistantMessage(message.message));
|
||||
},
|
||||
run: (message: ActionMessage) => {
|
||||
[ActionType.RUN]: (message: ActionMessage) => {
|
||||
store.dispatch(appendInput(message.args.command));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
} from "../state/chatSlice";
|
||||
import Socket from "./socket";
|
||||
import store from "../store";
|
||||
import ActionType from "../types/ActionType";
|
||||
import { SocketMessage } from "../types/ResponseType";
|
||||
import { ActionMessage } from "../types/Message";
|
||||
|
||||
export function sendChatMessage(message: string): void {
|
||||
store.dispatch(appendUserMessage(message));
|
||||
const event = { action: "start", args: { task: message } };
|
||||
const event = { action: ActionType.START, args: { task: message } };
|
||||
const eventString = JSON.stringify(event);
|
||||
Socket.send(eventString);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { appendAssistantMessage } from "../state/chatSlice";
|
||||
import { setInitialized } from "../state/taskSlice";
|
||||
import store from "../store";
|
||||
import ActionType from "../types/ActionType";
|
||||
import Socket from "./socket";
|
||||
import {
|
||||
setAgent,
|
||||
@@ -61,7 +62,7 @@ export function saveSettings(
|
||||
value,
|
||||
]),
|
||||
);
|
||||
const event = { action: "initialize", args: socketSettings };
|
||||
const event = { action: ActionType.INIT, args: socketSettings };
|
||||
const eventString = JSON.stringify(event);
|
||||
store.dispatch(setInitialized(false));
|
||||
Socket.send(eventString);
|
||||
|
||||
@@ -2,7 +2,13 @@ from .base import Action, NullAction
|
||||
from .bash import CmdRunAction, CmdKillAction
|
||||
from .browse import BrowseURLAction
|
||||
from .fileop import FileReadAction, FileWriteAction
|
||||
from .agent import AgentRecallAction, AgentThinkAction, AgentFinishAction, AgentEchoAction, AgentSummarizeAction
|
||||
from .agent import (
|
||||
AgentRecallAction,
|
||||
AgentThinkAction,
|
||||
AgentFinishAction,
|
||||
AgentEchoAction,
|
||||
AgentSummarizeAction,
|
||||
)
|
||||
from .tasks import AddTaskAction, ModifyTaskAction
|
||||
|
||||
actions = (
|
||||
@@ -18,7 +24,8 @@ actions = (
|
||||
ModifyTaskAction,
|
||||
)
|
||||
|
||||
ACTION_TYPE_TO_CLASS = {action_class.action:action_class for action_class in actions} # type: ignore[attr-defined]
|
||||
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def action_from_dict(action: dict) -> Action:
|
||||
action = action.copy()
|
||||
@@ -26,10 +33,13 @@ def action_from_dict(action: dict) -> Action:
|
||||
raise KeyError(f"'action' key is not found in {action=}")
|
||||
action_class = ACTION_TYPE_TO_CLASS.get(action["action"])
|
||||
if action_class is None:
|
||||
raise KeyError(f"'{action['action']=}' is not defined. Available actions: {ACTION_TYPE_TO_CLASS.keys()}")
|
||||
raise KeyError(
|
||||
f"'{action['action']=}' is not defined. Available actions: {ACTION_TYPE_TO_CLASS.keys()}"
|
||||
)
|
||||
args = action.get("args", {})
|
||||
return action_class(**args)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Action",
|
||||
"NullAction",
|
||||
@@ -44,5 +54,5 @@ __all__ = [
|
||||
"AgentEchoAction",
|
||||
"AgentSummarizeAction",
|
||||
"AddTaskAction",
|
||||
"ModifyTaskAction"
|
||||
"ModifyTaskAction",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from opendevin.observation import AgentRecallObservation, AgentMessageObservation, Observation
|
||||
from opendevin.observation import (
|
||||
AgentRecallObservation,
|
||||
AgentMessageObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.schema import ActionType
|
||||
from .base import ExecutableAction, NotExecutableAction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opendevin.controller import AgentController
|
||||
|
||||
@@ -10,22 +16,23 @@ if TYPE_CHECKING:
|
||||
@dataclass
|
||||
class AgentRecallAction(ExecutableAction):
|
||||
query: str
|
||||
action: str = "recall"
|
||||
action: str = ActionType.RECALL
|
||||
|
||||
def run(self, controller: "AgentController") -> AgentRecallObservation:
|
||||
return AgentRecallObservation(
|
||||
content="Recalling memories...",
|
||||
memories=controller.agent.search_memory(self.query)
|
||||
memories=controller.agent.search_memory(self.query),
|
||||
)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Let me dive into my memories to find what you're looking for! Searching for: '{self.query}'. This might take a moment."
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentThinkAction(NotExecutableAction):
|
||||
thought: str
|
||||
action: str = "think"
|
||||
action: str = ActionType.THINK
|
||||
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
raise NotImplementedError
|
||||
@@ -34,6 +41,7 @@ class AgentThinkAction(NotExecutableAction):
|
||||
def message(self) -> str:
|
||||
return self.thought
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentEchoAction(ExecutableAction):
|
||||
content: str
|
||||
@@ -46,19 +54,21 @@ class AgentEchoAction(ExecutableAction):
|
||||
def message(self) -> str:
|
||||
return self.content
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentSummarizeAction(NotExecutableAction):
|
||||
summary: str
|
||||
|
||||
action: str = "summarize"
|
||||
action: str = ActionType.SUMMARIZE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.summary
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentFinishAction(NotExecutableAction):
|
||||
action: str = "finish"
|
||||
action: str = ActionType.FINISH
|
||||
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import TYPE_CHECKING
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opendevin.controller import AgentController
|
||||
from opendevin.observation import Observation
|
||||
|
||||
|
||||
@dataclass
|
||||
class Action:
|
||||
def run(self, controller: "AgentController") -> "Observation":
|
||||
@@ -13,10 +15,10 @@ class Action:
|
||||
def to_dict(self):
|
||||
d = asdict(self)
|
||||
try:
|
||||
v = d.pop('action')
|
||||
v = d.pop("action")
|
||||
except KeyError:
|
||||
raise NotImplementedError(f'{self=} does not have action attribute set')
|
||||
return {'action': v, "args": d, "message": self.message}
|
||||
raise NotImplementedError(f"{self=} does not have action attribute set")
|
||||
return {"action": v, "args": d, "message": self.message}
|
||||
|
||||
@property
|
||||
def executable(self) -> bool:
|
||||
@@ -26,6 +28,7 @@ class Action:
|
||||
def message(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutableAction(Action):
|
||||
@property
|
||||
@@ -39,12 +42,14 @@ class NotExecutableAction(Action):
|
||||
def executable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class NullAction(NotExecutableAction):
|
||||
"""An action that does nothing.
|
||||
This is used when the agent need to receive user follow-up messages from the frontend.
|
||||
"""
|
||||
action: str = "null"
|
||||
|
||||
action: str = ActionType.NULL
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import ExecutableAction
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opendevin.controller import AgentController
|
||||
@@ -12,7 +13,7 @@ if TYPE_CHECKING:
|
||||
class CmdRunAction(ExecutableAction):
|
||||
command: str
|
||||
background: bool = False
|
||||
action: str = "run"
|
||||
action: str = ActionType.RUN
|
||||
|
||||
def run(self, controller: "AgentController") -> "CmdOutputObservation":
|
||||
return controller.command_manager.run_command(self.command, self.background)
|
||||
@@ -21,14 +22,15 @@ class CmdRunAction(ExecutableAction):
|
||||
def message(self) -> str:
|
||||
return f"Running command: {self.command}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CmdKillAction(ExecutableAction):
|
||||
id: int
|
||||
action: str = "kill"
|
||||
action: str = ActionType.KILL
|
||||
|
||||
def run(self, controller: "AgentController") -> "CmdOutputObservation":
|
||||
return controller.command_manager.kill_command(self.id)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Killing command: {self.id}"
|
||||
return f"Killing command: {self.id}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
from opendevin.observation import BrowserOutputObservation
|
||||
from opendevin.schema import ActionType
|
||||
from typing import TYPE_CHECKING
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
@@ -9,12 +10,13 @@ from .base import ExecutableAction
|
||||
if TYPE_CHECKING:
|
||||
from opendevin.controller import AgentController
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowseURLAction(ExecutableAction):
|
||||
url: str
|
||||
action: str = "browse"
|
||||
action: str = ActionType.BROWSE
|
||||
|
||||
async def run(self, controller: "AgentController") -> BrowserOutputObservation: # type: ignore
|
||||
async def run(self, controller: "AgentController") -> BrowserOutputObservation: # type: ignore
|
||||
try:
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
@@ -34,12 +36,9 @@ class BrowseURLAction(ExecutableAction):
|
||||
)
|
||||
except Exception as e:
|
||||
return BrowserOutputObservation(
|
||||
content=str(e),
|
||||
screenshot="",
|
||||
error=True,
|
||||
url=self.url
|
||||
content=str(e), screenshot="", error=True, url=self.url
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Browsing URL: {self.url}"
|
||||
return f"Browsing URL: {self.url}"
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.observation import FileReadObservation, FileWriteObservation
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
from .base import ExecutableAction
|
||||
|
||||
@@ -19,7 +20,7 @@ def resolve_path(base_path, file_path):
|
||||
@dataclass
|
||||
class FileReadAction(ExecutableAction):
|
||||
path: str
|
||||
action: str = "read"
|
||||
action: str = ActionType.READ
|
||||
|
||||
def run(self, controller) -> FileReadObservation:
|
||||
path = resolve_path(controller.workdir, self.path)
|
||||
@@ -35,7 +36,7 @@ class FileReadAction(ExecutableAction):
|
||||
class FileWriteAction(ExecutableAction):
|
||||
path: str
|
||||
content: str
|
||||
action: str = "write"
|
||||
action: str = ActionType.WRITE
|
||||
|
||||
def run(self, controller) -> FileWriteObservation:
|
||||
whole_path = resolve_path(controller.workdir, self.path)
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .base import NotExecutableAction
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddTaskAction(NotExecutableAction):
|
||||
parent: str
|
||||
goal: str
|
||||
subtasks: list = field(default_factory=list)
|
||||
action: str = "add_task"
|
||||
action: str = ActionType.ADD_TASK
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Added task: {self.goal}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModifyTaskAction(NotExecutableAction):
|
||||
id: str
|
||||
state: str
|
||||
action: str = "modify_task"
|
||||
action: str = ActionType.MODIFY_TASK
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"Set task {self.id} to {self.state}"
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, WebSocket
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
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."})
|
||||
|
||||
await websocket.send_json(
|
||||
{"action": ActionType.INIT, "message": "Control loop started."}
|
||||
)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# receive message
|
||||
@@ -21,10 +26,12 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
except Exception as e:
|
||||
print(f"WebSocket Error: {e}")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "This is a mock server"}
|
||||
|
||||
|
||||
@app.get("/litellm-models")
|
||||
def read_llm_models():
|
||||
return [
|
||||
@@ -34,6 +41,7 @@ def read_llm_models():
|
||||
"gpt-4-0613",
|
||||
]
|
||||
|
||||
|
||||
@app.get("/litellm-agents")
|
||||
def read_llm_agents():
|
||||
return [
|
||||
@@ -42,9 +50,11 @@ def read_llm_agents():
|
||||
"PlannerAgent",
|
||||
]
|
||||
|
||||
|
||||
@app.get("/default-model")
|
||||
def read_default_model():
|
||||
return "gpt-4"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=3000)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Observation:
|
||||
@@ -36,7 +38,8 @@ class NullObservation(Observation):
|
||||
This data class represents a null observation.
|
||||
This is used when the produced action is NOT executable.
|
||||
"""
|
||||
observation : str = "null"
|
||||
|
||||
observation: str = ObservationType.NULL
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import Observation
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowserOutputObservation(Observation):
|
||||
@@ -12,11 +14,8 @@ class BrowserOutputObservation(Observation):
|
||||
screenshot: str
|
||||
status_code: int = 200
|
||||
error: bool = False
|
||||
observation : str = "browse"
|
||||
observation: str = ObservationType.BROWSE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "Visited " + self.url
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import Observation
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentErrorObservation(Observation):
|
||||
"""
|
||||
This data class represents an error encountered by the agent.
|
||||
"""
|
||||
observation : str = "error"
|
||||
|
||||
observation: str = ObservationType.ERROR
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "Oops. Something went wrong: " + self.content
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import Observation
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileReadObservation(Observation):
|
||||
@@ -9,12 +11,13 @@ class FileReadObservation(Observation):
|
||||
"""
|
||||
|
||||
path: str
|
||||
observation : str = "read"
|
||||
observation: str = ObservationType.READ
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"I read the file {self.path}."
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileWriteObservation(Observation):
|
||||
"""
|
||||
@@ -22,10 +25,8 @@ class FileWriteObservation(Observation):
|
||||
"""
|
||||
|
||||
path: str
|
||||
observation : str = "write"
|
||||
observation: str = ObservationType.WRITE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"I wrote to the file {self.path}."
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import Observation
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMessageObservation(Observation):
|
||||
@@ -9,7 +11,7 @@ class UserMessageObservation(Observation):
|
||||
"""
|
||||
|
||||
role: str = "user"
|
||||
observation : str = "message"
|
||||
observation: str = ObservationType.MESSAGE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
@@ -23,11 +25,8 @@ class AgentMessageObservation(Observation):
|
||||
"""
|
||||
|
||||
role: str = "assistant"
|
||||
observation : str = "message"
|
||||
observation: str = ObservationType.MESSAGE
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from .base import Observation
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRecallObservation(Observation):
|
||||
@@ -11,11 +13,8 @@ class AgentRecallObservation(Observation):
|
||||
|
||||
memories: List[str]
|
||||
role: str = "assistant"
|
||||
observation : str = "recall"
|
||||
observation: str = ObservationType.RECALL
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return "The agent recalled memories."
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import Observation
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
@dataclass
|
||||
class CmdOutputObservation(Observation):
|
||||
@@ -11,7 +13,7 @@ class CmdOutputObservation(Observation):
|
||||
command_id: int
|
||||
command: str
|
||||
exit_code: int = 0
|
||||
observation : str = "run"
|
||||
observation: str = ObservationType.RUN
|
||||
|
||||
@property
|
||||
def error(self) -> bool:
|
||||
@@ -19,6 +21,4 @@ class CmdOutputObservation(Observation):
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Command `{self.command}` executed with exit code {self.exit_code}.'
|
||||
|
||||
|
||||
return f"Command `{self.command}` executed with exit code {self.exit_code}."
|
||||
|
||||
@@ -42,3 +42,13 @@ class ActionType(str, Enum):
|
||||
"""If you're absolutely certain that you've completed your task and have tested your work,
|
||||
use the finish action to stop working.
|
||||
"""
|
||||
|
||||
CHAT = "chat"
|
||||
|
||||
SUMMARIZE = "summarize"
|
||||
|
||||
ADD_TASK = "add_task"
|
||||
|
||||
MODIFY_TASK = "modify_task"
|
||||
|
||||
NULL = "null"
|
||||
@@ -6,6 +6,8 @@ class ObservationType(str, Enum):
|
||||
"""The content of a file
|
||||
"""
|
||||
|
||||
WRITE = "write"
|
||||
|
||||
BROWSE = "browse"
|
||||
"""The HTML content of a URL
|
||||
"""
|
||||
@@ -21,3 +23,9 @@ class ObservationType(str, Enum):
|
||||
CHAT = "chat"
|
||||
"""A message from the user
|
||||
"""
|
||||
|
||||
MESSAGE = "message"
|
||||
|
||||
ERROR = "error"
|
||||
|
||||
NULL = "null"
|
||||
@@ -12,6 +12,7 @@ from opendevin.controller import AgentController
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.observation import NullObservation, Observation, UserMessageObservation
|
||||
from opendevin.server.session import session_manager
|
||||
from opendevin.schema import ActionType
|
||||
|
||||
DEFAULT_API_KEY = config.get("LLM_API_KEY")
|
||||
DEFAULT_BASE_URL = config.get("LLM_BASE_URL")
|
||||
@@ -69,14 +70,14 @@ class AgentManager:
|
||||
await self.send_error("Invalid action")
|
||||
return
|
||||
|
||||
if action == "initialize":
|
||||
if action == ActionType.INIT:
|
||||
await self.create_controller(data)
|
||||
elif action == "start":
|
||||
elif action == ActionType.START:
|
||||
await self.start_task(data)
|
||||
else:
|
||||
if self.controller is None:
|
||||
await self.send_error("No agent started. Please wait a second...")
|
||||
elif action == "chat":
|
||||
elif action == ActionType.CHAT:
|
||||
self.controller.add_history(
|
||||
NullAction(), UserMessageObservation(data["message"])
|
||||
)
|
||||
@@ -141,7 +142,7 @@ class AgentManager:
|
||||
"Error creating controller. Please check Docker is running using `docker ps`."
|
||||
)
|
||||
return
|
||||
await self.send({"action": "initialize", "message": "Control loop started."})
|
||||
await self.send({"action": ActionType.INIT, "message": "Control loop started."})
|
||||
|
||||
async def start_task(self, start_event):
|
||||
"""Starts a task for the agent.
|
||||
|
||||
@@ -5,7 +5,7 @@ import signal
|
||||
import uuid
|
||||
from typing import Dict, List
|
||||
|
||||
from opendevin.server.schema.action import ActionType
|
||||
from opendevin.schema.action import ActionType
|
||||
|
||||
|
||||
CACHE_DIR = os.getenv("CACHE_DIR", "cache")
|
||||
|
||||
Reference in New Issue
Block a user