diff --git a/modules/agents.py b/modules/agents.py index 662fe9e..7a85180 100644 --- a/modules/agents.py +++ b/modules/agents.py @@ -1,137 +1,1098 @@ -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * -from PyQt5.QtGui import * +import json +import urllib.error +import urllib.request +from pathlib import Path + +from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal +from PyQt5.QtGui import QDesktopServices, QFont +from PyQt5.QtWidgets import ( + QButtonGroup, + QComboBox, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QScrollArea, + QSizePolicy, + QSplitter, + QVBoxLayout, + QWidget, +) + from framework.warden_theme import * -from framework.warden_widgets import * -from core.agent_manager import agent_manager +from framework.warden_widgets import WardenBackground +from modules.agent_workspace import EmbeddedAgentWorkspace + + +ARMOR_ROOT = Path("/home/shire3d/ARMOR") +REGISTRY_FILE = ARMOR_ROOT / "config/agents/registry.json" +BONUS_ROOT = Path("/SHiREVault/BONUS_APPS") +AGENT_HUB_URL = "http://127.0.0.1:8770" + +TAILSCALE_IP = "100.116.93.90" + +SOURCE_AGENT_MAP = { + "FlexiForge_SHIRE_v0.2.0.zip": "flexiforge", + "limb-forge-canvas.zip": "covercanvas", + "shire-limb-forge.zip": "limbforge", + "shire-scan-forge-shire-agent-ready.zip": "scanforge", + "SHIRE-Marketing-Forge-Easy-v1.3.0.zip": "marketing-boss", + "SHIRE3D-Manager-Standalone-v0.1.0.zip": "shire3d-manager", + "SHiRE-Dental-Studio-Independent-v2.0.0.zip": "dental-studio", + "shire-link-prime-live-shell.zip": "link-prime", +} + +SYNTHETIC_AGENTS = [ + { + "id": "relay", + "name": "RELAY", + "nickname": "Remote Thingy", + "category": "Infrastructure and Devices", + "description": ( + "Private remote-device manager for SHiRE Mobile OTA releases, " + "approved phones, wearables and future SHiRE nodes." + ), + "declaredStatus": "STAGED_OTA_AND_DEVICE_BRIDGE_REQUIRED", + "enabled": False, + "route": "/agents/relay", + "localUrl": None, + "appPath": "agents/apps/relay", + }, + { + "id": "shire3d-manager", + "name": "SHiRE3D Manager", + "category": "Commerce and Operations", + "description": ( + "Orders, marketplace listings, pricing, shipping and product " + "operations source recovered from BONUS_APPS." + ), + "declaredStatus": "SOURCE_AVAILABLE_IN_BONUS_APPS", + "enabled": False, + "route": "/agents/shire3d-manager", + "localUrl": None, + "appPath": "BONUS_APPS/SHIRE3D-Manager-Standalone-v0.1.0.zip", + }, + { + "id": "dental-studio", + "name": "SHiRE Dental Studio", + "category": "Specialist Design", + "description": ( + "Independent dental-design workspace source. Clinical and " + "manufacturing functions remain safety-gated." + ), + "declaredStatus": "SOURCE_AVAILABLE_SAFETY_REVIEW_REQUIRED", + "enabled": False, + "route": "/agents/dental-studio", + "localUrl": None, + "appPath": "BONUS_APPS/SHiRE-Dental-Studio-Independent-v2.0.0.zip", + }, + { + "id": "link-prime", + "name": "SHiRE Link Prime", + "category": "Companion and Communication", + "description": ( + "Live SHiRE companion shell with chat, device connection, " + "printer selection and command-centre concepts." + ), + "declaredStatus": "SOURCE_AVAILABLE_BASE44_REMOVAL_REQUIRED", + "enabled": False, + "route": "/agents/link-prime", + "localUrl": None, + "appPath": "BONUS_APPS/shire-link-prime-live-shell.zip", + }, +] + + +def http_json(url, timeout=2.5): + try: + request = urllib.request.Request( + url, + headers={"Accept": "application/json"}, + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + return response.status, json.load(response) + except Exception as exc: + return 0, {"error": str(exc)} + + +def load_registry(): + try: + payload = json.loads(REGISTRY_FILE.read_text(encoding="utf-8")) + return payload.get("agents", []) + except Exception: + return [] + + +def source_files(): + if not BONUS_ROOT.is_dir(): + return {} + + result = {} + for filename, agent_id in SOURCE_AGENT_MAP.items(): + path = BONUS_ROOT / filename + if path.exists(): + result.setdefault(agent_id, []).append(path) + return result + + +def friendly_status(raw, enabled=False, online=False): + value = str(raw or "").upper() + + if online: + return "LIVE" + + if "ERROR" in value or "FAILED" in value: + return "ERROR" + + if "BLOCK" in value: + return "BLOCKED" + + if enabled: + return "READY" + + if "READY" in value and "REQUIRED" not in value: + return "READY" + + if "SOURCE_AVAILABLE" in value: + return "SOURCE" + + if "STAGED" in value or "REQUIRED" in value: + return "STAGED" + + return "OFFLINE" + + +def status_colour(status): + return { + "LIVE": "#20e887", + "READY": "#53b7ff", + "STAGED": "#b174ff", + "SOURCE": "#f2b84b", + "BLOCKED": "#ff8c42", + "ERROR": "#ff4d5f", + "OFFLINE": "#758391", + }.get(status, "#758391") + + +# ARMOR_AGENT_EMBED_0007F1 + + +def dedicated_workspace_url(data): + url = str( + data.get("workspaceUrl") + or "" + ).strip() + + if url: + return url + + agent_id = str( + data.get("id") + or "" + ) + + registry_path = ( + ARMOR_ROOT + / "config" + / "agents" + / "registry.json" + ) + + try: + registry = json.loads( + registry_path.read_text( + encoding="utf-8" + ) + ) + except Exception: + return "" + + for agent in registry.get("agents", []): + if agent.get("id") == agent_id: + return str( + agent.get("workspaceUrl") + or "" + ).strip() + + return "" + + +class AgentCard(QFrame): + selected = pyqtSignal(str) + workspace_requested = pyqtSignal(str) + + def __init__(self, agent_id): + super().__init__() + self.agent_id = agent_id + self.data = {} + self.setCursor(Qt.PointingHandCursor) + self.setMinimumHeight(190) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + root = QVBoxLayout(self) + root.setContentsMargins(17, 15, 17, 15) + root.setSpacing(8) + top = QHBoxLayout() -class AgentRow(WardenPanel): - def __init__(self, key, data): - super().__init__(state_colour(data.get("state", "standby"))) - self.key = key - self.setMinimumHeight(44) + self.icon = QLabel("◆") + self.icon.setFixedSize(42, 42) + self.icon.setAlignment(Qt.AlignCenter) - layout = QGridLayout() - layout.setContentsMargins(6, 3, 6, 3) - layout.setSpacing(2) + name_box = QVBoxLayout() + name_box.setSpacing(1) self.name = QLabel() - self.role = QLabel() + self.name.setWordWrap(True) + + self.category = QLabel() + self.category.setWordWrap(True) + + name_box.addWidget(self.name) + name_box.addWidget(self.category) + + self.status = QLabel() + self.status.setAlignment(Qt.AlignCenter) + self.status.setMinimumWidth(78) + + top.addWidget(self.icon) + top.addLayout(name_box, 1) + top.addWidget(self.status) + + self.description = QLabel() + self.description.setWordWrap(True) + self.description.setMaximumHeight(48) + self.task = QLabel() - self.state = QLabel() - self.progress = QLabel() - - self.name.setStyleSheet(f"color:{GREEN}; font-size:11px; font-weight:bold; border:none; background:transparent;") - self.role.setStyleSheet(f"color:{WHITE}; font-size:8px; font-weight:bold; border:none; background:transparent;") - self.task.setStyleSheet(f"color:{MUTED}; font-size:8px; font-weight:bold; border:none; background:transparent;") - self.state.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - self.progress.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - - layout.addWidget(self.name, 0, 0) - layout.addWidget(self.role, 0, 1) - layout.addWidget(self.state, 0, 2) - layout.addWidget(self.task, 1, 0, 1, 2) - layout.addWidget(self.progress, 1, 2) - - self.setLayout(layout) - self.refresh() + self.task.setWordWrap(True) - def refresh(self): - data = agent_manager.agents[self.key] - state = data.get("state", "standby") - colour = state_colour(state) + footer = QHBoxLayout() + + self.source_badge = QLabel() + self.brain_badge = QLabel() + self.open_button = QPushButton("OPEN") + self.open_button.clicked.connect(self.open_workspace) + + footer.addWidget(self.source_badge) + footer.addWidget(self.brain_badge) + footer.addStretch(1) + footer.addWidget(self.open_button) - self.setStyleSheet(f""" - QFrame {{ - background-color: {PANEL_BG}; + root.addLayout(top) + root.addWidget(self.description) + root.addWidget(self.task) + root.addStretch(1) + root.addLayout(footer) + + def mousePressEvent(self, event): + self.selected.emit(self.agent_id) + super().mousePressEvent(event) + + def update_agent(self, data): + self.data = data + status = data.get("uiStatus", "OFFLINE") + colour = status_colour(status) + + self.setStyleSheet( + f""" + AgentCard {{ + background: rgba(7, 13, 22, 244); border: 2px solid {colour}; - border-radius: 8px; + border-radius: 18px; + }} + AgentCard:hover {{ + background: rgba(12, 22, 35, 248); + border: 2px solid #e8fff2; + }} + """ + ) + + self.icon.setText(data.get("icon", "◆")) + self.icon.setStyleSheet( + f""" + background: {colour}; + color: #02040a; + border-radius: 21px; + font-size: 21px; + font-weight: 900; + """ + ) + + self.name.setText(data.get("name", self.agent_id)) + self.name.setStyleSheet( + "color:#e8fff2;font-size:16px;font-weight:900;" + "background:transparent;border:none;" + ) + + self.category.setText(data.get("category", "Specialist Agent")) + self.category.setStyleSheet( + "color:#91a0aa;font-size:10px;font-weight:700;" + "background:transparent;border:none;" + ) + + self.status.setText(status) + self.status.setStyleSheet( + f""" + color:{colour}; + background:rgba(0,0,0,100); + border:1px solid {colour}; + border-radius:10px; + padding:6px 9px; + font-size:9px; + font-weight:900; + """ + ) + + self.description.setText(data.get("description", "No description supplied.")) + self.description.setStyleSheet( + "color:#cbd6dc;font-size:10px;background:transparent;border:none;" + ) + + detail = data.get("statusText") or data.get("declaredStatus") or "No live status." + self.task.setText(detail.replace("_", " ").title()) + self.task.setStyleSheet( + f""" + color:{colour}; + background:rgba(0,0,0,80); + border:none; + border-radius:8px; + padding:6px; + font-family:monospace; + font-size:9px; + """ + ) + + sources = data.get("sourceFiles", []) + self.source_badge.setText(f"SOURCE {len(sources)}" if sources else "NO SOURCE") + self.source_badge.setStyleSheet( + "color:#f2b84b;font-size:8px;font-weight:800;" + "background:transparent;border:none;" + ) + + brain = data.get("brain", {}) + fast = bool(brain.get("fastAvailable")) + deep = bool(brain.get("deepAvailable")) + self.brain_badge.setText( + "BRAINS ✓✓" if fast and deep else "BRAINS PARTIAL" + ) + self.brain_badge.setStyleSheet( + "color:#53b7ff;font-size:8px;font-weight:800;" + "background:transparent;border:none;" + ) + + url = dedicated_workspace_url(data) + self.open_button.setEnabled(bool(url)) + self.open_button.setText("OPEN" if url else "STAGED") + self.open_button.setStyleSheet( + f""" + QPushButton {{ + color:#02040a; + background:{colour if url else '#53606a'}; + border:none; + border-radius:9px; + padding:7px 12px; + font-size:9px; + font-weight:900; + }} + QPushButton:disabled {{ + color:#b9c1c6; + background:#303942; }} - """) + """ + ) + + def open_workspace(self): + if dedicated_workspace_url(self.data): + self.workspace_requested.emit( + self.agent_id + ) + + +class AgentDetails(QFrame): + refresh_requested = pyqtSignal() + workspace_requested = pyqtSignal(str) + + def __init__(self): + super().__init__() + self.data = {} + + self.setMinimumWidth(380) + self.setStyleSheet( + """ + AgentDetails { + background:rgba(4,8,14,248); + border:1px solid #263847; + border-radius:20px; + } + """ + ) + + root = QVBoxLayout(self) + root.setContentsMargins(22, 20, 22, 20) + root.setSpacing(12) + + self.heading = QLabel("SELECT AN AGENT") + self.heading.setWordWrap(True) + self.heading.setStyleSheet( + "color:#00ff66;font-size:22px;font-weight:900;" + "background:transparent;border:none;" + ) + + self.subheading = QLabel("Agent details and controls will appear here.") + self.subheading.setWordWrap(True) + self.subheading.setStyleSheet( + "color:#91a0aa;font-size:11px;background:transparent;border:none;" + ) + + self.status = QLabel("NO AGENT SELECTED") + self.status.setAlignment(Qt.AlignCenter) + + self.summary = QLabel() + self.summary.setWordWrap(True) + self.summary.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.summary.setStyleSheet( + """ + color:#dce8ed; + background:rgba(0,0,0,80); + border:1px solid #1d2b35; + border-radius:12px; + padding:12px; + font-family:monospace; + font-size:10px; + """ + ) + + self.open_button = QPushButton("OPEN WORKSPACE") + self.open_button.clicked.connect(self.open_workspace) + + self.health_button = QPushButton("REFRESH HEALTH") + self.health_button.clicked.connect(self.refresh_requested.emit) + + self.source_button = QPushButton("OPEN SOURCE") + self.source_button.clicked.connect(self.open_source) + + self.evidence_button = QPushButton("OPEN EVIDENCE") + self.evidence_button.clicked.connect(self.open_evidence) - self.name.setText(data["name"]) - self.role.setText(data["role"]) - self.task.setText(data.get("task", "---")) - self.state.setText(state.upper()) - self.progress.setText(f'{data.get("progress", 0)}%') + for button in ( + self.open_button, + self.health_button, + self.source_button, + self.evidence_button, + ): + button.setMinimumHeight(40) + button.setStyleSheet( + """ + QPushButton { + color:#e8fff2; + background:#17232d; + border:1px solid #385063; + border-radius:10px; + padding:8px; + font-size:10px; + font-weight:900; + } + QPushButton:hover { + background:#24394a; + border-color:#00ff66; + } + QPushButton:disabled { + color:#65727c; + background:#111820; + border-color:#27323a; + } + """ + ) - self.state.setStyleSheet(f"color:{colour}; font-size:9px; font-weight:bold; border:none; background:transparent;") - self.progress.setStyleSheet(f"color:{colour}; font-size:9px; font-weight:bold; border:none; background:transparent;") + root.addWidget(self.heading) + root.addWidget(self.subheading) + root.addWidget(self.status) + root.addWidget(self.summary, 1) + root.addWidget(self.open_button) + root.addWidget(self.health_button) + root.addWidget(self.source_button) + root.addWidget(self.evidence_button) + + def show_agent(self, data): + self.data = data + status = data.get("uiStatus", "OFFLINE") + colour = status_colour(status) + + self.heading.setText(data.get("name", "Unnamed Agent")) + + nickname = data.get("nickname") + subtitle = data.get("category", "Specialist Agent") + if nickname: + subtitle = f"{subtitle} · {nickname}" + + self.subheading.setText(subtitle) + + self.status.setText(status) + self.status.setStyleSheet( + f""" + color:{colour}; + background:rgba(0,0,0,100); + border:1px solid {colour}; + border-radius:12px; + padding:9px; + font-size:11px; + font-weight:900; + """ + ) + + brain = data.get("brain", {}) + memory = data.get("memory", {}) + application = data.get("application", {}) + source_names = [ + Path(path).name for path in data.get("sourceFiles", []) + ] + + lines = [ + data.get("description", "No description."), + "", + f"ID: {data.get('id', 'unknown')}", + f"Declared: {data.get('declaredStatus', 'unknown')}", + f"Enabled: {data.get('enabled', False)}", + f"Route: {data.get('route') or 'not assigned'}", + f"Workspace: {dedicated_workspace_url(data) or 'not yet available'}", + f"Runtime API: {data.get('localUrl') or 'not yet available'}", + "", + f"Fast brain: {brain.get('fastModel', 'not declared')}", + f"Fast available: {brain.get('fastAvailable', False)}", + f"Deep brain: {brain.get('deepModel', 'not declared')}", + f"Deep available: {brain.get('deepAvailable', False)}", + "", + f"Memory root: {memory.get('root', 'not reported')}", + f"Memory isolated: {memory.get('isolated', 'unknown')}", + f"Application online: {application.get('online', False)}", + "", + "Source packages:", + ] + + lines.extend( + [f"• {name}" for name in source_names] + if source_names + else ["• No matching BONUS_APPS source detected"] + ) + + lines.extend( + [ + "", + "Safety:", + "• External actions remain approval-gated", + "• Staged Agents are not presented as operational", + "• No service controls are enabled by this screen yet", + ] + ) + + self.summary.setText("\n".join(lines)) + + self.open_button.setEnabled( + bool( + dedicated_workspace_url(data) + ) + ) + self.source_button.setEnabled(bool(data.get("sourceFiles"))) + + evidence = ( + ARMOR_ROOT / "data" / "agents" / str(data.get("id")) / "evidence" + ) + self.evidence_button.setEnabled(evidence.exists()) + + def open_workspace(self): + agent_id = str( + self.data.get("id") + or "" + ) + + if ( + agent_id + and dedicated_workspace_url( + self.data + ) + ): + self.workspace_requested.emit( + agent_id + ) + + def open_source(self): + sources = self.data.get("sourceFiles", []) + if sources: + QDesktopServices.openUrl(QUrl.fromLocalFile(str(Path(sources[0]).parent))) + + def open_evidence(self): + path = ARMOR_ROOT / "data" / "agents" / str(self.data.get("id")) / "evidence" + if path.exists(): + QDesktopServices.openUrl(QUrl.fromLocalFile(str(path))) class AgentsPage(WardenBackground): def __init__(self, stack): super().__init__() self.stack = stack - self.rows = {} + self.cards = {} + self.agents = {} + self.selected_agent_id = None + self.active_workspace = None + + main = QVBoxLayout(self) + main.setContentsMargins(18, 14, 18, 14) + main.setSpacing(12) + + header = QHBoxLayout() + + title_box = QVBoxLayout() + title_box.setSpacing(1) + + title = QLabel("SHiRE AGENT OPERATIONS CENTRE") + title.setStyleSheet( + "color:#00ff66;font-size:23px;font-weight:900;" + "background:transparent;border:none;" + ) - main = QVBoxLayout() - main.setContentsMargins(8, 3, 8, 3) - main.setSpacing(3) + subtitle = QLabel( + "Live specialist brains, workspaces, source packages and safety states" + ) + subtitle.setStyleSheet( + "color:#91a0aa;font-size:10px;font-weight:700;" + "background:transparent;border:none;" + ) - title = QLabel("♟ AGENT MANAGER") - title.setAlignment(Qt.AlignCenter) - title.setStyleSheet(f"color:{GREEN}; font-size:18px; font-weight:bold; background:transparent;") - main.addWidget(title) + title_box.addWidget(title) + title_box.addWidget(subtitle) - subtitle = QLabel("SHIRE WORKER COUNCIL") - subtitle.setAlignment(Qt.AlignCenter) - subtitle.setStyleSheet(f"color:{WHITE}; font-size:8px; font-weight:bold; background:transparent;") - main.addWidget(subtitle) + self.hub_status = QLabel("CONNECTING") + self.hub_status.setAlignment(Qt.AlignCenter) + self.hub_status.setMinimumWidth(145) - for key, data in agent_manager.agents.items(): - row = AgentRow(key, data) - self.rows[key] = row - main.addWidget(row) + back = QPushButton("RETURN HOME") + back.setMinimumHeight(42) + back.clicked.connect(lambda: self.stack.setCurrentIndex(0)) + back.setStyleSheet( + """ + QPushButton { + color:#02040a; + background:#00ff66; + border:none; + border-radius:12px; + padding:9px 16px; + font-size:10px; + font-weight:900; + } + QPushButton:hover { background:#78ffaa; } + """ + ) - controls = QGridLayout() - controls.setSpacing(3) + header.addLayout(title_box, 1) + header.addWidget(self.hub_status) + header.addWidget(back) - btn_demo = WardenTouchButton("⚒", "NIGHT", "FORGE", "online") - btn_demo.clicked.connect(self.night_forge_demo) + main.addLayout(header) - btn_online = WardenTouchButton("⚡", "START", "ALL", "online") - btn_online.clicked.connect(self.start_all) + metrics = QHBoxLayout() + metrics.setSpacing(9) - btn_standby = WardenTouchButton("◌", "STANDBY", "ALL", "standby") - btn_standby.clicked.connect(self.standby_all) + self.total_metric = self.metric("0", "REGISTERED") + self.live_metric = self.metric("0", "LIVE") + self.ready_metric = self.metric("0", "READY") + self.staged_metric = self.metric("0", "STAGED") + self.source_metric = self.metric("0", "SOURCE PACKAGES") - btn_back = WardenTouchButton("↩", "RETURN", "HOME", "online") - btn_back.clicked.connect(lambda: self.stack.setCurrentIndex(0)) + for widget in ( + self.total_metric, + self.live_metric, + self.ready_metric, + self.staged_metric, + self.source_metric, + ): + metrics.addWidget(widget) - controls.addWidget(btn_demo, 0, 0) - controls.addWidget(btn_online, 0, 1) - controls.addWidget(btn_standby, 0, 2) - controls.addWidget(btn_back, 0, 3) + main.addLayout(metrics) - main.addLayout(controls) - self.setLayout(main) + toolbar = QHBoxLayout() - self.timer = QTimer() + self.search = QLineEdit() + self.search.setPlaceholderText("Search Agents, categories or capabilities…") + self.search.textChanged.connect(self.apply_filters) + self.search.setMinimumHeight(40) + self.search.setStyleSheet( + """ + QLineEdit { + color:#e8fff2; + background:#08111a; + border:1px solid #304657; + border-radius:11px; + padding:8px 13px; + font-size:11px; + } + QLineEdit:focus { border-color:#00ff66; } + """ + ) + + self.filter = QComboBox() + self.filter.addItems( + ["ALL STATES", "LIVE", "READY", "STAGED", "SOURCE", "BLOCKED", "ERROR"] + ) + self.filter.currentTextChanged.connect(self.apply_filters) + self.filter.setMinimumHeight(40) + self.filter.setStyleSheet( + """ + QComboBox { + color:#e8fff2; + background:#08111a; + border:1px solid #304657; + border-radius:11px; + padding:7px 12px; + min-width:145px; + } + """ + ) + + refresh = QPushButton("REFRESH LIVE STATUS") + refresh.clicked.connect(self.refresh) + refresh.setMinimumHeight(40) + refresh.setStyleSheet( + """ + QPushButton { + color:#e8fff2; + background:#162736; + border:1px solid #3d5b70; + border-radius:11px; + padding:8px 14px; + font-size:10px; + font-weight:900; + } + QPushButton:hover { border-color:#00ff66; } + """ + ) + + toolbar.addWidget(self.search, 1) + toolbar.addWidget(self.filter) + toolbar.addWidget(refresh) + + main.addLayout(toolbar) + + splitter = QSplitter(Qt.Horizontal) + splitter.setChildrenCollapsible(False) + + self.scroll = QScrollArea() + self.scroll.setWidgetResizable(True) + self.scroll.setFrameShape(QFrame.NoFrame) + self.scroll.setStyleSheet("background:transparent;border:none;") + + self.card_host = QWidget() + self.card_host.setStyleSheet("background:transparent;") + + self.card_grid = QGridLayout(self.card_host) + self.card_grid.setContentsMargins(2, 2, 8, 2) + self.card_grid.setHorizontalSpacing(13) + self.card_grid.setVerticalSpacing(13) + self.card_grid.setAlignment(Qt.AlignTop) + + self.scroll.setWidget(self.card_host) + + self.details = AgentDetails() + self.details.refresh_requested.connect(self.refresh) + self.details.workspace_requested.connect( + self.open_agent_workspace + ) + + splitter.addWidget(self.scroll) + splitter.addWidget(self.details) + splitter.setStretchFactor(0, 3) + splitter.setStretchFactor(1, 1) + splitter.setSizes([1150, 410]) + + main.addWidget(splitter, 1) + + self.timer = QTimer(self) self.timer.timeout.connect(self.refresh) - self.timer.start(1000) + self.timer.start(10000) + self.refresh() - def paintEvent(self, event): - painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing) - self.paint_warden_background(painter) - super().paintEvent(event) + @staticmethod + def metric(value, label): + frame = QFrame() + frame.setStyleSheet( + """ + QFrame { + background:rgba(5,11,18,238); + border:1px solid #263c4c; + border-radius:13px; + } + """ + ) - def night_forge_demo(self): - agent_manager.start_night_forge_demo() - self.refresh() + layout = QVBoxLayout(frame) + layout.setContentsMargins(13, 8, 13, 8) + layout.setSpacing(0) - def start_all(self): - for key in list(agent_manager.agents.keys()): - agent_manager.activate_agent(key) - self.refresh() + number = QLabel(value) + number.setObjectName("value") + number.setAlignment(Qt.AlignCenter) + number.setStyleSheet( + "color:#e8fff2;font-size:18px;font-weight:900;" + "background:transparent;border:none;" + ) - def standby_all(self): - agent_manager.standby_all() - self.refresh() + caption = QLabel(label) + caption.setAlignment(Qt.AlignCenter) + caption.setStyleSheet( + "color:#91a0aa;font-size:8px;font-weight:900;" + "background:transparent;border:none;" + ) + + layout.addWidget(number) + layout.addWidget(caption) + + return frame + + @staticmethod + def set_metric(frame, value): + label = frame.findChild(QLabel, "value") + if label: + label.setText(str(value)) + + def build_agent_data(self): + registry_agents = load_registry() + source_map = source_files() + + status_code, live_payload = http_json( + f"{AGENT_HUB_URL}/api/v1/agents", + timeout=2.5, + ) + + live_map = {} + if status_code == 200: + for item in live_payload.get("agents", []): + live_map[str(item.get("id"))] = item + + combined = {} + + for declared in registry_agents: + agent_id = str(declared.get("id")) + merged = dict(declared) + merged.update(live_map.get(agent_id, {})) + + application = merged.get("application") or {} + online = bool(application.get("online")) + + merged["description"] = declared.get( + "description", + merged.get("description", "Specialist SHiRE Agent."), + ) + merged["category"] = declared.get( + "category", + merged.get("category", "Specialist Agent"), + ) + merged["appPath"] = declared.get("appPath") + merged["sourceFiles"] = [ + str(path) for path in source_map.get(agent_id, []) + ] + merged["uiStatus"] = friendly_status( + merged.get("declaredStatus") or declared.get("status"), + enabled=bool(merged.get("enabled")), + online=online, + ) + merged["statusText"] = ( + application.get("health", {}).get("app") + if isinstance(application.get("health"), dict) + else None + ) or merged.get("declaredStatus") or declared.get("status") + + combined[agent_id] = merged + + for synthetic in SYNTHETIC_AGENTS: + agent_id = synthetic["id"] + if agent_id in combined: + continue + + merged = dict(synthetic) + merged["sourceFiles"] = [ + str(path) for path in source_map.get(agent_id, []) + ] + merged["uiStatus"] = friendly_status( + merged.get("declaredStatus"), + enabled=False, + online=False, + ) + merged["brain"] = { + "fastModel": "shire-mini-fast", + "fastAvailable": True, + "deepModel": "shire-mini-deep", + "deepAvailable": True, + } + merged["memory"] = { + "root": str(ARMOR_ROOT / "data" / "agents" / agent_id), + "isolated": True, + } + combined[agent_id] = merged + + return combined, status_code == 200 def refresh(self): - for row in self.rows.values(): - row.refresh() + self.agents, hub_online = self.build_agent_data() + + if hub_online: + self.hub_status.setText("AGENT HUB LIVE") + self.hub_status.setStyleSheet( + """ + color:#20e887; + background:rgba(0,0,0,100); + border:1px solid #20e887; + border-radius:12px; + padding:10px; + font-size:10px; + font-weight:900; + """ + ) + else: + self.hub_status.setText("AGENT HUB OFFLINE") + self.hub_status.setStyleSheet( + """ + color:#ff4d5f; + background:rgba(0,0,0,100); + border:1px solid #ff4d5f; + border-radius:12px; + padding:10px; + font-size:10px; + font-weight:900; + """ + ) + + for agent_id, data in self.agents.items(): + card = self.cards.get(agent_id) + + if card is None: + card = AgentCard(agent_id) + card.selected.connect(self.select_agent) + card.workspace_requested.connect( + self.open_agent_workspace + ) + self.cards[agent_id] = card + + card.update_agent(data) + + for agent_id in list(self.cards): + if agent_id not in self.agents: + self.cards[agent_id].deleteLater() + del self.cards[agent_id] + + statuses = [ + data.get("uiStatus", "OFFLINE") + for data in self.agents.values() + ] + + self.set_metric(self.total_metric, len(self.agents)) + self.set_metric(self.live_metric, statuses.count("LIVE")) + self.set_metric(self.ready_metric, statuses.count("READY")) + self.set_metric(self.staged_metric, statuses.count("STAGED")) + self.set_metric( + self.source_metric, + sum(len(data.get("sourceFiles", [])) for data in self.agents.values()), + ) + + self.apply_filters() + + if self.selected_agent_id in self.agents: + self.details.show_agent(self.agents[self.selected_agent_id]) + elif self.agents: + first = sorted(self.agents)[0] + self.select_agent(first) + + def select_agent(self, agent_id): + self.selected_agent_id = agent_id + data = self.agents.get(agent_id) + if data: + self.details.show_agent(data) + + def open_agent_workspace(self, agent_id): + data = self.agents.get(agent_id) + + if not data: + return + + workspace_url = dedicated_workspace_url( + data + ) + + if not workspace_url: + return + + if self.active_workspace is not None: + self.stack.setCurrentWidget( + self.active_workspace + ) + return + + workspace_data = dict(data) + workspace_data["workspaceUrl"] = ( + workspace_url + ) + + workspace = EmbeddedAgentWorkspace( + self.stack, + self, + workspace_data, + ) + + self.active_workspace = workspace + + workspace.destroyed.connect( + lambda *_: setattr( + self, + "active_workspace", + None, + ) + ) + + self.stack.addWidget(workspace) + self.stack.setCurrentWidget(workspace) + + def apply_filters(self): + query = self.search.text().strip().lower() + selected_state = self.filter.currentText() + + visible = [] + + for agent_id, card in self.cards.items(): + data = self.agents.get(agent_id, {}) + haystack = " ".join( + [ + str(data.get("name", "")), + str(data.get("category", "")), + str(data.get("description", "")), + str(data.get("declaredStatus", "")), + agent_id, + ] + ).lower() + + state_match = ( + selected_state == "ALL STATES" + or data.get("uiStatus") == selected_state + ) + text_match = not query or query in haystack + + if state_match and text_match: + visible.append(card) + else: + card.hide() + + while self.card_grid.count(): + item = self.card_grid.takeAt(0) + if item.widget(): + item.widget().setParent(self.card_host) + + columns = 2 + + for index, card in enumerate(visible): + row = index // columns + column = index % columns + self.card_grid.addWidget(card, row, column) + card.show() + + for column in range(columns): + self.card_grid.setColumnStretch(column, 1)