darkfire514's picture
Upload 160 files
399b80c verified
"""
OpenSpace Terminal UI System
Provides real-time CLI visualization for OpenSpace execution flow.
Displays agent activities, grounding backends, and detailed logs.
Uses native ANSI colors and custom box drawing for a clean, lightweight interface.
"""
from typing import Optional, Dict, Any, List, Tuple
from datetime import datetime
from enum import Enum
import asyncio
import sys
import shutil
from openspace.utils.display import Box, BoxStyle, colorize
class AgentStatus(Enum):
"""Agent execution status"""
IDLE = "idle"
THINKING = "thinking"
EXECUTING = "executing"
WAITING = "waiting"
class OpenSpaceUI:
"""
OpenSpace Terminal UI
Provides real-time visualization of:
- Agent activities and status
- Grounding backend operations
- Execution logs
- System metrics
Design Philosophy:
- Lightweight and fast (no heavy dependencies)
- Clean ANSI-based rendering
- Minimal CPU overhead
- Easy to customize
"""
def __init__(self, enable_live: bool = True, compact: bool = False):
"""
Initialize UI
Args:
enable_live: Whether to enable live display updates
compact: Use compact layout (for smaller terminals)
"""
self.enable_live = enable_live
self.compact = compact
# Terminal dimensions
self.term_width, self.term_height = self._get_terminal_size()
# State tracking
self.agent_status: Dict[str, AgentStatus] = {}
self.agent_activities: Dict[str, List[str]] = {}
self.grounding_operations: List[Dict[str, Any]] = []
self.grounding_backends: List[Dict[str, Any]] = [] # Backend info (type, servers, etc.)
self.log_buffer: List[Tuple[str, str, datetime]] = [] # (message, level, timestamp)
# Metrics
self.metrics: Dict[str, Any] = {
"start_time": None,
"iterations": 0,
"completed_tasks": 0,
"llm_calls": 0,
"grounding_calls": 0,
}
# Live display state
self._live_running = False
self._live_task: Optional[asyncio.Task] = None
self._last_render: List[str] = []
def _get_terminal_size(self) -> Tuple[int, int]:
"""Get terminal size"""
try:
size = shutil.get_terminal_size((80, 24))
return size.columns, size.lines
except:
return 80, 24
def _clear_screen(self):
"""Clear screen"""
if self.enable_live:
# Clear entire screen and move cursor to top-left
sys.stdout.write('\033[2J\033[H')
sys.stdout.flush()
def _move_cursor_home(self):
"""Move cursor to home position"""
sys.stdout.write('\033[H')
sys.stdout.flush()
def _hide_cursor(self):
"""Hide cursor"""
sys.stdout.write('\033[?25l')
sys.stdout.flush()
def _show_cursor(self):
"""Show cursor"""
sys.stdout.write('\033[?25h')
sys.stdout.flush()
# Banner and Startup
def print_banner(self):
"""Print startup banner"""
box = Box(width=70, style=BoxStyle.ROUNDED, color='c')
print()
print(box.top_line(indent=4))
print(box.empty_line(indent=4))
# Title
title = colorize("OpenSpace", 'c', bold=True)
print(box.text_line(title, align='center', indent=4, text_color=''))
# Subtitle
subtitle = "Self-Evolving Skill Worker & Community"
print(box.text_line(subtitle, align='center', indent=4, text_color='gr'))
print(box.empty_line(indent=4))
print(box.bottom_line(indent=4))
print()
def print_initialization(self, steps: List[Tuple[str, str]]):
"""
Print initialization steps
Args:
steps: List of (component_name, status) tuples
"""
box = Box(width=70, style=BoxStyle.ROUNDED, color='bl')
print(box.text_line("Initializing Components", align='center', indent=4, text_color='c'))
print(box.separator_line(indent=4))
for component, status in steps:
icon = colorize("✓", 'g') if status == "ok" else colorize("✗", 'rd')
line = f"{icon} {component}"
print(box.text_line(line, indent=4))
print(box.bottom_line(indent=4))
print()
async def start_live_display(self):
"""Start live display"""
if not self.enable_live or self._live_running:
return
self._live_running = True
self.metrics["start_time"] = datetime.now()
self._clear_screen()
self._hide_cursor()
# Start update loop
self._live_task = asyncio.create_task(self._live_update_loop())
async def stop_live_display(self):
"""Stop live display"""
if not self._live_running:
return
self._live_running = False
if self._live_task:
self._live_task.cancel()
try:
await self._live_task
except asyncio.CancelledError:
pass
self._show_cursor()
print() # Add newline after live display
async def _live_update_loop(self):
"""Live update loop"""
while self._live_running:
try:
self.render()
await asyncio.sleep(2.0)
except asyncio.CancelledError:
break
except Exception as e:
print(f"UI render error: {e}")
def render(self):
"""Render entire UI"""
if not self.enable_live or not self._live_running:
return
# Clear and redraw
self._clear_screen()
lines = []
# Header
lines.extend(self._render_header())
lines.append("")
# Stack all panels vertically
lines.extend(self._render_agents())
lines.append("")
lines.extend(self._render_grounding())
lines.append("")
lines.extend(self._render_logs())
output = "\n".join(lines)
sys.stdout.write(output)
sys.stdout.flush()
def update_display(self):
"""Update display (alias for render())"""
self.render()
def _render_header(self) -> List[str]:
"""Render header section"""
lines = []
# Calculate elapsed time
elapsed = "0s"
if self.metrics["start_time"]:
delta = datetime.now() - self.metrics["start_time"]
minutes = delta.seconds // 60
seconds = delta.seconds % 60
if minutes > 0:
elapsed = f"{minutes}m{seconds}s"
else:
elapsed = f"{seconds}s"
status_text = (
f"▶ {colorize('RUNNING', 'g')} | "
f"Time: {colorize(elapsed, 'c')} | "
f"Iter: {colorize(str(self.metrics['iterations']), 'y')} | "
f"Tasks: {colorize(str(self.metrics['completed_tasks']), 'g')} | "
f"LLM: {colorize(str(self.metrics['llm_calls']), 'bl')} | "
f"Grounding: {colorize(str(self.metrics['grounding_calls']), 'm')}"
)
lines.append(" " + status_text)
lines.append(" " + "─" * 60)
return lines
def _render_agents(self) -> List[str]:
"""Render agents section"""
lines = []
lines.append(" " + colorize("§ Agents", 'c', bold=True))
# Agent info
agents = [
("GroundingAgent", 'c', self.agent_status.get("GroundingAgent", AgentStatus.IDLE)),
]
for agent_name, color, status in agents:
# Status icon
status_icons = {
AgentStatus.IDLE: "○",
AgentStatus.THINKING: "◐",
AgentStatus.EXECUTING: "◉",
AgentStatus.WAITING: "◷",
}
icon = status_icons.get(status, "○")
# Recent activity
activities = self.agent_activities.get(agent_name, [])
activity = activities[-1][:40] if activities else "idle"
# Format line
line = f" {colorize(icon, 'y')} {colorize(agent_name, color):<20s} {activity}"
lines.append(line)
return lines
def _render_grounding(self) -> List[str]:
"""Render grounding operations section"""
lines = []
lines.append(" " + colorize("⊕ Grounding Backends", 'c', bold=True))
# Show backend types and servers
if self.grounding_backends:
for backend_info in self.grounding_backends:
backend_name = backend_info.get("name", "unknown")
backend_type = backend_info.get("type", "unknown")
servers = backend_info.get("servers", [])
# Backend type icon
type_icons = {
"gui": "■",
"shell": "$",
"mcp": "◆",
"system": "●",
"web": "◉",
}
icon = type_icons.get(backend_type, "○")
# Format backend line
if backend_type == "mcp" and servers:
servers_str = ", ".join([s[:15] for s in servers])
line = f" {icon} {colorize(backend_name, 'y')} ({backend_type}): {colorize(servers_str, 'gr')}"
else:
line = f" {icon} {colorize(backend_name, 'y')} ({backend_type})"
lines.append(line)
# Show last 3 operations
recent_ops = self.grounding_operations[-3:] if self.grounding_operations else []
if recent_ops:
lines.append(" " + colorize("Recent Operations:", 'gr'))
for op in recent_ops:
backend = op.get("backend", "unknown")
action = op.get("action", "unknown")[:40]
status = op.get("status", "pending")
# Status icon
if status == "success":
icon = colorize("✓", 'g')
elif status == "pending":
icon = colorize("⏳", 'y')
else:
icon = colorize("✗", 'rd')
line = f" {icon} {colorize(backend, 'bl')}: {action}"
lines.append(line)
return lines
def _render_logs(self) -> List[str]:
"""Render logs section"""
lines = []
lines.append(" " + colorize("⊞ Recent Events", 'c', bold=True))
# Show last 5 logs
recent_logs = self.log_buffer[-5:] if self.log_buffer else []
if recent_logs:
for message, level, timestamp in recent_logs:
time_str = timestamp.strftime("%H:%M:%S")
# Truncate long messages
msg_display = message[:55]
log_line = f" {colorize(time_str, 'gr')} | {msg_display}"
lines.append(log_line)
return lines
def update_agent_status(self, agent_name: str, status: AgentStatus):
"""Update agent status"""
self.agent_status[agent_name] = status
def add_agent_activity(self, agent_name: str, activity: str):
"""Add agent activity"""
if agent_name not in self.agent_activities:
self.agent_activities[agent_name] = []
self.agent_activities[agent_name].append(activity)
# Keep only last 10 activities
if len(self.agent_activities[agent_name]) > 10:
self.agent_activities[agent_name] = self.agent_activities[agent_name][-10:]
def update_grounding_backends(self, backends: List[Dict[str, Any]]):
"""
Update grounding backends information
Args:
backends: List of backend info dicts with keys:
- name: backend name
- type: backend type (gui, shell, mcp, system, web)
- servers: list of server names (for mcp)
"""
self.grounding_backends = backends
def add_grounding_operation(self, backend: str, action: str, status: str = "pending"):
"""Add grounding operation"""
self.grounding_operations.append({
"backend": backend,
"action": action,
"status": status,
"timestamp": datetime.now(),
})
self.metrics["grounding_calls"] += 1
def add_log(self, message: str, level: str = "info"):
"""Add log message"""
self.log_buffer.append((message, level, datetime.now()))
# Keep only last 100 logs
if len(self.log_buffer) > 100:
self.log_buffer = self.log_buffer[-100:]
def update_metrics(self, **kwargs):
"""Update metrics"""
self.metrics.update(kwargs)
def print_summary(self, result: Dict[str, Any]):
"""Print execution summary"""
box = Box(width=70, style=BoxStyle.ROUNDED, color='c')
print()
print(box.text_line(colorize("◈ Execution Summary", 'c', bold=True), align='center', indent=4, text_color=''))
print(box.separator_line(indent=4))
# Status
status = result.get("status", "unknown")
status_display = {
"completed": colorize("COMPLETED", 'g', bold=True),
"timeout": colorize("TIMEOUT", 'y', bold=True),
"error": colorize("ERROR", 'rd', bold=True),
}
status_text = status_display.get(status, status)
print(box.text_line(f" Status: {status_text}", indent=4, text_color=''))
print(box.text_line(f" Execution Time: {colorize(f'{result.get('execution_time', 0):.2f}s', 'c')}", indent=4, text_color=''))
print(box.text_line(f" Iterations: {colorize(str(result.get('iterations', 0)), 'y')}", indent=4, text_color=''))
print(box.text_line(f" Completed Tasks: {colorize(str(result.get('completed_tasks', 0)), 'g')}", indent=4, text_color=''))
if result.get('evaluation_results'):
print(box.text_line(f" Evaluations: {colorize(str(len(result['evaluation_results'])), 'bl')}", indent=4, text_color=''))
print(box.bottom_line(indent=4))
print()
def create_ui(enable_live: bool = True, compact: bool = False) -> OpenSpaceUI:
"""
Create OpenSpace UI instance
Args:
enable_live: Whether to enable live display updates
compact: Use compact layout for smaller terminals
"""
return OpenSpaceUI(enable_live=enable_live, compact=compact)