mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
[ALL-561] feat(frontend|backend): Display error messages in the chat (#4509)
This commit is contained in:
parent
2d5b360505
commit
385cc8f512
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
5
frontend/src/components/chat/message.d.ts
vendored
5
frontend/src/components/chat/message.d.ts
vendored
@ -4,3 +4,8 @@ type Message = {
|
||||
imageUrls: string[];
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
type ErrorMessage = {
|
||||
error: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -53,7 +53,7 @@ export interface BrowseObservation extends OpenHandsObservationEvent<"browse"> {
|
||||
}
|
||||
|
||||
export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
|
||||
source: "agent";
|
||||
source: "user";
|
||||
}
|
||||
|
||||
export type OpenHandsObservation =
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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', '')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user