@cognizant-ai-lab/ui-common 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -3
- package/dist/components/AgentChat/ChatCommon/ChatCommon.js +94 -27
- package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +7 -0
- package/dist/components/AgentChat/ChatCommon/ChatHistory.js +1 -1
- package/dist/components/AgentChat/ChatCommon/ControlButtons.d.ts +4 -2
- package/dist/components/AgentChat/ChatCommon/ControlButtons.js +8 -2
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +5 -5
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +1 -0
- package/dist/components/AgentChat/ChatCommon/Thinking.js +2 -2
- package/dist/components/AgentChat/Common/Utils.d.ts +0 -7
- package/dist/components/AgentChat/Common/Utils.js +0 -12
- package/dist/components/Common/Breadcrumbs.js +1 -1
- package/dist/components/Common/Footer.js +4 -0
- package/dist/components/Common/notification.d.ts +4 -4
- package/dist/components/MultiAgentAccelerator/{AgentFlow.d.ts → AgentFlow/AgentFlow.d.ts} +15 -5
- package/dist/components/MultiAgentAccelerator/{AgentFlow.js → AgentFlow/AgentFlow.js} +99 -58
- package/dist/components/MultiAgentAccelerator/{AgentNode.d.ts → AgentFlow/AgentNode.d.ts} +1 -1
- package/dist/components/MultiAgentAccelerator/{AgentNode.js → AgentFlow/AgentNode.js} +11 -9
- package/dist/components/MultiAgentAccelerator/{GraphLayouts.d.ts → AgentFlow/GraphLayouts.d.ts} +4 -4
- package/dist/components/MultiAgentAccelerator/{GraphLayouts.js → AgentFlow/GraphLayouts.js} +15 -41
- package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.d.ts +21 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.js +27 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow/PlasmaEdge.d.ts +7 -0
- package/dist/components/MultiAgentAccelerator/{PlasmaEdge.js → AgentFlow/PlasmaEdge.js} +13 -8
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +74 -83
- package/dist/components/MultiAgentAccelerator/Schema/SlyData.d.ts +17 -0
- package/dist/components/MultiAgentAccelerator/Schema/SlyData.js +23 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +2 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +22 -25
- package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +16 -22
- package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +27 -7
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +8 -0
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +61 -13
- package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.d.ts → ThoughtBubbles/ThoughtBubbleEdge.d.ts} +1 -1
- package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.js → ThoughtBubbles/ThoughtBubbleOverlay.js} +1 -1
- package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +7 -2
- package/dist/components/MultiAgentAccelerator/const.d.ts +11 -10
- package/dist/components/MultiAgentAccelerator/const.js +22 -20
- package/dist/components/Settings/SettingsDialog.js +26 -10
- package/dist/controller/agent/Agent.d.ts +14 -6
- package/dist/controller/agent/Agent.js +18 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/state/Settings.d.ts +81 -4
- package/dist/state/Settings.js +87 -7
- package/dist/state/TemporaryNetworks.js +12 -21
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/AgentName.d.ts +17 -0
- package/dist/utils/AgentName.js +46 -0
- package/dist/utils/BrowserNavigation.js +2 -0
- package/dist/utils/File.d.ts +4 -1
- package/dist/utils/File.js +4 -9
- package/dist/utils/text.js +3 -7
- package/package.json +9 -10
- package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +0 -3
- /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.js → ThoughtBubbles/ThoughtBubbleEdge.js} +0 -0
- /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.d.ts → ThoughtBubbles/ThoughtBubbleOverlay.d.ts} +0 -0
|
@@ -33,47 +33,50 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
|
|
33
33
|
import Tooltip from "@mui/material/Tooltip";
|
|
34
34
|
import Typography from "@mui/material/Typography";
|
|
35
35
|
import { applyNodeChanges, Background, ConnectionMode, ControlButton, Controls, ReactFlow, useReactFlow, useStore, } from "@xyflow/react";
|
|
36
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
36
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
37
37
|
import { AgentNode, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
|
|
38
|
-
import {
|
|
39
|
-
import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_NAME_KEY, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING, } from "./const.js";
|
|
38
|
+
import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_NAME_KEY, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING, } from "../const.js";
|
|
40
39
|
import { addThoughtBubbleEdge, layoutLinear, layoutRadial } from "./GraphLayouts.js";
|
|
41
40
|
import { PlasmaEdge } from "./PlasmaEdge.js";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
import { sendChatQuery } from "../../../controller/agent/Agent.js";
|
|
42
|
+
import { StreamingUnit } from "../../../controller/llm/LlmChat.js";
|
|
43
|
+
import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
|
|
44
|
+
import { usePalette, useSettingsStore } from "../../../state/Settings.js";
|
|
45
|
+
import { useTempNetworksStore } from "../../../state/TemporaryNetworks.js";
|
|
46
|
+
import { getZIndex } from "../../../utils/zIndexLayers.js";
|
|
47
|
+
import { chatMessageFromChunk } from "../../AgentChat/Common/Utils.js";
|
|
48
|
+
import { MUIAlert } from "../../Common/MUIAlert.js";
|
|
49
|
+
import { NotificationType, sendNotification } from "../../Common/notification.js";
|
|
50
|
+
import { AgentNodePopup } from "../AgentNodePopup.js";
|
|
51
|
+
import { convertReservationsToNetworks, extractNetworkHocon, extractReservations, isEditableAgent, mergeNetworks, } from "../TemporaryNetworks.js";
|
|
52
|
+
import { ThoughtBubbleEdge } from "../ThoughtBubbles/ThoughtBubbleEdge.js";
|
|
53
|
+
import { ThoughtBubbleOverlay } from "../ThoughtBubbles/ThoughtBubbleOverlay.js";
|
|
54
|
+
//#endregion: Types
|
|
55
|
+
//#region: Constants
|
|
56
|
+
const AGENT_SAVE_TIMEOUT_MS = 60_000;
|
|
57
|
+
const DOCK_STREAM_TIMEOUT_MS = 120_000;
|
|
57
58
|
const THOUGHT_BUBBLE_TIMEOUT_MS = 10_000;
|
|
58
59
|
// How long the dock's status banner stays visible before auto-dismissing. Error banners persist until dismissed.
|
|
59
60
|
// Exported for tests.
|
|
60
61
|
export const DOCK_BANNER_AUTO_DISMISS_MS = 5_000;
|
|
61
|
-
// #endregion: Constants
|
|
62
62
|
const DOCK_PROMPT_PLACEHOLDER = "Describe a change to the network";
|
|
63
|
-
|
|
63
|
+
//#endregion: Constants
|
|
64
|
+
//#region: Helpers
|
|
64
65
|
/**
|
|
65
66
|
* Streams the Agent Network Designer endpoint with a natural-language prompt and the current
|
|
66
67
|
* network definition, collecting any returned reservations.
|
|
67
68
|
*/
|
|
68
|
-
const
|
|
69
|
+
const applyNetworkDesignerChanges = async (neuroSanURL, signal, userPrompt, currentDefinition, agentNetworkName, currentUser) => {
|
|
69
70
|
let newNetworks = [];
|
|
70
71
|
await sendChatQuery(neuroSanURL, signal, userPrompt, AGENT_NETWORK_DESIGNER_ID, (chunk) => {
|
|
71
72
|
const chatMessage = chatMessageFromChunk(chunk);
|
|
72
|
-
if (!chatMessage)
|
|
73
|
+
if (!chatMessage) {
|
|
73
74
|
return;
|
|
75
|
+
}
|
|
74
76
|
const reservations = extractReservations(chatMessage);
|
|
75
|
-
if (reservations.length === 0)
|
|
77
|
+
if (reservations.length === 0) {
|
|
76
78
|
return;
|
|
79
|
+
}
|
|
77
80
|
const networkHocon = extractNetworkHocon(chatMessage);
|
|
78
81
|
const agentNetworkNameFromMessage = chatMessage.sly_data?.[AGENT_NETWORK_NAME_KEY];
|
|
79
82
|
const networkName = agentNetworkName ?? agentNetworkNameFromMessage;
|
|
@@ -86,7 +89,22 @@ const streamNetworkDesignerPrompt = async (neuroSanURL, signal, userPrompt, curr
|
|
|
86
89
|
}, currentUser, StreamingUnit.Line);
|
|
87
90
|
return newNetworks;
|
|
88
91
|
};
|
|
89
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Filters node events based on the current mode. Don't allow any topological-modifying events (adding, deleting nodes)
|
|
94
|
+
* and in Agent Network Designer mode, don't allow dragging nodes (position changes) either.
|
|
95
|
+
* @param change The node change event to filter.
|
|
96
|
+
* @param isAgentNetworkDesignerMode Whether the flow is in Agent Network Designer mode (read-only preview).
|
|
97
|
+
* @return True if the event should be allowed, false if it should be filtered out.
|
|
98
|
+
*/
|
|
99
|
+
export const filterNodeEvents = (change, isAgentNetworkDesignerMode) => {
|
|
100
|
+
// Only allow nodes to be dragged, no topological edits to the graph (read-only)
|
|
101
|
+
if (["remove", "add", "replace"].includes(change.type))
|
|
102
|
+
return false;
|
|
103
|
+
// Disallow dragging nodes in AND mode since it's supposed to be a read-only preview but
|
|
104
|
+
// pass along all other event types as
|
|
105
|
+
return !(change.type === "position" && isAgentNetworkDesignerMode);
|
|
106
|
+
};
|
|
107
|
+
//#endregion: Helpers
|
|
90
108
|
export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork, currentConversations, currentUser, id, isAgentNetworkDesignerMode, isAwaitingLlm, isEditMode, isStreaming, isSelectedNetworkTemporary: isTemporaryNetwork, networkDisplayName, networkId, neuroSanURL, onEnterEditMode, onNetworkReplaced, onExitEditMode, onSaveAgent, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
|
|
91
109
|
const theme = useTheme();
|
|
92
110
|
const { fitView } = useReactFlow();
|
|
@@ -304,26 +322,40 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
304
322
|
detail: "Nothing was changed. Your prompt is restored below.",
|
|
305
323
|
});
|
|
306
324
|
}, [showDockBanner]);
|
|
307
|
-
const
|
|
325
|
+
const openNodeEditor = useCallback((node) => {
|
|
308
326
|
// Popup is only available for temporary networks.
|
|
309
327
|
if (!isTemporaryNetwork)
|
|
310
328
|
return;
|
|
311
329
|
// Only llm_agent nodes support instructions/description editing.
|
|
312
330
|
if (!isEditableAgent(node.data.displayAs))
|
|
313
331
|
return;
|
|
314
|
-
// Find the
|
|
332
|
+
// Find the agent's existing instructions and description from the temp network definition.
|
|
315
333
|
const currentTempNetwork = networkId
|
|
316
334
|
? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
|
|
317
335
|
: undefined;
|
|
318
|
-
const
|
|
336
|
+
const currentAgentDefinition = currentTempNetwork?.agentNetworkDefinition?.find((e) => e.origin === node.id);
|
|
319
337
|
setSelectedAgent({
|
|
320
338
|
agentId: node.id,
|
|
321
339
|
agentName: node.data.agentName,
|
|
322
|
-
initialInstructions:
|
|
323
|
-
initialDescription:
|
|
340
|
+
initialInstructions: currentAgentDefinition?.instructions ?? "",
|
|
341
|
+
initialDescription: currentAgentDefinition?.description ?? "",
|
|
324
342
|
});
|
|
325
343
|
setIsPopupOpen(true);
|
|
326
344
|
}, [tempNetworks, isTemporaryNetwork, networkId]);
|
|
345
|
+
const handleNodeClick = useCallback((_event, node) => openNodeEditor(node), [openNodeEditor]);
|
|
346
|
+
// ReactFlow makes nodes focusable and selects them on Enter, but it never fires onNodeClick from the keyboard.
|
|
347
|
+
// Route Enter on a focused node to the editor so keyboard users can open a node the same way a click does.
|
|
348
|
+
const handleNodeKeyDown = useCallback((event) => {
|
|
349
|
+
if (event.key !== "Enter")
|
|
350
|
+
return;
|
|
351
|
+
const target = event.target;
|
|
352
|
+
if (!(target instanceof HTMLElement))
|
|
353
|
+
return;
|
|
354
|
+
const nodeId = target.closest(".react-flow__node")?.dataset["id"];
|
|
355
|
+
const node = nodeId ? nodes.find((n) => n.id === nodeId) : undefined;
|
|
356
|
+
if (node)
|
|
357
|
+
openNodeEditor(node);
|
|
358
|
+
}, [nodes, openNodeEditor]);
|
|
327
359
|
const handlePopupClose = useCallback(() => {
|
|
328
360
|
// If a save is in-flight, abort it immediately so the stream doesn't hang.
|
|
329
361
|
saveAbortControllerRef.current?.abort();
|
|
@@ -335,7 +367,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
335
367
|
* Applies the networks returned by the designer: upserts them and triggers navigation if needed.
|
|
336
368
|
* Returns true when a matching reservation was applied, false (and surfaces an error banner) otherwise.
|
|
337
369
|
*/
|
|
338
|
-
const
|
|
370
|
+
const saveUpdates = useCallback((newNetworksFromSave, currentAgentNetworkName) => {
|
|
339
371
|
if (newNetworksFromSave.length === 0) {
|
|
340
372
|
showDockBanner({
|
|
341
373
|
severity: "error",
|
|
@@ -344,6 +376,9 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
344
376
|
});
|
|
345
377
|
return false;
|
|
346
378
|
}
|
|
379
|
+
// Find the returned reservation that stands in for the current network (matched by name). When
|
|
380
|
+
// present, persist every returned network, then carry the open network's chat history over to the
|
|
381
|
+
// replacement and notify the parent so it can navigate to the new reservation.
|
|
347
382
|
const replacement = newNetworksFromSave.find((n) => n.agentNetworkName === currentAgentNetworkName);
|
|
348
383
|
if (replacement) {
|
|
349
384
|
useTempNetworksStore.getState().upsertTempNetworks(newNetworksFromSave);
|
|
@@ -362,7 +397,8 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
362
397
|
return false;
|
|
363
398
|
}, [networkId, onNetworkReplaced, showDockBanner]);
|
|
364
399
|
const handleDockApply = useCallback(async () => {
|
|
365
|
-
|
|
400
|
+
const readyToApplyEdit = Boolean(dockPrompt.trim() && neuroSanURL && currentUser);
|
|
401
|
+
if (!readyToApplyEdit)
|
|
366
402
|
return;
|
|
367
403
|
const currentTempNetwork = networkId
|
|
368
404
|
? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
|
|
@@ -375,11 +411,11 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
375
411
|
const timeoutId = setTimeout(() => {
|
|
376
412
|
hasTimedOut = true;
|
|
377
413
|
controller.abort();
|
|
378
|
-
},
|
|
414
|
+
}, DOCK_STREAM_TIMEOUT_MS);
|
|
379
415
|
try {
|
|
380
|
-
const newNetworks = await
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
416
|
+
const newNetworks = await applyNetworkDesignerChanges(neuroSanURL, controller.signal, dockPrompt, currentDefinition, currentTempNetwork?.agentNetworkName, currentUser);
|
|
417
|
+
const appliedSuccess = saveUpdates(newNetworks, currentTempNetwork?.agentNetworkName);
|
|
418
|
+
if (appliedSuccess) {
|
|
383
419
|
setDockPrompt("");
|
|
384
420
|
showDockBanner({
|
|
385
421
|
severity: "success",
|
|
@@ -408,7 +444,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
408
444
|
dockAbortControllerRef.current = null;
|
|
409
445
|
setIsDockStreaming(false);
|
|
410
446
|
}
|
|
411
|
-
}, [
|
|
447
|
+
}, [saveUpdates, currentUser, dockPrompt, networkId, neuroSanURL, showDockBanner, tempNetworks]);
|
|
412
448
|
const handleExitEditMode = useCallback(() => {
|
|
413
449
|
if (isDockStreaming) {
|
|
414
450
|
dockAbortControllerRef.current?.abort();
|
|
@@ -417,6 +453,18 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
417
453
|
}
|
|
418
454
|
onExitEditMode?.();
|
|
419
455
|
}, [isDockStreaming, onExitEditMode]);
|
|
456
|
+
// Pressing Escape exits edit mode, mirroring the explicit exit button. Skip while the
|
|
457
|
+
// node popup is open so Escape closes the popup first rather than the whole edit mode.
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
if (!isEditMode || isPopupOpen)
|
|
460
|
+
return undefined;
|
|
461
|
+
const handleEscape = (e) => {
|
|
462
|
+
if (e.key === "Escape")
|
|
463
|
+
handleExitEditMode();
|
|
464
|
+
};
|
|
465
|
+
document.addEventListener("keydown", handleEscape);
|
|
466
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
467
|
+
}, [isEditMode, isPopupOpen, handleExitEditMode]);
|
|
420
468
|
const handlePopupSave = useCallback(async (agentName, instructionsText, descriptionText) => {
|
|
421
469
|
if (!selectedAgent)
|
|
422
470
|
return;
|
|
@@ -426,11 +474,11 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
426
474
|
: undefined;
|
|
427
475
|
// Produce a new array with the saved agent's fields updated; all other entries pass through unchanged.
|
|
428
476
|
const currentDefinitions = currentTempNetwork?.agentNetworkDefinition ?? [];
|
|
429
|
-
const
|
|
477
|
+
const updatedDefinitions = currentDefinitions.map((entry) => entry.origin === selectedAgent.agentId
|
|
430
478
|
? { ...entry, instructions: instructionsText, description: descriptionText }
|
|
431
479
|
: entry);
|
|
432
480
|
if (networkId) {
|
|
433
|
-
updateTempNetworkDefinition(networkId,
|
|
481
|
+
updateTempNetworkDefinition(networkId, updatedDefinitions);
|
|
434
482
|
}
|
|
435
483
|
if (!onSaveAgent) {
|
|
436
484
|
setIsPopupOpen(false);
|
|
@@ -439,10 +487,9 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
439
487
|
setIsSavingAgent(true);
|
|
440
488
|
const saveController = new AbortController();
|
|
441
489
|
saveAbortControllerRef.current = saveController;
|
|
442
|
-
const saveTimeoutId = setTimeout(() => saveController.abort(new DOMException("Save timed out", "TimeoutError")),
|
|
443
|
-
);
|
|
490
|
+
const saveTimeoutId = setTimeout(() => saveController.abort(new DOMException("Save timed out", "TimeoutError")), AGENT_SAVE_TIMEOUT_MS);
|
|
444
491
|
try {
|
|
445
|
-
await onSaveAgent(agentName,
|
|
492
|
+
await onSaveAgent(agentName, updatedDefinitions, currentTempNetwork?.agentNetworkName, saveController.signal);
|
|
446
493
|
}
|
|
447
494
|
catch (e) {
|
|
448
495
|
console.error(`Error saving network ${agentName}. See onSaveAgent implementation for details.`, e);
|
|
@@ -466,10 +513,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
466
513
|
}, 50);
|
|
467
514
|
}, [agentsInNetwork, fitView, layout]);
|
|
468
515
|
const onNodesChange = useCallback((changes) => {
|
|
469
|
-
setNodes((currentNodes) => applyNodeChanges(
|
|
470
|
-
// For now, we only allow dragging, no updates. In agent network designer mode, it doesn't make
|
|
471
|
-
// sense to allow position changes since the user isn't actually manipulating a real network
|
|
472
|
-
changes.filter((c) => c.type === "position" && !isAgentNetworkDesignerMode), currentNodes));
|
|
516
|
+
setNodes((currentNodes) => applyNodeChanges(changes.filter((change) => filterNodeEvents(change, isAgentNetworkDesignerMode)), currentNodes));
|
|
473
517
|
}, [isAgentNetworkDesignerMode]);
|
|
474
518
|
const transform = useStore((state) => state.transform);
|
|
475
519
|
// Why not just a "const"? See: https://reactflow.dev/learn/customization/custom-nodes
|
|
@@ -529,7 +573,6 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
529
573
|
}, size: "small", sx: {
|
|
530
574
|
marginLeft: theme.spacing(2),
|
|
531
575
|
"& .MuiToggleButton-root": {
|
|
532
|
-
backgroundColor: theme.palette.background.paper,
|
|
533
576
|
borderColor: theme.palette.divider,
|
|
534
577
|
color: theme.palette.text.primary,
|
|
535
578
|
minHeight: 22,
|
|
@@ -554,7 +597,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
554
597
|
height: "1rem",
|
|
555
598
|
}, children: "Heatmap" })] })] }));
|
|
556
599
|
};
|
|
557
|
-
// Get the background color for the control buttons based on the
|
|
600
|
+
// Get the background color for the control buttons; differs based on whether the button is active or not
|
|
558
601
|
const getControlButtonBackgroundColor = (isActive) => {
|
|
559
602
|
return isActive ? theme.palette.action.selected : undefined;
|
|
560
603
|
};
|
|
@@ -593,12 +636,10 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
593
636
|
}, children: [_jsx(Tooltip, { title: networkDisplayName, placement: "top", children: _jsx(Box, { sx: { pointerEvents: "auto" }, children: _jsx(Typography, { id: `${id}-network-title`, variant: "subtitle1", sx: {
|
|
594
637
|
backdropFilter: "blur(6px)",
|
|
595
638
|
backgroundColor: titleBackgroundColor,
|
|
596
|
-
border: `1px solid ${alpha(theme.palette.divider, 0.
|
|
639
|
+
border: `1px solid ${alpha(theme.palette.divider, 0.75)}`,
|
|
597
640
|
borderRadius: 2,
|
|
598
|
-
boxShadow: theme.
|
|
599
|
-
|
|
600
|
-
: `0 6px 16px ${alpha(theme.palette.common.black, 0.12)}`,
|
|
601
|
-
color: theme.palette.getContrastText(alpha(titleBackgroundColor, 0.65)),
|
|
641
|
+
boxShadow: theme.shadows[6],
|
|
642
|
+
color: theme.palette.text.primary,
|
|
602
643
|
fontWeight: 600,
|
|
603
644
|
letterSpacing: "0.01em",
|
|
604
645
|
lineHeight: 1.35,
|
|
@@ -623,18 +664,18 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
623
664
|
"& .react-flow__node": {
|
|
624
665
|
border: "1px solid divider",
|
|
625
666
|
},
|
|
626
|
-
"& .react-flow__panel": {
|
|
667
|
+
"& .react-flow__panel, & .react-flow__controls-button": {
|
|
627
668
|
backgroundColor: theme.palette.background.paper,
|
|
628
|
-
border: "1px solid divider",
|
|
629
669
|
color: theme.palette.text.primary,
|
|
630
670
|
},
|
|
671
|
+
"& .react-flow__panel": {
|
|
672
|
+
border: "1px solid divider",
|
|
673
|
+
},
|
|
631
674
|
"& .react-flow__controls-button": {
|
|
632
|
-
backgroundColor: theme.palette.background.paper,
|
|
633
675
|
borderBottom: "1px solid divider",
|
|
634
|
-
color: theme.palette.text.primary,
|
|
635
676
|
fill: theme.palette.text.primary,
|
|
636
677
|
},
|
|
637
|
-
}, children: [_jsxs(Box, { id: `${id}-react-flow-wrapper`, sx: { position: "relative", flex: 1, minHeight: 0 }, children: [networkDisplayName ? _jsx(Box, { sx: { marginBottom: "1rem" }, children: getTitle() }) : null, _jsx(ReactFlow, { id: `${id}-react-flow`, nodes: nodes,
|
|
678
|
+
}, children: [_jsxs(Box, { id: `${id}-react-flow-wrapper`, sx: { position: "relative", flex: 1, minHeight: 0 }, children: [networkDisplayName ? _jsx(Box, { sx: { marginBottom: "1rem" }, children: getTitle() }) : null, _jsx(ReactFlow, { connectionMode: ConnectionMode.Loose, edgeTypes: edgeTypes, edges: edges, fitView: true, id: `${id}-react-flow`, nodeTypes: nodeTypes, nodes: nodes, nodesDraggable: !isAgentNetworkDesignerMode, onKeyDown: handleNodeKeyDown, onNodeClick: handleNodeClick, onNodesChange: onNodesChange, children: !isAwaitingLlm && (_jsxs(_Fragment, { children: [agentsInNetwork?.length && !isAgentNetworkDesignerMode && !isEditMode ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), !isAgentNetworkDesignerMode && !isEditMode && getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: thoughtBubbleEdgesForOverlay, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange })] }), isEditMode && isTemporaryNetwork && !isAwaitingLlm && (_jsxs(Box, { sx: {
|
|
638
679
|
backdropFilter: "blur(8px)",
|
|
639
680
|
backgroundColor: alpha(theme.palette.background.paper, 0.2),
|
|
640
681
|
borderTop: `2px solid ${theme.palette.primary.main}`,
|
|
@@ -672,7 +713,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
|
|
|
672
713
|
px: 1,
|
|
673
714
|
py: 0.5,
|
|
674
715
|
alignItems: "center",
|
|
675
|
-
}, children: [_jsx(TextField, { fullWidth: true, placeholder: DOCK_PROMPT_PLACEHOLDER, variant: "outlined", size: "small", value: dockPrompt, onChange: (e) => setDockPrompt(e.target.value), onKeyDown: (e) => {
|
|
716
|
+
}, children: [_jsx(TextField, { autoFocus: true, fullWidth: true, placeholder: DOCK_PROMPT_PLACEHOLDER, variant: "outlined", size: "small", value: dockPrompt, onChange: (e) => setDockPrompt(e.target.value), onKeyDown: (e) => {
|
|
676
717
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
677
718
|
e.preventDefault();
|
|
678
719
|
void handleDockApply();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NodeProps } from "@xyflow/react";
|
|
2
2
|
import type { Node as RFNode } from "@xyflow/react";
|
|
3
3
|
import { FC } from "react";
|
|
4
|
-
import { AgentConversation } from "
|
|
4
|
+
import { AgentConversation } from "../AgentConversations.js";
|
|
5
5
|
export interface AgentNodeProps extends Record<string, unknown> {
|
|
6
6
|
readonly agentCounts?: Map<string, number>;
|
|
7
7
|
readonly agentName: string;
|
|
@@ -27,11 +27,10 @@ import { keyframes, styled, useTheme } from "@mui/material/styles";
|
|
|
27
27
|
import Tooltip from "@mui/material/Tooltip";
|
|
28
28
|
import Typography from "@mui/material/Typography";
|
|
29
29
|
import { Handle, Position } from "@xyflow/react";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { getZIndex } from "../../utils/zIndexLayers.js";
|
|
30
|
+
import { usePalette, useSettingsStore } from "../../../state/Settings.js";
|
|
31
|
+
import { isLightColor } from "../../../Theme/Theme.js";
|
|
32
|
+
import { getZIndex } from "../../../utils/zIndexLayers.js";
|
|
33
|
+
import { DisplayAs } from "../const.js";
|
|
35
34
|
// A zero-width space character, used to give the browser a hint where to break long names
|
|
36
35
|
const ZERO_WIDTH_SPACE = "\u200B";
|
|
37
36
|
// Node dimensions
|
|
@@ -154,7 +153,6 @@ export const AgentNode = (props) => {
|
|
|
154
153
|
const glowColor = isLightColor(theme.palette.background.default)
|
|
155
154
|
? theme.palette.common.black
|
|
156
155
|
: theme.palette.common.white;
|
|
157
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
158
156
|
// Insert zero-width spaces into the agent name to give the browser hints where to wrap.
|
|
159
157
|
const wrappedAgentName = agentName
|
|
160
158
|
// Allow wrap after underscore
|
|
@@ -165,7 +163,7 @@ export const AgentNode = (props) => {
|
|
|
165
163
|
.replaceAll(/(?<abbr>[A-Z])(?<word>[A-Z][a-z])/gu, `$<abbr>${ZERO_WIDTH_SPACE}$<word>`);
|
|
166
164
|
const height = NODE_HEIGHT * (isFrontman ? FRONTMAN_SIZE_MULTIPLIER : 1.0);
|
|
167
165
|
const width = height;
|
|
168
|
-
const getEditButton = () => (_jsx(Tooltip, { title: "Edit", placement: "top", disableInteractive: true, children: _jsx(IconButton, { sx: {
|
|
166
|
+
const getEditButton = () => (_jsx(Tooltip, { title: "Edit", placement: "top", disableInteractive: true, children: _jsx(IconButton, { className: "agent-node-edit-icon", "aria-hidden": true, tabIndex: -1, sx: {
|
|
169
167
|
position: "absolute",
|
|
170
168
|
top: 0,
|
|
171
169
|
right: 0,
|
|
@@ -197,13 +195,17 @@ export const AgentNode = (props) => {
|
|
|
197
195
|
width,
|
|
198
196
|
zIndex: getZIndex(1, theme),
|
|
199
197
|
}, children: wrappedAgentName }) }));
|
|
200
|
-
const getNode = () => (_jsxs(AnimatedNode, { id: agentId, "data-testid": agentId, glowColor: glowColor, isActive: isActiveAgent,
|
|
198
|
+
const getNode = () => (_jsxs(AnimatedNode, { id: agentId, "data-testid": agentId, glowColor: glowColor, isActive: isActiveAgent, sx: {
|
|
201
199
|
backgroundColor,
|
|
202
200
|
color,
|
|
203
201
|
cursor: isEditable ? "pointer" : "grab",
|
|
204
202
|
height,
|
|
205
203
|
width,
|
|
206
204
|
zIndex: getZIndex(1, theme),
|
|
207
|
-
|
|
205
|
+
// Reveal the edit icon on hover, or when a keyboard user tabs to the node.
|
|
206
|
+
"& .agent-node-edit-icon": { opacity: 0, transition: "opacity 0.15s ease-in-out" },
|
|
207
|
+
"&:hover .agent-node-edit-icon": { opacity: 1 },
|
|
208
|
+
".react-flow__node:focus-visible & .agent-node-edit-icon": { opacity: 1 },
|
|
209
|
+
}, children: [getDisplayAsIcon(), isEditable && getEditButton(), _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 } })] }));
|
|
208
210
|
return (_jsxs(_Fragment, { children: [getNode(), getNodeName()] }));
|
|
209
211
|
};
|
package/dist/components/MultiAgentAccelerator/{GraphLayouts.d.ts → AgentFlow/GraphLayouts.d.ts}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Edge, Node as RFNode } from "@xyflow/react";
|
|
2
|
-
import { AgentConversation } from "
|
|
2
|
+
import { AgentConversation } from "../AgentConversations.js";
|
|
3
3
|
import { AgentNodeProps } from "./AgentNode.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { AgentIconSuggestions } from "../../../controller/Types/AgentIconSuggestions.js";
|
|
5
|
+
import { ConnectivityInfo } from "../../../generated/neuro-san/NeuroSanClient.js";
|
|
6
|
+
import { ThoughtBubbleEdgeShape } from "../ThoughtBubbles/ThoughtBubbleEdge.js";
|
|
7
7
|
export declare const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
|
|
8
8
|
/**
|
|
9
9
|
* Result of the layout algorithms, containing the nodes and edges with their computed positions and properties.
|
|
@@ -20,9 +20,11 @@ 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 {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
23
|
+
import { cleanUpAgentName } from "../../../utils/AgentName.js";
|
|
24
|
+
import { KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../../AgentChat/Common/Utils.js";
|
|
25
|
+
import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "../const.js";
|
|
26
|
+
import { getFrontman, getParentAgents, getParents } from "./GraphStructure.js";
|
|
27
|
+
import { isEditableAgent } from "../TemporaryNetworks.js";
|
|
26
28
|
export const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
|
|
27
29
|
const FRONTMAN_OFFSET_X = (NODE_WIDTH * (FRONTMAN_SIZE_MULTIPLIER - 1)) / 2;
|
|
28
30
|
const FRONTMAN_OFFSET_Y = (NODE_HEIGHT * (FRONTMAN_SIZE_MULTIPLIER - 1)) / 2;
|
|
@@ -60,34 +62,6 @@ const areInSameConversation = (conversations, sourceAgent, targetAgent) => {
|
|
|
60
62
|
// Name for custom node
|
|
61
63
|
const AGENT_NODE_TYPE_NAME = "agentNode";
|
|
62
64
|
// #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
65
|
// Generates the properties for an edge in the graph.
|
|
92
66
|
// Common for both radial and linear layouts.
|
|
93
67
|
const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAnimated) => {
|
|
@@ -102,6 +76,8 @@ const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAni
|
|
|
102
76
|
type: isAnimated ? "plasmaEdge" : undefined,
|
|
103
77
|
};
|
|
104
78
|
};
|
|
79
|
+
// An agent is editable if it belongs to a temporary network and is of an editable type.
|
|
80
|
+
const isNodeEditable = (displayAs, isTemporaryNetwork) => isTemporaryNetwork && isEditableAgent(displayAs);
|
|
105
81
|
export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsInNetwork, currentConversations, isAgentNetworkDesignerMode, isAwaitingLlm, isTemporaryNetwork = false, thoughtBubbleEdges, useNativeNames, }) => {
|
|
106
82
|
const nodesInNetwork = [];
|
|
107
83
|
const edgesInNetwork = [];
|
|
@@ -109,8 +85,7 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
|
|
|
109
85
|
const nodeDepths = new Map();
|
|
110
86
|
const queue = [];
|
|
111
87
|
const parentAgents = getParentAgents(agentsInNetwork);
|
|
112
|
-
const
|
|
113
|
-
const frontman = getFrontman(parentAgents, childAgents);
|
|
88
|
+
const frontman = getFrontman(agentsInNetwork);
|
|
114
89
|
if (frontman) {
|
|
115
90
|
// Add the frontman node to the network
|
|
116
91
|
queue.push({ id: frontman.origin, depth: 0 });
|
|
@@ -147,6 +122,7 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
|
|
|
147
122
|
const x = DEFAULT_FRONTMAN_X_POS + radius * Math.cos(angle);
|
|
148
123
|
const y = DEFAULT_FRONTMAN_Y_POS + radius * Math.sin(angle);
|
|
149
124
|
const isFrontman = frontman?.origin === nodeId;
|
|
125
|
+
const displayAs = agentsInNetwork.find((a) => a.origin === nodeId)?.display_as;
|
|
150
126
|
const parentNodes = getParents(nodeId, parentAgents);
|
|
151
127
|
// Create an edge from each parent node to this node
|
|
152
128
|
for (const parentNode of parentNodes) {
|
|
@@ -187,13 +163,12 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
|
|
|
187
163
|
agentCounts,
|
|
188
164
|
agentName: useNativeNames ? nodeId : cleanUpAgentName(nodeId),
|
|
189
165
|
depth,
|
|
190
|
-
displayAs
|
|
166
|
+
displayAs,
|
|
191
167
|
// Use current conversations for node highlighting (cleared at end)
|
|
192
168
|
getConversations: () => currentConversations,
|
|
193
169
|
isAwaitingLlm,
|
|
194
170
|
agentIconSuggestion: agentIconSuggestions?.[nodeId],
|
|
195
|
-
isEditable: isTemporaryNetwork
|
|
196
|
-
isEditableAgent(agentsInNetwork.find((a) => a.origin === nodeId)?.display_as),
|
|
171
|
+
isEditable: isNodeEditable(displayAs, isTemporaryNetwork),
|
|
197
172
|
},
|
|
198
173
|
// position: allow for Frontman being larger
|
|
199
174
|
position: isFrontman
|
|
@@ -220,9 +195,8 @@ export const layoutLinear = ({ agentCounts, agentIconSuggestions = null, agentsI
|
|
|
220
195
|
const edgesInNetwork = [];
|
|
221
196
|
// Do these calculations outside the loop for efficiency
|
|
222
197
|
const parentAgents = getParentAgents(agentsInNetwork);
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
agentsInNetwork.forEach(({ origin: nodeId }) => {
|
|
198
|
+
const frontman = getFrontman(agentsInNetwork);
|
|
199
|
+
agentsInNetwork.forEach(({ display_as: displayAs, origin: nodeId }) => {
|
|
226
200
|
const parentIds = getParents(nodeId, parentAgents);
|
|
227
201
|
const isFrontman = frontman?.origin === nodeId;
|
|
228
202
|
nodesInNetwork.push({
|
|
@@ -231,13 +205,13 @@ export const layoutLinear = ({ agentCounts, agentIconSuggestions = null, agentsI
|
|
|
231
205
|
data: {
|
|
232
206
|
agentCounts,
|
|
233
207
|
agentName: useNativeNames ? nodeId : cleanUpAgentName(nodeId),
|
|
234
|
-
displayAs
|
|
208
|
+
displayAs,
|
|
235
209
|
// Use current conversations for node highlighting (cleared at end)
|
|
236
210
|
getConversations: () => currentConversations,
|
|
237
211
|
isAwaitingLlm,
|
|
238
212
|
depth: undefined, // Depth will be computed later,
|
|
239
213
|
agentIconSuggestion: agentIconSuggestions?.[nodeId],
|
|
240
|
-
isEditable:
|
|
214
|
+
isEditable: isNodeEditable(displayAs, isTemporaryNetwork),
|
|
241
215
|
},
|
|
242
216
|
position: isFrontman ? { x: DEFAULT_FRONTMAN_X_POS, y: DEFAULT_FRONTMAN_Y_POS } : { x: 0, y: 0 },
|
|
243
217
|
style: {
|
|
@@ -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: (agents: readonly ConnectivityInfo[]) => ConnectivityInfo | 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);
|