Skip to content

Separate agent controller and server via EventStream #1538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 47 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
574ab6b
move towards event stream
rbren May 2, 2024
75d3a0d
refactor agent state changes
rbren May 2, 2024
428f099
move agent state logic
rbren May 2, 2024
9859eab
fix callbacks
rbren May 2, 2024
01ac7e3
break on finish
rbren May 2, 2024
1a94829
closer to working
rbren May 2, 2024
d21b17b
change frontend to accomodate new flow
rbren May 2, 2024
b0f28e7
handle start action
rbren May 2, 2024
b6b25df
fix locked stream
rbren May 2, 2024
04044fb
revert message
rbren May 2, 2024
e7c4745
logspam
rbren May 2, 2024
3765b74
no async on close
rbren May 2, 2024
39f5d1f
get rid of agent_task
rbren May 2, 2024
ae10589
fix up closing
rbren May 2, 2024
a1fcba2
better asyncio handling
rbren May 2, 2024
2873e85
sleep to give back control
rbren May 2, 2024
0fa3806
fix key
rbren May 2, 2024
855d8c1
logspam
rbren May 2, 2024
3d7543e
update frontend agent state actions
rbren May 2, 2024
59c6a02
fix pause and cancel
rbren May 2, 2024
71fea89
delint
rbren May 2, 2024
6fb0331
fix map
rbren May 2, 2024
6473272
delint
rbren May 2, 2024
6296309
wait for agent to finish
rbren May 2, 2024
175ba48
fix unit test
rbren May 2, 2024
0f68f47
Merge branch 'main' into rb/event-stream
rbren May 3, 2024
d3325d6
event stream enums
rbren May 3, 2024
a4edefa
Merge branch 'main' into rb/event-stream
rbren May 3, 2024
626a97e
Merge branch 'main' into rb/event-stream
rbren May 5, 2024
f8954c7
fix merge issues
rbren May 5, 2024
d2523b7
fix lint
rbren May 5, 2024
0eb027a
fix test
rbren May 5, 2024
38df5ba
fix test
rbren May 5, 2024
bd9472e
Merge branch 'main' into rb/event-stream
rbren May 5, 2024
f92cc32
add user message action
rbren May 5, 2024
2a7b6e8
Merge branch 'main' into rb/event-stream
rbren May 5, 2024
f5d48e8
add user message action
rbren May 5, 2024
0a41af8
fix up user messages
rbren May 5, 2024
14617eb
Merge branch 'rb/event-stream' of ssh://github.com/opendevin/opendevi…
rbren May 5, 2024
c62f6e6
fix main.py flow
rbren May 5, 2024
f9b7eaf
refactor message waiting
rbren May 5, 2024
5633269
lint
rbren May 5, 2024
963f2ab
fix test
rbren May 5, 2024
41340ca
fix test
rbren May 5, 2024
1e856ae
simplify if/else
rbren May 5, 2024
a9ef73c
fix state reset
rbren May 5, 2024
3553e08
logspam
rbren May 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions frontend/src/components/chat/ChatInterface.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { renderWithProviders } from "test-utils";
import ChatInterface from "./ChatInterface";
import Socket from "#/services/socket";
import ActionType from "#/types/ActionType";
import ObservationType from "#/types/ObservationType";
import { addAssistantMessage } from "#/state/chatSlice";
import AgentState from "#/types/AgentState";

Expand Down Expand Up @@ -116,8 +117,8 @@ describe("ChatInterface", () => {
});

const event = {
action: ActionType.USER_MESSAGE,
args: { message: "my message" },
observation: ObservationType.MESSAGE,
content: "my message",
};
expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event));
});
Expand Down
19 changes: 5 additions & 14 deletions frontend/src/components/chat/ChatInterface.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { IoMdChatbubbles } from "react-icons/io";
import ChatInput from "./ChatInput";
import Chat from "./Chat";
import { RootState } from "#/store";
import AgentState from "#/types/AgentState";
import { addUserMessage } from "#/state/chatSlice";
import ActionType from "#/types/ActionType";
import Socket from "#/services/socket";
import ObservationType from "#/types/ObservationType";
import { sendChatMessage } from "#/services/chatService";

function ChatInterface() {
const { messages } = useSelector((state: RootState) => state.chat);
const { curAgentState } = useSelector((state: RootState) => state.agent);

const dispatch = useDispatch();

const handleSendMessage = (content: string) => {
dispatch(addUserMessage(content));

let event;
if (curAgentState === AgentState.INIT) {
event = { action: ActionType.START, args: { task: content } };
} else {
event = { action: ActionType.USER_MESSAGE, args: { message: content } };
}

Socket.send(JSON.stringify(event));
const isTask = curAgentState === AgentState.INIT;
sendChatMessage(content, isTask);
};

return (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/chatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function sendChatMessage(message: string, isTask: boolean = true): void {
if (isTask) {
event = { action: ActionType.START, args: { task: message } };
} else {
event = { action: ActionType.USER_MESSAGE, args: { message } };
event = { action: ActionType.MESSAGE, args: {content: message } };
}
const eventString = JSON.stringify(event);
Socket.send(eventString);
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/types/ActionType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ enum ActionType {
// Initializes the agent. Only sent by client.
INIT = "initialize",

// Sends a message from the user
USER_MESSAGE = "user_message",

// Starts a new development task
// Starts a new development task.
START = "start",

// Represents a message from the user or agent.
MESSAGE = "message",

// Reads the contents of a file.
READ = "read",

Expand Down Expand Up @@ -45,6 +45,7 @@ enum ActionType {
// Updates a task in the plan.
MODIFY_TASK = "modify_task",

// Changes the state of the agent, e.g. to paused or running
CHANGE_AGENT_STATE = "change_agent_state",
}

Expand Down
28 changes: 19 additions & 9 deletions opendevin/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
AgentFinishAction,
AgentTalkAction,
ChangeAgentStateAction,
MessageAction,
NullAction,
)
from opendevin.events.event import Event
Expand Down Expand Up @@ -114,7 +115,9 @@ def update_state_after_step(self):
async def add_error_to_history(self, message: str):
await self.add_history(NullAction(), AgentErrorObservation(message))

async def add_history(self, action: Action, observation: Observation):
async def add_history(
self, action: Action, observation: Observation, add_to_stream=True
):
if self.state is None:
raise ValueError('Added history while state was None')
if not isinstance(action, Action):
Expand All @@ -127,8 +130,9 @@ async def add_history(self, action: Action, observation: Observation):
)
self.state.history.append((action, observation))
self.state.updated_info.append((action, observation))
await self.event_stream.add_event(action, EventSource.AGENT)
await self.event_stream.add_event(observation, EventSource.AGENT)
if add_to_stream:
await self.event_stream.add_event(action, EventSource.AGENT)
await self.event_stream.add_event(observation, EventSource.AGENT)

async def _run(self):
if self.state is None:
Expand Down Expand Up @@ -183,6 +187,8 @@ async def on_event(self, event: Event):
await self.set_agent_state_to(AgentState.FINISHED)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this if/elif more simplified? now, agent_state == StateX, set_agent_state_to = StateX. maybe an array contains all allowed states, and if event.agent_state is in the array, just set it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this because the typing was complaining (event.agent_state is str instead of AgentState). But I just added a type: ignore 😄

else:
logger.warning(f'Unknown agent state: {event.agent_state}')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems there're several other states in AgentState, like: ERROR/INIT/AWAITING_USER_INPUT, maybe logger.warning(f'Unexpected agent state: {event.agent_state}') is better?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

elif isinstance(event, MessageAction) and event.source == EventSource.USER:
self._await_user_message_queue.put_nowait(event)

async def reset_task(self):
if self.agent_task is not None:
Expand Down Expand Up @@ -217,7 +223,7 @@ def get_agent_state(self):
"""Returns the current state of the agent task."""
return self._agent_state

async def wait_for_user_input(self) -> UserMessageObservation:
async def wait_for_user_input(self) -> MessageAction:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
# FIXME: need a way to handle CLI input
user_message_observation = await self._await_user_message_queue.get()
Expand Down Expand Up @@ -274,12 +280,16 @@ async def step(self, i: int) -> bool:

self.update_state_after_step()

# whether to await for user messages
if isinstance(action, AgentTalkAction):
# await for the next user messages
user_message_observation = await self.wait_for_user_input()
logger.info(user_message_observation, extra={'msg_type': 'OBSERVATION'})
await self.add_history(action, user_message_observation)
await self.event_stream.add_event(action, EventSource.AGENT)
user_message = await self.wait_for_user_input()
logger.info(user_message, extra={'msg_type': 'ACTION'})
# FIXME: we're hacking a message action into a user message observation, for the benefit of CodeAct
await self.add_history(
action,
UserMessageObservation(user_message.content),
add_to_stream=False,
)
return False

finished = isinstance(action, AgentFinishAction)
Expand Down
4 changes: 2 additions & 2 deletions opendevin/core/schema/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class ActionTypeSchema(BaseModel):
"""Initializes the agent. Only sent by client.
"""

USER_MESSAGE: str = Field(default='user_message')
"""Sends a message from the user. Only sent by the client.
MESSAGE: str = Field(default='message')
"""Represents a message.
"""

START: str = Field(default='start')
Expand Down
5 changes: 3 additions & 2 deletions opendevin/events/action/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
from .empty import NullAction
from .files import FileReadAction, FileWriteAction
from .github import GitHubPushAction
from .message import MessageAction
from .tasks import AddTaskAction, ModifyTaskAction
from .user import UserMessageAction

actions = (
CmdKillAction,
Expand All @@ -35,6 +35,7 @@
ModifyTaskAction,
ChangeAgentStateAction,
GitHubPushAction,
MessageAction,
)

ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
Expand Down Expand Up @@ -78,5 +79,5 @@ def action_from_dict(action: dict) -> Action:
'ModifyTaskAction',
'ChangeAgentStateAction',
'IPythonRunCellAction',
'UserMessageAction',
'MessageAction',
]
15 changes: 15 additions & 0 deletions opendevin/events/action/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass

from opendevin.core.schema import ActionType

from .action import Action


@dataclass
class MessageAction(Action):
content: str
action: str = ActionType.MESSAGE

@property
def message(self) -> str:
return self.content
10 changes: 0 additions & 10 deletions opendevin/events/action/user.py

This file was deleted.