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 39 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
90 changes: 41 additions & 49 deletions frontend/src/components/AgentControlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,39 @@ import { useSelector } from "react-redux";
import ArrowIcon from "#/assets/arrow";
import PauseIcon from "#/assets/pause";
import PlayIcon from "#/assets/play";
import { changeTaskState } from "#/services/agentStateService";
import { changeAgentState } from "#/services/agentStateService";
import { clearMsgs } from "#/services/session";
import store, { RootState } from "#/store";
import AgentTaskAction from "#/types/AgentTaskAction";
import AgentTaskState from "#/types/AgentTaskState";
import AgentState from "#/types/AgentState";
import { clearMessages } from "#/state/chatSlice";

const TaskStateActionMap = {
[AgentTaskAction.START]: AgentTaskState.RUNNING,
[AgentTaskAction.PAUSE]: AgentTaskState.PAUSED,
[AgentTaskAction.RESUME]: AgentTaskState.RUNNING,
[AgentTaskAction.STOP]: AgentTaskState.STOPPED,
};

const IgnoreTaskStateMap: { [k: string]: AgentTaskState[] } = {
[AgentTaskAction.PAUSE]: [
AgentTaskState.INIT,
AgentTaskState.PAUSED,
AgentTaskState.STOPPED,
AgentTaskState.FINISHED,
AgentTaskState.AWAITING_USER_INPUT,
const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
[AgentState.PAUSED]: [
AgentState.INIT,
AgentState.PAUSED,
AgentState.STOPPED,
AgentState.FINISHED,
AgentState.AWAITING_USER_INPUT,
],
[AgentTaskAction.RESUME]: [
AgentTaskState.INIT,
AgentTaskState.RUNNING,
AgentTaskState.STOPPED,
AgentTaskState.FINISHED,
AgentTaskState.AWAITING_USER_INPUT,
[AgentState.RUNNING]: [
AgentState.INIT,
AgentState.RUNNING,
AgentState.STOPPED,
AgentState.FINISHED,
AgentState.AWAITING_USER_INPUT,
],
[AgentTaskAction.STOP]: [
AgentTaskState.INIT,
AgentTaskState.STOPPED,
AgentTaskState.FINISHED,
[AgentState.STOPPED]: [
AgentState.INIT,
AgentState.STOPPED,
AgentState.FINISHED,
],
};

interface ButtonProps {
isDisabled: boolean;
content: string;
action: AgentTaskAction;
handleAction: (action: AgentTaskAction) => void;
action: AgentState;
handleAction: (action: AgentState) => void;
large?: boolean;
}

Expand Down Expand Up @@ -75,53 +67,53 @@ ActionButton.defaultProps = {
};

function AgentControlBar() {
const { curTaskState } = useSelector((state: RootState) => state.agent);
const [desiredState, setDesiredState] = React.useState(AgentTaskState.INIT);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [desiredState, setDesiredState] = React.useState(AgentState.INIT);
const [isLoading, setIsLoading] = React.useState(false);

const handleAction = (action: AgentTaskAction) => {
if (IgnoreTaskStateMap[action].includes(curTaskState)) {
const handleAction = (action: AgentState) => {
if (IgnoreTaskStateMap[action].includes(curAgentState)) {
return;
}

let act = action;

if (act === AgentTaskAction.STOP) {
act = AgentTaskAction.STOP;
if (act === AgentState.STOPPED) {
act = AgentState.STOPPED;
clearMsgs().then().catch();
store.dispatch(clearMessages());
} else {
setIsLoading(true);
}

setDesiredState(TaskStateActionMap[act]);
changeTaskState(act);
setDesiredState(act);
changeAgentState(act);
};

useEffect(() => {
if (curTaskState === desiredState) {
if (curTaskState === AgentTaskState.STOPPED) {
if (curAgentState === desiredState) {
if (curAgentState === AgentState.STOPPED) {
clearMsgs().then().catch();
store.dispatch(clearMessages());
}
setIsLoading(false);
} else if (curTaskState === AgentTaskState.RUNNING) {
setDesiredState(AgentTaskState.RUNNING);
} else if (curAgentState === AgentState.RUNNING) {
setDesiredState(AgentState.RUNNING);
}
// We only want to run this effect when curTaskState changes
// We only want to run this effect when curAgentState changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [curTaskState]);
}, [curAgentState]);

return (
<div className="flex items-center gap-3">
{curTaskState === AgentTaskState.PAUSED ? (
{curAgentState === AgentState.PAUSED ? (
<ActionButton
isDisabled={
isLoading ||
IgnoreTaskStateMap[AgentTaskAction.RESUME].includes(curTaskState)
IgnoreTaskStateMap[AgentState.RUNNING].includes(curAgentState)
}
content="Resume the agent task"
action={AgentTaskAction.RESUME}
action={AgentState.RUNNING}
handleAction={handleAction}
large
>
Expand All @@ -131,10 +123,10 @@ function AgentControlBar() {
<ActionButton
isDisabled={
isLoading ||
IgnoreTaskStateMap[AgentTaskAction.PAUSE].includes(curTaskState)
IgnoreTaskStateMap[AgentState.PAUSED].includes(curAgentState)
}
content="Pause the agent task"
action={AgentTaskAction.PAUSE}
action={AgentState.PAUSED}
handleAction={handleAction}
large
>
Expand All @@ -144,7 +136,7 @@ function AgentControlBar() {
<ActionButton
isDisabled={isLoading}
content="Restart a new agent task"
action={AgentTaskAction.STOP}
action={AgentState.STOPPED}
handleAction={handleAction}
>
<ArrowIcon />
Expand Down
23 changes: 11 additions & 12 deletions frontend/src/components/AgentStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,39 @@ import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import AgentTaskState from "#/types/AgentTaskState";
import AgentState from "#/types/AgentState";

const AgentStatusMap: { [k: string]: { message: string; indicator: string } } =
{
[AgentTaskState.INIT]: {
[AgentState.INIT]: {
message: "Agent is initialized, waiting for task...",
indicator: "bg-blue-500",
},
[AgentTaskState.RUNNING]: {
[AgentState.RUNNING]: {
message: "Agent is running task...",
indicator: "bg-green-500",
},
[AgentTaskState.AWAITING_USER_INPUT]: {
[AgentState.AWAITING_USER_INPUT]: {
message: "Agent is awaiting user input...",
indicator: "bg-orange-500",
},
[AgentTaskState.PAUSED]: {
[AgentState.PAUSED]: {
message: "Agent has paused.",
indicator: "bg-yellow-500",
},
[AgentTaskState.STOPPED]: {
[AgentState.STOPPED]: {
message: "Agent has stopped.",
indicator: "bg-red-500",
},
[AgentTaskState.FINISHED]: {
[AgentState.FINISHED]: {
message: "Agent has finished the task.",
indicator: "bg-green-500",
},
};

function AgentStatusBar() {
const { t } = useTranslation();
const { initialized } = useSelector((state: RootState) => state.task);
const { curTaskState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useSelector((state: RootState) => state.agent);

// TODO: Extend the agent status, e.g.:
// - Agent is typing
Expand All @@ -46,13 +45,13 @@ function AgentStatusBar() {
// - Agent is not available
return (
<div className="flex items-center">
{initialized ? (
{curAgentState !== AgentState.LOADING ? (
<>
<div
className={`w-3 h-3 mr-2 rounded-full animate-pulse ${AgentStatusMap[curTaskState].indicator}`}
className={`w-3 h-3 mr-2 rounded-full animate-pulse ${AgentStatusMap[curAgentState].indicator}`}
/>
<span className="text-sm text-stone-400">
{AgentStatusMap[curTaskState].message}
{AgentStatusMap[curAgentState].message}
</span>
</>
) : (
Expand Down
29 changes: 19 additions & 10 deletions frontend/src/components/chat/ChatInterface.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ 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 AgentTaskState from "#/types/AgentTaskState";
import AgentState from "#/types/AgentState";

// avoid typing side-effect
vi.mock("#/hooks/useTyping", () => ({
Expand All @@ -25,7 +26,6 @@ const renderChatInterface = () =>
renderWithProviders(<ChatInterface />, {
preloadedState: {
task: {
initialized: true,
completed: false,
},
},
Expand All @@ -38,7 +38,16 @@ describe("ChatInterface", () => {
});

it("should render the new message the user has typed", async () => {
renderChatInterface();
renderWithProviders(<ChatInterface />, {
preloadedState: {
task: {
completed: false,
},
agent: {
curAgentState: AgentState.INIT,
},
},
});

const input = screen.getByRole("textbox");

Expand Down Expand Up @@ -73,11 +82,10 @@ describe("ChatInterface", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
task: {
initialized: true,
completed: false,
},
agent: {
curTaskState: AgentTaskState.INIT,
curAgentState: AgentState.INIT,
},
},
});
Expand All @@ -95,11 +103,10 @@ describe("ChatInterface", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
task: {
initialized: true,
completed: false,
},
agent: {
curTaskState: AgentTaskState.AWAITING_USER_INPUT,
curAgentState: AgentState.AWAITING_USER_INPUT,
},
},
});
Expand All @@ -110,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 All @@ -120,9 +127,11 @@ describe("ChatInterface", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
task: {
initialized: false,
completed: false,
},
agent: {
curAgentState: AgentState.LOADING,
},
},
});

Expand Down
29 changes: 11 additions & 18 deletions frontend/src/components/chat/ChatInterface.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +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 AgentTaskState from "#/types/AgentTaskState";
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 { initialized } = useSelector((state: RootState) => state.task);
const { messages } = useSelector((state: RootState) => state.chat);
const { curTaskState } = useSelector((state: RootState) => state.agent);

const dispatch = useDispatch();
const { curAgentState } = useSelector((state: RootState) => state.agent);

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

let event;
if (curTaskState === AgentTaskState.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 All @@ -42,7 +32,10 @@ function ChatInterface() {
{/* Fade between messages and input */}
<div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-b from-transparent to-neutral-800" />
</div>
<ChatInput disabled={!initialized} onSendMessage={handleSendMessage} />
<ChatInput
disabled={curAgentState === AgentState.LOADING}
onSendMessage={handleSendMessage}
/>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { renderWithProviders } from "test-utils";
import AgentTaskState from "#/types/AgentTaskState";
import AgentState from "#/types/AgentState";
import { Settings } from "#/services/settings";
import SettingsForm from "./SettingsForm";

Expand Down Expand Up @@ -80,7 +80,7 @@ describe("SettingsForm", () => {
onLanguageChange={onLanguageChangeMock}
onAPIKeyChange={onAPIKeyChangeMock}
/>,
{ preloadedState: { agent: { curTaskState: AgentTaskState.RUNNING } } },
{ preloadedState: { agent: { curAgentState: AgentState.RUNNING } } },
);
const modelInput = screen.getByRole("combobox", { name: "model" });
const agentInput = screen.getByRole("combobox", { name: "agent" });
Expand Down
Loading