Spaces:
Running
Running
| """ | |
| 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) |