- Implemented Agent-based Data Collection Nodes
- Added More Dark Theming throughout Borealis - Added API Data Collector Node - Added Image Viewer Node - Added Agent Deployment Script (Powershell)
This commit is contained in:
parent
dca79b8556
commit
a75c472c98
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
.vs/
|
||||
Borealis-Workflow-Automation-Tool/
|
||||
Borealis-Workflow-Automation-Tool/
|
||||
Borealis-API-Collector-Agent/
|
205
Data/Agent/api-collector-agent.py
Normal file
205
Data/Agent/api-collector-agent.py
Normal file
@ -0,0 +1,205 @@
|
||||
import sys
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import threading
|
||||
import requests
|
||||
from io import BytesIO
|
||||
import socket
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PIL import ImageGrab
|
||||
|
||||
# ---------------- Configuration ----------------
|
||||
SERVER_URL = "http://localhost:5000"
|
||||
CHECKIN_ENDPOINT = f"{SERVER_URL}/api/agent/checkin"
|
||||
CONFIG_ENDPOINT = f"{SERVER_URL}/api/agent/config"
|
||||
DATA_POST_ENDPOINT = f"{SERVER_URL}/api/agent/data"
|
||||
|
||||
HOSTNAME = socket.gethostname().lower()
|
||||
RANDOM_SUFFIX = uuid.uuid4().hex[:8]
|
||||
AGENT_ID = f"{HOSTNAME}-agent-{RANDOM_SUFFIX}"
|
||||
|
||||
CONFIG_POLL_INTERVAL = 5
|
||||
|
||||
# ---------------- State ----------------
|
||||
app_instance = None
|
||||
region_widget = None
|
||||
capture_thread_started = False
|
||||
current_interval = 1000
|
||||
config_ready = threading.Event()
|
||||
overlay_visible = True
|
||||
|
||||
# ---------------- Signal Bridge ----------------
|
||||
class RegionLauncher(QtCore.QObject):
|
||||
trigger = QtCore.pyqtSignal(int, int, int, int)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.trigger.connect(self.handle)
|
||||
|
||||
def handle(self, x, y, w, h):
|
||||
launch_region(x, y, w, h)
|
||||
|
||||
region_launcher = None
|
||||
|
||||
# ---------------- Agent Networking ----------------
|
||||
def check_in():
|
||||
try:
|
||||
requests.post(CHECKIN_ENDPOINT, json={"agent_id": AGENT_ID, "hostname": HOSTNAME})
|
||||
print(f"[INFO] Agent ID: {AGENT_ID}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Check-in failed: {e}")
|
||||
|
||||
def poll_for_config():
|
||||
try:
|
||||
res = requests.get(CONFIG_ENDPOINT, params={"agent_id": AGENT_ID})
|
||||
if res.status_code == 200:
|
||||
return res.json()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Config polling failed: {e}")
|
||||
return None
|
||||
|
||||
def send_image_data(image):
|
||||
try:
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format="PNG")
|
||||
encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
response = requests.post(DATA_POST_ENDPOINT, json={
|
||||
"agent_id": AGENT_ID,
|
||||
"type": "screenshot",
|
||||
"image_base64": encoded
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"[ERROR] Screenshot POST failed: {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to send image: {e}")
|
||||
|
||||
# ---------------- Region Overlay ----------------
|
||||
class ScreenshotRegion(QtWidgets.QWidget):
|
||||
def __init__(self, x=100, y=100, w=300, h=200):
|
||||
super().__init__()
|
||||
self.setGeometry(x, y, w, h)
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.drag_offset = None
|
||||
self.resizing = False
|
||||
self.resize_handle_size = 12
|
||||
self.setVisible(True)
|
||||
|
||||
self.label = QtWidgets.QLabel(self)
|
||||
self.label.setText(AGENT_ID)
|
||||
self.label.setStyleSheet("color: lime; background: transparent; font-size: 10px;")
|
||||
self.label.move(8, 4)
|
||||
|
||||
self.setMouseTracking(True)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
# Transparent fill
|
||||
painter.setBrush(QtCore.Qt.transparent)
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor(0, 255, 0), 2))
|
||||
painter.drawRect(self.rect())
|
||||
|
||||
# Resize Handle Visual (Bottom-Right)
|
||||
handle_rect = QtCore.QRect(
|
||||
self.width() - self.resize_handle_size,
|
||||
self.height() - self.resize_handle_size,
|
||||
self.resize_handle_size,
|
||||
self.resize_handle_size
|
||||
)
|
||||
painter.fillRect(handle_rect, QtGui.QColor(0, 255, 0))
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
if (event.pos().x() > self.width() - self.resize_handle_size and
|
||||
event.pos().y() > self.height() - self.resize_handle_size):
|
||||
self.resizing = True
|
||||
else:
|
||||
self.drag_offset = event.globalPos() - self.frameGeometry().topLeft()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.resizing:
|
||||
new_width = max(event.pos().x(), 100)
|
||||
new_height = max(event.pos().y(), 80)
|
||||
self.resize(new_width, new_height)
|
||||
elif event.buttons() & QtCore.Qt.LeftButton and self.drag_offset:
|
||||
self.move(event.globalPos() - self.drag_offset)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self.resizing = False
|
||||
self.drag_offset = None
|
||||
|
||||
def get_geometry(self):
|
||||
geo = self.geometry()
|
||||
return geo.x(), geo.y(), geo.width(), geo.height()
|
||||
|
||||
|
||||
# ---------------- Threads ----------------
|
||||
def capture_loop():
|
||||
global current_interval
|
||||
print("[INFO] Screenshot capture loop started")
|
||||
config_ready.wait()
|
||||
while region_widget is None:
|
||||
print("[WAIT] Waiting for region widget to initialize...")
|
||||
time.sleep(0.2)
|
||||
|
||||
print(f"[INFO] Agent Capturing Region: x:{region_widget.x()} y:{region_widget.y()} w:{region_widget.width()} h:{region_widget.height()}")
|
||||
|
||||
while True:
|
||||
if overlay_visible:
|
||||
x, y, w, h = region_widget.get_geometry()
|
||||
try:
|
||||
img = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
||||
send_image_data(img)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Screenshot error: {e}")
|
||||
time.sleep(current_interval / 1000)
|
||||
|
||||
def config_loop():
|
||||
global region_widget, capture_thread_started, current_interval, overlay_visible
|
||||
check_in()
|
||||
while True:
|
||||
config = poll_for_config()
|
||||
if config and config.get("task") == "screenshot":
|
||||
print("[PROVISIONING] Agent Provisioning Command Issued by Borealis")
|
||||
x = config.get("x", 100)
|
||||
y = config.get("y", 100)
|
||||
w = config.get("w", 300)
|
||||
h = config.get("h", 200)
|
||||
current_interval = config.get("interval", 1000)
|
||||
overlay_visible = config.get("visible", True)
|
||||
|
||||
print(f"[PROVISIONING] Agent Configured as \"Screenshot\" Collector w/ Polling Rate of <{current_interval/1000:.1f}s>")
|
||||
|
||||
if not region_widget:
|
||||
region_launcher.trigger.emit(x, y, w, h)
|
||||
elif region_widget:
|
||||
region_widget.setVisible(overlay_visible)
|
||||
|
||||
if not capture_thread_started:
|
||||
threading.Thread(target=capture_loop, daemon=True).start()
|
||||
capture_thread_started = True
|
||||
|
||||
config_ready.set()
|
||||
time.sleep(CONFIG_POLL_INTERVAL)
|
||||
|
||||
def launch_region(x, y, w, h):
|
||||
global region_widget
|
||||
if region_widget:
|
||||
return
|
||||
print(f"[INFO] Agent Starting...")
|
||||
region_widget = ScreenshotRegion(x, y, w, h)
|
||||
region_widget.show()
|
||||
|
||||
# ---------------- Main ----------------
|
||||
if __name__ == "__main__":
|
||||
app_instance = QtWidgets.QApplication(sys.argv)
|
||||
region_launcher = RegionLauncher()
|
||||
threading.Thread(target=config_loop, daemon=True).start()
|
||||
sys.exit(app_instance.exec_())
|
3
Data/Agent/requirements.txt
Normal file
3
Data/Agent/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
requests
|
||||
PyQt5
|
||||
Pillow
|
@ -451,7 +451,7 @@ export default function App() {
|
||||
}}
|
||||
sx={{ color: "#58a6ff", borderColor: "#58a6ff", fontSize: "0.75rem", textTransform: "none", px: 1.5 }}
|
||||
>
|
||||
Update Rate
|
||||
Apply Rate
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -61,4 +61,18 @@
|
||||
background: #58a6ff;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Global dark form inputs */
|
||||
input, select, button {
|
||||
background-color: #2a2a2a;
|
||||
color: #ccc;
|
||||
border: 1px solid #444;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Label / Dark Text styling */
|
||||
label {
|
||||
color: #aaa;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
177
Data/WebUI/src/nodes/Data Collection/API_Data_Collector.jsx
Normal file
177
Data/WebUI/src/nodes/Data Collection/API_Data_Collector.jsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Handle, Position, useReactFlow } from "reactflow";
|
||||
|
||||
const APINode = ({ 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 [imageData, setImageData] = useState("");
|
||||
const [intervalMs, setIntervalMs] = useState(data.interval || 1000);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [overlayVisible, setOverlayVisible] = useState(true);
|
||||
|
||||
// Refresh agents every 5s
|
||||
useEffect(() => {
|
||||
const fetchAgents = () => fetch("/api/agents").then(res => res.json()).then(setAgents);
|
||||
fetchAgents();
|
||||
const interval = setInterval(fetchAgents, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Pull image if agent provisioned
|
||||
useEffect(() => {
|
||||
if (!selectedAgent || paused) return;
|
||||
const interval = setInterval(() => {
|
||||
fetch(`/api/agent/image?agent_id=${selectedAgent}`)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
if (json.image_base64) {
|
||||
setImageData(json.image_base64);
|
||||
window.BorealisValueBus = window.BorealisValueBus || {};
|
||||
window.BorealisValueBus[id] = json.image_base64;
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
}, intervalMs);
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedAgent, id, paused, intervalMs]);
|
||||
|
||||
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(() => {
|
||||
setNodes(nds =>
|
||||
nds.map(n => n.id === id
|
||||
? {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
agent_id: selectedAgent,
|
||||
data_type: selectedType,
|
||||
interval: intervalMs
|
||||
}
|
||||
}
|
||||
: n
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const resetAgent = () => {
|
||||
if (!selectedAgent) return;
|
||||
fetch("/api/agent/reset", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ agent_id: selectedAgent })
|
||||
}).then(() => {
|
||||
setSelectedAgent("");
|
||||
});
|
||||
};
|
||||
|
||||
const toggleOverlay = () => {
|
||||
const newVisibility = !overlayVisible;
|
||||
setOverlayVisible(newVisibility);
|
||||
if (selectedAgent) {
|
||||
fetch("/api/agent/overlay_visibility", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ agent_id: selectedAgent, visible: newVisibility })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="borealis-node">
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
<div className="borealis-node-header">API Data Collector</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]) => (
|
||||
<option key={id} value={id} disabled={info.status === "provisioned"}>
|
||||
{id} {info.status === "provisioned" ? "(Adopted)" : ""}
|
||||
</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}
|
||||
>
|
||||
Provision
|
||||
</button>
|
||||
<button
|
||||
style={{ flex: 1, fontSize: "9px" }}
|
||||
onClick={resetAgent}
|
||||
>
|
||||
Reset Agent
|
||||
</button>
|
||||
<button
|
||||
style={{ flex: 1, fontSize: "9px" }}
|
||||
onClick={toggleOverlay}
|
||||
>
|
||||
{overlayVisible ? "Hide Overlay" : "Show Overlay"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "API_Data_Collector",
|
||||
label: "API Data Collector",
|
||||
description: "Connects to a remote agent via API and collects data such as screenshots, OCR results, and more.",
|
||||
content: "Publishes agent-collected data into the workflow ValueBus.",
|
||||
component: APINode
|
||||
};
|
60
Data/WebUI/src/nodes/Image Processing/Image_Viewer.jsx
Normal file
60
Data/WebUI/src/nodes/Image Processing/Image_Viewer.jsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Handle, Position, useReactFlow } from "reactflow";
|
||||
|
||||
const ImageViewerNode = ({ id, data }) => {
|
||||
const { getEdges } = useReactFlow();
|
||||
const [imageBase64, setImageBase64] = useState("");
|
||||
const [selectedType, setSelectedType] = useState("base64");
|
||||
|
||||
// Watch upstream value
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const edges = getEdges();
|
||||
const inputEdge = edges.find(e => e.target === id);
|
||||
if (inputEdge) {
|
||||
const sourceId = inputEdge.source;
|
||||
const valueBus = window.BorealisValueBus || {};
|
||||
const value = valueBus[sourceId];
|
||||
if (typeof value === "string") {
|
||||
setImageBase64(value);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [id, getEdges]);
|
||||
|
||||
return (
|
||||
<div className="borealis-node">
|
||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||
<div className="borealis-node-header">Image Viewer</div>
|
||||
<div className="borealis-node-content">
|
||||
<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="base64">Base64 Encoded Image</option>
|
||||
</select>
|
||||
|
||||
{imageBase64 ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${imageBase64}`}
|
||||
alt="Live"
|
||||
style={{ width: "100%", border: "1px solid #333", marginTop: "6px" }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ fontSize: "9px", color: "#888" }}>Waiting for image...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "Image_Viewer",
|
||||
label: "Image Viewer",
|
||||
description: "Displays base64 image pulled from ValueBus of upstream node.",
|
||||
content: "Visual preview of base64 image",
|
||||
component: ImageViewerNode
|
||||
};
|
@ -32,7 +32,8 @@ const BackdropGroupBoxNode = ({ id, data }) => {
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleTitleClick = () => {
|
||||
const handleTitleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
@ -47,22 +48,32 @@ const BackdropGroupBoxNode = ({ id, data }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ pointerEvents: "auto", zIndex: -1 }}> {/* Prevent blocking other nodes */}
|
||||
<div style={{ pointerEvents: "auto" }}>
|
||||
<ResizableBox
|
||||
width={200}
|
||||
height={120}
|
||||
minConstraints={[120, 80]}
|
||||
maxConstraints={[600, 600]}
|
||||
resizeHandles={["se"]}
|
||||
className="borealis-node"
|
||||
handle={(h) => (
|
||||
<span
|
||||
className={`react-resizable-handle react-resizable-handle-${h}`}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
backgroundColor: "rgba(44, 44, 44, 0.5)",
|
||||
border: "1px solid #3a3a3a",
|
||||
borderRadius: "4px",
|
||||
boxShadow: "0 0 5px rgba(88, 166, 255, 0.15)",
|
||||
overflow: "hidden",
|
||||
position: "relative"
|
||||
position: "relative",
|
||||
zIndex: 0
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()} // prevent drag on resize
|
||||
>
|
||||
<div
|
||||
onClick={handleTitleClick}
|
||||
@ -82,6 +93,8 @@ const BackdropGroupBoxNode = ({ id, data }) => {
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
onBlur={handleBlur}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
padding: "2px",
|
||||
|
104
Data/server.py
104
Data/server.py
@ -1,7 +1,10 @@
|
||||
from flask import Flask, send_from_directory
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
import time
|
||||
import os
|
||||
|
||||
# Determine the absolute path for the React build folder
|
||||
# ---------------------------------------------
|
||||
# React Frontend Hosting Configuration
|
||||
# ---------------------------------------------
|
||||
build_folder = os.path.join(os.getcwd(), "web-interface", "build")
|
||||
if not os.path.exists(build_folder):
|
||||
print("WARNING: web-interface build folder not found. Please build your React app.")
|
||||
@ -9,12 +12,103 @@ if not os.path.exists(build_folder):
|
||||
app = Flask(__name__, static_folder=build_folder, static_url_path="/")
|
||||
|
||||
@app.route("/")
|
||||
def serve_frontend():
|
||||
"""Serve the React app."""
|
||||
def serve_index():
|
||||
index_path = os.path.join(build_folder, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
return send_from_directory(app.static_folder, "index.html")
|
||||
return send_from_directory(build_folder, "index.html")
|
||||
return "<h1>Borealis React App Code Not Found</h1><p>Please re-deploy Borealis Workflow Automation Tool</p>", 404
|
||||
|
||||
# Wildcard route to serve React for sub-routes (e.g., /workflow)
|
||||
@app.route("/<path:path>")
|
||||
def serve_react_app(path):
|
||||
full_path = os.path.join(build_folder, path)
|
||||
if os.path.exists(full_path):
|
||||
return send_from_directory(build_folder, path)
|
||||
return send_from_directory(build_folder, "index.html")
|
||||
|
||||
# ---------------------------------------------
|
||||
# Borealis Agent API Endpoints
|
||||
# ---------------------------------------------
|
||||
registered_agents = {}
|
||||
agent_configurations = {}
|
||||
latest_images = {}
|
||||
|
||||
@app.route("/api/agent/checkin", methods=["POST"])
|
||||
def agent_checkin():
|
||||
data = request.json
|
||||
agent_id = data.get("agent_id")
|
||||
hostname = data.get("hostname", "unknown")
|
||||
|
||||
registered_agents[agent_id] = {
|
||||
"agent_id": agent_id,
|
||||
"hostname": hostname,
|
||||
"last_seen": time.time(),
|
||||
"status": "orphaned" if agent_id not in agent_configurations else "provisioned"
|
||||
}
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route("/api/agent/reset", methods=["POST"])
|
||||
def reset_agent():
|
||||
agent_id = request.json.get("agent_id")
|
||||
if agent_id in agents:
|
||||
agents[agent_id]["status"] = "orphaned"
|
||||
agents[agent_id]["config"] = None
|
||||
latest_images.pop(agent_id, None)
|
||||
return jsonify({"status": "reset"}), 200
|
||||
return jsonify({"error": "Agent not found"}), 404
|
||||
|
||||
@app.route("/api/agent/provision", methods=["POST"])
|
||||
def provision_agent():
|
||||
data = request.json
|
||||
agent_id = data.get("agent_id")
|
||||
config = {
|
||||
"task": "screenshot",
|
||||
"x": data.get("x", 100),
|
||||
"y": data.get("y", 100),
|
||||
"w": data.get("w", 300),
|
||||
"h": data.get("h", 200),
|
||||
"interval": data.get("interval", 1000)
|
||||
}
|
||||
agent_configurations[agent_id] = config
|
||||
if agent_id in registered_agents:
|
||||
registered_agents[agent_id]["status"] = "provisioned"
|
||||
return jsonify({"status": "provisioned"})
|
||||
|
||||
@app.route("/api/agent/config")
|
||||
def get_agent_config():
|
||||
agent_id = request.args.get("agent_id")
|
||||
config = agent_configurations.get(agent_id)
|
||||
return jsonify(config or {})
|
||||
|
||||
@app.route("/api/agent/data", methods=["POST"])
|
||||
def agent_data():
|
||||
data = request.json
|
||||
agent_id = data.get("agent_id")
|
||||
image = data.get("image_base64")
|
||||
|
||||
if not agent_id or not image:
|
||||
return jsonify({"error": "Missing data"}), 400
|
||||
|
||||
latest_images[agent_id] = {
|
||||
"image_base64": image,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
return jsonify({"status": "received"})
|
||||
|
||||
@app.route("/api/agent/image")
|
||||
def get_latest_image():
|
||||
agent_id = request.args.get("agent_id")
|
||||
entry = latest_images.get(agent_id)
|
||||
if entry:
|
||||
return jsonify(entry)
|
||||
return jsonify({"error": "No image"}), 404
|
||||
|
||||
@app.route("/api/agents")
|
||||
def get_agents():
|
||||
return jsonify(registered_agents)
|
||||
|
||||
# ---------------------------------------------
|
||||
# Server Start
|
||||
# ---------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
|
86
Launch-API-Collector-Agent.ps1
Normal file
86
Launch-API-Collector-Agent.ps1
Normal file
@ -0,0 +1,86 @@
|
||||
# Launch-API-Collector-Agent.ps1
|
||||
# Run this script with:
|
||||
# Set-ExecutionPolicy Unrestricted -Scope Process; .\Launch-API-Collector-Agent.ps1
|
||||
|
||||
# ---------------------- Initialization & Visuals ----------------------
|
||||
$symbols = @{
|
||||
Success = [char]0x2705
|
||||
Running = [char]0x23F3
|
||||
Fail = [char]0x274C
|
||||
Info = [char]0x2139
|
||||
}
|
||||
|
||||
function Write-ProgressStep {
|
||||
param (
|
||||
[string]$Message,
|
||||
[string]$Status = $symbols["Info"]
|
||||
)
|
||||
Write-Host "`r$Status $Message... " -NoNewline
|
||||
}
|
||||
|
||||
function Run-Step {
|
||||
param (
|
||||
[string]$Message,
|
||||
[scriptblock]$Script
|
||||
)
|
||||
Write-ProgressStep -Message $Message -Status "$($symbols.Running)"
|
||||
try {
|
||||
& $Script
|
||||
if ($LASTEXITCODE -eq 0 -or $?) {
|
||||
Write-Host "`r$($symbols.Success) $Message "
|
||||
} else {
|
||||
throw "Non-zero exit code"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "`r$($symbols.Fail) $Message - Failed: $_ " -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
Write-Host "Deploying Borealis API Collector Agent..." -ForegroundColor Green
|
||||
Write-Host "===================================================================================="
|
||||
|
||||
# ---------------------- Path Definitions ----------------------
|
||||
$venvFolder = "Borealis-API-Collector-Agent"
|
||||
$agentSourcePath = "Data\Agent\api-collector-agent.py"
|
||||
$agentRequirements = "Data\Agent\requirements.txt"
|
||||
$agentDestinationFolder = "$venvFolder\Agent"
|
||||
$agentDestinationFile = "$agentDestinationFolder\api-collector-agent.py"
|
||||
|
||||
# ---------------------- Create Python Virtual Environment & Copy Agent ----------------------
|
||||
Run-Step "Create Virtual Python Environment for Collector Agent" {
|
||||
if (!(Test-Path "$venvFolder\Scripts\Activate")) {
|
||||
python -m venv $venvFolder | Out-Null
|
||||
}
|
||||
|
||||
# Copy Agent Script
|
||||
if (Test-Path $agentSourcePath) {
|
||||
if (Test-Path $agentDestinationFolder) {
|
||||
Remove-Item -Recurse -Force $agentDestinationFolder | Out-Null
|
||||
}
|
||||
New-Item -Path $agentDestinationFolder -ItemType Directory -Force | Out-Null
|
||||
Copy-Item -Path $agentSourcePath -Destination $agentDestinationFile -Force
|
||||
} else {
|
||||
Write-Host "`r$($symbols.Info) Warning: Agent script not found at '$agentSourcePath', skipping copy." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
. "$venvFolder\Scripts\Activate"
|
||||
}
|
||||
|
||||
# ---------------------- Install Python Dependencies ----------------------
|
||||
Run-Step "Install Python Dependencies for Collector Agent" {
|
||||
if (Test-Path $agentRequirements) {
|
||||
pip install -q -r $agentRequirements 2>&1 | Out-Null
|
||||
} else {
|
||||
Write-Host "`r$($symbols.Info) Agent-specific requirements.txt not found at '$agentRequirements', skipping Python packages." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------- Launch Agent ----------------------
|
||||
Push-Location $venvFolder
|
||||
Write-Host "`nLaunching Borealis API Collector Agent..." -ForegroundColor Green
|
||||
Write-Host "===================================================================================="
|
||||
Write-Host "$($symbols.Running) Starting Agent..." -NoNewline
|
||||
python "Agent\api-collector-agent.py"
|
||||
Pop-Location
|
@ -54,15 +54,12 @@ $dataDestination = "$venvFolder\Borealis"
|
||||
$customUIPath = "$dataSource\WebUI"
|
||||
$webUIDestination = "$venvFolder\web-interface"
|
||||
|
||||
# ---------------------- Create Python Virtual Environment ----------------------
|
||||
Run-Step "Create Virtual Python Environment" {
|
||||
# ---------------------- Create Python Virtual Environment & Prepare Borealis Files ----------------------
|
||||
Run-Step "Create Borealis Virtual Python Environment" {
|
||||
if (!(Test-Path "$venvFolder\Scripts\Activate")) {
|
||||
python -m venv $venvFolder | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------- Copy Server Data ----------------------
|
||||
Run-Step "Copy Borealis Server Data into Virtual Python Environment" {
|
||||
# ---------------------- Copy Server Data ----------------------
|
||||
if (Test-Path $dataSource) {
|
||||
if (Test-Path $dataDestination) {
|
||||
Remove-Item -Recurse -Force $dataDestination | Out-Null
|
||||
@ -72,31 +69,24 @@ Run-Step "Copy Borealis Server Data into Virtual Python Environment" {
|
||||
} else {
|
||||
Write-Host "`r$($symbols.Info) Warning: Data folder not found, skipping copy." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------- React UI Deployment ----------------------
|
||||
Run-Step "Create a new ReactJS App in $webUIDestination" {
|
||||
# ---------------------- React UI Deployment ----------------------
|
||||
if (-not (Test-Path $webUIDestination)) {
|
||||
npx create-react-app $webUIDestination | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Overwrite ReactJS App Files with Borealis ReactJS Files" {
|
||||
if (Test-Path $customUIPath) {
|
||||
Copy-Item -Path "$customUIPath\*" -Destination $webUIDestination -Recurse -Force
|
||||
} else {
|
||||
Write-Host "`r$($symbols.Info) No custom UI found, using default React app." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Remove Existing ReactJS Build Folder (If Exists)" {
|
||||
# Remove Pre-Existing ReactJS Build Folder (If one Exists)
|
||||
if (Test-Path "$webUIDestination\build") {
|
||||
Remove-Item -Path "$webUIDestination\build" -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------- Activate Python Virtual Environment ----------------------
|
||||
Run-Step "Activate Virtual Python Environment" {
|
||||
# ---------------------- Activate Python Virtual Environment ----------------------
|
||||
. "$venvFolder\Scripts\Activate"
|
||||
}
|
||||
|
||||
@ -110,39 +100,32 @@ Run-Step "Install Python Dependencies into Virtual Python Environment" {
|
||||
}
|
||||
|
||||
# ---------------------- Build React App ----------------------
|
||||
Run-Step "ReactJS App: Install NPM" {
|
||||
Run-Step "ReactJS Web Frontend: Install Necessary NPM Packages" {
|
||||
$packageJsonPath = Join-Path $webUIDestination "package.json"
|
||||
if (Test-Path $packageJsonPath) {
|
||||
Push-Location $webUIDestination
|
||||
$env:npm_config_loglevel = "silent"
|
||||
|
||||
# Install NPM
|
||||
npm install --silent --no-fund --audit=false 2>&1 | Out-Null
|
||||
|
||||
# Install React Resizable
|
||||
npm install --silent react-resizable --no-fund --audit=false | Out-Null
|
||||
|
||||
# Install React Flow
|
||||
npm install --silent reactflow --no-fund --audit=false | Out-Null
|
||||
|
||||
# Install Material UI Libraries
|
||||
npm install --silent @mui/material @mui/icons-material @emotion/react @emotion/styled --no-fund --audit=false 2>&1 | Out-Null
|
||||
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "ReactJS App: Install React Resizable" {
|
||||
Run-Step "ReactJS Web Frontend: Build App" {
|
||||
Push-Location $webUIDestination
|
||||
npm install react-resizable --no-fund --audit=false | Out-Null
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Run-Step "ReactJS App: Install React Flow" {
|
||||
Push-Location $webUIDestination
|
||||
npm install reactflow --no-fund --audit=false | Out-Null
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Run-Step "ReactJS App: Install Material UI Libraries" {
|
||||
Push-Location $webUIDestination
|
||||
$env:npm_config_loglevel = "silent" # Force NPM to be completely silent
|
||||
npm install --silent @mui/material @mui/icons-material @emotion/react @emotion/styled --no-fund --audit=false 2>&1 | Out-Null
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Run-Step "ReactJS App: Building App" {
|
||||
Push-Location $webUIDestination
|
||||
#npm run build | Out-Null
|
||||
npm run build
|
||||
#npm run build | Out-Null # Suppress Compilation Output
|
||||
npm run build # Enabled during Development
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
@ -150,7 +133,6 @@ Run-Step "ReactJS App: Building App" {
|
||||
Push-Location $venvFolder
|
||||
Write-Host "`nLaunching Borealis..." -ForegroundColor Green
|
||||
Write-Host "===================================================================================="
|
||||
Write-Host "$($symbols.Running) Starting the Python Flask server..." -NoNewline
|
||||
Write-Host "$($symbols.Running) Starting Python Flask Server..." -NoNewline
|
||||
python "Borealis\server.py"
|
||||
Write-Host "`r$($symbols.Success) Borealis Launched Successfully!"
|
||||
Pop-Location
|
||||
|
@ -1,80 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-1743304886846",
|
||||
"type": "custom",
|
||||
"position": {
|
||||
"x": 214.63333129882812,
|
||||
"y": 146.66666666666666
|
||||
},
|
||||
"data": {
|
||||
"label": "Custom Node",
|
||||
"content": "Placeholder"
|
||||
},
|
||||
"width": 160,
|
||||
"height": 72
|
||||
},
|
||||
{
|
||||
"id": "node-1743304888268",
|
||||
"type": "custom",
|
||||
"position": {
|
||||
"x": 621.2999979654948,
|
||||
"y": 276.6666666666667
|
||||
},
|
||||
"data": {
|
||||
"label": "Custom Node",
|
||||
"content": "Placeholder"
|
||||
},
|
||||
"width": 160,
|
||||
"height": 72
|
||||
},
|
||||
{
|
||||
"id": "node-1743304891251",
|
||||
"type": "custom",
|
||||
"position": {
|
||||
"x": 814.6333312988281,
|
||||
"y": 65.33333333333334
|
||||
},
|
||||
"data": {
|
||||
"label": "Custom Node",
|
||||
"content": "Placeholder"
|
||||
},
|
||||
"width": 160,
|
||||
"height": 72,
|
||||
"selected": true,
|
||||
"positionAbsolute": {
|
||||
"x": 814.6333312988281,
|
||||
"y": 65.33333333333334
|
||||
},
|
||||
"dragging": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "node-1743304886846",
|
||||
"sourceHandle": null,
|
||||
"target": "node-1743304888268",
|
||||
"targetHandle": null,
|
||||
"type": "smoothstep",
|
||||
"animated": true,
|
||||
"style": {
|
||||
"strokeDasharray": "6 3",
|
||||
"stroke": "#58a6ff"
|
||||
},
|
||||
"id": "reactflow__edge-node-1743304886846-node-1743304888268"
|
||||
},
|
||||
{
|
||||
"source": "node-1743304886846",
|
||||
"sourceHandle": null,
|
||||
"target": "node-1743304891251",
|
||||
"targetHandle": null,
|
||||
"type": "smoothstep",
|
||||
"animated": true,
|
||||
"style": {
|
||||
"strokeDasharray": "6 3",
|
||||
"stroke": "#58a6ff"
|
||||
},
|
||||
"id": "reactflow__edge-node-1743304886846-node-1743304891251"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-1743545354663",
|
||||
"type": "dataNode",
|
||||
"position": {
|
||||
"x": 177.26666259765625,
|
||||
"y": 122.66666666666667
|
||||
},
|
||||
"data": {
|
||||
"label": "Data Node",
|
||||
"content": "Placeholder Node"
|
||||
},
|
||||
"width": 160,
|
||||
"height": 63
|
||||
},
|
||||
{
|
||||
"id": "node-1743545357071",
|
||||
"type": "dataNode",
|
||||
"position": {
|
||||
"x": 506.59999593098956,
|
||||
"y": 232
|
||||
},
|
||||
"data": {
|
||||
"label": "Data Node",
|
||||
"content": "Placeholder Node"
|
||||
},
|
||||
"width": 160,
|
||||
"height": 63,
|
||||
"selected": true,
|
||||
"positionAbsolute": {
|
||||
"x": 506.59999593098956,
|
||||
"y": 232
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"id": "node-1743545361694",
|
||||
"type": "ExportToCSVNode",
|
||||
"position": {
|
||||
"x": 187.2666625976562,
|
||||
"y": 316.66666666666663
|
||||
},
|
||||
"data": {
|
||||
"label": "Export to CSV",
|
||||
"content": "Placeholder Node"
|
||||
},
|
||||
"width": 160,
|
||||
"height": 63,
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 187.2666625976562,
|
||||
"y": 316.66666666666663
|
||||
},
|
||||
"dragging": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "node-1743545354663",
|
||||
"sourceHandle": null,
|
||||
"target": "node-1743545357071",
|
||||
"targetHandle": null,
|
||||
"type": "smoothstep",
|
||||
"animated": true,
|
||||
"style": {
|
||||
"strokeDasharray": "6 3",
|
||||
"stroke": "#58a6ff"
|
||||
},
|
||||
"id": "reactflow__edge-node-1743545354663-node-1743545357071"
|
||||
},
|
||||
{
|
||||
"source": "node-1743545354663",
|
||||
"sourceHandle": null,
|
||||
"target": "node-1743545361694",
|
||||
"targetHandle": null,
|
||||
"type": "smoothstep",
|
||||
"animated": true,
|
||||
"style": {
|
||||
"strokeDasharray": "6 3",
|
||||
"stroke": "#58a6ff"
|
||||
},
|
||||
"id": "reactflow__edge-node-1743545354663-node-1743545361694"
|
||||
}
|
||||
]
|
||||
}
|
@ -3,7 +3,7 @@ torch --index-url https://download.pytorch.org/whl/cu121
|
||||
torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||
torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||
|
||||
# Flask for API handling
|
||||
# Flask for API Hhandling
|
||||
Flask
|
||||
requests
|
||||
|
||||
@ -13,9 +13,11 @@ qtpy
|
||||
OdenGraphQt
|
||||
PyQt5
|
||||
|
||||
# Computer Vision & OCR dependencies
|
||||
# Computer Vision & OCR Dependencies
|
||||
numpy # Numerical operations
|
||||
opencv-python # Computer vision processing
|
||||
pytesseract # OCR engine
|
||||
easyocr # Deep-learning-based OCR
|
||||
Pillow # Image processing
|
||||
Pillow # Image processing
|
||||
|
||||
# API Collector Agent Dependencies
|
Loading…
x
Reference in New Issue
Block a user