@cognizant-ai-lab/ui-common 1.4.0 → 1.4.2
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.js +3 -3
- package/dist/components/Common/Navbar.js +6 -5
- package/dist/components/MultiAgentAccelerator/AgentNode.js +3 -3
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +11 -30
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +21 -2
- package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +5 -0
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +7 -0
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +17 -2
- package/dist/components/MultiAgentAccelerator/const.d.ts +2 -0
- package/dist/components/MultiAgentAccelerator/const.js +4 -0
- package/dist/controller/agent/Agent.d.ts +6 -4
- package/dist/controller/agent/Agent.js +7 -6
- package/dist/controller/llm/LlmChat.d.ts +11 -1
- package/dist/controller/llm/LlmChat.js +60 -13
- package/dist/generated/neuro-san/NeuroSanClient.d.ts +3 -1
- package/dist/state/TemporaryNetworks.d.ts +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/File.d.ts +29 -0
- package/dist/utils/File.js +61 -0
- package/package.json +9 -10
|
@@ -47,7 +47,7 @@ import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid
|
|
|
47
47
|
import { MicrophoneButton } from "./VoiceChat/MicrophoneButton.js";
|
|
48
48
|
import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "./VoiceChat/VoiceChat.js";
|
|
49
49
|
import { getAgentFunction, getConnectivity, sendChatQuery } from "../../controller/agent/Agent.js";
|
|
50
|
-
import { sendLlmRequest } from "../../controller/llm/LlmChat.js";
|
|
50
|
+
import { sendLlmRequest, StreamingUnit } from "../../controller/llm/LlmChat.js";
|
|
51
51
|
import { ChatMessageType, } from "../../generated/neuro-san/NeuroSanClient.js";
|
|
52
52
|
import { hashString, hasOnlyWhitespace } from "../../utils/text.js";
|
|
53
53
|
import { LlmChatOptionsButton } from "../Common/LlmChatOptionsButton.js";
|
|
@@ -433,13 +433,13 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
433
433
|
if (isLegacyAgentType(targetAgent)) {
|
|
434
434
|
// It's a legacy agent (these go directly to the LLM and are different from the Neuro-san agents).
|
|
435
435
|
// Send the chat query to the server. This will block until the stream ends from the server
|
|
436
|
-
await sendLlmRequest(handleChunk, controller?.current.signal, legacyAgentEndpoint, extraParams, query, chatHistory.current);
|
|
436
|
+
await sendLlmRequest(handleChunk, controller?.current.signal, legacyAgentEndpoint, extraParams, query, chatHistory.current, null, StreamingUnit.Chunk);
|
|
437
437
|
}
|
|
438
438
|
else {
|
|
439
439
|
// It's a Neuro-san agent.
|
|
440
440
|
// Some coded tools (data generator...) expect the username provided in slyData.
|
|
441
441
|
const slyDataWithUserName = { ...slyData.current, login: currentUser };
|
|
442
|
-
await sendChatQuery(neuroSanURL, controller?.current.signal, query, targetAgent, handleChunk, chatContext.current, slyDataWithUserName, currentUser);
|
|
442
|
+
await sendChatQuery(neuroSanURL, controller?.current.signal, query, targetAgent, handleChunk, chatContext.current, slyDataWithUserName, currentUser, StreamingUnit.Line);
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
445
|
catch (error) {
|
|
@@ -87,17 +87,18 @@ export const Navbar = ({ authenticationType, id, logo, logoServiceToken, pathnam
|
|
|
87
87
|
// Customer for branding
|
|
88
88
|
const customer = useSettingsStore((state) => state.settings.branding.customer);
|
|
89
89
|
const primary = useSettingsStore((state) => state.settings.branding.primary);
|
|
90
|
+
const hasCustomer = customer?.trim().length > 0;
|
|
90
91
|
return hydrated ? (_jsxs(Grid, { id: "nav-bar-container", container: true, alignItems: "center", sx: {
|
|
91
92
|
...MENU_ITEM_TEXT_PROPS,
|
|
92
93
|
padding: "0.5rem",
|
|
93
|
-
}, children: [settingsDialogOpen && (_jsx(SettingsDialog, { id: "settings-dialog", isOpen: settingsDialogOpen, logoServiceToken: logoServiceToken, onClose: () => setSettingsDialogOpen(false) })), _jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 2 }, children: [_jsx(CustomerLogo, { logoServiceToken: logoServiceToken }), _jsx(Typography, {
|
|
94
|
+
}, children: [settingsDialogOpen && (_jsx(SettingsDialog, { id: "settings-dialog", isOpen: settingsDialogOpen, logoServiceToken: logoServiceToken, onClose: () => setSettingsDialogOpen(false) })), _jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 2 }, children: [_jsx(CustomerLogo, { logoServiceToken: logoServiceToken }), hasCustomer && (_jsx(Typography, { "data-testid": "customer-branding", sx: {
|
|
94
95
|
fontSize: "20px",
|
|
95
96
|
fontWeight: "600",
|
|
96
97
|
paddingLeft: "0.15rem",
|
|
97
98
|
width: "200px",
|
|
98
99
|
display: "flex",
|
|
99
100
|
alignItems: "center",
|
|
100
|
-
}, children: customer })] }), _jsxs(Grid, { id: id, sx: { display: "flex", alignItems: "center" }, children: [
|
|
101
|
+
}, children: customer }))] }), _jsxs(Grid, { id: id, sx: { display: "flex", alignItems: "center" }, children: [hasCustomer ? getCognizantLogoImage() : null, _jsx(Typography, { id: "nav-bar-brand", sx: {
|
|
101
102
|
...MENU_ITEM_TEXT_PROPS,
|
|
102
103
|
marginLeft: "0.85rem",
|
|
103
104
|
fontSize: "16px",
|
|
@@ -142,15 +143,15 @@ export const Navbar = ({ authenticationType, id, logo, logoServiceToken, pathnam
|
|
|
142
143
|
whiteSpace: "normal",
|
|
143
144
|
wordWrap: "break-word",
|
|
144
145
|
fontSize: "smaller",
|
|
145
|
-
}, children: userInfo.name }), _jsx(MenuItem, { id: "auth-type-title", disabled: true, sx: { fontWeight: "bold" }, children: "Authentication" }), _jsx(MenuItem, { id: "authentication-type-menu-item", disabled: true, sx: { fontSize: "smaller" }, children: authenticationType }), authenticationEnabled() && (_jsx(MenuItem, { id: "user-sign-out", sx: { ...DISABLE_OUTLINE_PROPS, fontWeight: "bold" }, onClick: signOut, children: "Sign out" }))] })] })) : null, _jsx(Tooltip, { id: "dark-mode-toggle", title:
|
|
146
|
+
}, children: userInfo.name }), _jsx(MenuItem, { id: "auth-type-title", disabled: true, sx: { fontWeight: "bold" }, children: "Authentication" }), _jsx(MenuItem, { id: "authentication-type-menu-item", disabled: true, sx: { fontSize: "smaller" }, children: authenticationType }), authenticationEnabled() && (_jsx(MenuItem, { id: "user-sign-out", sx: { ...DISABLE_OUTLINE_PROPS, fontWeight: "bold" }, onClick: signOut, children: "Sign out" }))] })] })) : null, _jsx(Tooltip, { id: "dark-mode-toggle", title: hasCustomer
|
|
146
147
|
? "Dark mode toggle is not available when customer branding is active. Reset via Settings menu."
|
|
147
148
|
: "Toggle dark mode", children: _jsx(DarkModeIcon, { id: "dark-mode-icon", sx: {
|
|
148
149
|
marginRight: "1rem",
|
|
149
150
|
fontSize: "1rem",
|
|
150
|
-
cursor:
|
|
151
|
+
cursor: hasCustomer ? "not-allowed" : "pointer",
|
|
151
152
|
color: darkMode ? "var(--bs-yellow)" : "var(--bs-gray-dark)",
|
|
152
153
|
}, onClick: () => {
|
|
153
|
-
!
|
|
154
|
+
!hasCustomer && setMode(darkMode ? "light" : "dark");
|
|
154
155
|
} }) }), showSettingsButton && (_jsx(Tooltip, { title: "Settings", children: _jsx(SettingsIcon, { sx: {
|
|
155
156
|
...MENU_ITEM_TEXT_PROPS,
|
|
156
157
|
marginRight: "1rem",
|
|
@@ -113,8 +113,8 @@ export const AgentNode = (props) => {
|
|
|
113
113
|
const colorIndex = depth % palette.length;
|
|
114
114
|
backgroundColor = palette[colorIndex];
|
|
115
115
|
}
|
|
116
|
-
// Hide handles when awaiting LLM response ("zen mode")
|
|
117
|
-
const handleVisibility = isAwaitingLlm ? "
|
|
116
|
+
// Hide handles when awaiting LLM response ("zen mode").
|
|
117
|
+
const handleVisibility = isAwaitingLlm ? "hidden" : "visible";
|
|
118
118
|
// Determine which icon to display based on the agent type whether it is Frontman or not
|
|
119
119
|
const getDisplayAsIcon = () => {
|
|
120
120
|
const id = `${agentId}-icon`;
|
|
@@ -156,7 +156,7 @@ export const AgentNode = (props) => {
|
|
|
156
156
|
height: NODE_HEIGHT * (isFrontman ? 1.25 : 1.0),
|
|
157
157
|
width: NODE_WIDTH * (isFrontman ? 1.25 : 1.0),
|
|
158
158
|
zIndex: getZIndex(1, theme),
|
|
159
|
-
}, children: [getDisplayAsIcon(), _jsx(Handle, { id: `${agentId}-left-handle`, position: Position.Left, type: "source", style: {
|
|
159
|
+
}, children: [getDisplayAsIcon(), _jsx(Handle, { id: `${agentId}-left-handle`, position: Position.Left, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-right-handle`, position: Position.Right, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-top-handle`, position: Position.Top, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-bottom-handle`, position: Position.Bottom, type: "source", style: { visibility: handleVisibility } })] }), _jsx(Tooltip, { id: `${agentId}-tooltip`, title: agentName, placement: "top", disableInteractive: true, children: _jsx(Typography, { id: `${agentId}-name`, sx: {
|
|
160
160
|
display: "-webkit-box",
|
|
161
161
|
fontSize: "18px",
|
|
162
162
|
fontWeight: "bold",
|
|
@@ -16,7 +16,6 @@ limitations under the License.
|
|
|
16
16
|
*/
|
|
17
17
|
import StopCircle from "@mui/icons-material/StopCircle";
|
|
18
18
|
import Box from "@mui/material/Box";
|
|
19
|
-
import Collapse from "@mui/material/Collapse";
|
|
20
19
|
import Grid from "@mui/material/Grid";
|
|
21
20
|
import Slide from "@mui/material/Slide";
|
|
22
21
|
import { ReactFlowProvider } from "@xyflow/react";
|
|
@@ -26,7 +25,7 @@ import { getUpdatedAgentCounts } from "./AgentCounts.js";
|
|
|
26
25
|
import { AgentFlow } from "./AgentFlow.js";
|
|
27
26
|
import { TEMPORARY_NETWORK_FOLDER } from "./const.js";
|
|
28
27
|
import { Sidebar } from "./Sidebar/Sidebar.js";
|
|
29
|
-
import { extractReservations } from "./TemporaryNetworks.js";
|
|
28
|
+
import { extractNetworkHocon, extractReservations } from "./TemporaryNetworks.js";
|
|
30
29
|
import { getAgentIconSuggestions, getAgentNetworks, getConnectivity, getNetworkIconSuggestions, } from "../../controller/agent/Agent.js";
|
|
31
30
|
import { useSettingsStore } from "../../state/Settings.js";
|
|
32
31
|
import { useTempNetworksStore } from "../../state/TemporaryNetworks.js";
|
|
@@ -35,7 +34,6 @@ import { ChatCommon } from "../AgentChat/ChatCommon.js";
|
|
|
35
34
|
import { SmallLlmChatButton } from "../AgentChat/LlmChatButton.js";
|
|
36
35
|
import { chatMessageFromChunk, cleanUpAgentName, removeTrailingUuid } from "../AgentChat/Utils.js";
|
|
37
36
|
import { ConfirmationModal } from "../Common/ConfirmationModal.js";
|
|
38
|
-
import { MUIAlert } from "../Common/MUIAlert.js";
|
|
39
37
|
import { closeNotification, NotificationType, sendNotification } from "../Common/notification.js";
|
|
40
38
|
// Display expired temporary networks for this amount of time after they expire so users can see what happened
|
|
41
39
|
const GRACE_PERIOD_MS = 5 * 60 * 1000; // 5 minutes
|
|
@@ -45,9 +43,13 @@ const GROW_ANIMATION_TIME_MS = 800;
|
|
|
45
43
|
* Helper function to convert agent reservations received from the backend into temporary networks that can be displayed
|
|
46
44
|
* in the tree.
|
|
47
45
|
* @param agentReservations List of "agent reservations" (temporary networks) received from the backend
|
|
46
|
+
* @param networkHocon Optional network HOCON string that may be included in the same message as the
|
|
47
|
+
* reservations. Note: for now we assume that all reservations are associated with the same network definition.
|
|
48
|
+
* This will fail if ever we get multiple reservations for different networks in a single chat stream, but that is
|
|
49
|
+
* not a valid scenario currently; we are focusing on Agent Network Design which has a simple output.
|
|
48
50
|
* @returns List of TemporaryNetwork objects that can be displayed in the UI
|
|
49
51
|
*/
|
|
50
|
-
const convertReservationsToNetworks = (agentReservations) => {
|
|
52
|
+
const convertReservationsToNetworks = (agentReservations, networkHocon) => {
|
|
51
53
|
return agentReservations.map((reservation) => ({
|
|
52
54
|
reservation,
|
|
53
55
|
agentInfo: {
|
|
@@ -55,6 +57,7 @@ const convertReservationsToNetworks = (agentReservations) => {
|
|
|
55
57
|
origin: reservation.reservation_id,
|
|
56
58
|
status: "active",
|
|
57
59
|
},
|
|
60
|
+
networkHocon,
|
|
58
61
|
}));
|
|
59
62
|
};
|
|
60
63
|
/**
|
|
@@ -90,8 +93,6 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
90
93
|
const [networkToBeDeleted, setNetworkToBeDeleted] = useState(null);
|
|
91
94
|
// State to hold thought bubble edges - avoids duplicates across layout recalculations
|
|
92
95
|
const [thoughtBubbleEdges, setThoughtBubbleEdges] = useState(new Map());
|
|
93
|
-
// For controlling alert when temporary network is created
|
|
94
|
-
const [alertContents, setAlertContents] = useState(null);
|
|
95
96
|
const customURLCallback = useCallback((url) => {
|
|
96
97
|
setNeuroSanURL(url || backendNeuroSanApiUrl);
|
|
97
98
|
setCustomURLLocalStorage(url === "" ? null : url);
|
|
@@ -244,7 +245,9 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
244
245
|
const reservationsResult = extractReservations(chatMessage);
|
|
245
246
|
// Handle agent reservations (temporary networks) that come in through the chat stream.
|
|
246
247
|
if (reservationsResult?.length > 0) {
|
|
247
|
-
|
|
248
|
+
// Retrieve network definition, if present
|
|
249
|
+
const networkHocon = extractNetworkHocon(chatMessage);
|
|
250
|
+
const newTemporaryNetworks = convertReservationsToNetworks(reservationsResult, networkHocon);
|
|
248
251
|
const currentNetworks = useTempNetworksStore.getState().tempNetworks;
|
|
249
252
|
useTempNetworksStore.getState().setTempNetworks([...currentNetworks, ...newTemporaryNetworks]);
|
|
250
253
|
// record the new temporary networks so we can select them for the user. For now, we only
|
|
@@ -258,7 +261,6 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
258
261
|
agentCountsRef.current = new Map();
|
|
259
262
|
// Reset newly added temporary networks
|
|
260
263
|
setNewlyAddedTemporaryNetworks(new Set());
|
|
261
|
-
setAlertContents(null);
|
|
262
264
|
// Show info popup only once per session
|
|
263
265
|
if (!haveShownPopup) {
|
|
264
266
|
sendNotification(NotificationType.info, "Agents working", "Click the stop button or hit Escape to exit.");
|
|
@@ -268,31 +270,11 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
268
270
|
setIsStreaming(true);
|
|
269
271
|
}, [haveShownPopup]);
|
|
270
272
|
const onStreamingComplete = useCallback(() => {
|
|
271
|
-
const network = newlyAddedTemporaryNetworks?.values().next().value;
|
|
272
|
-
if (network?.length > 0) {
|
|
273
|
-
// We show an Alert after streaming completes (in case of Zen mode where the user might miss it)
|
|
274
|
-
const agentNameDisplay = cleanUpAgentName(removeTrailingUuid(network));
|
|
275
|
-
setAlertContents(`A temporary network "${agentNameDisplay}" has been created.`);
|
|
276
|
-
}
|
|
277
273
|
// When streaming is complete, clean up any refs and state
|
|
278
274
|
conversationsRef.current = null;
|
|
279
275
|
setCurrentConversations(null);
|
|
280
276
|
resetState();
|
|
281
277
|
}, [newlyAddedTemporaryNetworks]);
|
|
282
|
-
useEffect(() => {
|
|
283
|
-
if (alertContents?.length > 0) {
|
|
284
|
-
// Set a timer to clear the alert after a few seconds so it doesn't overstay its welcome
|
|
285
|
-
const timeoutId = window.setTimeout(() => {
|
|
286
|
-
setAlertContents(null);
|
|
287
|
-
}, 10_000);
|
|
288
|
-
return () => {
|
|
289
|
-
window.clearTimeout(timeoutId);
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
}, [alertContents]);
|
|
296
278
|
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
|
|
297
279
|
const handleDeleteNetwork = (networkId, isExpired) => {
|
|
298
280
|
if (isExpired) {
|
|
@@ -354,8 +336,7 @@ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, userInfo, }) => {
|
|
|
354
336
|
setNetworkToBeDeleted(null);
|
|
355
337
|
setConfirmationModalOpen(false);
|
|
356
338
|
}, title: "Delete Network" })) : null;
|
|
357
|
-
|
|
358
|
-
return (_jsxs(_Fragment, { children: [getAlert(), getConfirmationModal(), _jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
|
|
339
|
+
return (_jsxs(_Fragment, { children: [getConfirmationModal(), _jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
|
|
359
340
|
display: "flex",
|
|
360
341
|
flex: 1,
|
|
361
342
|
height: "85%",
|
|
@@ -6,6 +6,7 @@ export interface AgentNetworkNodeProps extends TreeItemProps {
|
|
|
6
6
|
readonly onDeleteNetwork?: (network: string, isExpired: boolean) => void;
|
|
7
7
|
readonly networkIconSuggestions: Record<string, string>;
|
|
8
8
|
readonly temporaryNetworkExpirationTimes?: Record<string, Date>;
|
|
9
|
+
readonly temporaryNetworkHoconStrings?: Record<string, string | null>;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* Custom Tree Item for MUI RichTreeView to display agent networks with tags
|
|
@@ -4,14 +4,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
import * as MuiIcons from "@mui/icons-material";
|
|
5
5
|
import BookmarkIcon from "@mui/icons-material/Bookmark";
|
|
6
6
|
import Delete from "@mui/icons-material/Delete";
|
|
7
|
+
import DownloadIcon from "@mui/icons-material/Download";
|
|
7
8
|
import Box from "@mui/material/Box";
|
|
8
9
|
import Chip from "@mui/material/Chip";
|
|
10
|
+
import IconButton from "@mui/material/IconButton";
|
|
9
11
|
import { useTheme } from "@mui/material/styles";
|
|
10
12
|
import Tooltip from "@mui/material/Tooltip";
|
|
11
13
|
import { TreeItemContent, TreeItemGroupTransition, TreeItemLabel, TreeItemRoot, } from "@mui/x-tree-view/TreeItem";
|
|
12
14
|
import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
|
|
13
15
|
import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
|
|
14
16
|
import { useRef } from "react";
|
|
17
|
+
import { downloadFile, toSafeFilename } from "../../../utils/File.js";
|
|
15
18
|
import { cleanUpAgentName } from "../../AgentChat/Utils.js";
|
|
16
19
|
// Palette of colors we can use for tags
|
|
17
20
|
const TAG_COLORS = [
|
|
@@ -40,7 +43,7 @@ const isTemporaryNetworkExpired = (expirationDate) => {
|
|
|
40
43
|
* @param props - see AgentNetworkNode interface
|
|
41
44
|
* @returns JSX.Element containing the custom tree item
|
|
42
45
|
*/
|
|
43
|
-
export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, networkIconSuggestions, nodeIndex, onDeleteNetwork, temporaryNetworkExpirationTimes, }) => {
|
|
46
|
+
export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, networkIconSuggestions, nodeIndex, onDeleteNetwork, temporaryNetworkExpirationTimes, temporaryNetworkHoconStrings, }) => {
|
|
44
47
|
const theme = useTheme();
|
|
45
48
|
// We know all labels are strings because we set them that way in the tree view items
|
|
46
49
|
const labelString = label;
|
|
@@ -66,6 +69,7 @@ export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, networ
|
|
|
66
69
|
const expirationTime = temporaryNetworkExpirationTimes?.[itemId];
|
|
67
70
|
const isTemporaryNetwork = Boolean(expirationTime);
|
|
68
71
|
const isExpired = isChild && isTemporaryNetwork && isTemporaryNetworkExpired(expirationTime);
|
|
72
|
+
const networkHocon = isTemporaryNetwork ? temporaryNetworkHoconStrings?.[itemId] : null;
|
|
69
73
|
const iconNameSuggestion = isTemporaryNetwork ? "HourglassTop" : isChild ? networkIconSuggestions?.[itemId] : null;
|
|
70
74
|
let muiIconElement = null;
|
|
71
75
|
if (iconNameSuggestion && MuiIcons[iconNameSuggestion]) {
|
|
@@ -93,7 +97,22 @@ export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, networ
|
|
|
93
97
|
.map((tag) => (_jsx(Chip, { label: tag, style: {
|
|
94
98
|
margin: "0.25rem",
|
|
95
99
|
backgroundColor: `var(${tagsToColors.get(tag) || TAG_COLORS[0]})`,
|
|
96
|
-
} }, tag))), placement: "right", arrow: true, children: _jsx(BookmarkIcon, { sx: { fontSize: "0.75rem", color: "var(--bs-accent1-medium)" } }) })) : null
|
|
100
|
+
} }, tag))), placement: "right", arrow: true, children: _jsx(BookmarkIcon, { sx: { fontSize: "0.75rem", color: "var(--bs-accent1-medium)" } }) })) : null, isTemporaryNetwork && networkHocon && (_jsx(Tooltip, { title: isExpired ? "Network expired" : "Download network definition", children: _jsx("span", { children: _jsx(IconButton, { onClick: (e) => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
if (isExpired) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const fileName = `${toSafeFilename(labelString)}.hocon`;
|
|
106
|
+
downloadFile(networkHocon, fileName);
|
|
107
|
+
}, disabled: isExpired, "aria-label": "Download network definition", size: "small", sx: {
|
|
108
|
+
padding: 0,
|
|
109
|
+
color: "var(--bs-secondary)",
|
|
110
|
+
"&:hover": { color: "var(--bs-secondary-dark)" },
|
|
111
|
+
"&.Mui-disabled": {
|
|
112
|
+
color: "var(--bs-secondary)",
|
|
113
|
+
opacity: 0.3,
|
|
114
|
+
},
|
|
115
|
+
}, children: _jsx(DownloadIcon, { sx: { fontSize: "0.75rem" } }) }) }) }))] }), isChild && isTemporaryNetwork && (_jsx(Tooltip, { title: "Delete network", children: _jsx(Delete, { onClick: (e) => {
|
|
97
116
|
e.stopPropagation();
|
|
98
117
|
onDeleteNetwork?.(itemId, isExpired);
|
|
99
118
|
}, sx: {
|
|
@@ -203,6 +203,10 @@ export const Sidebar = ({ customURLCallback, customURLLocalStorage, id, isAwaiti
|
|
|
203
203
|
acc[tempNetwork.agentInfo.agent_name] = new Date(tempNetwork.reservation.expiration_time_in_seconds * 1000);
|
|
204
204
|
return acc;
|
|
205
205
|
}, {});
|
|
206
|
+
const temporaryNetworkHoconStrings = temporaryNetworks.reduce((acc, tempNetwork) => {
|
|
207
|
+
acc[tempNetwork.agentInfo.agent_name] = tempNetwork.networkHocon;
|
|
208
|
+
return acc;
|
|
209
|
+
}, {});
|
|
206
210
|
const [selectedItem, setSelectedItem] = useState(null);
|
|
207
211
|
const handleSelectedItemsChange = (_event, itemId) => {
|
|
208
212
|
if (!itemId) {
|
|
@@ -263,6 +267,7 @@ export const Sidebar = ({ customURLCallback, customURLLocalStorage, id, isAwaiti
|
|
|
263
267
|
nodeIndex,
|
|
264
268
|
onDeleteNetwork,
|
|
265
269
|
temporaryNetworkExpirationTimes,
|
|
270
|
+
temporaryNetworkHoconStrings,
|
|
266
271
|
},
|
|
267
272
|
} }, Object.keys(networkIconSuggestions || {}).length)] }), _jsxs(Popover, { id: "agent-network-settings-popover", open: settingsPopoverOpen, anchorEl: settingsAnchorEl, onClose: () => handleSettingsClose(true), anchorOrigin: {
|
|
268
273
|
vertical: "bottom",
|
|
@@ -24,3 +24,10 @@ export type AgentReservation = {
|
|
|
24
24
|
* if the message is not of the expected type.
|
|
25
25
|
*/
|
|
26
26
|
export declare const extractReservations: (message: ChatMessage) => AgentReservation[];
|
|
27
|
+
/**
|
|
28
|
+
* Extracts the agent network HOCON from a chat message, if it exists.
|
|
29
|
+
* @param message The chat message to extract network HOCON from.
|
|
30
|
+
* We expect the network definition to be present in messages of type AGENT_FRAMEWORK only.
|
|
31
|
+
* @return The network HOCON as a string, or null if not found or not the right type of message.
|
|
32
|
+
*/
|
|
33
|
+
export declare const extractNetworkHocon: (message: ChatMessage) => string | null;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { AGENT_NETWORK_HOCON, AGENT_RESERVATIONS_KEY } from "./const.js";
|
|
1
2
|
import { ChatMessageType } from "../../generated/neuro-san/NeuroSanClient.js";
|
|
2
|
-
// We expect the agent reservations to be stored in sly_data under this key
|
|
3
|
-
const AGENT_RESERVATIONS_KEY = "agent_reservations";
|
|
4
3
|
/**
|
|
5
4
|
* Extracts agent reservations from a chat message, if they exist.
|
|
6
5
|
* @param message The chat message to extract reservations from. We expect reservations to be present in messages of
|
|
@@ -18,3 +17,19 @@ export const extractReservations = (message) => {
|
|
|
18
17
|
return [];
|
|
19
18
|
}
|
|
20
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the agent network HOCON from a chat message, if it exists.
|
|
22
|
+
* @param message The chat message to extract network HOCON from.
|
|
23
|
+
* We expect the network definition to be present in messages of type AGENT_FRAMEWORK only.
|
|
24
|
+
* @return The network HOCON as a string, or null if not found or not the right type of message.
|
|
25
|
+
*/
|
|
26
|
+
export const extractNetworkHocon = (message) => {
|
|
27
|
+
// Check for agent network HOCON in sly_data
|
|
28
|
+
if (message?.type === ChatMessageType.AGENT_FRAMEWORK && message?.sly_data?.[AGENT_NETWORK_HOCON]) {
|
|
29
|
+
return message.sly_data[AGENT_NETWORK_HOCON];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Not the type of message that would contain a network HOCON, or no network HOCON found, return null
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -3,3 +3,5 @@ export declare const DEFAULT_FRONTMAN_Y_POS = 450;
|
|
|
3
3
|
export declare const BASE_RADIUS = 100;
|
|
4
4
|
export declare const LEVEL_SPACING = 150;
|
|
5
5
|
export declare const TEMPORARY_NETWORK_FOLDER = "temporary";
|
|
6
|
+
export declare const AGENT_RESERVATIONS_KEY = "agent_reservations";
|
|
7
|
+
export declare const AGENT_NETWORK_HOCON = "agent_network_hocon_text";
|
|
@@ -23,3 +23,7 @@ export const LEVEL_SPACING = 150;
|
|
|
23
23
|
// they come from the backend, but we need to put them somewhere in the UI, and this makes it clear that they're
|
|
24
24
|
// temporary.
|
|
25
25
|
export const TEMPORARY_NETWORK_FOLDER = "temporary";
|
|
26
|
+
// We expect the agent reservations to be stored in sly_data under this key
|
|
27
|
+
export const AGENT_RESERVATIONS_KEY = "agent_reservations";
|
|
28
|
+
// We expect the agent network HOCON to be stored in sly_data under this key, if it is provided by the backend
|
|
29
|
+
export const AGENT_NETWORK_HOCON = "agent_network_hocon_text";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AgentInfo, ChatContext, ChatResponse, ConnectivityResponse, FunctionResponse } from "../../generated/neuro-san/NeuroSanClient.js";
|
|
2
|
+
import { StreamingUnit } from "../llm/LlmChat.js";
|
|
2
3
|
import { AgentIconSuggestions } from "../Types/AgentIconSuggestions.js";
|
|
3
4
|
import { BrandingSuggestions } from "../Types/Branding.js";
|
|
4
5
|
import { NetworkIconSuggestions } from "../Types/NetworkIconSuggestions.js";
|
|
@@ -44,17 +45,18 @@ export declare const getAgentNetworks: (url: string) => Promise<readonly AgentIn
|
|
|
44
45
|
* @param url The neuro-san server URL
|
|
45
46
|
* @param signal The AbortSignal to use for the request. Used to cancel the request on user demand
|
|
46
47
|
* @param userInput The user input to send to the agent.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @param targetAgent The target agent to send the request to. See CombinedAgentType for the list of available agents.
|
|
48
|
+
* @param targetAgent The target agent to send the request to. See CombinedAgentType for some available agents, or
|
|
49
|
+
* could be a string with an arbitrary agent name.
|
|
50
50
|
* @param callback The callback function to be called when a chunk of data is received from the server.
|
|
51
51
|
* @param chatContext "Opaque" conversation context for maintaining conversation state with the server. Neuro-san
|
|
52
52
|
* agents do not use ChatHistory directly, but rather, ChatContext, which is a collection of ChatHistory objects.
|
|
53
53
|
* @param slyData Data items that should not be sent to the LLM. Generated by the server.
|
|
54
54
|
* @param userId Current user ID in the session.
|
|
55
|
+
* @param streamingUnit Determines whether to send data to the callback as soon as it's received (Chunk)
|
|
56
|
+
* or to accumulate it until a newline is received (Line).
|
|
55
57
|
* @returns The response from the agent network.
|
|
56
58
|
*/
|
|
57
|
-
export declare const sendChatQuery: (url: string, signal: AbortSignal, userInput: string, targetAgent: string, callback: (chunk: string) => void, chatContext: ChatContext, slyData: Record<string, unknown>, userId: string) => Promise<ChatResponse>;
|
|
59
|
+
export declare const sendChatQuery: (url: string, signal: AbortSignal, userInput: string, targetAgent: string, callback: (chunk: string) => void, chatContext: ChatContext, slyData: Record<string, unknown>, userId: string, streamingUnit?: StreamingUnit) => Promise<ChatResponse>;
|
|
58
60
|
/**
|
|
59
61
|
* Gets information on the agent and tool connections within a network
|
|
60
62
|
* @param url The neuro-san server URL
|
|
@@ -20,7 +20,7 @@ import { TEMPORARY_NETWORK_FOLDER } from "../../components/MultiAgentAccelerator
|
|
|
20
20
|
import { ApiPaths,
|
|
21
21
|
// eslint-disable-next-line camelcase
|
|
22
22
|
ChatFilterChat_filter_type, ChatMessageType, } from "../../generated/neuro-san/NeuroSanClient.js";
|
|
23
|
-
import { sendLlmRequest } from "../llm/LlmChat.js";
|
|
23
|
+
import { sendLlmRequest, StreamingUnit } from "../llm/LlmChat.js";
|
|
24
24
|
/**
|
|
25
25
|
* Insert the target agent name into the path. The paths Api enum contains values like:
|
|
26
26
|
* <code>"/api/v1/{agent_name}/connectivity"</code> so unfortunately we need to do a `replace()` to insert the target
|
|
@@ -139,17 +139,18 @@ const handleJsonLines = (chunk, callback) => {
|
|
|
139
139
|
* @param url The neuro-san server URL
|
|
140
140
|
* @param signal The AbortSignal to use for the request. Used to cancel the request on user demand
|
|
141
141
|
* @param userInput The user input to send to the agent.
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* @param targetAgent The target agent to send the request to. See CombinedAgentType for the list of available agents.
|
|
142
|
+
* @param targetAgent The target agent to send the request to. See CombinedAgentType for some available agents, or
|
|
143
|
+
* could be a string with an arbitrary agent name.
|
|
145
144
|
* @param callback The callback function to be called when a chunk of data is received from the server.
|
|
146
145
|
* @param chatContext "Opaque" conversation context for maintaining conversation state with the server. Neuro-san
|
|
147
146
|
* agents do not use ChatHistory directly, but rather, ChatContext, which is a collection of ChatHistory objects.
|
|
148
147
|
* @param slyData Data items that should not be sent to the LLM. Generated by the server.
|
|
149
148
|
* @param userId Current user ID in the session.
|
|
149
|
+
* @param streamingUnit Determines whether to send data to the callback as soon as it's received (Chunk)
|
|
150
|
+
* or to accumulate it until a newline is received (Line).
|
|
150
151
|
* @returns The response from the agent network.
|
|
151
152
|
*/
|
|
152
|
-
export const sendChatQuery = async (url, signal, userInput, targetAgent, callback, chatContext, slyData, userId) => {
|
|
153
|
+
export const sendChatQuery = async (url, signal, userInput, targetAgent, callback, chatContext, slyData, userId, streamingUnit = StreamingUnit.Chunk) => {
|
|
153
154
|
// Create request
|
|
154
155
|
const userMessage = {
|
|
155
156
|
type: ChatMessageType.HUMAN,
|
|
@@ -164,7 +165,7 @@ export const sendChatQuery = async (url, signal, userInput, targetAgent, callbac
|
|
|
164
165
|
};
|
|
165
166
|
const fetchUrl = `${url}${insertTargetAgent(targetAgent, ApiPaths.AgentService_StreamingChat)}`;
|
|
166
167
|
const requestRecord = Object.entries(agentChatRequest).reduce((acc, [key, value]) => (value ? { ...acc, [key]: value } : acc), {});
|
|
167
|
-
return sendLlmRequest((chunk) => handleJsonLines(chunk, callback), signal, fetchUrl, requestRecord, null, null, userId);
|
|
168
|
+
return sendLlmRequest((chunk) => handleJsonLines(chunk, callback), signal, fetchUrl, requestRecord, null, null, userId, streamingUnit);
|
|
168
169
|
};
|
|
169
170
|
/**
|
|
170
171
|
* Gets information on the agent and tool connections within a network
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
* Allows streaming callback for a more interactive experience.
|
|
4
4
|
*/
|
|
5
5
|
import { BaseMessage } from "@langchain/core/messages";
|
|
6
|
+
/**
|
|
7
|
+
* Determines whether to send data to the callback as soon as it's received (Chunk) or to accumulate it
|
|
8
|
+
* until a newline is received (Line).
|
|
9
|
+
*/
|
|
10
|
+
export declare enum StreamingUnit {
|
|
11
|
+
Chunk = 0,
|
|
12
|
+
Line = 1
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Send a request to an LLM and stream the response to a callback.
|
|
8
16
|
* @param callback The callback function to be called when a chunk of data is received from the server.
|
|
@@ -12,7 +20,9 @@ import { BaseMessage } from "@langchain/core/messages";
|
|
|
12
20
|
* @param userQuery The user query to send to the server (sometimes part of chat history instead).
|
|
13
21
|
* @param chatHistory The chat history to be sent to the server. Contains user requests and server responses.
|
|
14
22
|
* @param userId Current user ID in the session.
|
|
23
|
+
* @param streamingUnit Determines whether to send data to the callback as soon as it's received (Chunk)
|
|
24
|
+
* or to accumulate it until a newline is received (Line). Default is Chunk.
|
|
15
25
|
* @returns Either the JSON result of the call, or, if a callback is provided, nothing, but tokens are streamed
|
|
16
26
|
* to the callback as they are received from the server.
|
|
17
27
|
*/
|
|
18
|
-
export declare const sendLlmRequest: (callback: (token: string) => void, signal: AbortSignal, fetchUrl: string, params: Record<string, unknown>, userQuery?: string, chatHistory?: BaseMessage[], userId?: string) => Promise<any>;
|
|
28
|
+
export declare const sendLlmRequest: (callback: (token: string) => void, signal: AbortSignal, fetchUrl: string, params: Record<string, unknown>, userQuery?: string, chatHistory?: BaseMessage[], userId?: string, streamingUnit?: StreamingUnit) => Promise<any>;
|
|
@@ -13,6 +13,61 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Determines whether to send data to the callback as soon as it's received (Chunk) or to accumulate it
|
|
18
|
+
* until a newline is received (Line).
|
|
19
|
+
*/
|
|
20
|
+
export var StreamingUnit;
|
|
21
|
+
(function (StreamingUnit) {
|
|
22
|
+
StreamingUnit[StreamingUnit["Chunk"] = 0] = "Chunk";
|
|
23
|
+
StreamingUnit[StreamingUnit["Line"] = 1] = "Line";
|
|
24
|
+
})(StreamingUnit || (StreamingUnit = {}));
|
|
25
|
+
const handleStreamingCallback = async (res, callback, streamingUnit) => {
|
|
26
|
+
const reader = res.body.getReader();
|
|
27
|
+
const utf8decoder = new TextDecoder("utf8");
|
|
28
|
+
let buffer = "";
|
|
29
|
+
while (true) {
|
|
30
|
+
const { done, value } = await reader.read();
|
|
31
|
+
// If the caller wants to process chunk by chunk, send it to the callback immediately.
|
|
32
|
+
if (streamingUnit === StreamingUnit.Chunk) {
|
|
33
|
+
if (done) {
|
|
34
|
+
break; // End of stream
|
|
35
|
+
}
|
|
36
|
+
// Decode chunk from server and send to callback
|
|
37
|
+
const chunk = utf8decoder.decode(value);
|
|
38
|
+
callback(chunk);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Otherwise, accumulate in buffer until we have a full line (delimited by newline character)
|
|
42
|
+
// to send to the callback.
|
|
43
|
+
if (done) {
|
|
44
|
+
// Handle any remaining data in buffer (last line without newline)
|
|
45
|
+
if (buffer.trim().length > 0) {
|
|
46
|
+
callback(buffer);
|
|
47
|
+
}
|
|
48
|
+
break; // End of stream
|
|
49
|
+
}
|
|
50
|
+
// Decode chunk from server. Note: pass stream: true to handle multibyte characters that may be split
|
|
51
|
+
// across chunks
|
|
52
|
+
const chunk = utf8decoder.decode(value, { stream: true });
|
|
53
|
+
// Append chunk to buffer
|
|
54
|
+
buffer += chunk;
|
|
55
|
+
// Process all complete lines in the buffer
|
|
56
|
+
let newlineIndex;
|
|
57
|
+
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
58
|
+
// Extract the complete line (without the newline)
|
|
59
|
+
const line = buffer.substring(0, newlineIndex).trim();
|
|
60
|
+
// Keep the rest for next iteration
|
|
61
|
+
buffer = buffer.substring(newlineIndex + 1);
|
|
62
|
+
// Skip empty lines
|
|
63
|
+
if (line.length > 0) {
|
|
64
|
+
// Send the current line
|
|
65
|
+
callback(line);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
16
71
|
/**
|
|
17
72
|
* Send a request to an LLM and stream the response to a callback.
|
|
18
73
|
* @param callback The callback function to be called when a chunk of data is received from the server.
|
|
@@ -22,10 +77,12 @@ limitations under the License.
|
|
|
22
77
|
* @param userQuery The user query to send to the server (sometimes part of chat history instead).
|
|
23
78
|
* @param chatHistory The chat history to be sent to the server. Contains user requests and server responses.
|
|
24
79
|
* @param userId Current user ID in the session.
|
|
80
|
+
* @param streamingUnit Determines whether to send data to the callback as soon as it's received (Chunk)
|
|
81
|
+
* or to accumulate it until a newline is received (Line). Default is Chunk.
|
|
25
82
|
* @returns Either the JSON result of the call, or, if a callback is provided, nothing, but tokens are streamed
|
|
26
83
|
* to the callback as they are received from the server.
|
|
27
84
|
*/
|
|
28
|
-
export const sendLlmRequest = async (callback, signal, fetchUrl, params, userQuery, chatHistory, userId) => {
|
|
85
|
+
export const sendLlmRequest = async (callback, signal, fetchUrl, params, userQuery, chatHistory, userId, streamingUnit = StreamingUnit.Chunk) => {
|
|
29
86
|
const res = await fetch(fetchUrl, {
|
|
30
87
|
method: "POST",
|
|
31
88
|
headers: {
|
|
@@ -45,18 +102,8 @@ export const sendLlmRequest = async (callback, signal, fetchUrl, params, userQue
|
|
|
45
102
|
throw new Error(`Failed to fetch: ${res.statusText} error code ${res.status}`);
|
|
46
103
|
}
|
|
47
104
|
if (callback) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
while (true) {
|
|
51
|
-
const { done, value } = await reader.read();
|
|
52
|
-
if (done) {
|
|
53
|
-
break; // End of stream
|
|
54
|
-
}
|
|
55
|
-
// Decode chunk from server
|
|
56
|
-
const chunk = utf8decoder.decode(value);
|
|
57
|
-
// Send current chunk to callback
|
|
58
|
-
callback(chunk);
|
|
59
|
-
}
|
|
105
|
+
// If a callback was provided, we assume the response is a stream and handle it accordingly
|
|
106
|
+
await handleStreamingCallback(res, callback, streamingUnit);
|
|
60
107
|
return null;
|
|
61
108
|
}
|
|
62
109
|
else {
|
|
@@ -193,8 +193,10 @@ export interface components {
|
|
|
193
193
|
readonly description?: string;
|
|
194
194
|
/** @description Optional map of parameters passed in via the natural-language chat text channel that the agent needs in order to work. This is really a pydantic/OpenAI function description, which is a bit too complex to specify directly in protobuf. */
|
|
195
195
|
readonly parameters?: Record<string, unknown>;
|
|
196
|
-
/** @description Optional map of parameters passed in via the sly_data dictionary private data channel that the agent needs in order to work. Just like the parameters above, this is really a pydantic/OpenAI function description, which is a bit too complex to specify directly in protobuf. */
|
|
196
|
+
/** @description This is a description of what data is expected to come *in* via the sly_data channel. Optional map of parameters passed in via the sly_data dictionary private data channel that the agent needs in order to work. Just like the parameters above, this is really a pydantic/OpenAI function description, which is a bit too complex to specify directly in protobuf. */
|
|
197
197
|
readonly sly_data_schema?: Record<string, unknown>;
|
|
198
|
+
/** @description This is a description of what data is expected to come *out* via the sly_data channel. Optional map of parameters returned via the sly_data dictionary private data channel that the agent will return after its work. Just like the parameters above, this is really a pydantic/OpenAI function description, which is a bit too complex to specify directly in protobuf. */
|
|
199
|
+
readonly sly_data_output_schema?: Record<string, unknown>;
|
|
198
200
|
};
|
|
199
201
|
/** @description Response structure for Function gRPC method */
|
|
200
202
|
readonly FunctionResponse: {
|
|
@@ -7,6 +7,7 @@ type AgentReservation = {
|
|
|
7
7
|
export type TemporaryNetwork = {
|
|
8
8
|
readonly reservation: AgentReservation;
|
|
9
9
|
readonly agentInfo: AgentInfo;
|
|
10
|
+
readonly networkHocon?: string | null;
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
13
|
* Zustand state store for temporary networks, such as vibe coded networks created by the user.
|