@cognizant-ai-lab/ui-common 1.6.0 → 1.7.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 +7 -6
- package/dist/components/AgentChat/ChatCommon/ChatCommon.js +55 -53
- package/dist/components/ChatBot/ChatBot.d.ts +0 -4
- package/dist/components/ChatBot/ChatBot.js +2 -2
- package/dist/components/Common/CustomerLogo.js +1 -3
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +76 -17
- package/dist/components/Settings/ApiKeyInput.d.ts +16 -0
- package/dist/components/Settings/ApiKeyInput.js +70 -0
- package/dist/components/Settings/SettingsDialog.js +30 -3
- package/dist/controller/llm/Providers.d.ts +2 -0
- package/dist/controller/llm/Providers.js +41 -0
- package/dist/state/Settings.d.ts +2 -0
- package/dist/state/Settings.js +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Dispatch, Ref, SetStateAction } from "react";
|
|
2
|
+
import { LLMProvider } from "../../../state/Settings.js";
|
|
2
3
|
import { CombinedAgentType } from "../Common/Types.js";
|
|
3
4
|
export interface ChatCommonProps {
|
|
4
5
|
/**
|
|
@@ -9,10 +10,6 @@ export interface ChatCommonProps {
|
|
|
9
10
|
* The current username of the logged-in user. Used for fetching things from APIs mainly
|
|
10
11
|
*/
|
|
11
12
|
readonly currentUser: string;
|
|
12
|
-
/**
|
|
13
|
-
* Path to image for user avatar
|
|
14
|
-
*/
|
|
15
|
-
readonly userImage: string;
|
|
16
13
|
/**
|
|
17
14
|
* Function to set the state of the component to indicate whether we are awaiting a response from the LLM
|
|
18
15
|
*/
|
|
@@ -22,9 +19,9 @@ export interface ChatCommonProps {
|
|
|
22
19
|
*/
|
|
23
20
|
readonly isAwaitingLlm: boolean;
|
|
24
21
|
/**
|
|
25
|
-
* The
|
|
22
|
+
* The network to send the request to.
|
|
26
23
|
*/
|
|
27
|
-
readonly
|
|
24
|
+
readonly selectedNetwork: string | null;
|
|
28
25
|
/**
|
|
29
26
|
* Special endpoint for legacy agents since they do not have a single unified endpoint like Neuro-san agents.
|
|
30
27
|
*/
|
|
@@ -94,6 +91,10 @@ export interface ChatCommonProps {
|
|
|
94
91
|
* Sample queries for the current network that the user can "click to send"
|
|
95
92
|
*/
|
|
96
93
|
readonly sampleQueries?: string[];
|
|
94
|
+
/**
|
|
95
|
+
* Array of LLM providers for which API keys are required but missing. Only applies to BYOK networks.
|
|
96
|
+
*/
|
|
97
|
+
readonly missingApiKeys?: LLMProvider[];
|
|
97
98
|
}
|
|
98
99
|
export type ChatCommonHandle = {
|
|
99
100
|
handleStop: () => void;
|
|
@@ -32,7 +32,6 @@ import ListItemText from "@mui/material/ListItemText";
|
|
|
32
32
|
import Menu from "@mui/material/Menu";
|
|
33
33
|
import MenuItem from "@mui/material/MenuItem";
|
|
34
34
|
import { useTheme } from "@mui/material/styles";
|
|
35
|
-
import Tooltip from "@mui/material/Tooltip";
|
|
36
35
|
import Typography from "@mui/material/Typography";
|
|
37
36
|
import { isEmpty } from "lodash-es";
|
|
38
37
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
@@ -72,7 +71,7 @@ export const MAX_TURNS = 50;
|
|
|
72
71
|
* agent responses. Customization for inputs and outputs is provided via event handlers-like props.
|
|
73
72
|
*/
|
|
74
73
|
export const ChatCommon = ({ ref, ...props }) => {
|
|
75
|
-
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, setIsAwaitingLlm, setPreviousResponse,
|
|
74
|
+
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, missingApiKeys = [], networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, setIsAwaitingLlm, setPreviousResponse, selectedNetwork, title, } = props;
|
|
76
75
|
// MUI theme
|
|
77
76
|
const theme = useTheme();
|
|
78
77
|
// User LLM chat input
|
|
@@ -99,7 +98,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
99
98
|
const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
|
|
100
99
|
// Persistent agent chat history store, which is where we store both kinds of chat histories
|
|
101
100
|
// (see store implementation for details)
|
|
102
|
-
const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[
|
|
101
|
+
const storedChatHistory = useAgentChatHistoryStore((state) => selectedNetwork ? state?.history?.[selectedNetwork] : undefined);
|
|
103
102
|
const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
|
|
104
103
|
// Access store for context items
|
|
105
104
|
const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
|
|
@@ -135,7 +134,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
135
134
|
};
|
|
136
135
|
// Keeps track of whether the agent completed its task
|
|
137
136
|
const succeeded = useRef(false);
|
|
138
|
-
const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(
|
|
137
|
+
const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(selectedNetwork)), [selectedNetwork]);
|
|
139
138
|
useEffect(() => {
|
|
140
139
|
// Set up speech recognition
|
|
141
140
|
const handlers = setupSpeechRecognition(setChatInput, setVoiceInputState, speechRecognitionRef);
|
|
@@ -196,13 +195,13 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
196
195
|
}
|
|
197
196
|
// Shallow merge existing slyData with incoming chatMessage.sly_data
|
|
198
197
|
if (chatMessage.sly_data) {
|
|
199
|
-
updateSlyData(
|
|
198
|
+
updateSlyData(selectedNetwork, chatMessage.sly_data);
|
|
200
199
|
}
|
|
201
200
|
// It's a ChatMessage. Does it have chat context? Only AGENT_FRAMEWORK messages can have chat context.
|
|
202
201
|
if (chatMessage.type === ChatMessageType.AGENT_FRAMEWORK && chatMessage.chat_context) {
|
|
203
202
|
// Save the chat context, potentially overwriting any previous ones we received during this session.
|
|
204
203
|
// We only care about the last one received.
|
|
205
|
-
updateChatContext(
|
|
204
|
+
updateChatContext(selectedNetwork, chatMessage.chat_context);
|
|
206
205
|
}
|
|
207
206
|
// Check if there is an error block in the "structure" field of the chat message.
|
|
208
207
|
const errorMessage = checkError(chatMessage.structure);
|
|
@@ -215,7 +214,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
215
214
|
});
|
|
216
215
|
succeeded.current = false;
|
|
217
216
|
}
|
|
218
|
-
else if (chatMessage?.text?.trim().length > 0 || chatMessage.structure) {
|
|
217
|
+
else if (chatMessage?.text?.trim().length > 0 || !isEmpty(chatMessage.structure)) {
|
|
219
218
|
// Not an error, so output it if it has text or a structure.
|
|
220
219
|
// This is the normal happy path for an incoming message.
|
|
221
220
|
// The backend sometimes sends messages with no text content, and we don't want to display those to the
|
|
@@ -236,7 +235,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
236
235
|
currentResponse.current += chatMessage.text;
|
|
237
236
|
}
|
|
238
237
|
}
|
|
239
|
-
}, [addTurn,
|
|
238
|
+
}, [addTurn, selectedNetwork, updateChatContext, updateSlyData]);
|
|
240
239
|
/**
|
|
241
240
|
* Handle a chunk of response from the server. Called each time the server streams a chunk.
|
|
242
241
|
*/
|
|
@@ -244,14 +243,14 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
244
243
|
// Give container a chance to process the chunk first
|
|
245
244
|
const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
|
|
246
245
|
succeeded.current = succeeded.current || onChunkReceivedResult;
|
|
247
|
-
if (isLegacyAgentType(
|
|
246
|
+
if (isLegacyAgentType(selectedNetwork)) {
|
|
248
247
|
// For legacy agents, we either get plain text or Markdown. Just output it as-is.
|
|
249
248
|
handleLegacyAgentChunk(chunk);
|
|
250
249
|
}
|
|
251
250
|
else {
|
|
252
251
|
handleNeuroSanAgentChunk(chunk);
|
|
253
252
|
}
|
|
254
|
-
}, [onChunkReceived,
|
|
253
|
+
}, [onChunkReceived, selectedNetwork, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
|
|
255
254
|
/**
|
|
256
255
|
* Reset the state of the component. This is called after a request is completed, regardless of success or failure.
|
|
257
256
|
*/
|
|
@@ -259,10 +258,10 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
259
258
|
// Reset state, whatever happened during request
|
|
260
259
|
setIsAwaitingLlm(false);
|
|
261
260
|
setChatInput("");
|
|
262
|
-
setPreviousResponse?.(
|
|
261
|
+
setPreviousResponse?.(selectedNetwork, currentResponse.current);
|
|
263
262
|
currentResponse.current = "";
|
|
264
263
|
legacyTurnIdRef.current = null;
|
|
265
|
-
}, [setIsAwaitingLlm, setPreviousResponse,
|
|
264
|
+
}, [setIsAwaitingLlm, setPreviousResponse, selectedNetwork]);
|
|
266
265
|
/*
|
|
267
266
|
* The main logic for sending a query to the server, with retries on errors.
|
|
268
267
|
*/
|
|
@@ -275,7 +274,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
275
274
|
// Increment the attempt number and set the state to indicate we're awaiting a response
|
|
276
275
|
attemptNumber += 1;
|
|
277
276
|
// Check which agent type we are dealing with
|
|
278
|
-
if (isLegacyAgentType(
|
|
277
|
+
if (isLegacyAgentType(selectedNetwork)) {
|
|
279
278
|
// It's a legacy agent (these go directly to the LLM and are different from
|
|
280
279
|
// the Neuro-san agents).
|
|
281
280
|
// Send the chat query to the server. This will block until the stream ends from the server
|
|
@@ -285,7 +284,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
285
284
|
// It's a Neuro-san agent.
|
|
286
285
|
// Some coded tools (data generator...) expect the username provided in slyData.
|
|
287
286
|
const slyDataWithUserName = { ...agentChatHistory?.slyData, ...extraSlyData, login: currentUser };
|
|
288
|
-
await sendChatQuery(neuroSanURL, controller?.current.signal, query,
|
|
287
|
+
await sendChatQuery(neuroSanURL, controller?.current.signal, query, selectedNetwork, handleChunk, agentChatHistory.chatContext, slyDataWithUserName, currentUser, StreamingUnit.Line);
|
|
289
288
|
}
|
|
290
289
|
}
|
|
291
290
|
catch (error) {
|
|
@@ -317,7 +316,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
317
316
|
handleChunk,
|
|
318
317
|
legacyAgentEndpoint,
|
|
319
318
|
neuroSanURL,
|
|
320
|
-
|
|
319
|
+
selectedNetwork,
|
|
321
320
|
]);
|
|
322
321
|
const getFinalAnswerErrorTurn = () => ({
|
|
323
322
|
id: uuid(),
|
|
@@ -329,7 +328,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
329
328
|
// Prefer the most recent matching turn
|
|
330
329
|
const idx = currentTurns.reduceRight((foundIndex, turn, i) => foundIndex !== -1 || extractFinalAnswer(turn.text) === undefined ? foundIndex : i, -1);
|
|
331
330
|
if (idx === -1) {
|
|
332
|
-
if (givesFinalAnswer(
|
|
331
|
+
if (givesFinalAnswer(selectedNetwork)) {
|
|
333
332
|
// This agent is supposed to give final answers, but didn't this time. An error.
|
|
334
333
|
setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
|
|
335
334
|
return;
|
|
@@ -342,13 +341,13 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
342
341
|
// Just set the last turn as the final answer
|
|
343
342
|
setTurns((prev) => prev.map((turn) => (turn.id === lastTurn.id ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
|
|
344
343
|
// Save it to chat history
|
|
345
|
-
updateChatHistory(
|
|
344
|
+
updateChatHistory(selectedNetwork, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
|
|
346
345
|
return;
|
|
347
346
|
}
|
|
348
347
|
}
|
|
349
348
|
const sourceTurn = currentTurns[idx];
|
|
350
349
|
// Save item to chat history (same as original behavior)
|
|
351
|
-
updateChatHistory(
|
|
350
|
+
updateChatHistory(selectedNetwork, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
|
|
352
351
|
// Extract the final answer from the turn.
|
|
353
352
|
const finalAnswer = extractFinalAnswer(sourceTurn.text)?.trim();
|
|
354
353
|
// Update the turn to be a final answer turn, and add a new final answer turn with just the final answer text.
|
|
@@ -365,7 +364,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
365
364
|
});
|
|
366
365
|
return updated;
|
|
367
366
|
});
|
|
368
|
-
}, [
|
|
367
|
+
}, [selectedNetwork, updateChatHistory]);
|
|
369
368
|
/**
|
|
370
369
|
* Extract the final answer from the turns for a Neuro-san agent. For Neuro-san agents, we expect the final answer
|
|
371
370
|
* to be the most recent turn messageType === ChatMessageType.AGENT_FRAMEWORK.
|
|
@@ -384,23 +383,16 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
384
383
|
}
|
|
385
384
|
// Extract final answer from that turn
|
|
386
385
|
const finalAnswerTurn = currentTurns[idx];
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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]);
|
|
386
|
+
// Update relevant turn to be the final answer
|
|
387
|
+
setTurns((prev) => prev.map((turn, i) => (i === idx ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
|
|
388
|
+
// Save final answer to chat history
|
|
389
|
+
const finalAnswerContent = finalAnswerTurn.text || JSON.stringify(finalAnswerTurn.structure, null, 2);
|
|
390
|
+
updateChatHistory(selectedNetwork, [new AIMessage({ content: finalAnswerContent, id: uuid() })]);
|
|
391
|
+
}, [selectedNetwork, updateChatHistory]);
|
|
400
392
|
const handleSend = useCallback(async (query) => {
|
|
401
393
|
// Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
|
|
402
394
|
const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
|
|
403
|
-
updateChatHistory(
|
|
395
|
+
updateChatHistory(selectedNetwork, [userQueryMessage]);
|
|
404
396
|
// Allow parent to intercept and modify the query before sending if needed
|
|
405
397
|
const queryToSend = onSend?.(query) ?? query;
|
|
406
398
|
// Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
|
|
@@ -428,7 +420,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
428
420
|
if (!wasAborted) {
|
|
429
421
|
if (succeeded.current) {
|
|
430
422
|
// Success: infer final answer depending on agent type
|
|
431
|
-
if (isLegacyAgentType(
|
|
423
|
+
if (isLegacyAgentType(selectedNetwork)) {
|
|
432
424
|
handleFinalAnswerLegacyAgent();
|
|
433
425
|
}
|
|
434
426
|
else {
|
|
@@ -460,7 +452,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
460
452
|
onStreamingStarted,
|
|
461
453
|
resetState,
|
|
462
454
|
setIsAwaitingLlm,
|
|
463
|
-
|
|
455
|
+
selectedNetwork,
|
|
464
456
|
updateChatHistory,
|
|
465
457
|
]);
|
|
466
458
|
const handleStop = useCallback(() => {
|
|
@@ -485,29 +477,34 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
485
477
|
const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
|
|
486
478
|
// Enable Clear Chat button if not awaiting response and there is chat output to clear
|
|
487
479
|
const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
|
|
488
|
-
const getPlaceholder = () =>
|
|
480
|
+
const getPlaceholder = () => selectedNetwork ? agentPlaceholders[selectedNetwork] || `Chat with ${agentDisplayName}` : null;
|
|
489
481
|
const handleClearChat = useCallback(() => {
|
|
490
482
|
setTurns([]);
|
|
491
|
-
resetHistory(
|
|
483
|
+
resetHistory(selectedNetwork);
|
|
492
484
|
setPreviousUserQuery("");
|
|
493
485
|
currentResponse.current = "";
|
|
494
|
-
}, [resetHistory,
|
|
486
|
+
}, [resetHistory, selectedNetwork]);
|
|
495
487
|
// Expose the handleStop and handleClearChat methods to parent components via ref for external control
|
|
496
488
|
useImperativeHandle(ref, () => ({
|
|
497
489
|
handleStop,
|
|
498
490
|
handleClearChat,
|
|
499
491
|
}), [handleStop, handleClearChat]);
|
|
500
|
-
const
|
|
492
|
+
const getErrorOverlay = (errorText) => (_jsx(Box, { id: "chat-disabled-overlay", sx: {
|
|
493
|
+
position: "absolute",
|
|
494
|
+
top: 0,
|
|
495
|
+
left: 0,
|
|
496
|
+
right: 0,
|
|
497
|
+
bottom: 0,
|
|
498
|
+
zIndex: theme.zIndex.modal - 1,
|
|
499
|
+
cursor: "not-allowed",
|
|
500
|
+
// Capture all pointer events to prevent interaction with the chat when no agent is selected
|
|
501
|
+
pointerEvents: "all",
|
|
502
|
+
}, children: _jsx(Typography, { sx: {
|
|
501
503
|
position: "absolute",
|
|
502
|
-
top:
|
|
503
|
-
left:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
zIndex: theme.zIndex.modal - 1,
|
|
507
|
-
cursor: "not-allowed",
|
|
508
|
-
// Capture all pointer events to prevent interaction with the chat when no agent is selected
|
|
509
|
-
pointerEvents: "all",
|
|
510
|
-
} }) }));
|
|
504
|
+
top: "50%",
|
|
505
|
+
left: "50%",
|
|
506
|
+
transform: "translate(-50%, -50%)",
|
|
507
|
+
}, children: errorText }) }));
|
|
511
508
|
const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
|
|
512
509
|
alignItems: "center",
|
|
513
510
|
borderTopLeftRadius: "var(--bs-border-radius)",
|
|
@@ -527,7 +524,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
527
524
|
setOptionsMenuAnchorEl(e.currentTarget);
|
|
528
525
|
setOptionsMenuOpen(true);
|
|
529
526
|
}, children: _jsx(TuneIcon, { sx: { fontSize: "1.2rem" } }) }) }));
|
|
530
|
-
const agentGreeting = customAgentGreetings[
|
|
527
|
+
const agentGreeting = customAgentGreetings[selectedNetwork] ?? "Hi, how can I help?";
|
|
531
528
|
const handleOptionsMenuClose = () => {
|
|
532
529
|
setOptionsMenuAnchorEl(null);
|
|
533
530
|
setOptionsMenuOpen(false);
|
|
@@ -569,7 +566,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
569
566
|
paddingTop: "7.5px",
|
|
570
567
|
scrollbarGutter: "stable",
|
|
571
568
|
width: "100%",
|
|
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: [
|
|
569
|
+
}, 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: [selectedNetwork, 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(selectedNetwork), shouldWrapOutput: shouldWrapOutput, turns: turns }), !isAwaitingLlm && turns.length > 0 && (
|
|
573
570
|
// Only show thinking once streaming is complete
|
|
574
571
|
_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: {
|
|
575
572
|
color: "var(--bs-primary)",
|
|
@@ -613,8 +610,8 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
613
610
|
flexDirection: "column",
|
|
614
611
|
flexGrow: 1,
|
|
615
612
|
height: "100%",
|
|
616
|
-
opacity:
|
|
617
|
-
pointerEvents:
|
|
613
|
+
opacity: selectedNetwork ? 1 : 0.4,
|
|
614
|
+
pointerEvents: selectedNetwork ? "auto" : "none",
|
|
618
615
|
position: "relative",
|
|
619
616
|
}, children: [title && getTitle(), getResponseBox(), getUserInputBox()] }));
|
|
620
617
|
return (_jsx(Box, { id: `llm-chat-${id}`, sx: {
|
|
@@ -623,5 +620,10 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
623
620
|
flexGrow: 1,
|
|
624
621
|
height: "100%",
|
|
625
622
|
position: "relative",
|
|
626
|
-
}, children:
|
|
623
|
+
}, children: selectedNetwork
|
|
624
|
+
? missingApiKeys?.length === 0
|
|
625
|
+
? getChatBox()
|
|
626
|
+
: getErrorOverlay(`API key(s) required for: ${missingApiKeys.join(", ")}. ` +
|
|
627
|
+
"Please add the required key(s) in Settings to use this Network.")
|
|
628
|
+
: getErrorOverlay("Please select a Network from the list to start the chat.") }));
|
|
627
629
|
};
|
|
@@ -29,7 +29,7 @@ import { LegacyAgentType } from "../AgentChat/Common/Types.js";
|
|
|
29
29
|
* Site-wide Chatbot component.
|
|
30
30
|
*/
|
|
31
31
|
// Temporarily disabled but will be used once we migrated the backend to use Neuro-san RAG.
|
|
32
|
-
export const ChatBot = ({ id,
|
|
32
|
+
export const ChatBot = ({ id, pageContext }) => {
|
|
33
33
|
const [chatOpen, setChatOpen] = useState(false);
|
|
34
34
|
const [isAwaitingLlm, setIsAwaitingLlm] = useState(false);
|
|
35
35
|
const { user: { name: currentUser }, } = useAuthentication().data;
|
|
@@ -51,7 +51,7 @@ export const ChatBot = ({ id, userAvatar, pageContext }) => {
|
|
|
51
51
|
borderWidth: 1,
|
|
52
52
|
borderColor: darkMode ? "var(--bs-white)" : "var(--bs-gray-light)",
|
|
53
53
|
zIndex: getZIndex(2, theme),
|
|
54
|
-
}, children: _jsx(ChatCommon, { id: "chatbot-window", currentUser: currentUser, setIsAwaitingLlm: setIsAwaitingLlm, isAwaitingLlm: isAwaitingLlm,
|
|
54
|
+
}, children: _jsx(ChatCommon, { id: "chatbot-window", currentUser: currentUser, setIsAwaitingLlm: setIsAwaitingLlm, isAwaitingLlm: isAwaitingLlm, selectedNetwork: LegacyAgentType.ChatBot, legacyAgentEndpoint: CHATBOT_ENDPOINT, extraParams: { pageContext }, backgroundColor: darkMode ? "var(--bs-gray-dark)" : "var(--bs-tertiary-blue)", title: "Cognizant Neuro AI Assistant", onClose: () => setChatOpen(false) }) }) }), !chatOpen && (_jsx(Box, { id: `chatbot-icon-${id}`, sx: {
|
|
55
55
|
display: "flex",
|
|
56
56
|
alignItems: "center",
|
|
57
57
|
backgroundColor: darkMode ? "var(--bs-dark-mode-dim)" : "var(--bs-white)",
|
|
@@ -43,7 +43,5 @@ export const CustomerLogo = ({ fallbackElement, logoServiceToken }) => {
|
|
|
43
43
|
const logoUrl = logoServiceToken && customer?.trim().length > 0
|
|
44
44
|
? `https://img.logo.dev/name/${encodeURIComponent(customer)}?token=${logoServiceToken}&theme=dark&format=png&size=75`
|
|
45
45
|
: null;
|
|
46
|
-
return logoUrl ? (
|
|
47
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
48
|
-
_jsx("img", { src: logoUrl, alt: `${customer} Logo`, width: 40, height: 40, style: { borderRadius: "50%" } })) : (fallbackElement);
|
|
46
|
+
return logoUrl ? (_jsx("img", { src: logoUrl, alt: `${customer} Logo`, width: 40, height: 40, style: { borderRadius: "50%" } })) : (fallbackElement);
|
|
49
47
|
};
|
|
@@ -97,6 +97,7 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
97
97
|
// MUI theme
|
|
98
98
|
const theme = useTheme();
|
|
99
99
|
const enableZenMode = useSettingsStore((state) => state.settings.behavior.enableZenMode);
|
|
100
|
+
const apiKeys = useSettingsStore((state) => state.settings.apiKeys);
|
|
100
101
|
// Stores whether are currently awaiting LLM response (for knowing when to show spinners)
|
|
101
102
|
const [isAwaitingLlm, setIsAwaitingLlm] = useState(false);
|
|
102
103
|
const [isEditingNetwork, setIsEditingNetwork] = useState(false);
|
|
@@ -118,12 +119,13 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
118
119
|
const [selectedNetwork, setSelectedNetwork] = useState(null);
|
|
119
120
|
const [networkDescription, setNetworkDescription] = useState("");
|
|
120
121
|
const networkDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(selectedNetwork)), [selectedNetwork]);
|
|
122
|
+
const [providerKeysRequired, setProviderKeysRequired] = useState(new Set());
|
|
121
123
|
const [customURLLocalStorage, setCustomURLLocalStorage] = useLocalStorage("customAgentNetworkURL", null);
|
|
122
124
|
// An extra set of quotes is making it in the string in local storage.
|
|
123
125
|
const [neuroSanURL, setNeuroSanURL] = useState(customURLLocalStorage?.replaceAll('"', "") || backendNeuroSanApiUrl);
|
|
124
126
|
// Tracks how many times each agent has been involved in the conversation
|
|
125
127
|
const [agentCounts, setAgentCounts] = useState(new Map());
|
|
126
|
-
//
|
|
128
|
+
// Common function to change the selected network and reset related state
|
|
127
129
|
const changeSelectedNetwork = useCallback((next) => {
|
|
128
130
|
setSelectedNetwork(next);
|
|
129
131
|
setAgentCounts(new Map());
|
|
@@ -203,23 +205,62 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
203
205
|
const designerTempNetwork = designerNetworkName
|
|
204
206
|
? temporaryNetworks.find((n) => n.agentNetworkName === designerNetworkName)
|
|
205
207
|
: undefined;
|
|
206
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Builds the API keys object to be sent as extraSlyData with each request, based on the LLM providers required
|
|
210
|
+
* by the currently selected network. Only includes keys for providers that are required.
|
|
211
|
+
*/
|
|
212
|
+
const getApiKeys = () => {
|
|
213
|
+
const llmConfig = {};
|
|
214
|
+
if (providerKeysRequired.has("OpenAI")) {
|
|
215
|
+
llmConfig["openai_api_key"] = apiKeys["OpenAI"];
|
|
216
|
+
}
|
|
217
|
+
if (providerKeysRequired.has("Anthropic")) {
|
|
218
|
+
llmConfig["anthropic_api_key"] = apiKeys["Anthropic"];
|
|
219
|
+
}
|
|
220
|
+
return { llm_config: llmConfig };
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Builds the extraSlyData object to be sent with each request, including information for Agent Network Designer
|
|
224
|
+
* and (if required) API keys for LLM providers.
|
|
225
|
+
*/
|
|
226
|
+
const buildExtraSlyData = () => {
|
|
227
|
+
if (currentTempNetwork) {
|
|
228
|
+
const result = {
|
|
229
|
+
[AGENT_NETWORK_DEFINITION_KEY]: currentTempNetwork.agentNetworkDefinition,
|
|
230
|
+
};
|
|
231
|
+
// Use agentNetworkName, not reservation_id
|
|
232
|
+
if (currentTempNetwork.agentNetworkName) {
|
|
233
|
+
result[AGENT_NETWORK_NAME_KEY] = currentTempNetwork.agentNetworkName;
|
|
234
|
+
}
|
|
235
|
+
if (currentTempNetwork.networkHocon) {
|
|
236
|
+
result[AGENT_NETWORK_HOCON] = currentTempNetwork.networkHocon;
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
if (designerTempNetwork) {
|
|
241
|
+
return {
|
|
242
|
+
[AGENT_NETWORK_DEFINITION_KEY]: designerTempNetwork.agentNetworkDefinition,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
};
|
|
247
|
+
// Whether any API keys are required
|
|
248
|
+
const anyApiKeysRequired = providerKeysRequired.size > 0;
|
|
249
|
+
// Build base extraSlyData
|
|
250
|
+
const baseExtraSlyData = buildExtraSlyData();
|
|
251
|
+
// Add API keys to extraSlyData if needed, merging with baseExtraSlyData if it exists
|
|
252
|
+
const extraSlyData = anyApiKeysRequired || baseExtraSlyData
|
|
207
253
|
? {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
...(currentTempNetwork.agentNetworkName
|
|
211
|
-
? { [AGENT_NETWORK_NAME_KEY]: currentTempNetwork.agentNetworkName }
|
|
212
|
-
: {}),
|
|
213
|
-
...(currentTempNetwork.networkHocon ? { [AGENT_NETWORK_HOCON]: currentTempNetwork.networkHocon } : {}),
|
|
254
|
+
...baseExtraSlyData,
|
|
255
|
+
...(anyApiKeysRequired ? getApiKeys() : {}),
|
|
214
256
|
}
|
|
215
|
-
:
|
|
216
|
-
? { [AGENT_NETWORK_DEFINITION_KEY]: designerTempNetwork.agentNetworkDefinition }
|
|
217
|
-
: undefined;
|
|
257
|
+
: undefined;
|
|
218
258
|
// Handle external stop button click - stops streaming and exits zen mode
|
|
219
259
|
const handleExternalStop = useCallback(() => {
|
|
220
260
|
chatRef.current?.handleStop();
|
|
221
261
|
resetState();
|
|
222
262
|
}, [resetState]);
|
|
263
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
223
264
|
useEffect(() => {
|
|
224
265
|
;
|
|
225
266
|
(async () => {
|
|
@@ -242,9 +283,24 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
242
283
|
try {
|
|
243
284
|
const agentFunction = await getAgentFunction(neuroSanURL, selectedNetwork, userInfo.userName);
|
|
244
285
|
setNetworkDescription(agentFunction?.function?.description || "");
|
|
286
|
+
const schema = agentFunction?.function?.sly_data_schema;
|
|
287
|
+
const schemaProperties = isRecord(schema) ? schema["properties"] : undefined;
|
|
288
|
+
const llmConfig = isRecord(schemaProperties) ? schemaProperties["llm_config"] : undefined;
|
|
289
|
+
const llmConfigRequired = isRecord(llmConfig) ? llmConfig["required"] : undefined;
|
|
290
|
+
setProviderKeysRequired(new Set((Array.isArray(llmConfigRequired) ? llmConfigRequired : []).flatMap((key) => {
|
|
291
|
+
if (key === "openai_api_key") {
|
|
292
|
+
return ["OpenAI"];
|
|
293
|
+
}
|
|
294
|
+
if (key === "anthropic_api_key") {
|
|
295
|
+
return ["Anthropic"];
|
|
296
|
+
}
|
|
297
|
+
console.warn(`Unknown API key requirement "${key}" for network ${selectedNetwork}. Skipping.`);
|
|
298
|
+
// Will get dropped by flatMap
|
|
299
|
+
return [];
|
|
300
|
+
})));
|
|
245
301
|
}
|
|
246
|
-
catch {
|
|
247
|
-
|
|
302
|
+
catch (e) {
|
|
303
|
+
console.warn(`Unable to get agent details for network ${selectedNetwork}:`, e);
|
|
248
304
|
}
|
|
249
305
|
};
|
|
250
306
|
// Clear out existing
|
|
@@ -506,6 +562,9 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
506
562
|
setTourRequested(false);
|
|
507
563
|
}
|
|
508
564
|
}, [tourRequested, selectedNetwork, agentsInNetwork, networks, controls, setTourStatus]);
|
|
565
|
+
const getMissingApiKeys = () => {
|
|
566
|
+
return providerKeysRequired.size > 0 ? [...providerKeysRequired].filter((provider) => !apiKeys?.[provider]) : [];
|
|
567
|
+
};
|
|
509
568
|
const getLeftPanel = () => {
|
|
510
569
|
return (_jsx(Slide, { id: "multi-agent-accelerator-grid-sidebar-slide", in: !enableZenMode || !isAwaitingLlm, direction: "right", timeout: GROW_ANIMATION_TIME_MS, onExited: () => {
|
|
511
570
|
setIsStreaming(true);
|
|
@@ -531,11 +590,11 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
531
590
|
setIsStreaming(true);
|
|
532
591
|
}, children: _jsx(Grid, { id: "multi-agent-accelerator-grid-agent-chat-common", size: enableZenMode && isStreaming ? 0 : 6.5, sx: {
|
|
533
592
|
height: "100%",
|
|
534
|
-
}, children: _jsx(ChatCommon, {
|
|
535
|
-
[AGENT_NETWORK_DESIGNER_ID]: "Let's build a network together!",
|
|
536
|
-
}, agentPlaceholders: {
|
|
593
|
+
}, children: _jsx(ChatCommon, { agentPlaceholders: {
|
|
537
594
|
[AGENT_NETWORK_DESIGNER_ID]: "Describe in plain language the network you would like to build.",
|
|
538
|
-
}, currentUser: userInfo.userName,
|
|
595
|
+
}, currentUser: userInfo.userName, customAgentGreetings: {
|
|
596
|
+
[AGENT_NETWORK_DESIGNER_ID]: "Let's build a network together!",
|
|
597
|
+
}, extraSlyData: extraSlyData, id: "agent-network-ui", isAwaitingLlm: isAwaitingLlm, missingApiKeys: getMissingApiKeys(), networkDescription: networkDescription, neuroSanURL: neuroSanURL, onChunkReceived: onChunkReceived, onStreamingComplete: onStreamingComplete, onStreamingStarted: onStreamingStarted, ref: chatRef, sampleQueries: sampleQueries, setIsAwaitingLlm: setIsAwaitingLlm, selectedNetwork: selectedNetwork }, selectedNetwork ?? "no-network") }) }));
|
|
539
598
|
};
|
|
540
599
|
const getStopButton = () => {
|
|
541
600
|
return (_jsx(_Fragment, { children: isAwaitingLlm && enableZenMode && (_jsx(Box, { id: "stop-button-container", sx: {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FC } from "react";
|
|
2
|
+
interface ApiKeyInputProps {
|
|
3
|
+
readonly forgetKey: () => void;
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly logo: string;
|
|
6
|
+
readonly onSave: (key: string) => void;
|
|
7
|
+
readonly onTest: (key: string) => Promise<boolean>;
|
|
8
|
+
readonly persistedValue: string;
|
|
9
|
+
readonly placeholder: string;
|
|
10
|
+
readonly vendor: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Component for inputting an API key for a given vendor, with the ability to test the key and forget the saved key.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ApiKeyInput: FC<ApiKeyInputProps>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
3
|
+
import ClearIcon from "@mui/icons-material/Clear";
|
|
4
|
+
import ErrorIcon from "@mui/icons-material/Error";
|
|
5
|
+
import Visibility from "@mui/icons-material/Visibility";
|
|
6
|
+
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
|
7
|
+
import Box from "@mui/material/Box";
|
|
8
|
+
import Button from "@mui/material/Button";
|
|
9
|
+
import FormLabel from "@mui/material/FormLabel";
|
|
10
|
+
import IconButton from "@mui/material/IconButton";
|
|
11
|
+
import InputAdornment from "@mui/material/InputAdornment";
|
|
12
|
+
import TextField from "@mui/material/TextField";
|
|
13
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { ConfirmationModal } from "../Common/ConfirmationModal.js";
|
|
16
|
+
/**
|
|
17
|
+
* Component for inputting an API key for a given vendor, with the ability to test the key and forget the saved key.
|
|
18
|
+
*/
|
|
19
|
+
export const ApiKeyInput = ({ forgetKey, id, logo, onSave, onTest, persistedValue, placeholder, vendor, }) => {
|
|
20
|
+
const [inputValue, setInputValue] = useState(persistedValue ?? "");
|
|
21
|
+
const [keyValidated, setKeyValidated] = useState(null);
|
|
22
|
+
const [isValidating, setIsValidating] = useState(false);
|
|
23
|
+
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
|
24
|
+
const [showKey, setShowKey] = useState(false);
|
|
25
|
+
const handleValueChange = (e) => {
|
|
26
|
+
setKeyValidated(null);
|
|
27
|
+
setInputValue(e.target.value);
|
|
28
|
+
};
|
|
29
|
+
const handleOnTest = async () => {
|
|
30
|
+
setIsValidating(true);
|
|
31
|
+
setKeyValidated(null);
|
|
32
|
+
try {
|
|
33
|
+
const isValid = await onTest(inputValue);
|
|
34
|
+
setKeyValidated(isValid);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setIsValidating(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const disableActions = !inputValue || inputValue === persistedValue || isValidating;
|
|
41
|
+
// Sync with persisted value changes - if the persisted value changes from outside
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setInputValue(persistedValue ?? "");
|
|
44
|
+
setKeyValidated(null);
|
|
45
|
+
}, [persistedValue]);
|
|
46
|
+
return (_jsxs(Box, { "data-testid": `${id}-input`, sx: { display: "flex", alignItems: "center", width: "100%", gap: 2 }, children: [confirmationDialogOpen ? (_jsx(ConfirmationModal, { id: `${id}-forget-key-confirmation-modal`, content: `This will forget the currently saved API key for ${vendor} and you will need to enter ` +
|
|
47
|
+
" the key again to use networks that require it. Are you sure you want to continue?", handleCancel: () => {
|
|
48
|
+
setConfirmationDialogOpen(false);
|
|
49
|
+
}, handleOk: () => {
|
|
50
|
+
setConfirmationDialogOpen(false);
|
|
51
|
+
setInputValue("");
|
|
52
|
+
forgetKey();
|
|
53
|
+
}, okBtnLabel: "Yes, forget key", title: `Forget ${vendor} API key?` })) : null, _jsx(Box, { sx: {
|
|
54
|
+
width: 25,
|
|
55
|
+
height: 25,
|
|
56
|
+
display: "flex",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
}, children: _jsx("img", { src: logo, alt: `${vendor} logo`, style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain" } }) }), _jsx(FormLabel, { id: `${id}-label`, sx: { width: 90, flexShrink: 0 }, children: vendor }), _jsx(TextField, { "aria-labelledby": `${id}-label`, autoComplete: "off", onChange: handleValueChange, placeholder: placeholder, size: "small", slotProps: {
|
|
60
|
+
input: {
|
|
61
|
+
endAdornment: (_jsxs(InputAdornment, { position: "end", children: [_jsx(Tooltip, { title: showKey ? "Hide API key" : "Show API key", children: _jsx("span", { children: _jsx(IconButton, { "aria-label": "toggle key visibility", disabled: !inputValue, onClick: () => setShowKey(!showKey), onMouseDown: (e) => e.preventDefault(), size: "small", children: showKey ? (_jsx(VisibilityOff, { fontSize: "small" })) : (_jsx(Visibility, { fontSize: "small" })) }) }) }), _jsx(IconButton, { "aria-label": "Clear input", edge: "end", onClick: () => setInputValue(""), size: "small", children: _jsx(ClearIcon, { fontSize: "small" }) })] })),
|
|
62
|
+
},
|
|
63
|
+
}, sx: { flex: 1 },
|
|
64
|
+
// Type depends on whether we're showing or hiding the key
|
|
65
|
+
type: showKey ? "text" : "password", value: inputValue, variant: "outlined" }), _jsx(Box, { sx: {
|
|
66
|
+
width: 24,
|
|
67
|
+
height: 24,
|
|
68
|
+
color: (theme) => (keyValidated ? theme.palette.success.main : theme.palette.error.main),
|
|
69
|
+
}, children: keyValidated === null ? null : keyValidated ? (_jsx(CheckIcon, { fontSize: "small" })) : (_jsx(ErrorIcon, { fontSize: "small" })) }), _jsx(Button, { disabled: disableActions, loading: isValidating, onClick: handleOnTest, size: "small", variant: "contained", children: "Test" }), _jsx(Button, { onClick: () => onSave(inputValue), size: "small", variant: "contained", disabled: disableActions, children: "Save" }), _jsx(Button, { onClick: () => setConfirmationDialogOpen(true), size: "small", variant: "contained", disabled: !persistedValue || isValidating, children: "Forget" })] }));
|
|
70
|
+
};
|