summaryrefslogtreecommitdiff
path: root/src/dashboard.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/dashboard.py')
-rw-r--r--src/dashboard.py288
1 files changed, 288 insertions, 0 deletions
diff --git a/src/dashboard.py b/src/dashboard.py
new file mode 100644
index 0000000..5be6960
--- /dev/null
+++ b/src/dashboard.py
@@ -0,0 +1,288 @@
+from typing import List, Dict, Optional, Deque
+import threading
+import time
+from collections import deque
+from rich.console import Console
+from rich.live import Live
+from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn
+from rich.table import Table
+from rich.panel import Panel
+from rich.layout import Layout
+from rich.text import Text
+from rich.columns import Columns
+
+# Thread-safe lock for dashboard updates
+_dashboard_lock = threading.Lock()
+
+class ExperimentDashboard:
+ """Rich dashboard for real-time experiment tracking with per-model stats."""
+
+ def __init__(self, total_runs: int, models: List[str], skipped: int = 0):
+ self.console = Console()
+ self.total_runs = total_runs
+ self.skipped = skipped
+ self.success_count = 0
+ self.incomplete_count = 0
+ self.failed_count = 0
+ self.total_tokens = 0
+ self.start_time = time.time()
+
+ # Per-model stats
+ self.model_stats = {
+ model: {
+ "success": 0,
+ "total": 0,
+ "target": 0,
+ "tokens": 0,
+ "time": 0.0
+ } for model in models
+ }
+
+ # Track currently active runs
+ self.active_runs = {} # thread_id -> info string
+
+ # Recent activity log
+ self.activity_log: Deque[str] = deque(maxlen=5)
+
+ # Progress bars
+ self.progress = Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(bar_width=None),
+ TaskProgressColumn(),
+ TimeRemainingColumn(),
+ expand=True
+ )
+
+ self.overall_task = self.progress.add_task("[yellow]Overall Progress", total=total_runs)
+ self.model_tasks = {}
+ for model in models:
+ self.model_tasks[model] = self.progress.add_task(f"[blue]{model}", total=0)
+
+ def update_model_total(self, model: str, count: int):
+ """Update total runs for a specific model task."""
+ with _dashboard_lock:
+ if model in self.model_tasks:
+ self.progress.update(self.model_tasks[model], total=count)
+ self.model_stats[model]["target"] = count
+
+ def start_run(self, thread_id: int, model: str, scenario: str, goal: str):
+ """Mark a run as active."""
+ with _dashboard_lock:
+ goal_str = f" ({goal})" if goal else ""
+ self.active_runs[thread_id] = f"[bold blue]{model}[/] | {scenario}{goal_str}"
+
+ def complete_run(self, thread_id: int, model: str, success: bool, tokens: int = 0, duration: float = 0.0, error: bool = False, label: str = ""):
+ """Update counts and progress when a run completes."""
+ with _dashboard_lock:
+ # Update global counts
+ if error:
+ self.failed_count += 1
+ status_msg = "[red]ERROR[/]"
+ elif success:
+ self.success_count += 1
+ status_msg = "[green]SUCCESS[/]"
+ else:
+ self.incomplete_count += 1
+ status_msg = "[yellow]INCOMPLETE[/]"
+
+ self.total_tokens += tokens
+
+ # Update per-model stats
+ if model in self.model_stats:
+ m_stats = self.model_stats[model]
+ m_stats["total"] += 1
+ if success and not error:
+ m_stats["success"] += 1
+ m_stats["tokens"] += tokens
+ m_stats["time"] += duration
+
+ # Update progress bars
+ self.progress.update(self.overall_task, advance=1)
+ if model in self.model_tasks:
+ self.progress.update(self.model_tasks[model], advance=1)
+
+ # Update activity log
+ timestamp = time.strftime("%H:%M:%S")
+ self.activity_log.append(f"[{timestamp}] {status_msg} {label}")
+
+ # Remove from active runs
+ if thread_id in self.active_runs:
+ del self.active_runs[thread_id]
+
+ def generate_model_table(self) -> Table:
+ """Generate a detailed per-model statistics table."""
+ table = Table(expand=True, box=None)
+ table.add_column("Model", style="blue", ratio=2)
+ table.add_column("Progress", justify="right", ratio=1)
+ table.add_column("Success %", justify="right", ratio=1)
+ table.add_column("Tokens", justify="right", ratio=1)
+ table.add_column("Avg Time", justify="right", ratio=1)
+
+ for model, stats in self.model_stats.items():
+ if stats["target"] == 0: continue
+
+ progress = f"{stats['total']}/{stats['target']}"
+ success_rate = (stats["success"] / stats["total"] * 100) if stats["total"] > 0 else 0
+ avg_time = (stats["time"] / stats["total"]) if stats["total"] > 0 else 0
+
+ table.add_row(
+ model,
+ progress,
+ f"{success_rate:.0f}%",
+ f"{stats['tokens']:,}",
+ f"{avg_time:.1f}s"
+ )
+ return table
+
+ def format_time(self, seconds: float) -> str:
+ """Format seconds into HH:MM:SS."""
+ if seconds is None:
+ return "--:--:--"
+ h = int(seconds // 3600)
+ m = int((seconds % 3600) // 60)
+ s = int(seconds % 60)
+ return f"{h:02d}:{m:02d}:{s:02d}"
+
+ def generate_status_table(self) -> Table:
+ """Generate a summary table of the current status."""
+ table = Table(expand=True, box=None)
+ table.add_column("Metric", style="cyan")
+ table.add_column("Value", justify="right", style="magenta")
+
+ completed = self.success_count + self.incomplete_count + self.failed_count
+ remaining = self.total_runs - completed
+
+ # Time calculations
+ elapsed = time.time() - self.start_time
+ time_remaining = self.progress.tasks[self.overall_task].time_remaining
+
+ table.add_row("Success", f"[green]{self.success_count}[/green]")
+ table.add_row("Incomplete", f"[yellow]{self.incomplete_count}[/yellow]")
+ table.add_row("Failed", f"[red]{self.failed_count}[/red]")
+ table.add_row("Remaining", f"[white]{remaining}[/white]")
+ table.add_section()
+ table.add_row("Total Tokens", f"[bold white]{self.total_tokens:,}[/bold white]")
+ table.add_row("Elapsed", self.format_time(elapsed))
+ table.add_row("Est. Left", self.format_time(time_remaining))
+ table.add_row("Skipped", f"[grey50]{self.skipped}[/grey50]")
+
+ return table
+
+ def generate_active_panel(self) -> Panel:
+ """Generate a panel showing currently active runs."""
+ if not self.active_runs:
+ content = Text("Waiting for workers...", style="italic grey50")
+ else:
+ # Fix: Join using Text.from_markup to ensure colors render
+ lines = [Text.from_markup(line) for line in self.active_runs.values()]
+ content = Text("\n").join(lines)
+ return Panel(content, title="Currently Running", border_style="dim")
+
+ def generate_log_panel(self) -> Panel:
+ """Generate a panel showing recent activity log."""
+ # Fix: Join using Text.from_markup to ensure colors render
+ lines = [Text.from_markup(line) for line in self.activity_log]
+ content = Text("\n").join(lines)
+ return Panel(content, title="Recent Activity", border_style="dim")
+
+ def get_layout(self) -> Layout:
+ """Create the dashboard layout."""
+ layout = Layout()
+ layout.split_column(
+ Layout(name="header", size=3),
+ Layout(name="main"),
+ Layout(name="footer", size=7)
+ )
+
+ layout["main"].split_row(
+ Layout(name="progress_col", ratio=2),
+ Layout(name="stats_col", ratio=1)
+ )
+
+ layout["progress_col"].split_column(
+ Layout(name="bars", ratio=1),
+ Layout(name="model_details", ratio=1)
+ )
+
+ layout["footer"].split_row(
+ Layout(name="active", ratio=1),
+ Layout(name="logs", ratio=1)
+ )
+
+ layout["header"].update(Panel(Text("AI Agent Experiment Framework", justify="center", style="bold white"), style="blue"))
+ layout["bars"].update(Panel(self.progress, title="Overall Progress", style="white"))
+ layout["model_details"].update(Panel(self.generate_model_table(), title="Model Stats", style="white"))
+ layout["stats_col"].update(Panel(self.generate_status_table(), title="Totals", style="white"))
+ layout["active"].update(self.generate_active_panel())
+ layout["logs"].update(self.generate_log_panel())
+
+ return layout
+
+def print_final_summary(results: List[Dict]):
+ """Print a pretty grouped summary table at the end."""
+ console = Console()
+ console.print("\n")
+
+ # Group results by Model and Scenario
+ grouped = {}
+ for r in results:
+ key = (r['model'], r['scenario'])
+ if key not in grouped:
+ grouped[key] = []
+ grouped[key].append(r)
+
+ table = Table(title="[bold]Final Experiment Summary[/bold]", show_header=True, header_style="bold magenta", expand=True)
+ table.add_column("Model", style="blue", no_wrap=True)
+ table.add_column("Scenario", style="cyan")
+ table.add_column("Goal Type", style="yellow")
+ table.add_column("Successes", justify="center")
+ table.add_column("Total Tokens", justify="right")
+ table.add_column("Avg Time", justify="right")
+
+ for (model, scenario), group in sorted(grouped.items()):
+ total = len(group)
+ successes = sum(1 for r in group if r.get("success"))
+ total_tokens = sum(r.get("total_tokens", 0) for r in group)
+ avg_time = sum(r.get("duration_seconds", 0.0) for r in group) / total
+
+ # Collect distinct goal types in this group
+ goals = ", ".join(sorted(list(set(r.get("goal_type", "default") or "default" for r in group))))
+
+ success_color = "green" if successes == total else "yellow" if successes > 0 else "red"
+
+ table.add_row(
+ model,
+ scenario,
+ goals,
+ f"[{success_color}]{successes}/{total}[/]",
+ f"{total_tokens:,}",
+ f"{avg_time:.1f}s"
+ )
+
+ console.print(table)
+
+ # Aggregated Per-Model Table
+ model_table = Table(title="[bold]Aggregated Model Performance[/bold]", show_header=True, header_style="bold blue")
+ model_table.add_column("Model")
+ model_table.add_column("Total Runs", justify="right")
+ model_table.add_column("Overall Success %", justify="right")
+ model_table.add_column("Total Tokens", justify="right")
+
+ models = sorted(list(set(r['model'] for r in results)))
+ for model in models:
+ m_results = [r for r in results if r['model'] == model]
+ m_total = len(m_results)
+ m_successes = sum(1 for r in m_results if r.get("success"))
+ m_tokens = sum(r.get("total_tokens", 0) for r in m_results)
+
+ rate = (m_successes / m_total * 100) if m_total > 0 else 0
+ model_table.add_row(
+ model,
+ str(m_total),
+ f"{rate:.1f}%",
+ f"{m_tokens:,}"
+ )
+
+ console.print("\n")
+ console.print(model_table)