feat: Implement user confirmation mode, request confirmation when running bash/python code in this mode (#2774)

* [feat] confirmation mode for bash actions

* feat: Add modal setting for Confirmation Mode

* fix: frontend tests for confirmation mode switch

* fix: add missing CONFIRMATION_MODE value in SettingsModal.test.tsx

* fix: update test to integrate new setting

* feat: Implement user confirmation for running bash/python code

* fix: don't display rejected actions

* fix: linting, rename/refactor based on feedback

* fix: add property only to commands, pass serialization tests

* fix: package-lock.json, lint test_action_serialization.py

* test: add is_confirmed to integration test outputs

---------

Co-authored-by: Mislav Balunovic <mislav.balunovic@gmail.com>
This commit is contained in:
adragos
2024-07-11 13:57:21 +02:00
committed by GitHub
parent 1d4f422638
commit 5f61885e44
60 changed files with 524 additions and 106 deletions

View File

@@ -0,0 +1,18 @@
import React from "react";
function ConfirmIcon(): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);
}
export default ConfirmIcon;

View File

@@ -0,0 +1,22 @@
import React from "react";
function RejectIcon(): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
}
export default RejectIcon;

View File

@@ -17,6 +17,7 @@ const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
AgentState.FINISHED,
AgentState.REJECTED,
AgentState.AWAITING_USER_INPUT,
AgentState.AWAITING_USER_CONFIRMATION,
],
[AgentState.RUNNING]: [
AgentState.INIT,
@@ -25,8 +26,12 @@ const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
AgentState.FINISHED,
AgentState.REJECTED,
AgentState.AWAITING_USER_INPUT,
AgentState.AWAITING_USER_CONFIRMATION,
],
[AgentState.STOPPED]: [AgentState.INIT, AgentState.STOPPED],
[AgentState.USER_CONFIRMED]: [AgentState.RUNNING],
[AgentState.USER_REJECTED]: [AgentState.RUNNING],
[AgentState.AWAITING_USER_CONFIRMATION]: [],
};
interface ButtonProps {
@@ -101,42 +106,44 @@ function AgentControlBar() {
}, [curAgentState]);
return (
<div className="flex items-center gap-3">
{curAgentState === AgentState.PAUSED ? (
<div className="flex justify-between items-center gap-20">
<div className="flex items-center gap-3">
{curAgentState === AgentState.PAUSED ? (
<ActionButton
isDisabled={
isLoading ||
IgnoreTaskStateMap[AgentState.RUNNING].includes(curAgentState)
}
content="Resume the agent task"
action={AgentState.RUNNING}
handleAction={handleAction}
large
>
<PlayIcon />
</ActionButton>
) : (
<ActionButton
isDisabled={
isLoading ||
IgnoreTaskStateMap[AgentState.PAUSED].includes(curAgentState)
}
content="Pause the current task"
action={AgentState.PAUSED}
handleAction={handleAction}
large
>
<PauseIcon />
</ActionButton>
)}
<ActionButton
isDisabled={
isLoading ||
IgnoreTaskStateMap[AgentState.RUNNING].includes(curAgentState)
}
content="Resume the agent task"
action={AgentState.RUNNING}
isDisabled={isLoading}
content="Start a new task"
action={AgentState.STOPPED}
handleAction={handleAction}
large
>
<PlayIcon />
<ArrowIcon />
</ActionButton>
) : (
<ActionButton
isDisabled={
isLoading ||
IgnoreTaskStateMap[AgentState.PAUSED].includes(curAgentState)
}
content="Pause the current task"
action={AgentState.PAUSED}
handleAction={handleAction}
large
>
<PauseIcon />
</ActionButton>
)}
<ActionButton
isDisabled={isLoading}
content="Start a new task"
action={AgentState.STOPPED}
handleAction={handleAction}
>
<ArrowIcon />
</ActionButton>
</div>
</div>
);
}

View File

@@ -58,6 +58,20 @@ function AgentStatusBar() {
message: t(I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE),
indicator: IndicatorColor.RED,
},
[AgentState.AWAITING_USER_CONFIRMATION]: {
message: t(
I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE,
),
indicator: IndicatorColor.ORANGE,
},
[AgentState.USER_CONFIRMED]: {
message: t(I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE),
indicator: IndicatorColor.GREEN,
},
[AgentState.USER_REJECTED]: {
message: t(I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE),
indicator: IndicatorColor.RED,
},
};
// TODO: Extend the agent status, e.g.:

View File

@@ -1,6 +1,7 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { renderWithProviders } from "test-utils";
import Chat from "./Chat";
const MESSAGES: Message[] = [
@@ -13,7 +14,7 @@ HTMLElement.prototype.scrollTo = vi.fn(() => {});
describe("Chat", () => {
it("should render chat messages", () => {
render(<Chat messages={MESSAGES} />);
renderWithProviders(<Chat messages={MESSAGES} />);
const messages = screen.getAllByTestId("message");

View File

@@ -1,15 +1,24 @@
import React from "react";
import ChatMessage from "./ChatMessage";
import AgentState from "#/types/AgentState";
interface ChatProps {
messages: Message[];
curAgentState?: AgentState;
}
function Chat({ messages }: ChatProps) {
function Chat({ messages, curAgentState }: ChatProps) {
return (
<div className="flex flex-col gap-3">
{messages.map((message, index) => (
<ChatMessage key={index} message={message} />
<ChatMessage
key={index}
message={message}
isLastMessage={messages && index === messages.length - 1}
awaitingUserConfirmation={
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
/>
))}
</div>
);

View File

@@ -123,7 +123,7 @@ function ChatInterface() {
className="overflow-y-auto p-3"
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
>
<Chat messages={messages} />
<Chat messages={messages} curAgentState={curAgentState} />
</div>
</div>
@@ -169,7 +169,10 @@ function ChatInterface() {
</div>
<ChatInput
disabled={curAgentState === AgentState.LOADING}
disabled={
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
onSendMessage={handleSendMessage}
/>
<FeedbackModal

View File

@@ -10,14 +10,24 @@ vi.mock("#/hooks/useTyping", () => ({
describe("Message", () => {
it("should render a user message", () => {
render(<ChatMessage message={{ sender: "user", content: "Hello" }} />);
render(
<ChatMessage
message={{ sender: "user", content: "Hello" }}
isLastMessage={false}
/>,
);
expect(screen.getByTestId("message")).toBeInTheDocument();
expect(screen.getByTestId("message")).toHaveClass("self-end"); // user message should be on the right side
});
it("should render an assistant message", () => {
render(<ChatMessage message={{ sender: "assistant", content: "Hi" }} />);
render(
<ChatMessage
message={{ sender: "assistant", content: "Hi" }}
isLastMessage={false}
/>,
);
expect(screen.getByTestId("message")).toBeInTheDocument();
expect(screen.getByTestId("message")).not.toHaveClass("self-end"); // assistant message should be on the left side
@@ -30,6 +40,7 @@ describe("Message", () => {
sender: "user",
content: "```js\nconsole.log('Hello')\n```",
}}
isLastMessage={false}
/>,
);

View File

@@ -3,15 +3,26 @@ import Markdown from "react-markdown";
import { FaClipboard, FaClipboardCheck } from "react-icons/fa";
import { twMerge } from "tailwind-merge";
import { useTranslation } from "react-i18next";
import { Tooltip } from "@nextui-org/react";
import AgentState from "#/types/AgentState";
import { code } from "../markdown/code";
import toast from "#/utils/toast";
import { I18nKey } from "#/i18n/declaration";
import ConfirmIcon from "#/assets/confirm";
import RejectIcon from "#/assets/reject";
import { changeAgentState } from "#/services/agentStateService";
interface MessageProps {
message: Message;
isLastMessage?: boolean;
awaitingUserConfirmation?: boolean;
}
function ChatMessage({ message }: MessageProps) {
function ChatMessage({
message,
isLastMessage,
awaitingUserConfirmation,
}: MessageProps) {
const [isCopy, setIsCopy] = useState(false);
const [isHovering, setIsHovering] = useState(false);
@@ -58,6 +69,45 @@ function ChatMessage({ message }: MessageProps) {
</button>
)}
<Markdown components={{ code }}>{message.content}</Markdown>
{isLastMessage &&
message.sender === "assistant" &&
awaitingUserConfirmation && (
<div className="flex justify-between items-center pt-4">
<p>{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}</p>
<div className="flex items-center gap-3">
<Tooltip
content={t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)}
closeDelay={100}
>
<button
type="button"
aria-label="Confirm action"
className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
onClick={() => {
changeAgentState(AgentState.USER_CONFIRMED);
}}
>
<ConfirmIcon />
</button>
</Tooltip>
<Tooltip
content={t(I18nKey.CHAT_INTERFACE$USER_REJECTED)}
closeDelay={100}
>
<button
type="button"
aria-label="Reject action"
className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
onClick={() => {
changeAgentState(AgentState.USER_REJECTED);
}}
>
<RejectIcon />
</button>
</Tooltip>
</div>
</div>
)}
</div>
);
}

View File

@@ -9,6 +9,7 @@ const onModelChangeMock = vi.fn();
const onAgentChangeMock = vi.fn();
const onLanguageChangeMock = vi.fn();
const onAPIKeyChangeMock = vi.fn();
const onConfirmationModeChangeMock = vi.fn();
const renderSettingsForm = (settings?: Settings) => {
renderWithProviders(
@@ -20,6 +21,7 @@ const renderSettingsForm = (settings?: Settings) => {
AGENT: "agent1",
LANGUAGE: "en",
LLM_API_KEY: "sk-...",
CONFIRMATION_MODE: true,
}
}
models={["model1", "model2", "model3"]}
@@ -28,6 +30,7 @@ const renderSettingsForm = (settings?: Settings) => {
onAgentChange={onAgentChangeMock}
onLanguageChange={onLanguageChangeMock}
onAPIKeyChange={onAPIKeyChangeMock}
onConfirmationModeChange={onConfirmationModeChangeMock}
/>,
);
};
@@ -40,11 +43,13 @@ describe("SettingsForm", () => {
const agentInput = screen.getByRole("combobox", { name: "agent" });
const languageInput = screen.getByRole("combobox", { name: "language" });
const apiKeyInput = screen.getByTestId("apikey");
const confirmationModeInput = screen.getByTestId("confirmationmode");
expect(modelInput).toHaveValue("model1");
expect(agentInput).toHaveValue("agent1");
expect(languageInput).toHaveValue("English");
expect(apiKeyInput).toHaveValue("sk-...");
expect(confirmationModeInput).toHaveAttribute("data-selected", "true");
});
it("should display the existing values if it they are present", () => {
@@ -53,6 +58,7 @@ describe("SettingsForm", () => {
AGENT: "agent2",
LANGUAGE: "es",
LLM_API_KEY: "sk-...",
CONFIRMATION_MODE: true,
});
const modelInput = screen.getByRole("combobox", { name: "model" });
@@ -72,6 +78,7 @@ describe("SettingsForm", () => {
AGENT: "agent1",
LANGUAGE: "en",
LLM_API_KEY: "sk-...",
CONFIRMATION_MODE: true,
}}
models={["model1", "model2", "model3"]}
agents={["agent1", "agent2", "agent3"]}
@@ -80,15 +87,18 @@ describe("SettingsForm", () => {
onAgentChange={onAgentChangeMock}
onLanguageChange={onLanguageChangeMock}
onAPIKeyChange={onAPIKeyChangeMock}
onConfirmationModeChange={onConfirmationModeChangeMock}
/>,
);
const modelInput = screen.getByRole("combobox", { name: "model" });
const agentInput = screen.getByRole("combobox", { name: "agent" });
const languageInput = screen.getByRole("combobox", { name: "language" });
const confirmationModeInput = screen.getByTestId("confirmationmode");
expect(modelInput).toBeDisabled();
expect(agentInput).toBeDisabled();
expect(languageInput).toBeDisabled();
expect(confirmationModeInput).toHaveAttribute("data-disabled", "true");
});
describe("onChange handlers", () => {

View File

@@ -1,4 +1,4 @@
import { Input, useDisclosure } from "@nextui-org/react";
import { Input, Switch, Tooltip, useDisclosure } from "@nextui-org/react";
import React from "react";
import { useTranslation } from "react-i18next";
import { FaEye, FaEyeSlash } from "react-icons/fa";
@@ -17,6 +17,7 @@ interface SettingsFormProps {
onAPIKeyChange: (apiKey: string) => void;
onAgentChange: (agent: string) => void;
onLanguageChange: (language: string) => void;
onConfirmationModeChange: (confirmationMode: boolean) => void;
}
function SettingsForm({
@@ -28,6 +29,7 @@ function SettingsForm({
onAPIKeyChange,
onAgentChange,
onLanguageChange,
onConfirmationModeChange,
}: SettingsFormProps) {
const { t } = useTranslation();
const { isOpen: isVisible, onOpenChange: onVisibleChange } = useDisclosure();
@@ -86,6 +88,21 @@ function SettingsForm({
tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
disabled={disabled}
/>
<Switch
aria-label="confirmationmode"
data-testid="confirmationmode"
defaultSelected={settings.CONFIRMATION_MODE}
onValueChange={onConfirmationModeChange}
isDisabled={disabled}
>
<Tooltip
content={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
closeDelay={100}
delay={500}
>
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
</Tooltip>
</Switch>
</>
);
}

View File

@@ -27,12 +27,14 @@ vi.mock("#/services/settings", async (importOriginal) => ({
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY: "sk-...",
CONFIRMATION_MODE: true,
}),
getDefaultSettings: vi.fn().mockReturnValue({
LLM_MODEL: "gpt-4o",
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY: "",
CONFIRMATION_MODE: false,
}),
settingsAreUpToDate: vi.fn().mockReturnValue(true),
saveSettings: vi.fn(),
@@ -107,6 +109,7 @@ describe("SettingsModal", () => {
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY: "sk-...",
CONFIRMATION_MODE: true,
};
it("should save the settings", async () => {
@@ -196,7 +199,7 @@ describe("SettingsModal", () => {
await userEvent.click(saveButton);
});
expect(toastSpy).toHaveBeenCalledTimes(2);
expect(toastSpy).toHaveBeenCalledTimes(3);
});
it("should change the language", async () => {

View File

@@ -48,7 +48,8 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
const isRunning =
curAgentState === AgentState.RUNNING ||
curAgentState === AgentState.PAUSED ||
curAgentState === AgentState.AWAITING_USER_INPUT;
curAgentState === AgentState.AWAITING_USER_INPUT ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION;
setAgentIsRunning(isRunning);
}, [curAgentState]);
@@ -89,6 +90,10 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
setSettings((prev) => ({ ...prev, LLM_API_KEY: key }));
};
const handleConfirmationModeChange = (confirmationMode: boolean) => {
setSettings((prev) => ({ ...prev, CONFIRMATION_MODE: confirmationMode }));
};
const handleResetSettings = () => {
setSettings(getDefaultSettings);
};
@@ -170,6 +175,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
onAgentChange={handleAgentChange}
onLanguageChange={handleLanguageChange}
onAPIKeyChange={handleAPIKeyChange}
onConfirmationModeChange={handleConfirmationModeChange}
/>
)}
</BaseModal>

View File

@@ -567,6 +567,21 @@
"de": "Agent ist auf einen Fehler gelaufen.",
"zh-CN": "智能体遇到错误"
},
"CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE": {
"en": "Agent is awaiting user confirmation for the pending action.",
"de": "Agent wartet auf die Bestätigung des Benutzers für die ausstehende Aktion.",
"zh-CN": "代理正在等待用户确认待处理的操作。"
},
"CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE": {
"en": "Agent action has been confirmed!",
"de": "Die Aktion des Agenten wurde bestätigt!",
"zh-CN": "代理操作已确认!"
},
"CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE": {
"en": "Agent action has been rejected!",
"de": "Die Aktion des Agenten wurde abgelehnt!",
"zh-CN": "代理操作已被拒绝!"
},
"CHAT_INTERFACE$INPUT_PLACEHOLDER": {
"en": "Message assistant...",
"zh-CN": "给助理发消息",
@@ -586,6 +601,21 @@
"zh-CN": "继续",
"de": "Fortfahren"
},
"CHAT_INTERFACE$USER_ASK_CONFIRMATION": {
"en": "Do you want to continue with this action?",
"de": "Möchten Sie mit dieser Aktion fortfahren?",
"zh-CN": "您要继续此操作吗?"
},
"CHAT_INTERFACE$USER_CONFIRMED": {
"en": "Confirm the requested action",
"de": "Bestätigen Sie die angeforderte Aktion",
"zh-CN": "确认请求的操作"
},
"CHAT_INTERFACE$USER_REJECTED": {
"en": "Reject the requested action",
"de": "Lehnen Sie die angeforderte Aktion ab",
"zh-CN": "拒绝请求的操作"
},
"CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
"en": "Send",
"zh-CN": "发送",
@@ -681,6 +711,16 @@
"zh-TW": "輸入您的 API 金鑰。",
"de": "Modell API Schlüssel."
},
"SETTINGS$CONFIRMATION_MODE": {
"en": "Enable Confirmation Mode",
"de": "Bestätigungsmodus aktivieren",
"zh-CN": "启用确认模式"
},
"SETTINGS$CONFIRMATION_MODE_TOOLTIP": {
"en": "Awaits for user confirmation before executing code.",
"de": "Wartet auf die Bestätigung des Benutzers, bevor der Code ausgeführt wird.",
"zh-CN": "在执行代码之前等待用户确认。"
},
"BROWSER$EMPTY_MESSAGE": {
"en": "No page loaded.",
"zh-CN": "页面未加载",

View File

@@ -46,13 +46,23 @@ const messageActions = {
if (message.args.thought) {
store.dispatch(addAssistantMessage(message.args.thought));
}
store.dispatch(appendInput(message.args.command));
if (
!message.args.is_confirmed ||
message.args.is_confirmed !== "rejected"
) {
store.dispatch(appendInput(message.args.command));
}
},
[ActionType.RUN_IPYTHON]: (message: ActionMessage) => {
if (message.args.thought) {
store.dispatch(addAssistantMessage(message.args.thought));
}
store.dispatch(appendJupyterInput(message.args.code));
if (
!message.args.is_confirmed ||
message.args.is_confirmed !== "rejected"
) {
store.dispatch(appendJupyterInput(message.args.code));
}
},
[ActionType.ADD_TASK]: () => {
getRootTask().then((fetchedRootTask) =>
@@ -67,6 +77,32 @@ const messageActions = {
};
export function handleActionMessage(message: ActionMessage) {
if (
(message.action === ActionType.RUN ||
message.action === ActionType.RUN_IPYTHON) &&
message.args.is_confirmed === "awaiting_confirmation"
) {
if (message.args.thought) {
store.dispatch(addAssistantMessage(message.args.thought));
}
if (message.args.command) {
store.dispatch(
addAssistantMessage(
`Running this command now: \n\`\`\`\`bash\n${message.args.command}\n\`\`\`\`\n`,
),
);
} else if (message.args.code) {
store.dispatch(
addAssistantMessage(
`Running this code now: \n\`\`\`\`python\n${message.args.code}\n\`\`\`\`\n`,
),
);
} else {
store.dispatch(addAssistantMessage(message.message));
}
return;
}
if (message.action in messageActions) {
const actionFn =
messageActions[message.action as keyof typeof messageActions];

View File

@@ -20,6 +20,7 @@ describe("startNewSession", () => {
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "sk-...",
CONFIRMATION_MODE: true,
};
const event = {

View File

@@ -20,7 +20,8 @@ describe("getSettings", () => {
.mockReturnValueOnce("llm_value")
.mockReturnValueOnce("agent_value")
.mockReturnValueOnce("language_value")
.mockReturnValueOnce("api_key");
.mockReturnValueOnce("api_key")
.mockReturnValueOnce("true");
const settings = getSettings();
@@ -29,6 +30,7 @@ describe("getSettings", () => {
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "api_key",
CONFIRMATION_MODE: true,
});
});
@@ -46,6 +48,7 @@ describe("getSettings", () => {
AGENT: DEFAULT_SETTINGS.AGENT,
LANGUAGE: DEFAULT_SETTINGS.LANGUAGE,
LLM_API_KEY: "",
CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
});
});
});
@@ -57,6 +60,7 @@ describe("saveSettings", () => {
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "some_key",
CONFIRMATION_MODE: true,
};
saveSettings(settings);

View File

@@ -5,13 +5,17 @@ export type Settings = {
AGENT: string;
LANGUAGE: string;
LLM_API_KEY: string;
CONFIRMATION_MODE: boolean;
};
type SettingsInput = Settings[keyof Settings];
export const DEFAULT_SETTINGS: Settings = {
LLM_MODEL: "gpt-4o",
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY: "",
CONFIRMATION_MODE: false,
};
const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[];
@@ -51,12 +55,14 @@ export const getSettings = (): Settings => {
const agent = localStorage.getItem("AGENT");
const language = localStorage.getItem("LANGUAGE");
const apiKey = localStorage.getItem("LLM_API_KEY");
const confirmationMode = localStorage.getItem("CONFIRMATION_MODE") === "true";
return {
LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL,
AGENT: agent || DEFAULT_SETTINGS.AGENT,
LANGUAGE: language || DEFAULT_SETTINGS.LANGUAGE,
LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY,
CONFIRMATION_MODE: confirmationMode || DEFAULT_SETTINGS.CONFIRMATION_MODE,
};
};
@@ -69,7 +75,8 @@ export const saveSettings = (settings: Partial<Settings>) => {
const isValid = validKeys.includes(key as keyof Settings);
const value = settings[key as keyof Settings];
if (isValid && value) localStorage.setItem(key, value);
if (isValid && (value || typeof value === "boolean"))
localStorage.setItem(key, value.toString());
});
localStorage.setItem("SETTINGS_VERSION", LATEST_SETTINGS_VERSION.toString());
};
@@ -91,11 +98,14 @@ export const getSettingsDifference = (settings: Partial<Settings>) => {
const updatedSettings: Partial<Settings> = {};
Object.keys(settings).forEach((key) => {
const typedKey = key as keyof Settings;
if (
validKeys.includes(key as keyof Settings) &&
settings[key as keyof Settings] !== currentSettings[key as keyof Settings]
validKeys.includes(typedKey) &&
settings[typedKey] !== currentSettings[typedKey]
) {
updatedSettings[key as keyof Settings] = settings[key as keyof Settings];
(updatedSettings[typedKey] as SettingsInput) = settings[
typedKey
] as SettingsInput;
}
});

View File

@@ -8,6 +8,9 @@ enum AgentState {
FINISHED = "finished",
REJECTED = "rejected",
ERROR = "error",
AWAITING_USER_CONFIRMATION = "awaiting_user_confirmation",
USER_CONFIRMED = "user_confirmed",
USER_REJECTED = "user_rejected",
}
export default AgentState;

View File

@@ -16,11 +16,14 @@ from opendevin.core.schema import AgentState
from opendevin.events import EventSource, EventStream, EventStreamSubscriber
from opendevin.events.action import (
Action,
ActionConfirmationStatus,
AddTaskAction,
AgentDelegateAction,
AgentFinishAction,
AgentRejectAction,
ChangeAgentStateAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
ModifyTaskAction,
NullAction,
@@ -49,6 +52,7 @@ class AgentController:
max_iterations: int
event_stream: EventStream
state: State
confirmation_mode: bool
agent_task: Optional[asyncio.Task] = None
parent: 'AgentController | None' = None
delegate: 'AgentController | None' = None
@@ -60,6 +64,7 @@ class AgentController:
event_stream: EventStream,
sid: str = 'default',
max_iterations: int | None = MAX_ITERATIONS,
confirmation_mode: bool = False,
max_budget_per_task: float | None = MAX_BUDGET_PER_TASK,
initial_state: State | None = None,
is_delegate: bool = False,
@@ -92,6 +97,7 @@ class AgentController:
self.set_initial_state(
state=initial_state,
max_iterations=max_iterations,
confirmation_mode=confirmation_mode,
)
self.max_budget_per_task = max_budget_per_task
@@ -170,8 +176,19 @@ class AgentController:
self.state.outputs = event.outputs # type: ignore[attr-defined]
await self.set_agent_state_to(AgentState.REJECTED)
elif isinstance(event, Observation):
if (
self._pending_action
and hasattr(self._pending_action, 'is_confirmed')
and self._pending_action.is_confirmed
== ActionConfirmationStatus.AWAITING_CONFIRMATION
):
return
if self._pending_action and self._pending_action.id == event.cause:
self._pending_action = None
if self.state.agent_state == AgentState.USER_CONFIRMED:
await self.set_agent_state_to(AgentState.RUNNING)
if self.state.agent_state == AgentState.USER_REJECTED:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
logger.info(event, extra={'msg_type': 'OBSERVATION'})
elif isinstance(event, CmdOutputObservation):
logger.info(event, extra={'msg_type': 'OBSERVATION'})
@@ -205,6 +222,18 @@ class AgentController:
if new_state == AgentState.STOPPED or new_state == AgentState.ERROR:
self.reset_task()
if self._pending_action is not None and (
new_state == AgentState.USER_CONFIRMED
or new_state == AgentState.USER_REJECTED
):
if hasattr(self._pending_action, 'thought'):
self._pending_action.thought = '' # type: ignore[union-attr]
if new_state == AgentState.USER_CONFIRMED:
self._pending_action.is_confirmed = ActionConfirmationStatus.CONFIRMED # type: ignore[attr-defined]
else:
self._pending_action.is_confirmed = ActionConfirmationStatus.REJECTED # type: ignore[attr-defined]
self.event_stream.add_event(self._pending_action, EventSource.AGENT)
self.event_stream.add_event(
AgentStateChangedObservation('', self.state.agent_state), EventSource.AGENT
)
@@ -346,9 +375,19 @@ class AgentController:
return
if action.runnable:
if self.state.confirmation_mode and (
type(action) is CmdRunAction or type(action) is IPythonRunCellAction
):
action.is_confirmed = ActionConfirmationStatus.AWAITING_CONFIRMATION
self._pending_action = action
if not isinstance(action, NullAction):
if (
hasattr(action, 'is_confirmed')
and action.is_confirmed
== ActionConfirmationStatus.AWAITING_CONFIRMATION
):
await self.set_agent_state_to(AgentState.AWAITING_USER_CONFIRMATION)
self.event_stream.add_event(action, EventSource.AGENT)
await self.update_state_after_step()
@@ -362,12 +401,19 @@ class AgentController:
return self.state
def set_initial_state(
self, state: State | None, max_iterations: int = MAX_ITERATIONS
self,
state: State | None,
max_iterations: int = MAX_ITERATIONS,
confirmation_mode: bool = False,
):
# state from the previous session, state from a parent agent, or a new state
# note that this is called twice when restoring a previous session, first with state=None
if state is None:
self.state = State(inputs={}, max_iterations=max_iterations)
self.state = State(
inputs={},
max_iterations=max_iterations,
confirmation_mode=confirmation_mode,
)
else:
self.state = state

View File

@@ -39,6 +39,7 @@ class State:
root_task: RootTask = field(default_factory=RootTask)
iteration: int = 0
max_iterations: int = 100
confirmation_mode: bool = False
history: ShortTermHistory = field(default_factory=ShortTermHistory)
inputs: dict = field(default_factory=dict)
outputs: dict = field(default_factory=dict)

View File

@@ -223,6 +223,7 @@ class AppConfig(metaclass=Singleton):
workspace_mount_rewrite: str | None = None
cache_dir: str = '/tmp/cache'
run_as_devin: bool = True
confirmation_mode: bool = False
max_iterations: int = 100
max_budget_per_task: float | None = None
e2b_api_key: str = ''

View File

@@ -37,3 +37,15 @@ class AgentState(str, Enum):
ERROR = 'error'
"""An error occurred during the task.
"""
AWAITING_USER_CONFIRMATION = 'awaiting_user_confirmation'
"""The agent is awaiting user confirmation.
"""
USER_CONFIRMED = 'user_confirmed'
"""The user confirmed the agent's action.
"""
USER_REJECTED = 'user_rejected'
"""The user rejected the agent's action.
"""

View File

@@ -20,6 +20,7 @@ class ConfigType(str, Enum):
WORKSPACE_MOUNT_PATH_IN_SANDBOX = 'WORKSPACE_MOUNT_PATH_IN_SANDBOX'
CACHE_DIR = 'CACHE_DIR'
LLM_MODEL = 'LLM_MODEL'
CONFIRMATION_MODE = 'CONFIRMATION_MODE'
SANDBOX_CONTAINER_IMAGE = 'SANDBOX_CONTAINER_IMAGE'
RUN_AS_DEVIN = 'RUN_AS_DEVIN'
LLM_EMBEDDING_MODEL = 'LLM_EMBEDDING_MODEL'

View File

@@ -44,5 +44,7 @@ class ObservationTypeSchema(BaseModel):
AGENT_STATE_CHANGED: str = Field(default='agent_state_changed')
USER_REJECTED: str = Field(default='user_rejected')
ObservationType = ObservationTypeSchema()

View File

@@ -1,4 +1,4 @@
from .action import Action
from .action import Action, ActionConfirmationStatus
from .agent import (
AgentDelegateAction,
AgentFinishAction,
@@ -32,4 +32,5 @@ __all__ = [
'ChangeAgentStateAction',
'IPythonRunCellAction',
'MessageAction',
'ActionConfirmationStatus',
]

View File

@@ -1,9 +1,16 @@
from dataclasses import dataclass
from enum import Enum
from typing import ClassVar
from opendevin.events.event import Event
class ActionConfirmationStatus(str, Enum):
CONFIRMED = 'confirmed'
REJECTED = 'rejected'
AWAITING_CONFIRMATION = 'awaiting_confirmation'
@dataclass
class Action(Event):
runnable: ClassVar[bool] = False

View File

@@ -3,7 +3,7 @@ from typing import ClassVar
from opendevin.core.schema import ActionType
from .action import Action
from .action import Action, ActionConfirmationStatus
@dataclass
@@ -12,6 +12,7 @@ class CmdRunAction(Action):
thought: str = ''
action: str = ActionType.RUN
runnable: ClassVar[bool] = True
is_confirmed: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
@property
def message(self) -> str:
@@ -31,6 +32,7 @@ class IPythonRunCellAction(Action):
thought: str = ''
action: str = ActionType.RUN_IPYTHON
runnable: ClassVar[bool] = True
is_confirmed: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
kernel_init_code: str = '' # code to run in the kernel (if the kernel is restarted)
def __str__(self) -> str:

View File

@@ -7,6 +7,7 @@ from .error import ErrorObservation
from .files import FileReadObservation, FileWriteObservation
from .observation import Observation
from .recall import AgentRecallObservation
from .reject import RejectObservation
from .success import SuccessObservation
__all__ = [
@@ -22,4 +23,5 @@ __all__ = [
'AgentStateChangedObservation',
'AgentDelegateObservation',
'SuccessObservation',
'RejectObservation',
]

View File

@@ -0,0 +1,18 @@
from dataclasses import dataclass
from opendevin.core.schema import ObservationType
from .observation import Observation
@dataclass
class RejectObservation(Observation):
"""
This data class represents the result of a successful action.
"""
observation: str = ObservationType.USER_REJECTED
@property
def message(self) -> str:
return self.content

View File

@@ -10,6 +10,7 @@ from opendevin.events.observation.error import ErrorObservation
from opendevin.events.observation.files import FileReadObservation, FileWriteObservation
from opendevin.events.observation.observation import Observation
from opendevin.events.observation.recall import AgentRecallObservation
from opendevin.events.observation.reject import RejectObservation
from opendevin.events.observation.success import SuccessObservation
observations = (
@@ -24,6 +25,7 @@ observations = (
SuccessObservation,
ErrorObservation,
AgentStateChangedObservation,
RejectObservation,
)
OBSERVATION_TYPE_TO_CLASS = {

View File

@@ -7,6 +7,7 @@ from opendevin.core.logger import opendevin_logger as logger
from opendevin.events import EventStream, EventStreamSubscriber
from opendevin.events.action import (
Action,
ActionConfirmationStatus,
AgentRecallAction,
BrowseInteractiveAction,
BrowseURLAction,
@@ -20,6 +21,7 @@ from opendevin.events.observation import (
ErrorObservation,
NullObservation,
Observation,
RejectObservation,
)
from opendevin.events.serialization.action import ACTION_TYPE_TO_CLASS
from opendevin.runtime import (
@@ -115,6 +117,11 @@ class Runtime:
"""
if not action.runnable:
return NullObservation('')
if (
hasattr(action, 'is_confirmed')
and action.is_confirmed == ActionConfirmationStatus.AWAITING_CONFIRMATION
):
return NullObservation('')
action_type = action.action # type: ignore[attr-defined]
if action_type not in ACTION_TYPE_TO_CLASS:
return ErrorObservation(f'Action {action_type} does not exist.')
@@ -122,6 +129,13 @@ class Runtime:
return ErrorObservation(
f'Action {action_type} is not supported in the current runtime.'
)
if (
hasattr(action, 'is_confirmed')
and action.is_confirmed == ActionConfirmationStatus.REJECTED
):
return RejectObservation(
'Action has been rejected by the user! Waiting for further user input.'
)
observation = await getattr(self, action_type)(action)
observation._parent = action.id # type: ignore[attr-defined]
return observation

View File

@@ -217,7 +217,7 @@ async def websocket_endpoint(websocket: WebSocket):
```
- Run a command:
```json
{"action": "run", "args": {"command": "ls -l", "thought": ""}}
{"action": "run", "args": {"command": "ls -l", "thought": "", "is_confirmed": "confirmed"}}
```
- Run an IPython command:
```json

View File

@@ -91,6 +91,9 @@ class AgentSession:
model = args.get(ConfigType.LLM_MODEL, llm_config.model)
api_key = args.get(ConfigType.LLM_API_KEY, llm_config.api_key)
api_base = llm_config.base_url
confirmation_mode = args.get(
ConfigType.CONFIRMATION_MODE, config.confirmation_mode
)
max_iterations = args.get(ConfigType.MAX_ITERATIONS, config.max_iterations)
logger.info(f'Creating agent {agent_cls} using LLM {model}')
@@ -110,6 +113,7 @@ class AgentSession:
event_stream=self.event_stream,
agent=agent,
max_iterations=int(max_iterations),
confirmation_mode=confirmation_mode,
)
try:
agent_state = State.restore_from_session(self.sid)

View File

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": ""}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": ""}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}, {"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "bad.txt"}}]
[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}, {"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "bad.txt"}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]
[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}, {"source": "agent", "action": "run", "args": {"command": "./hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]
[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}, {"source": "agent", "action": "run", "args": {"command": "./hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -39,7 +39,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'\n", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]
[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'\n", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": ""}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}]
[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}]
If the last item in the history is an error, you should try to fix it.

View File

@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": ""}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
If the last item in the history is an error, you should try to fix it.

View File

@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": ""}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
If the last item in the history is an error, you should try to fix it.

View File

@@ -38,7 +38,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]]
[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -38,7 +38,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}], [{"source": "agent", "action": "run", "args": {"command": "./hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]]
[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}], [{"source": "agent", "action": "run", "args": {"command": "./hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -60,7 +60,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": ""}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}]]
[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": "", "is_confirmed": "confirmed"}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}]]
## Available Actions
* `delegate` - send a task to another agent from the list provided. Arguments:

View File

@@ -40,7 +40,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'\n", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]]
[[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'\n", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:

View File

@@ -60,7 +60,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": ""}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}], [{"source": "agent", "action": "delegate", "args": {"agent": "VerifierAgent", "inputs": {"task": "Verify that the shell script 'hello.sh' prints 'hello'."}, "thought": ""}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {"completed": true}}}]]
[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": "", "is_confirmed": "confirmed"}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}], [{"source": "agent", "action": "delegate", "args": {"agent": "VerifierAgent", "inputs": {"task": "Verify that the shell script 'hello.sh' prints 'hello'."}, "thought": "", "is_confirmed": "confirmed"}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {"completed": true}}}]]
## Available Actions
* `delegate` - send a task to another agent from the list provided. Arguments:

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
},
{
@@ -401,7 +405,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "chmod +x hello.sh",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "pwd",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
},
{
@@ -401,7 +405,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "chmod +x hello.sh",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{
@@ -419,7 +424,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "./hello.sh",
"thought": "I need to run the 'hello.sh' script to verify that it prints 'hello'."
"thought": "I need to run the 'hello.sh' script to verify that it prints 'hello'.",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -3,7 +3,8 @@
"action": "run",
"args": {
"command": "ls",
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
"thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
"is_confirmed": "confirmed"
}
}
```

View File

@@ -196,7 +196,8 @@ ten actions--more happened before that.
"action": "run",
"args": {
"command": "bash hello.sh",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -195,7 +195,8 @@ ten actions--more happened before that.
"action": "run",
"args": {
"command": "bash hello.sh",
"thought": ""
"thought": "",
"is_confirmed": "confirmed"
}
},
{

View File

@@ -13,6 +13,7 @@ from opendevin.events.action import (
MessageAction,
ModifyTaskAction,
)
from opendevin.events.action.action import ActionConfirmationStatus
from opendevin.events.serialization import (
event_from_dict,
event_to_dict,
@@ -90,7 +91,11 @@ def test_agent_reject_action_serialization_deserialization():
def test_cmd_run_action_serialization_deserialization():
original_action_dict = {
'action': 'run',
'args': {'command': 'echo "Hello world"', 'thought': ''},
'args': {
'command': 'echo "Hello world"',
'thought': '',
'is_confirmed': ActionConfirmationStatus.CONFIRMED,
},
}
serialization_deserialization(original_action_dict, CmdRunAction)