@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.
@@ -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, { id: "customer-branding", sx: {
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: [customer ? getCognizantLogoImage() : null, _jsx(Typography, { id: "nav-bar-brand", sx: {
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: customer
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: customer ? "not-allowed" : "pointer",
151
+ cursor: hasCustomer ? "not-allowed" : "pointer",
151
152
  color: darkMode ? "var(--bs-yellow)" : "var(--bs-gray-dark)",
152
153
  }, onClick: () => {
153
- !customer && setMode(darkMode ? "light" : "dark");
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 ? "none" : "block";
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: { display: handleVisibility } }), _jsx(Handle, { id: `${agentId}-right-handle`, position: Position.Right, type: "source", style: { display: handleVisibility } }), _jsx(Handle, { id: `${agentId}-top-handle`, position: Position.Top, type: "source", style: { display: handleVisibility } }), _jsx(Handle, { id: `${agentId}-bottom-handle`, position: Position.Bottom, type: "source", style: { display: handleVisibility } })] }), _jsx(Tooltip, { id: `${agentId}-tooltip`, title: agentName, placement: "top", disableInteractive: true, children: _jsx(Typography, { id: `${agentId}-name`, sx: {
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
- const newTemporaryNetworks = convertReservationsToNetworks(reservationsResult);
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
- const getAlert = () => (_jsx(Collapse, { in: alertContents?.length > 0, timeout: 1000, children: _jsx(MUIAlert, { id: "temporary-network-created-alert", closeable: true, severity: "success", sx: { marginTop: "1rem", marginBottom: 0 }, children: alertContents }) }));
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] }), isChild && isTemporaryNetwork && (_jsx(Tooltip, { title: "Delete network", children: _jsx(Delete, { onClick: (e) => {
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
- * In practice this "input" will actually be the output from one of the previous agents such as the data generator
48
- * or scoping agent.
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
- * In practice this "input" will actually be the output from one of the previous agents such as the data generator
143
- * or scoping agent.
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
- const reader = res.body.getReader();
49
- const utf8decoder = new TextDecoder("utf8");
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.