Redesigned Agent Architecture

This commit is contained in:
2025-04-15 20:27:23 -06:00
parent 17e0d20063
commit 8d67e847e9
9 changed files with 487 additions and 334 deletions

View File

@ -48,6 +48,15 @@ import React, {
} from "./Dialogs";
import StatusBar from "./Status_Bar";
// Websocket Functionality
import { io } from "socket.io-client";
if (!window.BorealisSocket) {
window.BorealisSocket = io(window.location.origin, {
transports: ["websocket"]
});
}
// Global Node Update Timer Variable
if (!window.BorealisUpdateRate) {
window.BorealisUpdateRate = 200;

View File

@ -35,7 +35,7 @@ export default function NodeSidebar({
return (
<div
style={{
width: 320,
width: 300, //Width of the Node Sidebar
backgroundColor: "#121212",
borderRight: "1px solid #333",
overflowY: "auto"

View File

@ -0,0 +1,154 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Agent/Node_Agent.jsx
import React, { useEffect, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
const BorealisAgentNode = ({ id, data }) => {
const { getNodes, getEdges, setNodes } = useReactFlow();
const [agents, setAgents] = useState([]);
const [selectedAgent, setSelectedAgent] = useState(data.agent_id || "");
// -------------------------------
// Load agent list from backend
// -------------------------------
useEffect(() => {
fetch("/api/agents")
.then(res => res.json())
.then(setAgents);
const interval = setInterval(() => {
fetch("/api/agents")
.then(res => res.json())
.then(setAgents);
}, 5000);
return () => clearInterval(interval);
}, []);
// -------------------------------
// Helper: Get all provisioner role nodes connected to bottom port
// -------------------------------
const getAttachedProvisioners = () => {
const allNodes = getNodes();
const allEdges = getEdges();
const attached = [];
for (const edge of allEdges) {
if (edge.source === id && edge.sourceHandle === "provisioner") {
const roleNode = allNodes.find(n => n.id === edge.target);
if (roleNode && typeof window.__BorealisInstructionNodes?.[roleNode.id] === "function") {
attached.push(window.__BorealisInstructionNodes[roleNode.id]());
}
}
}
return attached;
};
// -------------------------------
// Provision Agent with all Roles
// -------------------------------
const handleProvision = () => {
if (!selectedAgent) return;
const provisionRoles = getAttachedProvisioners();
if (!provisionRoles.length) {
console.warn("No provisioner nodes connected to agent.");
return;
}
const configPayload = {
agent_id: selectedAgent,
roles: provisionRoles
};
fetch("/api/agent/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(configPayload)
})
.then(res => res.json())
.then(() => {
console.log(`[Provision] Agent ${selectedAgent} updated with ${provisionRoles.length} roles.`);
});
};
return (
<div className="borealis-node">
{/* This bottom port is used for bi-directional provisioning & feedback */}
<Handle
type="source"
position={Position.Bottom}
id="provisioner"
className="borealis-handle"
style={{ top: "100%", background: "#58a6ff" }}
/>
<div className="borealis-node-header">Borealis Agent</div>
<div className="borealis-node-content" style={{ fontSize: "9px" }}>
<label>Agent:</label>
<select
value={selectedAgent}
onChange={(e) => {
const newId = e.target.value;
setSelectedAgent(newId);
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, agent_id: newId } }
: n
)
);
}}
style={{ width: "100%", marginBottom: "6px", fontSize: "9px" }}
>
<option value="">-- Select --</option>
{Object.entries(agents).map(([id, info]) => {
const label = info.status === "provisioned" ? "(Provisioned)" : "(Idle)";
return (
<option key={id} value={id}>
{id} {label}
</option>
);
})}
</select>
<button
onClick={handleProvision}
style={{ width: "100%", fontSize: "9px", padding: "4px", marginTop: "4px" }}
>
Provision Agent
</button>
<hr style={{ margin: "6px 0", borderColor: "#444" }} />
<div style={{ fontSize: "8px", color: "#aaa" }}>
Connect <strong>Instruction Nodes</strong> below to define roles.
Each instruction node will send back its results (like screenshots) and act as a separate data output.
</div>
<div style={{ fontSize: "8px", color: "#aaa", marginTop: "4px" }}>
<strong>Supported Roles:</strong>
<ul style={{ paddingLeft: "14px", marginTop: "2px", marginBottom: "0" }}>
<li><code>screenshot</code>: Capture a region with interval and overlay</li>
{/* Future roles will be listed here */}
</ul>
</div>
</div>
</div>
);
};
export default {
type: "Borealis_Agent",
label: "Borealis Agent",
description: `
Main Agent Node
- Selects an available agent
- Connect instruction nodes below to assign tasks (roles)
- Roles include screenshots, keyboard macros, etc.
`.trim(),
content: "Select and provision a Borealis Agent with task roles",
component: BorealisAgentNode
};

View File

@ -0,0 +1,186 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Agent/Node_Agent_Role_Screenshot.jsx
import React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
import ShareIcon from "@mui/icons-material/Share";
import IconButton from "@mui/material/IconButton";
if (!window.BorealisValueBus) {
window.BorealisValueBus = {};
}
if (!window.BorealisUpdateRate) {
window.BorealisUpdateRate = 100;
}
const ScreenshotInstructionNode = ({ id, data }) => {
const { setNodes, getNodes } = useReactFlow();
const edges = useStore(state => state.edges);
const [interval, setInterval] = useState(data?.interval || 1000);
const [region, setRegion] = useState({
x: data?.x ?? 250,
y: data?.y ?? 100,
w: data?.w ?? 300,
h: data?.h ?? 200,
});
const [visible, setVisible] = useState(data?.visible ?? true);
const [alias, setAlias] = useState(data?.alias || "");
const [imageBase64, setImageBase64] = useState("");
const base64Ref = useRef("");
const handleCopyLiveViewLink = () => {
const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner");
if (!agentEdge) {
alert("No upstream agent connection found.");
return;
}
const agentNode = getNodes().find(n => n.id === agentEdge.source);
const selectedAgentId = agentNode?.data?.agent_id;
if (!selectedAgentId) {
alert("Upstream agent node does not have a selected agent.");
return;
}
const liveUrl = `${window.location.origin}/api/agent/${selectedAgentId}/node/${id}/screenshot/live`;
navigator.clipboard.writeText(liveUrl)
.then(() => console.log(`[Clipboard] Copied Live View URL: ${liveUrl}`))
.catch(err => console.error("Clipboard copy failed:", err));
};
useEffect(() => {
const intervalId = setInterval(() => {
const val = base64Ref.current;
console.log(`[Screenshot Node] setInterval update. Current base64 length: ${val?.length || 0}`);
if (!val) return;
window.BorealisValueBus[id] = val;
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, value: val } }
: n
)
);
}, window.BorealisUpdateRate || 100);
return () => clearInterval(intervalId);
}, [id, setNodes]);
useEffect(() => {
const socket = window.BorealisSocket || null;
if (!socket) {
console.warn("[Screenshot Node] BorealisSocket not available");
return;
}
console.log(`[Screenshot Node] Listening for agent_screenshot_task with node_id: ${id}`);
const handleScreenshot = (payload) => {
console.log("[Screenshot Node] Received payload:", payload);
if (payload?.node_id === id && payload?.image_base64) {
base64Ref.current = payload.image_base64;
setImageBase64(payload.image_base64);
window.BorealisValueBus[id] = payload.image_base64;
console.log(`[Screenshot Node] Updated base64Ref and ValueBus for ${id}, length: ${payload.image_base64.length}`);
} else {
console.log(`[Screenshot Node] Ignored payload for mismatched node_id (${payload?.node_id})`);
}
};
socket.on("agent_screenshot_task", handleScreenshot);
return () => socket.off("agent_screenshot_task", handleScreenshot);
}, [id]);
window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {};
window.__BorealisInstructionNodes[id] = () => ({
node_id: id,
role: "screenshot",
interval,
visible,
alias,
...region
});
return (
<div className="borealis-node" style={{ position: "relative" }}>
<Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">Agent Role: Screenshot</div>
<div className="borealis-node-content" style={{ fontSize: "9px" }}>
<label>Update Interval (ms):</label>
<input
type="number"
min="100"
step="100"
value={interval}
onChange={(e) => setInterval(Number(e.target.value))}
style={{ width: "100%", marginBottom: "4px" }}
/>
<label>Region X / Y / W / H:</label>
<div style={{ display: "flex", gap: "4px", marginBottom: "4px" }}>
<input type="number" value={region.x} onChange={(e) => setRegion({ ...region, x: Number(e.target.value) })} style={{ width: "25%" }} />
<input type="number" value={region.y} onChange={(e) => setRegion({ ...region, y: Number(e.target.value) })} style={{ width: "25%" }} />
<input type="number" value={region.w} onChange={(e) => setRegion({ ...region, w: Number(e.target.value) })} style={{ width: "25%" }} />
<input type="number" value={region.h} onChange={(e) => setRegion({ ...region, h: Number(e.target.value) })} style={{ width: "25%" }} />
</div>
<div style={{ marginBottom: "4px" }}>
<label>
<input
type="checkbox"
checked={visible}
onChange={() => setVisible(!visible)}
style={{ marginRight: "4px" }}
/>
Show Overlay on Agent
</label>
</div>
<label>Overlay Label:</label>
<input
type="text"
value={alias}
onChange={(e) => setAlias(e.target.value)}
placeholder="Label (optional)"
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
/>
<div style={{ textAlign: "center", fontSize: "8px", color: "#aaa" }}>
{imageBase64
? `Last image: ${Math.round(imageBase64.length / 1024)} KB`
: "Awaiting Screenshot Data..."}
</div>
</div>
<div style={{ position: "absolute", top: 4, right: 4 }}>
<IconButton size="small" onClick={handleCopyLiveViewLink}>
<ShareIcon style={{ fontSize: 14 }} />
</IconButton>
</div>
</div>
);
};
export default {
type: "Agent_Role_Screenshot",
label: "Agent Role: Screenshot",
description: `
Agent Role Node: Screenshot Region
- Defines a single region capture role
- Allows custom update interval and overlay
- Emits captured base64 PNG data from agent
`.trim(),
content: "Capture screenshot region via agent",
component: ScreenshotInstructionNode
};

View File

@ -233,7 +233,7 @@ const numberInputStyle = {
export default {
type: "OCR_Text_Extraction",
label: "OCR-Based Text Extraction",
label: "OCR Text Extraction",
description: `
Extract text from upstream image using backend OCR engine via API.
Includes rate limiting and sensitivity detection for smart processing.`,

View File

@ -1,172 +0,0 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: Node_Borealis_Agent.jsx
import React, { useEffect, useState, useRef } from "react";
import { Handle, Position, useReactFlow } from "reactflow";
import { io } from "socket.io-client";
const socket = io(window.location.origin, {
transports: ["websocket"]
});
const BorealisAgentNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const [agents, setAgents] = useState([]);
const [selectedAgent, setSelectedAgent] = useState(data.agent_id || "");
const [selectedType, setSelectedType] = useState(data.data_type || "screenshot");
const [intervalMs, setIntervalMs] = useState(data.interval || 1000);
const [paused, setPaused] = useState(false);
const [overlayVisible, setOverlayVisible] = useState(true);
const [imageData, setImageData] = useState("");
const imageRef = useRef("");
useEffect(() => {
fetch("/api/agents").then(res => res.json()).then(setAgents);
const interval = setInterval(() => {
fetch("/api/agents").then(res => res.json()).then(setAgents);
}, 5000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
socket.on('new_screenshot', (data) => {
if (data.agent_id === selectedAgent) {
setImageData(data.image_base64);
imageRef.current = data.image_base64;
}
});
return () => socket.off('new_screenshot');
}, [selectedAgent]);
useEffect(() => {
const interval = setInterval(() => {
if (!paused && imageRef.current) {
window.BorealisValueBus = window.BorealisValueBus || {};
window.BorealisValueBus[id] = imageRef.current;
setNodes(nds => {
const updated = [...nds];
const node = updated.find(n => n.id === id);
if (node) {
node.data = {
...node.data,
value: imageRef.current
};
}
return updated;
});
}
}, window.BorealisUpdateRate || 100);
return () => clearInterval(interval);
}, [id, paused, setNodes]);
const provisionAgent = () => {
if (!selectedAgent) return;
fetch("/api/agent/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_id: selectedAgent,
x: 250,
y: 100,
w: 300,
h: 200,
interval: intervalMs,
visible: overlayVisible,
task: selectedType
})
})
.then(res => res.json())
.then(() => {
console.log("[DEBUG] Agent provisioned");
});
};
const toggleOverlay = () => {
const newVisibility = !overlayVisible;
setOverlayVisible(newVisibility);
provisionAgent();
};
return (
<div className="borealis-node">
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">Borealis Agent</div>
<div className="borealis-node-content">
<label style={{ fontSize: "10px" }}>Agent:</label>
<select
value={selectedAgent}
onChange={(e) => setSelectedAgent(e.target.value)}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
>
<option value="">-- Select --</option>
{Object.entries(agents).map(([id, info]) => {
const statusLabel = info.status === "provisioned"
? "(Provisioned)"
: "(Not Provisioned)";
return (
<option key={id} value={id}>
{id} {statusLabel}
</option>
);
})}
</select>
<label style={{ fontSize: "10px" }}>Data Type:</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
>
<option value="screenshot">Screenshot Region</option>
</select>
<label style={{ fontSize: "10px" }}>Update Rate (ms):</label>
<input
type="number"
min="100"
step="100"
value={intervalMs}
onChange={(e) => setIntervalMs(Number(e.target.value))}
style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }}
/>
<div style={{ marginBottom: "6px" }}>
<label style={{ fontSize: "10px" }}>
<input
type="checkbox"
checked={paused}
onChange={() => setPaused(!paused)}
style={{ marginRight: "4px" }}
/>
Pause Data Collection
</label>
</div>
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
<button
style={{ flex: 1, fontSize: "9px" }}
onClick={provisionAgent}
>
(Re)Provision
</button>
<button
style={{ flex: 1, fontSize: "9px" }}
onClick={toggleOverlay}
>
{overlayVisible ? "Hide Overlay" : "Show Overlay"}
</button>
</div>
</div>
</div>
);
};
export default {
type: "Borealis_Agent",
label: "Borealis Agent",
description: "Connects to and controls a Borealis Agent via WebSocket in real-time.",
content: "Provisions a Borealis Agent and streams collected data into the workflow graph.",
component: BorealisAgentNode
};