| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- import { createSlice, PayloadAction } from "@reduxjs/toolkit";
- import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
- import {
- OpenHandsObservation,
- CommandObservation,
- IPythonObservation,
- } from "#/types/core/observations";
- import { OpenHandsAction } from "#/types/core/actions";
- import { OpenHandsEventType } from "#/types/core/base";
- type SliceState = { messages: Message[] };
- const MAX_CONTENT_LENGTH = 1000;
- const HANDLED_ACTIONS: OpenHandsEventType[] = [
- "run",
- "run_ipython",
- "write",
- "read",
- "browse",
- ];
- function getRiskText(risk: ActionSecurityRisk) {
- switch (risk) {
- case ActionSecurityRisk.LOW:
- return "Low Risk";
- case ActionSecurityRisk.MEDIUM:
- return "Medium Risk";
- case ActionSecurityRisk.HIGH:
- return "High Risk";
- case ActionSecurityRisk.UNKNOWN:
- default:
- return "Unknown Risk";
- }
- }
- const initialState: SliceState = {
- messages: [],
- };
- export const chatSlice = createSlice({
- name: "chat",
- initialState,
- reducers: {
- addUserMessage(
- state,
- action: PayloadAction<{
- content: string;
- imageUrls: string[];
- timestamp: string;
- pending?: boolean;
- }>,
- ) {
- const message: Message = {
- type: "thought",
- sender: "user",
- content: action.payload.content,
- imageUrls: action.payload.imageUrls,
- timestamp: action.payload.timestamp || new Date().toISOString(),
- pending: !!action.payload.pending,
- };
- // Remove any pending messages
- let i = state.messages.length;
- while (i) {
- i -= 1;
- const m = state.messages[i] as Message;
- if (m.pending) {
- state.messages.splice(i, 1);
- }
- }
- state.messages.push(message);
- },
- addAssistantMessage(state, action: PayloadAction<string>) {
- const message: Message = {
- type: "thought",
- sender: "assistant",
- content: action.payload,
- imageUrls: [],
- timestamp: new Date().toISOString(),
- pending: false,
- };
- state.messages.push(message);
- },
- addAssistantAction(state, action: PayloadAction<OpenHandsAction>) {
- const actionID = action.payload.action;
- if (!HANDLED_ACTIONS.includes(actionID)) {
- return;
- }
- const translationID = `ACTION_MESSAGE$${actionID.toUpperCase()}`;
- let text = "";
- if (actionID === "run") {
- text = `\`${action.payload.args.command}\``;
- } else if (actionID === "run_ipython") {
- text = `\`\`\`\n${action.payload.args.code}\n\`\`\``;
- } else if (actionID === "write") {
- let { content } = action.payload.args;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
- }
- text = `${action.payload.args.path}\n${content}`;
- } else if (actionID === "read") {
- text = action.payload.args.path;
- } else if (actionID === "browse") {
- text = `Browsing ${action.payload.args.url}`;
- }
- if (actionID === "run" || actionID === "run_ipython") {
- if (
- action.payload.args.confirmation_state === "awaiting_confirmation"
- ) {
- text += `\n\n${getRiskText(action.payload.args.security_risk as unknown as ActionSecurityRisk)}`;
- }
- }
- const message: Message = {
- type: "action",
- sender: "assistant",
- translationID,
- eventID: action.payload.id,
- content: text,
- imageUrls: [],
- timestamp: new Date().toISOString(),
- };
- state.messages.push(message);
- },
- addAssistantObservation(
- state,
- observation: PayloadAction<OpenHandsObservation>,
- ) {
- const observationID = observation.payload.observation;
- if (!HANDLED_ACTIONS.includes(observationID)) {
- return;
- }
- const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
- const causeID = observation.payload.cause;
- const causeMessage = state.messages.find(
- (message) => message.eventID === causeID,
- );
- if (!causeMessage) {
- return;
- }
- causeMessage.translationID = translationID;
- // Set success property based on observation type
- if (observationID === "run") {
- const commandObs = observation.payload as CommandObservation;
- causeMessage.success = commandObs.extras.exit_code === 0;
- } else if (observationID === "run_ipython") {
- // For IPython, we consider it successful if there's no error message
- const ipythonObs = observation.payload as IPythonObservation;
- causeMessage.success = !ipythonObs.message
- .toLowerCase()
- .includes("error");
- }
- if (observationID === "run" || observationID === "run_ipython") {
- let { content } = observation.payload;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
- }
- content = `\`\`\`\n${content}\n\`\`\``;
- causeMessage.content = content; // Observation content includes the action
- } else if (observationID === "browse") {
- let content = `**URL:** ${observation.payload.extras.url}\n`;
- if (observation.payload.extras.error) {
- content += `**Error:**\n${observation.payload.extras.error}\n`;
- }
- content += `**Output:**\n${observation.payload.content}`;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
- }
- causeMessage.content = content;
- }
- },
- addErrorMessage(
- state,
- action: PayloadAction<{ id?: string; message: string }>,
- ) {
- const { id, message } = action.payload;
- state.messages.push({
- translationID: id,
- content: message,
- type: "error",
- sender: "assistant",
- timestamp: new Date().toISOString(),
- });
- },
- clearMessages(state) {
- state.messages = [];
- },
- },
- });
- export const {
- addUserMessage,
- addAssistantMessage,
- addAssistantAction,
- addAssistantObservation,
- addErrorMessage,
- clearMessages,
- } = chatSlice.actions;
- export default chatSlice.reducer;
|