diff --git a/frontend/src/components/chat/Chat.tsx b/frontend/src/components/chat/Chat.tsx
index e31d40b2ec..66d8301242 100644
--- a/frontend/src/components/chat/Chat.tsx
+++ b/frontend/src/components/chat/Chat.tsx
@@ -1,24 +1,37 @@
import ChatMessage from "./ChatMessage";
import AgentState from "#/types/AgentState";
+const isMessage = (message: Message | ErrorMessage): message is Message =>
+ "sender" in message;
+
interface ChatProps {
- messages: Message[];
+ messages: (Message | ErrorMessage)[];
curAgentState?: AgentState;
}
function Chat({ messages, curAgentState }: ChatProps) {
return (
- {messages.map((message, index) => (
-
- ))}
+ {messages.map((message, index) =>
+ isMessage(message) ? (
+
+ ) : (
+
+
+
+
{message.error}
+
{message.message}
+
+
+ ),
+ )}
);
}
diff --git a/frontend/src/components/chat/message.d.ts b/frontend/src/components/chat/message.d.ts
index 2d696e6f04..e7248fbd64 100644
--- a/frontend/src/components/chat/message.d.ts
+++ b/frontend/src/components/chat/message.d.ts
@@ -4,3 +4,8 @@ type Message = {
imageUrls: string[];
timestamp: string;
};
+
+type ErrorMessage = {
+ error: string;
+ message: string;
+};
diff --git a/frontend/src/routes/_oh.app.tsx b/frontend/src/routes/_oh.app.tsx
index 9e009fcd23..d25d844b2e 100644
--- a/frontend/src/routes/_oh.app.tsx
+++ b/frontend/src/routes/_oh.app.tsx
@@ -20,7 +20,11 @@ import store, { RootState } from "#/store";
import { Container } from "#/components/container";
import ActionType from "#/types/ActionType";
import { handleAssistantMessage } from "#/services/actions";
-import { addUserMessage, clearMessages } from "#/state/chatSlice";
+import {
+ addErrorMessage,
+ addUserMessage,
+ clearMessages,
+} from "#/state/chatSlice";
import { useSocket } from "#/context/socket";
import {
getGitHubTokenCommand,
@@ -46,6 +50,18 @@ import { clearJupyter } from "#/state/jupyterSlice";
import { FilesProvider } from "#/context/files";
import { clearSession } from "#/utils/clear-session";
import { userIsAuthenticated } from "#/utils/user-is-authenticated";
+import { ErrorObservation } from "#/types/core/observations";
+
+interface ServerError {
+ error: boolean | string;
+ message: string;
+ [key: string]: unknown;
+}
+
+const isServerError = (data: object): data is ServerError => "error" in data;
+
+const isErrorObservation = (data: object): data is ErrorObservation =>
+ "observation" in data && data.observation === "error";
const isAgentStateChange = (
data: object,
@@ -159,6 +175,21 @@ function App() {
if (q) addIntialQueryToChat(q, files);
}, [settings]);
+ const handleError = (message: string) => {
+ const [error, ...rest] = message.split(":");
+ const details = rest.join(":");
+ if (!details) {
+ dispatch(
+ addErrorMessage({
+ error: "An error has occured",
+ message: error,
+ }),
+ );
+ } else {
+ dispatch(addErrorMessage({ error, message: details }));
+ }
+ };
+
const handleMessage = React.useCallback(
(message: MessageEvent) => {
// set token received from the server
@@ -168,9 +199,23 @@ function App() {
return;
}
- if ("error" in parsed) {
- toast.error(parsed.error);
- fetcher.submit({}, { method: "POST", action: "/end-session" });
+ if (isServerError(parsed)) {
+ if (parsed.error_code === 401) {
+ toast.error("Session expired.");
+ fetcher.submit({}, { method: "POST", action: "/end-session" });
+ return;
+ }
+
+ if (typeof parsed.error === "string") {
+ toast.error(parsed.error);
+ } else {
+ toast.error(parsed.message);
+ }
+
+ return;
+ }
+ if (isErrorObservation(parsed)) {
+ handleError(parsed.message);
return;
}
diff --git a/frontend/src/state/chatSlice.ts b/frontend/src/state/chatSlice.ts
index bfc48384e5..46f156ebdd 100644
--- a/frontend/src/state/chatSlice.ts
+++ b/frontend/src/state/chatSlice.ts
@@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-type SliceState = { messages: Message[] };
+type SliceState = { messages: (Message | ErrorMessage)[] };
const initialState: SliceState = {
messages: [],
@@ -37,12 +37,24 @@ export const chatSlice = createSlice({
state.messages.push(message);
},
+ addErrorMessage(
+ state,
+ action: PayloadAction<{ error: string; message: string }>,
+ ) {
+ const { error, message } = action.payload;
+ state.messages.push({ error, message });
+ },
+
clearMessages(state) {
state.messages = [];
},
},
});
-export const { addUserMessage, addAssistantMessage, clearMessages } =
- chatSlice.actions;
+export const {
+ addUserMessage,
+ addAssistantMessage,
+ addErrorMessage,
+ clearMessages,
+} = chatSlice.actions;
export default chatSlice.reducer;
diff --git a/frontend/src/types/core/observations.ts b/frontend/src/types/core/observations.ts
index 3da12cf3fa..9de2a70e8b 100644
--- a/frontend/src/types/core/observations.ts
+++ b/frontend/src/types/core/observations.ts
@@ -53,7 +53,7 @@ export interface BrowseObservation extends OpenHandsObservationEvent<"browse"> {
}
export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
- source: "agent";
+ source: "user";
}
export type OpenHandsObservation =
diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py
index b1414b61b4..55ca61dddd 100644
--- a/openhands/controller/agent_controller.py
+++ b/openhands/controller/agent_controller.py
@@ -3,6 +3,8 @@ import copy
import traceback
from typing import Type
+import litellm
+
from openhands.controller.agent import Agent
from openhands.controller.state.state import State, TrafficControlState
from openhands.controller.stuck import StuckDetector
@@ -145,7 +147,12 @@ class AgentController:
self.state.last_error = message
if exception:
self.state.last_error += f': {exception}'
- self.event_stream.add_event(ErrorObservation(message), EventSource.USER)
+ detail = str(exception) if exception is not None else ''
+ if exception is not None and isinstance(exception, litellm.AuthenticationError):
+ detail = 'Please check your credentials. Is your API key correct?'
+ self.event_stream.add_event(
+ ErrorObservation(f'{message}:{detail}'), EventSource.USER
+ )
async def start_step_loop(self):
"""The main loop for the agent's step-by-step execution."""
diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py
index 94606d085c..512be02fab 100644
--- a/openhands/server/session/session.py
+++ b/openhands/server/session/session.py
@@ -17,6 +17,7 @@ from openhands.events.observation import (
CmdOutputObservation,
NullObservation,
)
+from openhands.events.observation.error import ErrorObservation
from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.events.stream import EventStreamSubscriber
from openhands.llm.llm import LLM
@@ -141,6 +142,8 @@ class Session:
event, CmdOutputObservation
):
await self.send(event_to_dict(event))
+ elif isinstance(event, ErrorObservation):
+ await self.send(event_to_dict(event))
async def dispatch(self, data: dict):
action = data.get('action', '')