@cognizant-ai-lab/ui-common 1.8.0 → 1.9.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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -3
  3. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +89 -23
  4. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +7 -0
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +1 -1
  6. package/dist/components/AgentChat/ChatCommon/ControlButtons.d.ts +4 -2
  7. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +8 -2
  8. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +5 -5
  9. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +1 -0
  10. package/dist/components/AgentChat/Common/Utils.d.ts +4 -0
  11. package/dist/components/AgentChat/Common/Utils.js +5 -1
  12. package/dist/components/Common/Breadcrumbs.js +1 -1
  13. package/dist/components/Common/notification.d.ts +4 -4
  14. package/dist/components/MultiAgentAccelerator/{AgentFlow.d.ts → AgentFlow/AgentFlow.d.ts} +15 -5
  15. package/dist/components/MultiAgentAccelerator/{AgentFlow.js → AgentFlow/AgentFlow.js} +44 -35
  16. package/dist/components/MultiAgentAccelerator/{AgentNode.d.ts → AgentFlow/AgentNode.d.ts} +1 -1
  17. package/dist/components/MultiAgentAccelerator/{AgentNode.js → AgentFlow/AgentNode.js} +4 -4
  18. package/dist/components/MultiAgentAccelerator/{GraphLayouts.d.ts → AgentFlow/GraphLayouts.d.ts} +4 -4
  19. package/dist/components/MultiAgentAccelerator/{GraphLayouts.js → AgentFlow/GraphLayouts.js} +6 -35
  20. package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.d.ts +21 -0
  21. package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.js +27 -0
  22. package/dist/components/MultiAgentAccelerator/AgentFlow/PlasmaEdge.d.ts +7 -0
  23. package/dist/components/MultiAgentAccelerator/{PlasmaEdge.js → AgentFlow/PlasmaEdge.js} +13 -8
  24. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +116 -78
  25. package/dist/components/MultiAgentAccelerator/Schema/SlyData.d.ts +17 -0
  26. package/dist/components/MultiAgentAccelerator/Schema/SlyData.js +23 -0
  27. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +9 -4
  28. package/dist/components/MultiAgentAccelerator/Sidebar/ImportNetworkModal.d.ts +67 -0
  29. package/dist/components/MultiAgentAccelerator/Sidebar/ImportNetworkModal.js +585 -0
  30. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +2 -0
  31. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +19 -26
  32. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +2 -1
  33. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +24 -4
  34. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +22 -0
  35. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +74 -3
  36. package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.d.ts → ThoughtBubbles/ThoughtBubbleEdge.d.ts} +1 -1
  37. package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.js → ThoughtBubbles/ThoughtBubbleOverlay.js} +1 -1
  38. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +5 -0
  39. package/dist/components/MultiAgentAccelerator/const.d.ts +1 -0
  40. package/dist/components/MultiAgentAccelerator/const.js +2 -0
  41. package/dist/components/Settings/SettingsDialog.js +26 -10
  42. package/dist/controller/agent/Agent.d.ts +14 -6
  43. package/dist/controller/agent/Agent.js +18 -14
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/state/Settings.d.ts +81 -4
  47. package/dist/state/Settings.js +87 -7
  48. package/dist/tsconfig.build.tsbuildinfo +1 -1
  49. package/dist/utils/BrowserNavigation.js +2 -0
  50. package/dist/utils/File.d.ts +4 -1
  51. package/dist/utils/File.js +4 -9
  52. package/dist/utils/text.js +3 -7
  53. package/package.json +9 -10
  54. package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +0 -3
  55. /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.js → ThoughtBubbles/ThoughtBubbleEdge.js} +0 -0
  56. /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.d.ts → ThoughtBubbles/ThoughtBubbleOverlay.d.ts} +0 -0
@@ -20,9 +20,10 @@ import dagre from "@dagrejs/dagre";
20
20
  import { MarkerType } from "@xyflow/react";
21
21
  import cloneDeep from "lodash-es/cloneDeep.js";
22
22
  import { FRONTMAN_SIZE_MULTIPLIER, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
23
- import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "./const.js";
24
- import { isEditableAgent } from "./TemporaryNetworks.js";
25
- import { cleanUpAgentName, KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../AgentChat/Common/Utils.js";
23
+ import { cleanUpAgentName, KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../../AgentChat/Common/Utils.js";
24
+ import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "../const.js";
25
+ import { getFrontman, getParentAgents, getParents } from "./GraphStructure.js";
26
+ import { isEditableAgent } from "../TemporaryNetworks.js";
26
27
  export const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
27
28
  const FRONTMAN_OFFSET_X = (NODE_WIDTH * (FRONTMAN_SIZE_MULTIPLIER - 1)) / 2;
28
29
  const FRONTMAN_OFFSET_Y = (NODE_HEIGHT * (FRONTMAN_SIZE_MULTIPLIER - 1)) / 2;
@@ -60,34 +61,6 @@ const areInSameConversation = (conversations, sourceAgent, targetAgent) => {
60
61
  // Name for custom node
61
62
  const AGENT_NODE_TYPE_NAME = "agentNode";
62
63
  // #endregion: Constants
63
- /**
64
- * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
65
- * higher are not included.
66
- *
67
- * @param node Node ID for which to find parents
68
- * @param parentAgents Full list of parent agents in the network
69
- * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
70
- */
71
- const getParents = (node, parentAgents) => {
72
- return parentAgents.filter((agent) => agent.tools.includes(node)).map((parentNode) => parentNode.origin);
73
- };
74
- // "Parent agents" are those that have tools, aka "child agents"
75
- const getParentAgents = (agentsInNetwork) => agentsInNetwork.length === 1 ? agentsInNetwork : agentsInNetwork.filter((agent) => agent.tools?.length > 0);
76
- // "Child agents" are those that are declared as tools by other agents
77
- const getChildAgents = (parentAgents) => {
78
- const childAgentsSet = new Set();
79
- parentAgents.forEach((agent) => {
80
- agent?.tools?.forEach((tool) => {
81
- childAgentsSet.add(tool);
82
- });
83
- });
84
- return childAgentsSet;
85
- };
86
- // Frontman is defined as the agent that has no parent. There should be only one in the graph.
87
- // Frontman is one of the parent agents.
88
- // There is no explicit field that tells us which agent is the frontman, so we determine it by checking
89
- // which parent agent does not appear in the set of child agents.
90
- const getFrontman = (parentAgents, childAgents) => parentAgents.find((agent) => !childAgents.has(agent.origin));
91
64
  // Generates the properties for an edge in the graph.
92
65
  // Common for both radial and linear layouts.
93
66
  const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAnimated) => {
@@ -109,8 +82,7 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
109
82
  const nodeDepths = new Map();
110
83
  const queue = [];
111
84
  const parentAgents = getParentAgents(agentsInNetwork);
112
- const childAgents = getChildAgents(parentAgents);
113
- const frontman = getFrontman(parentAgents, childAgents);
85
+ const frontman = getFrontman(agentsInNetwork);
114
86
  if (frontman) {
115
87
  // Add the frontman node to the network
116
88
  queue.push({ id: frontman.origin, depth: 0 });
@@ -220,8 +192,7 @@ export const layoutLinear = ({ agentCounts, agentIconSuggestions = null, agentsI
220
192
  const edgesInNetwork = [];
221
193
  // Do these calculations outside the loop for efficiency
222
194
  const parentAgents = getParentAgents(agentsInNetwork);
223
- const childAgents = getChildAgents(parentAgents);
224
- const frontman = getFrontman(parentAgents, childAgents);
195
+ const frontman = getFrontman(agentsInNetwork);
225
196
  agentsInNetwork.forEach(({ origin: nodeId }) => {
226
197
  const parentIds = getParents(nodeId, parentAgents);
227
198
  const isFrontman = frontman?.origin === nodeId;
@@ -0,0 +1,21 @@
1
+ import { ConnectivityInfo } from "../../../generated/neuro-san/NeuroSanClient.js";
2
+ export declare const getParentAgents: <T extends ConnectivityInfo>(agents: readonly T[]) => readonly T[];
3
+ /**
4
+ * Determines the frontman of an agent network — the single source of truth for "who is the frontman".
5
+ *
6
+ * The frontman is the agent with no parent: a "parent" agent (one that has tools) that is not itself
7
+ * listed as a tool by any other agent. A lone single-agent network is its own frontman. Returns
8
+ * undefined when no agent qualifies (e.g. a fully cyclic network); callers decide how to fall back.
9
+ *
10
+ * Consumed by the graph layouts (to seed the depth-0 node) and by the network import flow.
11
+ */
12
+ export declare const getFrontman: <T extends ConnectivityInfo>(agents: readonly T[]) => T | undefined;
13
+ /**
14
+ * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
15
+ * higher are not included.
16
+ *
17
+ * @param node Node ID for which to find parents
18
+ * @param parentAgents Full list of parent agents in the network
19
+ * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
20
+ */
21
+ export declare const getParents: (node: string, parentAgents: readonly ConnectivityInfo[]) => string[];
@@ -0,0 +1,27 @@
1
+ // "Parent agents" are those that have tools, aka agents with "child agents". A lone single-agent
2
+ // network is treated as its own parent.
3
+ export const getParentAgents = (agents) => agents.length === 1 ? agents : agents.filter((agent) => (agent.tools?.length ?? 0) > 0);
4
+ /**
5
+ * Determines the frontman of an agent network — the single source of truth for "who is the frontman".
6
+ *
7
+ * The frontman is the agent with no parent: a "parent" agent (one that has tools) that is not itself
8
+ * listed as a tool by any other agent. A lone single-agent network is its own frontman. Returns
9
+ * undefined when no agent qualifies (e.g. a fully cyclic network); callers decide how to fall back.
10
+ *
11
+ * Consumed by the graph layouts (to seed the depth-0 node) and by the network import flow.
12
+ */
13
+ export const getFrontman = (agents) => {
14
+ const parentAgents = getParentAgents(agents);
15
+ // Child agents are everything referenced as a tool by a parent.
16
+ const childAgents = new Set(parentAgents.flatMap((agent) => agent.tools ?? []));
17
+ return parentAgents.find((agent) => agent.origin !== undefined && !childAgents.has(agent.origin));
18
+ };
19
+ /**
20
+ * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
21
+ * higher are not included.
22
+ *
23
+ * @param node Node ID for which to find parents
24
+ * @param parentAgents Full list of parent agents in the network
25
+ * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
26
+ */
27
+ export const getParents = (node, parentAgents) => parentAgents.filter((agent) => agent.tools.includes(node)).map((parentNode) => parentNode.origin);
@@ -0,0 +1,7 @@
1
+ import { EdgeProps } from "@xyflow/react";
2
+ import { FC } from "react";
3
+ export type PlasmaEdgeProps = {
4
+ readonly maxParticles?: number;
5
+ readonly particlesPerFrame?: number;
6
+ };
7
+ export declare const PlasmaEdge: FC<EdgeProps & PlasmaEdgeProps>;
@@ -16,7 +16,11 @@ limitations under the License.
16
16
  */
17
17
  import { getBezierPath } from "@xyflow/react";
18
18
  import { useEffect, useRef } from "react";
19
- import { useSettingsStore } from "../../state/Settings.js";
19
+ import { useSettingsStore } from "../../../state/Settings.js";
20
+ //#region Constants
21
+ const DEFAULT_MAX_PARTICLES = 250;
22
+ const DEFAULT_PARTICLES_PER_FRAME = 25;
23
+ //#endregion Interfaces and Types
20
24
  const createFunnelParticleOnPath = (pathEl, canvasOffset, baseProgress, plasmaColor) => {
21
25
  const totalLength = pathEl.getTotalLength();
22
26
  const speed = 0.02 + Math.random() * 0.003;
@@ -64,7 +68,7 @@ const createFunnelParticleOnPath = (pathEl, canvasOffset, baseProgress, plasmaCo
64
68
  const isAlive = () => progress * totalLength < totalLength * 0.98;
65
69
  return { update, draw, isAlive };
66
70
  };
67
- export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, }) => {
71
+ export const PlasmaEdge = ({ maxParticles = DEFAULT_MAX_PARTICLES, particlesPerFrame = DEFAULT_PARTICLES_PER_FRAME, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY, }) => {
68
72
  const canvasRef = useRef(null);
69
73
  const pathRef = useRef(null);
70
74
  const animationRef = useRef(null);
@@ -97,12 +101,10 @@ export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition,
97
101
  ctx.setTransform(1, 0, 0, 1, 0, 0);
98
102
  ctx.scale(dpr, dpr);
99
103
  const canvasOffset = { x, y };
100
- const MAX_PARTICLES = 250;
101
- const PARTICLES_PER_FRAME = 25;
102
104
  const animate = () => {
103
105
  ctx.clearRect(0, 0, width, height);
104
- for (let i = 0; i < PARTICLES_PER_FRAME; i += 1) {
105
- if (particles.current.length < MAX_PARTICLES) {
106
+ for (let i = 0; i < particlesPerFrame; i += 1) {
107
+ if (particles.current.length < maxParticles) {
106
108
  const t = Math.random();
107
109
  if (Math.random() < 1 - t) {
108
110
  particles.current.push(createFunnelParticleOnPath(pathEl, canvasOffset, t, plasmaColor));
@@ -118,9 +120,12 @@ export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition,
118
120
  };
119
121
  animate();
120
122
  return () => {
121
- if (animationRef.current !== undefined)
123
+ if (animationRef.current !== undefined) {
122
124
  cancelAnimationFrame(animationRef.current);
125
+ animationRef.current = undefined;
126
+ }
127
+ particles.current = [];
123
128
  };
124
- }, [edgePath, width, height, plasmaColor, x, y]);
129
+ }, [edgePath, width, height, plasmaColor, x, y, particlesPerFrame, maxParticles]);
125
130
  return (_jsxs(_Fragment, { children: [_jsx("foreignObject", { id: `foreign-object-${x}-${y}`, width: width, height: height, x: x, y: y, style: { pointerEvents: "none", overflow: "visible" }, children: _jsx("canvas", { id: `canvas-${x}-${y}`, ref: canvasRef }) }), _jsx("path", { id: `path-${edgePath}`, ref: pathRef, d: edgePath, fill: "none", stroke: "none", style: { position: "absolute", visibility: "hidden" } })] }));
126
131
  };
@@ -15,8 +15,11 @@ See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  */
17
17
  import StopCircle from "@mui/icons-material/StopCircle";
18
+ import Backdrop from "@mui/material/Backdrop";
18
19
  import Box from "@mui/material/Box";
20
+ import CircularProgress from "@mui/material/CircularProgress";
19
21
  import Grid from "@mui/material/Grid";
22
+ import Paper from "@mui/material/Paper";
20
23
  import Slide from "@mui/material/Slide";
21
24
  import { useTheme } from "@mui/material/styles";
22
25
  import Tooltip from "@mui/material/Tooltip";
@@ -26,15 +29,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
29
  import { useJoyride } from "react-joyride";
27
30
  import { extractConversations } from "./AgentConversations.js";
28
31
  import { getUpdatedAgentCounts } from "./AgentCounts.js";
29
- import { AgentFlow } from "./AgentFlow.js";
32
+ import { AgentFlow } from "./AgentFlow/AgentFlow.js";
30
33
  import { extractAgentNetworkDesignerProgress } from "./AgentNetworkDesigner.js";
31
- import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_HOCON, AGENT_NETWORK_NAME_KEY, GRACE_PERIOD_MS, SHOW_TOUR_DELAY_MS, TRIGGER_APP_TOUR_EVENT_NAME, } from "./const.js";
34
+ import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_HOCON, AGENT_NETWORK_NAME_KEY, EXPIRED_NETWORKS_CHECK_INTERVAL_MS, GRACE_PERIOD_MS, SHOW_TOUR_DELAY_MS, TRIGGER_APP_TOUR_EVENT_NAME, } from "./const.js";
35
+ import { ImportNetworkModal } from "./Sidebar/ImportNetworkModal.js";
32
36
  import { Sidebar } from "./Sidebar/Sidebar.js";
33
- import { extractTemporaryNetworksFromMessage, isTemporaryNetwork, mergeNetworks } from "./TemporaryNetworks.js";
34
- import { getAgentFunction, getAgentNetworks, getConnectivity, sendNetworkDesignerUpdate, } from "../../controller/agent/Agent.js";
37
+ import { extractTemporaryNetworksFromMessage, IMPORT_FAILURE_DETAIL, importNetworkFromJson, isTemporaryNetwork, notifySaveError, streamNetworkDesignerUpsert, } from "./TemporaryNetworks.js";
38
+ import { MAIN_TOUR_STEPS } from "./Tour/MainTourSteps.js";
39
+ import { getAgentFunction, getAgentNetworks, getConnectivity } from "../../controller/agent/Agent.js";
35
40
  import { getAgentIconSuggestions, getNetworkIconSuggestions } from "../../controller/agent/IconSuggestions.js";
36
41
  import { useAgentChatHistoryStore } from "../../state/ChatHistory.js";
37
- import { useSettingsStore } from "../../state/Settings.js";
38
42
  import { useTempNetworksStore } from "../../state/TemporaryNetworks.js";
39
43
  import { TourPromptState, useTourStore } from "../../state/Tour.js";
40
44
  import { getZIndex } from "../../utils/zIndexLayers.js";
@@ -43,47 +47,15 @@ import { LlmChatButton } from "../AgentChat/Common/LlmChatButton.js";
43
47
  import { isLegacyAgentType } from "../AgentChat/Common/Types.js";
44
48
  import { chatMessageFromChunk, cleanUpAgentName, removeTrailingUuid } from "../AgentChat/Common/Utils.js";
45
49
  import { ConfirmationModal, StyledButton } from "../Common/ConfirmationModal.js";
46
- import { closeNotification, NotificationType, sendNotification } from "../Common/notification.js";
47
- import { MAIN_TOUR_STEPS } from "./Tour/MainTourSteps.js";
50
+ import { MUIAlert } from "../Common/MUIAlert.js";
48
51
  import { MUIDialog } from "../Common/MUIDialog.js";
49
- // Check for expired networks every this many milliseconds
50
- const EXPIRED_NETWORKS_CHECK_INTERVAL_MS = 10 * 1000;
52
+ import { closeNotification, NotificationType, sendNotification } from "../Common/notification.js";
53
+ import { BYOK } from "./Schema/SlyData.js";
54
+ import { getApiKey, LLM_PROVIDER_API_KEY_FIELD, useSettingsStore } from "../../state/Settings.js";
51
55
  // Animation time for the left and right panels to slide in or out when launching the animation
52
56
  const GROW_ANIMATION_TIME_MS = 800;
53
57
  // Optimization to avoid creating a new empty map on every render
54
58
  const EMPTY_THOUGHT_BUBBLE_EDGES = new Map();
55
- // #region: Agent-save helpers
56
- /**
57
- * Extracts TemporaryNetworks from a single streamed chunk, merging into `accumulated`.
58
- * Returns `accumulated` unchanged if the chunk yields no reservations or on parse error.
59
- */
60
- const collectNetworksFromChunk = (chunk, updated, accumulated) => {
61
- try {
62
- const chatMessage = chatMessageFromChunk(chunk);
63
- if (!chatMessage)
64
- return accumulated;
65
- // Always use the user's edited definition as the authoritative value.
66
- const converted = extractTemporaryNetworksFromMessage(chatMessage, updated);
67
- if (converted.length === 0)
68
- return accumulated;
69
- return mergeNetworks(accumulated, converted);
70
- }
71
- catch (e) {
72
- console.warn("Failed to process chunk from network designer:", e);
73
- return accumulated;
74
- }
75
- };
76
- /** Logs and notifies about a save error. Suppresses AbortError (user-canceled). */
77
- const notifySaveError = (agentName, e) => {
78
- if (e instanceof DOMException && e.name === "AbortError")
79
- return;
80
- console.error("Failed to submit agent network update:", e);
81
- const detail = e instanceof DOMException && e.name === "TimeoutError"
82
- ? "The request timed out waiting for the server. Please try again."
83
- : String(e);
84
- sendNotification(NotificationType.error, `Failed to update network "${agentName}".`, detail);
85
- };
86
- // #endregion: Agent-save helpers
87
59
  /**
88
60
  * Main Multi-Agent Accelerator component that contains the sidebar, agent flow, and chat components.
89
61
  * @param backendNeuroSanApiUrl Initial URL of the backend Neuro-San API. User can change this in the UI.
@@ -107,6 +79,8 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
107
79
  const temporaryNetworks = useTempNetworksStore((state) => state.tempNetworks);
108
80
  // Track newly added temp networks so we can highlight them
109
81
  const [newlyAddedTemporaryNetworks, setNewlyAddedTemporaryNetworks] = useState(new Set());
82
+ // True while a file import is in-flight (after modal confirm, before the new network appears)
83
+ const [isImporting, setIsImporting] = useState(false);
110
84
  const [networkIconSuggestions, setNetworkIconSuggestions] = useState({});
111
85
  const [agentsInNetwork, setAgentsInNetwork] = useState([]);
112
86
  const [sampleQueries, setSampleQueries] = useState([]);
@@ -117,7 +91,8 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
117
91
  const [selectedNetwork, setSelectedNetwork] = useState(null);
118
92
  const [networkDescription, setNetworkDescription] = useState("");
119
93
  const networkDisplayName = useMemo(() => (useNativeNames ? selectedNetwork : cleanUpAgentName(removeTrailingUuid(selectedNetwork))), [selectedNetwork, useNativeNames]);
120
- const [providerKeysRequired, setProviderKeysRequired] = useState(new Set());
94
+ // LLM providers for which BYOK keys are supported by the current network
95
+ const [supportedByokProviders, setSupportedByokProviders] = useState(new Set());
121
96
  const neuroSanURL = useSettingsStore((state) => state.settings.externalServices.neuroSanUrl) ?? defaultNeuroSanUrl;
122
97
  // Tracks how many times each agent has been involved in the conversation
123
98
  const [agentCounts, setAgentCounts] = useState(new Map());
@@ -132,6 +107,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
132
107
  // State to hold thought bubble edges - avoids duplicates across layout recalculations
133
108
  const [thoughtBubbleEdges, setThoughtBubbleEdges] = useState(new Map());
134
109
  const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
110
+ const [importModalOpen, setImportModalOpen] = useState(false);
135
111
  const [tourModalOpen, setTourModalOpen] = useState(false);
136
112
  const [haveShownTourModal, setHaveShownTourModal] = useState(false);
137
113
  // Memoized key for agent names to trigger icon suggestion updates when the set of agents changes, not just
@@ -201,16 +177,14 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
201
177
  * Builds the API keys object to be sent as extraSlyData with each request, based on the LLM providers required
202
178
  * by the currently selected network. Only includes keys for providers that are required.
203
179
  */
204
- const getApiKeys = () => {
205
- const llmConfig = {};
206
- if (providerKeysRequired.has("OpenAI")) {
207
- llmConfig["openai_api_key"] = apiKeys["OpenAI"];
208
- }
209
- if (providerKeysRequired.has("Anthropic")) {
210
- llmConfig["anthropic_api_key"] = apiKeys["Anthropic"];
211
- }
212
- return { llm_config: llmConfig };
213
- };
180
+ const getApiKeys = () => ({
181
+ llm_config: Object.fromEntries([...supportedByokProviders]
182
+ .map((provider) => [
183
+ LLM_PROVIDER_API_KEY_FIELD[provider],
184
+ getApiKey(apiKeys, provider),
185
+ ])
186
+ .filter((entry) => Boolean(entry[1]))),
187
+ });
214
188
  /**
215
189
  * Builds the extraSlyData object to be sent with each request, including information for Agent Network Designer
216
190
  * and (if required) API keys for LLM providers.
@@ -237,7 +211,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
237
211
  return undefined;
238
212
  };
239
213
  // Whether any API keys are required
240
- const anyApiKeysRequired = providerKeysRequired.size > 0;
214
+ const anyApiKeysRequired = supportedByokProviders.size > 0;
241
215
  // Build base extraSlyData
242
216
  const baseExtraSlyData = buildExtraSlyData();
243
217
  // Add API keys to extraSlyData if needed, merging with baseExtraSlyData if it exists
@@ -252,7 +226,33 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
252
226
  chatRef.current?.handleStop();
253
227
  resetState();
254
228
  }, [resetState]);
255
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
229
+ /**
230
+ * Maps an API key name to the corresponding LLM provider
231
+ */
232
+ const apiKeyToProvider = useCallback((key) => {
233
+ switch (key) {
234
+ case "openai_api_key":
235
+ return "OpenAI";
236
+ case "anthropic_api_key":
237
+ return "Anthropic";
238
+ default:
239
+ return null;
240
+ }
241
+ }, []);
242
+ /**
243
+ * Given a SlyData schema, returns the set of LLM providers that are supported by the agent network.
244
+ * @note Semantics are "oneOf": a key must be supplied for at least one of the named providers
245
+ */
246
+ const getSupportedLlmProviders = useCallback((schema) => {
247
+ const parsed = BYOK.safeParse(schema);
248
+ if (!parsed.success || !parsed.data.required?.includes("llm_config")) {
249
+ return new Set();
250
+ }
251
+ const providerKeys = Object.keys(parsed.data.properties?.llm_config?.properties ?? {});
252
+ return new Set(providerKeys
253
+ .map((key) => apiKeyToProvider(key))
254
+ .filter((provider) => provider !== null));
255
+ }, [apiKeyToProvider]);
256
256
  useEffect(() => {
257
257
  ;
258
258
  (async () => {
@@ -273,23 +273,14 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
273
273
  const fetchAgentDetails = async () => {
274
274
  // It is a Neuro-san agent, so get the function and connectivity info
275
275
  try {
276
+ // Reset before fetching
277
+ setNetworkDescription("");
278
+ setSupportedByokProviders(new Set());
279
+ // Get agent function from server
276
280
  const agentFunction = await getAgentFunction(neuroSanURL, selectedNetwork, username);
277
281
  setNetworkDescription(agentFunction?.function?.description || "");
278
282
  const schema = agentFunction?.function?.sly_data_schema;
279
- const schemaProperties = isRecord(schema) ? schema["properties"] : undefined;
280
- const llmConfig = isRecord(schemaProperties) ? schemaProperties["llm_config"] : undefined;
281
- const llmConfigRequired = isRecord(llmConfig) ? llmConfig["required"] : undefined;
282
- setProviderKeysRequired(new Set((Array.isArray(llmConfigRequired) ? llmConfigRequired : []).flatMap((key) => {
283
- if (key === "openai_api_key") {
284
- return ["OpenAI"];
285
- }
286
- if (key === "anthropic_api_key") {
287
- return ["Anthropic"];
288
- }
289
- console.warn(`Unknown API key requirement "${key}" for network ${selectedNetwork}. Skipping.`);
290
- // Will get dropped by flatMap
291
- return [];
292
- })));
283
+ setSupportedByokProviders(getSupportedLlmProviders(schema));
293
284
  }
294
285
  catch (e) {
295
286
  console.warn(`Unable to get agent details for network ${selectedNetwork}:`, e);
@@ -300,7 +291,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
300
291
  if (selectedNetwork && !isLegacyAgentType(selectedNetwork)) {
301
292
  void fetchAgentDetails();
302
293
  }
303
- }, [neuroSanURL, selectedNetwork, username]);
294
+ }, [getSupportedLlmProviders, neuroSanURL, selectedNetwork, username]);
304
295
  useEffect(() => {
305
296
  ;
306
297
  (async () => {
@@ -466,10 +457,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
466
457
  */
467
458
  const onSaveAgent = useCallback(async (agentName, updated, agentNetworkName, signal) => {
468
459
  try {
469
- let newNetworks = [];
470
- await sendNetworkDesignerUpdate(neuroSanURL, signal, agentName, updated, agentNetworkName, username, (chunk) => {
471
- newNetworks = collectNetworksFromChunk(chunk, updated, newNetworks);
472
- });
460
+ const newNetworks = await streamNetworkDesignerUpsert(neuroSanURL, signal, agentName, updated, agentNetworkName, username);
473
461
  if (newNetworks.length === 0) {
474
462
  sendNotification(NotificationType.error, `Failed to update network "${agentName}".`, "The network designer did not return a reservation. Please try again.");
475
463
  return;
@@ -492,6 +480,35 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
492
480
  notifySaveError(agentName, e);
493
481
  }
494
482
  }, [neuroSanURL, username, selectedNetwork]);
483
+ /**
484
+ * Handles an import from the ImportNetworkModal: converts the JSON content to a
485
+ * network definition, streams it through the network designer to obtain a reservation,
486
+ * then upserts the result into the temporary-networks store and navigates to the new network.
487
+ */
488
+ const handleImportNetwork = useCallback(async (agentNetworkName, content) => {
489
+ setIsImporting(true);
490
+ try {
491
+ const result = await importNetworkFromJson(content, agentNetworkName, neuroSanURL, username);
492
+ if ("failure" in result) {
493
+ sendNotification(NotificationType.error, `Failed to import "${agentNetworkName}".`, IMPORT_FAILURE_DETAIL[result.failure]);
494
+ return;
495
+ }
496
+ useTempNetworksStore.getState().upsertTempNetworks(result.networks);
497
+ // An import defines a single network, so the designer returns one reservation. Highlight and
498
+ // navigate to it; any further entries (none expected) are still upserted above.
499
+ const newNetworkName = result.networks[0]?.agentInfo.agent_name;
500
+ if (newNetworkName) {
501
+ setNewlyAddedTemporaryNetworks(new Set([newNetworkName]));
502
+ changeSelectedNetwork(newNetworkName);
503
+ }
504
+ }
505
+ catch (e) {
506
+ notifySaveError(agentNetworkName, e);
507
+ }
508
+ finally {
509
+ setIsImporting(false);
510
+ }
511
+ }, [changeSelectedNetwork, neuroSanURL, username]);
495
512
  const onStreamingStarted = useCallback(() => {
496
513
  // Reset agent counts
497
514
  setAgentCounts(new Map());
@@ -511,7 +528,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
511
528
  }, [resetState]);
512
529
  const handleDeleteNetwork = (networkId, isExpired) => {
513
530
  if (isExpired) {
514
- // It's expired so just delete it without confirmation
531
+ // It's expired, so just delete it without confirmation
515
532
  const tempNetworksWithoutThisOne = temporaryNetworks.filter((network) => network.agentInfo.agent_name !== networkId);
516
533
  useTempNetworksStore.getState().setTempNetworks(tempNetworksWithoutThisOne);
517
534
  useAgentChatHistoryStore.getState().resetHistory(networkId);
@@ -555,14 +572,17 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
555
572
  }
556
573
  }, [tourRequested, selectedNetwork, agentsInNetwork, networks, controls, setTourStatus]);
557
574
  const getMissingApiKeys = () => {
558
- return providerKeysRequired.size > 0 ? [...providerKeysRequired].filter((provider) => !apiKeys?.[provider]) : [];
575
+ // Calculate intersection of what we have (apiKeys) and what is required (providerKeysRequired)
576
+ const intersection = new Set([...supportedByokProviders].filter((provider) => getApiKey(apiKeys, provider)));
577
+ return intersection.size === 0 ? supportedByokProviders : new Set();
559
578
  };
579
+ const hasMissingApiKeys = getMissingApiKeys().size > 0;
560
580
  const getLeftPanel = () => {
561
581
  return (_jsx(Slide, { id: "multi-agent-accelerator-grid-sidebar-slide", in: !enableZenMode || !isAwaitingLlm, direction: "right", timeout: GROW_ANIMATION_TIME_MS, onExited: () => {
562
582
  setIsStreaming(true);
563
583
  }, children: _jsx(Grid, { id: "multi-agent-accelerator-grid-sidebar", size: enableZenMode && isStreaming ? 0 : 3.25, sx: {
564
584
  height: "100%",
565
- }, children: _jsx(Sidebar, { id: "multi-agent-accelerator-sidebar", isAwaitingLlm: isAwaitingLlm, networkIconSuggestions: networkIconSuggestions, networks: networks, neuroSanServerURL: neuroSanURL, newlyAddedTemporaryNetworks: newlyAddedTemporaryNetworks, onDeleteNetwork: handleDeleteNetwork, onEditNetwork: handleEditNetwork, setSelectedNetwork: (newNetwork) => changeSelectedNetwork(newNetwork), temporaryNetworks: temporaryNetworks }) }) }));
585
+ }, children: _jsx(Sidebar, { id: "multi-agent-accelerator-sidebar", isAwaitingLlm: isAwaitingLlm, isImporting: isImporting, networkIconSuggestions: networkIconSuggestions, networks: networks, neuroSanServerURL: neuroSanURL, newlyAddedTemporaryNetworks: newlyAddedTemporaryNetworks, onDeleteNetwork: handleDeleteNetwork, onEditNetwork: handleEditNetwork, onImportClick: () => setImportModalOpen(true), setSelectedNetwork: (newNetwork) => changeSelectedNetwork(newNetwork), temporaryNetworks: temporaryNetworks }) }) }));
566
586
  };
567
587
  const getCenterPanel = () => {
568
588
  return (_jsx(Grid, { id: "multi-agent-accelerator-grid-agent-flow", size: enableZenMode && isStreaming ? 18 : 8.25, sx: {
@@ -586,7 +606,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
586
606
  [AGENT_NETWORK_DESIGNER_ID]: "Describe in plain language the network you would like to build.",
587
607
  }, currentUser: username, customAgentGreetings: {
588
608
  [AGENT_NETWORK_DESIGNER_ID]: "Let's build a network together!",
589
- }, 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, setSelectedNetwork: changeSelectedNetwork }, selectedNetwork ?? "no-network") }) }));
609
+ }, extraSlyData: extraSlyData, id: "agent-network-ui", isAwaitingLlm: isAwaitingLlm, hasMissingApiKeys: hasMissingApiKeys, networkDescription: networkDescription, neuroSanURL: neuroSanURL, onChunkReceived: onChunkReceived, onStreamingComplete: onStreamingComplete, onStreamingStarted: onStreamingStarted, ref: chatRef, sampleQueries: sampleQueries, setIsAwaitingLlm: setIsAwaitingLlm, selectedNetwork: selectedNetwork, setSelectedNetwork: changeSelectedNetwork }, selectedNetwork ?? "no-network") }) }));
590
610
  };
591
611
  const getStopButton = () => {
592
612
  return (_jsx(_Fragment, { children: isAwaitingLlm && enableZenMode && (_jsx(Box, { id: "stop-button-container", sx: {
@@ -615,6 +635,17 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
615
635
  setNetworkToBeDeleted(null);
616
636
  setConfirmationModalOpen(false);
617
637
  }, title: "Delete Network" })) : null;
638
+ const getImportNetworkModal = () => (_jsx(ImportNetworkModal, { existingNetworkNames: temporaryNetworks.map((n) => n.agentNetworkName), isOpen: importModalOpen, onClose: () => setImportModalOpen(false), onImport: handleImportNetwork }));
639
+ // Blocking overlay shown while an imported network is being created on the backend.
640
+ const getImportBackdrop = () => (_jsx(Backdrop, { id: "multi-agent-accelerator-import-backdrop", "data-testid": "multi-agent-accelerator-import-backdrop", open: isImporting, sx: { zIndex: getZIndex(3, theme) }, children: _jsxs(Paper, { elevation: 6, role: "status", "aria-live": "polite", sx: {
641
+ display: "flex",
642
+ alignItems: "center",
643
+ gap: 2,
644
+ px: 4,
645
+ py: 2.5,
646
+ borderRadius: 2,
647
+ maxWidth: 480,
648
+ }, children: [_jsx(CircularProgress, { id: "multi-agent-accelerator-import-spinner", size: 24 }), _jsx(Typography, { id: "multi-agent-accelerator-import-title", variant: "body1", sx: { fontWeight: "bold" }, children: "Importing network..." })] }) }));
618
649
  const getTourModal = () => tourModalOpen && (_jsx(MUIDialog, { contentSx: { fontSize: "0.8rem", minWidth: "550px", paddingTop: "0" }, footer: _jsxs(_Fragment, { children: [_jsx(StyledButton, { id: "tour-dont-show-again", onClick: () => {
619
650
  setTourStatus(TourPromptState.DontShowAgain);
620
651
  dismissTourModal();
@@ -656,7 +687,14 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
656
687
  opacity: "95%",
657
688
  width: "100%",
658
689
  }, children: [_jsx(Typography, { variant: "h6", sx: { padding: "0.5rem 1rem", fontWeight: "bold", color: "white" }, children: "Network Preview" }), agentsInNetworkDesigner?.length > 0 ? (_jsx(AgentFlow, { id: "and-network-preview", agentsInNetwork: agentsInNetworkDesigner, isAgentNetworkDesignerMode: true, isAwaitingLlm: false, isStreaming: false, thoughtBubbleEdges: EMPTY_THOUGHT_BUBBLE_EDGES }, "and-network-preview")) : (_jsx(Typography, { variant: "body1", sx: { color: "white" }, children: "Awaiting status from Agent Network Designer..." }))] }) }) }));
659
- return (_jsxs(_Fragment, { children: [Tour, getTourModal(), getProgressPopper(), getDeleteNetworkConfirmationModal(), _jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
690
+ const getMissingApiKeysAlert = () => hasMissingApiKeys && (_jsx(MUIAlert, { closeable: false, id: "llm-chat-missing-api-keys-alert", severity: "warning", sx: {
691
+ my: 1.5,
692
+ "& .MuiAlert-message": {
693
+ width: "100%",
694
+ textAlign: "center",
695
+ },
696
+ }, children: _jsxs(Typography, { sx: { fontSize: "larger" }, children: ["API key(s) required for at least one of these providers:", " ", _jsx("strong", { children: [...getMissingApiKeys()].join(", ") }), ". Please add the required key(s) in \"Settings\" to use this Network."] }) }));
697
+ return (_jsxs(_Fragment, { children: [getMissingApiKeysAlert(), Tour, getTourModal(), getProgressPopper(), getDeleteNetworkConfirmationModal(), getImportNetworkModal(), getImportBackdrop(), _jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
660
698
  display: "flex",
661
699
  flex: 1,
662
700
  height: "85%",
@@ -0,0 +1,17 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Zod schema for sly_data BYOK structure
4
+ */
5
+ export declare const BYOK: z.ZodObject<{
6
+ type: z.ZodOptional<z.ZodString>;
7
+ required: z.ZodOptional<z.ZodTuple<[z.ZodLiteral<"llm_config">], null>>;
8
+ properties: z.ZodOptional<z.ZodObject<{
9
+ llm_config: z.ZodOptional<z.ZodObject<{
10
+ type: z.ZodOptional<z.ZodString>;
11
+ properties: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
12
+ type: z.ZodOptional<z.ZodString>;
13
+ description: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$loose>>>;
15
+ }, z.core.$loose>>;
16
+ }, z.core.$loose>>;
17
+ }, z.core.$loose>;
@@ -0,0 +1,23 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Zod schema for sly_data BYOK structure
4
+ */
5
+ export const BYOK = z.looseObject({
6
+ type: z.string().optional(),
7
+ required: z.tuple([z.literal("llm_config")]).optional(),
8
+ properties: z
9
+ .looseObject({
10
+ llm_config: z
11
+ .looseObject({
12
+ type: z.string().optional(),
13
+ properties: z
14
+ .record(z.string(), z.looseObject({
15
+ type: z.string().optional(),
16
+ description: z.string().optional(),
17
+ }))
18
+ .optional(),
19
+ })
20
+ .optional(),
21
+ })
22
+ .optional(),
23
+ });
@@ -16,6 +16,7 @@ import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
16
16
  import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
17
17
  import { useRef } from "react";
18
18
  import { downloadFile, toSafeFilename } from "../../../utils/File.js";
19
+ import { removeTrailingUuid } from "../../AgentChat/Common/Utils.js";
19
20
  // Palette of colors we can use for tags
20
21
  const TAG_COLORS = [
21
22
  "--bs-accent2-light",
@@ -63,7 +64,7 @@ export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, onDele
63
64
  const expirationTime = item?.temporaryNetworkExpirationTime;
64
65
  const isTemporaryNetwork = Boolean(expirationTime);
65
66
  const isExpired = isChild && isTemporaryNetwork && isTemporaryNetworkExpired(expirationTime);
66
- const networkHocon = item?.temporaryNetworkHocon ?? null;
67
+ const networkDefinition = item?.temporaryNetworkDefinition ?? null;
67
68
  const iconNameSuggestion = item.iconSuggestion;
68
69
  let muiIconElement;
69
70
  // If the item is a child (i.e., a network), we want to render an icon next to it.
@@ -105,10 +106,14 @@ export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, onDele
105
106
  .map((tag) => (_jsx(Chip, { label: tag, style: {
106
107
  margin: "0.25rem",
107
108
  backgroundColor: `var(${tagsToColors.get(tag) || TAG_COLORS[0]})`,
108
- } }, tag))), placement: "right", arrow: true, children: _jsx(BookmarkIcon, { sx: { fontSize: "0.75rem", color: "var(--bs-accent1-medium)" } }) })) : null] }), isChild && isTemporaryNetwork && (_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: "0.25rem", marginLeft: "auto" }, children: [networkHocon && (_jsx(Tooltip, { title: isExpired ? "Expired" : "Download network definition", children: _jsx("span", { children: _jsx(IconButton, { onClick: (e) => {
109
+ } }, tag))), placement: "right", arrow: true, children: _jsx(BookmarkIcon, { sx: { fontSize: "0.75rem", color: "var(--bs-accent1-medium)" } }) })) : null] }), isChild && isTemporaryNetwork && (_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: "0.25rem", marginLeft: "auto" }, children: [networkDefinition && (_jsx(Tooltip, { title: isExpired ? "Expired" : "Download network definition", children: _jsx("span", { children: _jsx(IconButton, { onClick: (e) => {
109
110
  e.stopPropagation();
110
- const fileName = `${toSafeFilename(labelString)}.hocon`;
111
- downloadFile(networkHocon, fileName);
111
+ // Strip the reservation UUID before building the filename so
112
+ // exported files carry a clean name (toSafeFilename would
113
+ // otherwise flatten the UUID's hyphens to underscores).
114
+ const cleanName = removeTrailingUuid(labelString);
115
+ const fileName = `${toSafeFilename(cleanName)}.json`;
116
+ downloadFile(JSON.stringify(networkDefinition, null, 2), fileName, "application/json");
112
117
  }, disabled: isExpired, "aria-label": "Download network definition", size: "small", sx: {
113
118
  padding: 0,
114
119
  color: "var(--bs-secondary)",