From 385cc8f512a6424c6c27b4e31042b79dea88fc57 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:56:00 +0400 Subject: [PATCH] [ALL-561] feat(frontend|backend): Display error messages in the chat (#4509) --- frontend/src/components/chat/Chat.tsx | 35 ++++++++++----- frontend/src/components/chat/message.d.ts | 5 +++ frontend/src/routes/_oh.app.tsx | 53 +++++++++++++++++++++-- frontend/src/state/chatSlice.ts | 18 ++++++-- frontend/src/types/core/observations.ts | 2 +- openhands/controller/agent_controller.py | 9 +++- openhands/server/session/session.py | 3 ++ 7 files changed, 105 insertions(+), 20 deletions(-) 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', '')