@cognizant-ai-lab/ui-common 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -0
- package/dist/components/AgentChat/ChatCommon/ChatCommon.js +220 -138
- package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +1 -7
- package/dist/components/AgentChat/ChatCommon/ChatHistory.js +33 -22
- package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +3 -5
- package/dist/components/AgentChat/ChatCommon/Conversation.js +35 -57
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +9 -5
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +3 -2
- package/dist/components/AgentChat/ChatCommon/FormattedMarkdown.js +5 -3
- package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +3 -0
- package/dist/components/AgentChat/ChatCommon/SampleQueries.js +6 -3
- package/dist/components/AgentChat/ChatCommon/Thinking.d.ts +12 -0
- package/dist/components/AgentChat/ChatCommon/Thinking.js +51 -0
- package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
- package/dist/components/AgentChat/Common/Types.d.ts +6 -5
- package/dist/components/AgentChat/Common/Types.js +5 -0
- package/dist/components/AgentChat/Common/Utils.d.ts +1 -1
- package/dist/components/AgentChat/Common/Utils.js +13 -7
- package/dist/components/Common/AccordionLite.d.ts +14 -0
- package/dist/components/Common/AccordionLite.js +25 -0
- package/dist/components/Common/ConfirmationModal.d.ts +1 -1
- package/dist/components/Common/MUIAlert.d.ts +1 -0
- package/dist/components/Common/MUIAlert.js +3 -4
- package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow.js +154 -59
- package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/AgentNode.js +46 -45
- package/dist/components/MultiAgentAccelerator/GraphLayouts.js +12 -4
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +30 -10
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +0 -14
- package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +0 -23
- package/dist/components/AgentChat/ChatCommon/AgentIntro.d.ts +0 -12
- package/dist/components/AgentChat/ChatCommon/AgentIntro.js +0 -19
- package/dist/components/AgentChat/ChatCommon/AgentMetadata.d.ts +0 -14
- package/dist/components/AgentChat/ChatCommon/AgentMetadata.js +0 -43
- package/dist/components/AgentChat/ChatCommon/Const.d.ts +0 -1
- package/dist/components/AgentChat/ChatCommon/Const.js +0 -2
- package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +0 -1
- package/dist/components/AgentChat/ChatCommon/Greetings.js +0 -38
- package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +0 -7
- package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.js +0 -32
- package/dist/components/Common/LlmChatOptionsButton.d.ts +0 -6
- package/dist/components/Common/LlmChatOptionsButton.js +0 -31
|
@@ -86,11 +86,20 @@ export interface ChatCommonProps {
|
|
|
86
86
|
* to re-supply data that lives outside the IndexedDB slyData store (e.g. localStorage).
|
|
87
87
|
*/
|
|
88
88
|
readonly extraSlyData?: Record<string, unknown>;
|
|
89
|
+
/**
|
|
90
|
+
* Optional description of the network to display in the UI.
|
|
91
|
+
*/
|
|
92
|
+
readonly networkDescription?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Sample queries for the current network that the user can "click to send"
|
|
95
|
+
*/
|
|
96
|
+
readonly sampleQueries?: string[];
|
|
89
97
|
}
|
|
90
98
|
export type ChatCommonHandle = {
|
|
91
99
|
handleStop: () => void;
|
|
92
100
|
handleClearChat: () => void;
|
|
93
101
|
};
|
|
102
|
+
export declare const MAX_TURNS = 50;
|
|
94
103
|
/**
|
|
95
104
|
* Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
|
|
96
105
|
* experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
3
|
Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
|
|
4
4
|
|
|
@@ -18,41 +18,41 @@ limitations under the License.
|
|
|
18
18
|
* See main function description.
|
|
19
19
|
*/
|
|
20
20
|
import { AIMessage, HumanMessage } from "@langchain/core/messages";
|
|
21
|
-
import AccountTreeIcon from "@mui/icons-material/AccountTree";
|
|
22
21
|
import ClearIcon from "@mui/icons-material/Clear";
|
|
23
22
|
import CloseIcon from "@mui/icons-material/Close";
|
|
24
|
-
import
|
|
25
|
-
import WrapTextIcon from "@mui/icons-material/WrapText";
|
|
23
|
+
import TuneIcon from "@mui/icons-material/Tune";
|
|
26
24
|
import Box from "@mui/material/Box";
|
|
25
|
+
import Checkbox from "@mui/material/Checkbox";
|
|
27
26
|
import CircularProgress from "@mui/material/CircularProgress";
|
|
28
27
|
import IconButton from "@mui/material/IconButton";
|
|
29
28
|
import Input from "@mui/material/Input";
|
|
30
29
|
import InputAdornment from "@mui/material/InputAdornment";
|
|
30
|
+
import ListItemIcon from "@mui/material/ListItemIcon";
|
|
31
|
+
import ListItemText from "@mui/material/ListItemText";
|
|
32
|
+
import Menu from "@mui/material/Menu";
|
|
33
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
31
34
|
import { useTheme } from "@mui/material/styles";
|
|
32
35
|
import Tooltip from "@mui/material/Tooltip";
|
|
33
36
|
import Typography from "@mui/material/Typography";
|
|
37
|
+
import { isEmpty } from "lodash-es";
|
|
34
38
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
35
39
|
import { v4 as uuid } from "uuid";
|
|
36
|
-
import { AgentIntro } from "./AgentIntro.js";
|
|
37
|
-
import { AgentMetadata } from "./AgentMetadata.js";
|
|
38
40
|
import { ChatHistory } from "./ChatHistory.js";
|
|
39
|
-
import { AGENT_IMAGE } from "./Const.js";
|
|
40
41
|
import { ControlButtons } from "./ControlButtons.js";
|
|
41
42
|
import { Conversation } from "./Conversation.js";
|
|
42
43
|
import { MessageRole } from "./ConversationTurn.js";
|
|
44
|
+
import { SampleQueries } from "./SampleQueries.js";
|
|
43
45
|
import { SendButton } from "./SendButton.js";
|
|
46
|
+
import { Thinking } from "./Thinking.js";
|
|
44
47
|
import { sendChatQuery } from "../../../controller/agent/Agent.js";
|
|
45
48
|
import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
|
|
46
49
|
import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
|
|
47
50
|
import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
|
|
48
51
|
import { hasOnlyWhitespace } from "../../../utils/text.js";
|
|
49
|
-
import {
|
|
50
|
-
import { isLegacyAgentType } from "../Common/Types.js";
|
|
52
|
+
import { givesFinalAnswer, isLegacyAgentType } from "../Common/Types.js";
|
|
51
53
|
import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
|
|
52
54
|
import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
|
|
53
55
|
import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "../VoiceChat/VoiceChat.js";
|
|
54
|
-
// Key for the chat history, which gets special treatment; always visible even if "show thinking" is off.
|
|
55
|
-
const CHAT_HISTORY_KEY = "chat-history-accordion";
|
|
56
56
|
// Define fancy EMPTY constant to avoid linter error about using object literals as default props
|
|
57
57
|
const EMPTY = {};
|
|
58
58
|
// How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
|
|
@@ -64,15 +64,15 @@ const MAX_AGENT_RETRIES = 3;
|
|
|
64
64
|
* @returns The final answer from the agent, if it exists or undefined if it doesn't
|
|
65
65
|
*/
|
|
66
66
|
const extractFinalAnswer = (response) => /Final Answer: (?<finalAnswerText>.*)/su.exec(response)?.groups?.["finalAnswerText"];
|
|
67
|
-
// Maximum number of
|
|
68
|
-
const
|
|
67
|
+
// Maximum number of turns to save
|
|
68
|
+
export const MAX_TURNS = 50;
|
|
69
69
|
/**
|
|
70
70
|
* Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
|
|
71
71
|
* experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
|
|
72
72
|
* agent responses. Customization for inputs and outputs is provided via event handlers-like props.
|
|
73
73
|
*/
|
|
74
74
|
export const ChatCommon = ({ ref, ...props }) => {
|
|
75
|
-
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, setIsAwaitingLlm, setPreviousResponse, targetAgent, title,
|
|
75
|
+
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, setIsAwaitingLlm, setPreviousResponse, targetAgent, title, } = props;
|
|
76
76
|
// MUI theme
|
|
77
77
|
const theme = useTheme();
|
|
78
78
|
// User LLM chat input
|
|
@@ -94,8 +94,9 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
94
94
|
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
|
95
95
|
// Whether to wrap output text
|
|
96
96
|
const [shouldWrapOutput, setShouldWrapOutput] = useState(true);
|
|
97
|
-
//
|
|
98
|
-
const
|
|
97
|
+
// Options menu control
|
|
98
|
+
const [optionsMenuAnchorEl, setOptionsMenuAnchorEl] = useState(null);
|
|
99
|
+
const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
|
|
99
100
|
// Persistent agent chat history store, which is where we store both kinds of chat histories
|
|
100
101
|
// (see store implementation for details)
|
|
101
102
|
const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
|
|
@@ -105,10 +106,8 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
105
106
|
const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
|
|
106
107
|
const updateSlyData = useAgentChatHistoryStore((state) => state.updateSlyData);
|
|
107
108
|
const resetHistory = useAgentChatHistoryStore((state) => state.resetHistory);
|
|
108
|
-
// Ref
|
|
109
|
-
const
|
|
110
|
-
// Track state of "show thinking" toggle
|
|
111
|
-
const [showThinking, setShowThinking] = useState(false);
|
|
109
|
+
// Ref copy of current turns, so we can safely use it in callbacks without worrying about stale closures
|
|
110
|
+
const turnsRef = useRef([]);
|
|
112
111
|
// Microphone state for voice input
|
|
113
112
|
const [isMicOn, setIsMicOn] = useState(false);
|
|
114
113
|
// Ref for speech recognition
|
|
@@ -152,53 +151,42 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
152
151
|
const container = chatOutputRef.current;
|
|
153
152
|
if (!container)
|
|
154
153
|
return;
|
|
155
|
-
// Scroll the final answer into view
|
|
156
|
-
if (finalAnswerRef.current && !isAwaitingLlm) {
|
|
157
|
-
container.scrollTop = finalAnswerRef.current.offsetTop - 50;
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
154
|
// Live-streaming auto-scroll
|
|
161
155
|
if (autoScrollEnabled) {
|
|
162
156
|
container.scrollTop = container.scrollHeight;
|
|
163
157
|
}
|
|
164
158
|
}, [autoScrollEnabled, isAwaitingLlm, turns]);
|
|
159
|
+
// Keep a ref copy of the turns array
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
turnsRef.current = turns;
|
|
162
|
+
}, [turns]);
|
|
165
163
|
const addTurn = useCallback((turn) => {
|
|
166
164
|
setTurns((current) => {
|
|
167
165
|
const next = [...current, turn];
|
|
168
|
-
return next.length >
|
|
166
|
+
return next.length > MAX_TURNS ? next.slice(-MAX_TURNS) : next;
|
|
169
167
|
});
|
|
170
168
|
}, []);
|
|
171
169
|
// We use this to update the same "turn" as chunks come in from legacy agents
|
|
172
170
|
const legacyTurnIdRef = useRef(null);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
id: legacyTurnIdRef.current,
|
|
186
|
-
role: MessageRole.LegacyAgent,
|
|
187
|
-
text: currentResponse.current,
|
|
188
|
-
alwaysShow: true,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
// We already have a turn for this response, so just update the text of that turn.
|
|
193
|
-
setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
|
|
194
|
-
}
|
|
195
|
-
// Check for Final Answer from legacy agent
|
|
196
|
-
const finalAnswerMatch = extractFinalAnswer(currentResponse.current);
|
|
197
|
-
if (finalAnswerMatch) {
|
|
198
|
-
lastAIMessage.current = finalAnswerMatch;
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
171
|
+
const handleLegacyAgentChunk = useCallback((chunk) => {
|
|
172
|
+
currentResponse.current += chunk;
|
|
173
|
+
if (!legacyTurnIdRef.current) {
|
|
174
|
+
// We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
|
|
175
|
+
// update the text of this turn.
|
|
176
|
+
legacyTurnIdRef.current = uuid();
|
|
177
|
+
addTurn({
|
|
178
|
+
id: legacyTurnIdRef.current,
|
|
179
|
+
messageType: ChatMessageType.AGENT,
|
|
180
|
+
role: MessageRole.Agent,
|
|
181
|
+
text: currentResponse.current,
|
|
182
|
+
});
|
|
201
183
|
}
|
|
184
|
+
else {
|
|
185
|
+
// We already have a turn for this response, so just update the text of that turn.
|
|
186
|
+
setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
|
|
187
|
+
}
|
|
188
|
+
}, [addTurn]);
|
|
189
|
+
const handleNeuroSanAgentChunk = useCallback((chunk) => {
|
|
202
190
|
// For Neuro-san agents, we expect a ChatMessage structure in the chunk.
|
|
203
191
|
const chatMessage = chatMessageFromChunk(chunk);
|
|
204
192
|
if (!chatMessage) {
|
|
@@ -206,10 +194,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
206
194
|
// But don't want to spam output by logging errors for every bad message.
|
|
207
195
|
return;
|
|
208
196
|
}
|
|
209
|
-
// Keep track of AI messages. The last one is (by definition) the "final answer" from the agents.
|
|
210
|
-
if (chatMessage.type === ChatMessageType.AI && chatMessage.text) {
|
|
211
|
-
lastAIMessage.current = chatMessage.text;
|
|
212
|
-
}
|
|
213
197
|
// Shallow merge existing slyData with incoming chatMessage.sly_data
|
|
214
198
|
if (chatMessage.sly_data) {
|
|
215
199
|
updateSlyData(targetAgent, chatMessage.sly_data);
|
|
@@ -221,35 +205,53 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
221
205
|
updateChatContext(targetAgent, chatMessage.chat_context);
|
|
222
206
|
}
|
|
223
207
|
// Check if there is an error block in the "structure" field of the chat message.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
234
|
-
succeeded.current = false;
|
|
235
|
-
}
|
|
208
|
+
const errorMessage = checkError(chatMessage.structure);
|
|
209
|
+
if (errorMessage) {
|
|
210
|
+
// If there is an error block, display it.
|
|
211
|
+
addTurn({
|
|
212
|
+
id: uuid(),
|
|
213
|
+
role: MessageRole.Warning,
|
|
214
|
+
text: errorMessage,
|
|
215
|
+
});
|
|
216
|
+
succeeded.current = false;
|
|
236
217
|
}
|
|
237
|
-
else if (chatMessage?.text?.trim()
|
|
238
|
-
// Not an error, so output it if it has text
|
|
239
|
-
//
|
|
240
|
-
//
|
|
218
|
+
else if (chatMessage?.text?.trim().length > 0 || chatMessage.structure) {
|
|
219
|
+
// Not an error, so output it if it has text or a structure.
|
|
220
|
+
// This is the normal happy path for an incoming message.
|
|
221
|
+
// The backend sometimes sends messages with no text content, and we don't want to display those to the
|
|
222
|
+
// user. Agent name is the last tool in the origin array. If it's not there, use a default name.
|
|
241
223
|
const agentName = chatMessage.origin?.length > 0
|
|
242
224
|
? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
|
|
243
|
-
: "Agent
|
|
225
|
+
: "Agent";
|
|
244
226
|
addTurn({
|
|
227
|
+
agentName,
|
|
245
228
|
id: uuid(),
|
|
229
|
+
messageType: chatMessage.type,
|
|
246
230
|
role: MessageRole.Agent,
|
|
247
|
-
|
|
231
|
+
structure: chatMessage.structure,
|
|
248
232
|
text: chatMessage.text,
|
|
249
233
|
});
|
|
250
|
-
|
|
234
|
+
if (chatMessage?.text?.trim().length > 0) {
|
|
235
|
+
// Append to current response if present
|
|
236
|
+
currentResponse.current += chatMessage.text;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}, [addTurn, targetAgent, updateChatContext, updateSlyData]);
|
|
240
|
+
/**
|
|
241
|
+
* Handle a chunk of response from the server. Called each time the server streams a chunk.
|
|
242
|
+
*/
|
|
243
|
+
const handleChunk = useCallback((chunk) => {
|
|
244
|
+
// Give container a chance to process the chunk first
|
|
245
|
+
const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
|
|
246
|
+
succeeded.current = succeeded.current || onChunkReceivedResult;
|
|
247
|
+
if (isLegacyAgentType(targetAgent)) {
|
|
248
|
+
// For legacy agents, we either get plain text or Markdown. Just output it as-is.
|
|
249
|
+
handleLegacyAgentChunk(chunk);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
handleNeuroSanAgentChunk(chunk);
|
|
251
253
|
}
|
|
252
|
-
}, [onChunkReceived, targetAgent,
|
|
254
|
+
}, [onChunkReceived, targetAgent, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
|
|
253
255
|
/**
|
|
254
256
|
* Reset the state of the component. This is called after a request is completed, regardless of success or failure.
|
|
255
257
|
*/
|
|
@@ -257,8 +259,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
257
259
|
// Reset state, whatever happened during request
|
|
258
260
|
setIsAwaitingLlm(false);
|
|
259
261
|
setChatInput("");
|
|
260
|
-
lastAIMessage.current = "";
|
|
261
|
-
finalAnswerRef.current = null;
|
|
262
262
|
setPreviousResponse?.(targetAgent, currentResponse.current);
|
|
263
263
|
currentResponse.current = "";
|
|
264
264
|
legacyTurnIdRef.current = null;
|
|
@@ -303,7 +303,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
303
303
|
id: uuid(),
|
|
304
304
|
role: MessageRole.Error,
|
|
305
305
|
text: `Error occurred: ${error}`,
|
|
306
|
-
alwaysShow: true,
|
|
307
306
|
});
|
|
308
307
|
}
|
|
309
308
|
}
|
|
@@ -320,6 +319,84 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
320
319
|
neuroSanURL,
|
|
321
320
|
targetAgent,
|
|
322
321
|
]);
|
|
322
|
+
const getFinalAnswerErrorTurn = () => ({
|
|
323
|
+
id: uuid(),
|
|
324
|
+
role: MessageRole.Error,
|
|
325
|
+
text: "The agent did not provide a final answer in the expected format. This is an internal error.",
|
|
326
|
+
});
|
|
327
|
+
const handleFinalAnswerLegacyAgent = useCallback(() => {
|
|
328
|
+
const currentTurns = turnsRef.current;
|
|
329
|
+
// Prefer the most recent matching turn
|
|
330
|
+
const idx = currentTurns.reduceRight((foundIndex, turn, i) => foundIndex !== -1 || extractFinalAnswer(turn.text) === undefined ? foundIndex : i, -1);
|
|
331
|
+
if (idx === -1) {
|
|
332
|
+
if (givesFinalAnswer(targetAgent)) {
|
|
333
|
+
// This agent is supposed to give final answers, but didn't this time. An error.
|
|
334
|
+
setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Use the last received turn as the final answer
|
|
339
|
+
const lastTurn = currentTurns.slice(-1)[0];
|
|
340
|
+
if (!lastTurn)
|
|
341
|
+
return;
|
|
342
|
+
// Just set the last turn as the final answer
|
|
343
|
+
setTurns((prev) => prev.map((turn) => (turn.id === lastTurn.id ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
|
|
344
|
+
// Save it to chat history
|
|
345
|
+
updateChatHistory(targetAgent, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const sourceTurn = currentTurns[idx];
|
|
350
|
+
// Save item to chat history (same as original behavior)
|
|
351
|
+
updateChatHistory(targetAgent, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
|
|
352
|
+
// Extract the final answer from the turn.
|
|
353
|
+
const finalAnswer = extractFinalAnswer(sourceTurn.text)?.trim();
|
|
354
|
+
// Update the turn to be a final answer turn, and add a new final answer turn with just the final answer text.
|
|
355
|
+
setTurns((prev) => {
|
|
356
|
+
const sourceTurnIndex = prev.findIndex(({ id: itemId }) => itemId === sourceTurn.id);
|
|
357
|
+
const updated = sourceTurnIndex === -1
|
|
358
|
+
? [...prev]
|
|
359
|
+
: prev.map((turn, index) => (index === sourceTurnIndex ? { ...turn, text: sourceTurn.text } : turn));
|
|
360
|
+
// Add explicit final answer as a new terminal turn
|
|
361
|
+
updated.push({
|
|
362
|
+
id: uuid(),
|
|
363
|
+
role: MessageRole.FinalAnswer,
|
|
364
|
+
text: finalAnswer,
|
|
365
|
+
});
|
|
366
|
+
return updated;
|
|
367
|
+
});
|
|
368
|
+
}, [targetAgent, updateChatHistory]);
|
|
369
|
+
/**
|
|
370
|
+
* Extract the final answer from the turns for a Neuro-san agent. For Neuro-san agents, we expect the final answer
|
|
371
|
+
* to be the most recent turn messageType === ChatMessageType.AGENT_FRAMEWORK.
|
|
372
|
+
*/
|
|
373
|
+
const handleFinalAnswerNeuroSanAgent = useCallback(() => {
|
|
374
|
+
// Get current turns snapshot
|
|
375
|
+
const currentTurns = turnsRef.current;
|
|
376
|
+
// Find the most recent turn that is from the agent framework, which should be the one that contains the
|
|
377
|
+
// final answer.
|
|
378
|
+
const idx = currentTurns.reduceRight((found, turn, i) => (found !== -1 || turn.messageType !== ChatMessageType.AGENT_FRAMEWORK ? found : i), -1);
|
|
379
|
+
// Check for final answer
|
|
380
|
+
if (idx === -1) {
|
|
381
|
+
// No final answer found in the turns. Should never happen for a Neuro-san agent.
|
|
382
|
+
setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Extract final answer from that turn
|
|
386
|
+
const finalAnswerTurn = currentTurns[idx];
|
|
387
|
+
const hasFinalAnswer = finalAnswerTurn.text?.trim().length > 0 || !isEmpty(finalAnswerTurn.structure);
|
|
388
|
+
if (hasFinalAnswer) {
|
|
389
|
+
// Update relevant turn to be the final answer
|
|
390
|
+
setTurns((prev) => prev.map((turn, i) => (i === idx ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
|
|
391
|
+
// Save final answer to chat history
|
|
392
|
+
const finalAnswerContent = finalAnswerTurn.text || JSON.stringify(finalAnswerTurn.structure, null, 2);
|
|
393
|
+
updateChatHistory(targetAgent, [new AIMessage({ content: finalAnswerContent, id: uuid() })]);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// No final answer found, display error
|
|
397
|
+
setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
|
|
398
|
+
}
|
|
399
|
+
}, [targetAgent, updateChatHistory]);
|
|
323
400
|
const handleSend = useCallback(async (query) => {
|
|
324
401
|
// Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
|
|
325
402
|
const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
|
|
@@ -338,56 +415,34 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
338
415
|
id: uuid(),
|
|
339
416
|
role: MessageRole.User,
|
|
340
417
|
text: query,
|
|
341
|
-
alwaysShow: true,
|
|
342
|
-
});
|
|
343
|
-
// Add ID block for agent
|
|
344
|
-
addTurn({
|
|
345
|
-
agentDisplayName,
|
|
346
|
-
agentName: targetAgent,
|
|
347
|
-
alwaysShow: true,
|
|
348
|
-
id: uuid(),
|
|
349
|
-
role: MessageRole.AgentHeader,
|
|
350
|
-
text: agentDisplayName,
|
|
351
418
|
});
|
|
352
419
|
// Allow clients to do something when streaming starts
|
|
353
420
|
onStreamingStarted?.();
|
|
354
421
|
// Set up the abort controller
|
|
355
422
|
controller.current = new AbortController();
|
|
356
423
|
setIsAwaitingLlm(true);
|
|
357
|
-
addTurn({
|
|
358
|
-
agentName: `Contacting ${agentDisplayName}...`,
|
|
359
|
-
id: uuid(),
|
|
360
|
-
role: MessageRole.Agent,
|
|
361
|
-
text: `Query: ${queryToSend}`,
|
|
362
|
-
});
|
|
363
424
|
try {
|
|
364
425
|
// Invoke the logic to send the request and retry as necessary
|
|
365
426
|
const wasAborted = await doRetryLoop(queryToSend);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
updateChatHistory(targetAgent, [new AIMessage({ content: lastAIMessage.current, id: uuid() })]);
|
|
427
|
+
// Abort condition is handled elsewhere
|
|
428
|
+
if (!wasAborted) {
|
|
429
|
+
if (succeeded.current) {
|
|
430
|
+
// Success: infer final answer depending on agent type
|
|
431
|
+
if (isLegacyAgentType(targetAgent)) {
|
|
432
|
+
handleFinalAnswerLegacyAgent();
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
handleFinalAnswerNeuroSanAgent();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Exhausted retries without success. Display error to user.
|
|
440
|
+
addTurn({
|
|
441
|
+
id: uuid(),
|
|
442
|
+
role: MessageRole.Error,
|
|
443
|
+
text: `Gave up after ${MAX_AGENT_RETRIES} attempts.`,
|
|
444
|
+
});
|
|
385
445
|
}
|
|
386
|
-
}
|
|
387
|
-
else if (isLegacyAgentType(targetAgent) && currentResponse.current.length > 0) {
|
|
388
|
-
// It's a legacy agent that didn't provide a "Final Answer", so just record the whole response
|
|
389
|
-
// as the bot answer in that case.
|
|
390
|
-
updateChatHistory(targetAgent, [new AIMessage({ content: currentResponse.current, id: uuid() })]);
|
|
391
446
|
}
|
|
392
447
|
}
|
|
393
448
|
finally {
|
|
@@ -397,8 +452,9 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
397
452
|
}
|
|
398
453
|
}, [
|
|
399
454
|
addTurn,
|
|
400
|
-
agentDisplayName,
|
|
401
455
|
doRetryLoop,
|
|
456
|
+
handleFinalAnswerLegacyAgent,
|
|
457
|
+
handleFinalAnswerNeuroSanAgent,
|
|
402
458
|
onSend,
|
|
403
459
|
onStreamingComplete,
|
|
404
460
|
onStreamingStarted,
|
|
@@ -412,7 +468,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
412
468
|
controller?.current?.abort();
|
|
413
469
|
controller.current = null;
|
|
414
470
|
addTurn({
|
|
415
|
-
alwaysShow: true,
|
|
416
471
|
id: uuid(),
|
|
417
472
|
role: MessageRole.Warning,
|
|
418
473
|
text: "Request cancelled.",
|
|
@@ -429,14 +484,13 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
429
484
|
// Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
|
|
430
485
|
const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
|
|
431
486
|
// Enable Clear Chat button if not awaiting response and there is chat output to clear
|
|
432
|
-
const enableClearChatButton = !isAwaitingLlm && turns.length > 0;
|
|
487
|
+
const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
|
|
433
488
|
const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
|
|
434
489
|
const handleClearChat = useCallback(() => {
|
|
435
490
|
setTurns([]);
|
|
436
491
|
resetHistory(targetAgent);
|
|
437
492
|
setPreviousUserQuery("");
|
|
438
493
|
currentResponse.current = "";
|
|
439
|
-
lastAIMessage.current = "";
|
|
440
494
|
}, [resetHistory, targetAgent]);
|
|
441
495
|
// Expose the handleStop and handleClearChat methods to parent components via ref for external control
|
|
442
496
|
useImperativeHandle(ref, () => ({
|
|
@@ -465,31 +519,59 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
465
519
|
paddingTop: "0.25rem",
|
|
466
520
|
paddingBottom: "0.25rem",
|
|
467
521
|
}, children: [_jsx(Typography, { id: `llm-chat-title-${id}-text`, sx: { fontSize: "0.9rem" }, children: title }), onClose && (_jsx(IconButton, { "data-testid": `close-button-${id}`, id: `close-button-${id}`, onClick: onClose, children: _jsx(CloseIcon, { id: `close-icon-${id}` }) }))] }));
|
|
468
|
-
const
|
|
469
|
-
|
|
522
|
+
const getOptionsMenuButton = () => (_jsx(Box, { sx: {
|
|
523
|
+
position: "absolute",
|
|
524
|
+
top: "0.25rem",
|
|
525
|
+
right: "0.0rem",
|
|
526
|
+
}, children: _jsx(IconButton, { onClick: (e) => {
|
|
527
|
+
setOptionsMenuAnchorEl(e.currentTarget);
|
|
528
|
+
setOptionsMenuOpen(true);
|
|
529
|
+
}, children: _jsx(TuneIcon, { sx: { fontSize: "1.2rem" } }) }) }));
|
|
530
|
+
const agentGreeting = customAgentGreetings[targetAgent] ?? "Hi, how can I help?";
|
|
531
|
+
const handleOptionsMenuClose = () => {
|
|
532
|
+
setOptionsMenuAnchorEl(null);
|
|
533
|
+
setOptionsMenuOpen(false);
|
|
534
|
+
};
|
|
535
|
+
const handleToggleAutoScroll = () => {
|
|
536
|
+
setAutoScrollEnabled((prev) => !prev);
|
|
537
|
+
};
|
|
538
|
+
const handleToggleWrapOutput = () => {
|
|
539
|
+
setShouldWrapOutput((prev) => !prev);
|
|
540
|
+
};
|
|
541
|
+
const getOptionsMenu = () => (_jsxs(Menu, { id: `${id}-options-menu`, anchorEl: optionsMenuAnchorEl, open: optionsMenuOpen, onClose: handleOptionsMenuClose, slotProps: {
|
|
542
|
+
list: {
|
|
543
|
+
dense: true,
|
|
544
|
+
sx: {
|
|
545
|
+
py: 0,
|
|
546
|
+
"& .MuiMenuItem-root": { minHeight: 30, py: 0.5, px: 1 },
|
|
547
|
+
"& .MuiCheckbox-root": { p: 0.5 },
|
|
548
|
+
"& .MuiListItemText-primary": { fontSize: "smaller" },
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
}, children: [_jsxs(MenuItem, { onClick: handleToggleAutoScroll, children: [_jsx(ListItemIcon, { children: _jsx(Checkbox, { checked: autoScrollEnabled, tabIndex: -1, sx: { pointerEvents: "none" } }) }), _jsx(ListItemText, { primary: "Auto-scroll output" })] }), _jsxs(MenuItem, { onClick: handleToggleWrapOutput, children: [_jsx(ListItemIcon, { children: _jsx(Checkbox, { checked: shouldWrapOutput, tabIndex: -1, sx: { pointerEvents: "none" } }) }), _jsx(ListItemText, { primary: "Wrap output" })] })] }));
|
|
470
552
|
const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
|
|
471
553
|
...divStyle,
|
|
472
554
|
border: "var(--bs-border-width) var(--bs-border-style)",
|
|
473
555
|
borderRadius: "var(--bs-border-radius)",
|
|
474
556
|
display: "flex",
|
|
475
557
|
flexGrow: 1,
|
|
476
|
-
|
|
477
|
-
margin: "10px",
|
|
558
|
+
marginLeft: "10px",
|
|
478
559
|
position: "relative",
|
|
479
560
|
overflowY: "auto",
|
|
480
|
-
}, children: [
|
|
481
|
-
backgroundColor
|
|
482
|
-
borderWidth: "1px",
|
|
561
|
+
}, children: [_jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
|
|
562
|
+
backgroundColor,
|
|
483
563
|
borderRadius: "0.5rem",
|
|
484
|
-
fontSize: "
|
|
485
|
-
|
|
486
|
-
overflowY: "auto", // Enable vertical scrollbar
|
|
564
|
+
fontSize: "16px",
|
|
565
|
+
overflowY: "auto",
|
|
487
566
|
paddingBottom: "60px",
|
|
488
|
-
paddingTop: "7.5px",
|
|
489
567
|
paddingLeft: "15px",
|
|
490
568
|
paddingRight: "15px",
|
|
569
|
+
paddingTop: "7.5px",
|
|
570
|
+
scrollbarGutter: "stable",
|
|
491
571
|
width: "100%",
|
|
492
|
-
}, tabIndex: -1, children: [agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, {
|
|
572
|
+
}, tabIndex: -1, children: [getOptionsMenu(), getOptionsMenuButton(), agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, { id: id, messages: agentChatHistory.chatHistory })), _jsxs(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem", color: "var(--bs-gray)" }, children: [_jsxs(Typography, { component: "span", sx: { fontWeight: 700 }, variant: "inherit", children: [targetAgent, networkDescription && ":"] }), networkDescription && (_jsxs(Typography, { component: "span", sx: { ml: 0.5 }, variant: "inherit", children: [" ", networkDescription] }))] }), _jsx(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem" }, children: agentGreeting }), _jsx(SampleQueries, { disabled: isAwaitingLlm, handleSend: handleSend, sampleQueries: sampleQueries }), _jsx(Conversation, { id: id, includeAgentMessages: !givesFinalAnswer(targetAgent), shouldWrapOutput: shouldWrapOutput, turns: turns }), !isAwaitingLlm && turns.length > 0 && (
|
|
573
|
+
// Only show thinking once streaming is complete
|
|
574
|
+
_jsx(Thinking, { id: id, turns: turns })), isAwaitingLlm && (_jsxs(Box, { id: "awaitingOutputContainer", sx: { display: "flex", alignItems: "center", fontSize: "smaller" }, children: [_jsx("span", { id: "working-span", style: { marginRight: "1rem" }, children: "Working..." }), _jsx(CircularProgress, { id: "awaitingOutputSpinner", sx: {
|
|
493
575
|
color: "var(--bs-primary)",
|
|
494
576
|
}, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
|
|
495
577
|
const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
|
|
@@ -503,7 +585,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
503
585
|
borderRadius: "var(--bs-border-radius)",
|
|
504
586
|
display: "flex",
|
|
505
587
|
flexGrow: 1,
|
|
506
|
-
fontSize: "
|
|
588
|
+
fontSize: "17px",
|
|
507
589
|
marginRight: "0.75rem",
|
|
508
590
|
paddingBottom: "0.5rem",
|
|
509
591
|
paddingTop: "0.5rem",
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { BaseMessage } from "@langchain/core/messages";
|
|
2
2
|
import { FC } from "react";
|
|
3
3
|
interface ChatHistoryProps {
|
|
4
|
-
readonly agentDisplayName: string;
|
|
5
|
-
readonly agentImage: string;
|
|
6
|
-
readonly chatHistoryKey: string;
|
|
7
|
-
readonly currentUser: string;
|
|
8
|
-
readonly id: string;
|
|
9
4
|
readonly messages: BaseMessage[];
|
|
10
|
-
readonly
|
|
11
|
-
readonly userImage: string;
|
|
5
|
+
readonly id: string;
|
|
12
6
|
}
|
|
13
7
|
/**
|
|
14
8
|
* Component for displaying chat history from previous interactions with the agent.
|