[ALL-561] feat(frontend|backend): Display error messages in the chat (#4509)

This commit is contained in:
sp.wack 2024-10-23 18:56:00 +04:00 committed by GitHub
parent 2d5b360505
commit 385cc8f512
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 105 additions and 20 deletions

View File

@ -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 (
<div className="flex flex-col gap-3 px-3 pt-3 mb-6">
{messages.map((message, index) => (
<ChatMessage
key={index}
message={message}
isLastMessage={messages && index === messages.length - 1}
awaitingUserConfirmation={
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
/>
))}
{messages.map((message, index) =>
isMessage(message) ? (
<ChatMessage
key={index}
message={message}
isLastMessage={messages && index === messages.length - 1}
awaitingUserConfirmation={
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
/>
) : (
<div key={index} className="flex gap-2 items-center justify-start">
<div className="bg-danger w-2 h-full rounded" />
<div className="text-sm leading-4 flex flex-col gap-2">
<p className="text-danger font-bold">{message.error}</p>
<p className="text-neutral-300">{message.message}</p>
</div>
</div>
),
)}
</div>
);
}

View File

@ -4,3 +4,8 @@ type Message = {
imageUrls: string[];
timestamp: string;
};
type ErrorMessage = {
error: string;
message: string;
};

View File

@ -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<WebSocket.Data>) => {
// 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;
}

View File

@ -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;

View File

@ -53,7 +53,7 @@ export interface BrowseObservation extends OpenHandsObservationEvent<"browse"> {
}
export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
source: "agent";
source: "user";
}
export type OpenHandsObservation =

View File

@ -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."""

View File

@ -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', '')