demo_tui.py
1 from __future__ import annotations 2 3 import argparse 4 import json 5 import logging 6 import sys 7 import time 8 from dataclasses import dataclass 9 from typing import Any, Literal 10 from urllib.error import HTTPError, URLError 11 from urllib.request import urlopen 12 13 from rich.align import Align 14 from rich.columns import Columns 15 from rich.console import Console, Group, RenderableType 16 from rich.json import JSON 17 from rich.layout import Layout 18 from rich.live import Live 19 from rich.panel import Panel 20 from rich.rule import Rule 21 from rich.syntax import Syntax 22 from rich.table import Table 23 from rich.text import Text 24 25 from app.client import VllmGrpcClient, VllmGrpcError 26 from app.discover_vllm_grpc import discover_surface, write_summary 27 from app.incident_samples import get_incident, scenario_names 28 from app.parser import FINAL_DECISION_PATH, parse_final_decision, save_final_decision 29 from app.schemas import FinalDecision, TriageUpdate 30 31 logging.basicConfig( 32 level=logging.INFO, 33 format="%(asctime)s %(levelname)s %(name)s: %(message)s", 34 ) 35 logging.getLogger("app.client").setLevel(logging.WARNING) 36 37 ACCENT = "bold bright_cyan" 38 AMBER = "bold bright_yellow" 39 SUCCESS = "bold bright_green" 40 DIM = "grey70" 41 MUTED = "grey50" 42 DANGER = "bold bright_red" 43 INK = "white" 44 45 TITLE_ART = """\ 46 ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗ █████╗ ██╗ ██╗███████╗ 47 ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██║ ██║██╔══██╗██║ ██║██╔════╝ 48 ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║██║ █╗ ██║███████║██║ ██║█████╗ 49 ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║██║███╗██║██╔══██║╚██╗ ██╔╝██╔══╝ 50 ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝╚███╔███╔╝██║ ██║ ╚████╔╝ ███████╗ 51 ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ 52 """ 53 54 MILESTONE_ORDER: list[tuple[str, str]] = [ 55 ("discovery", "DISCOVERY"), 56 ("transport", "STREAM OPEN"), 57 ("schema_lock", "JSON SCHEMA"), 58 ("first_chunk", "FIRST CHUNK"), 59 ("situation_assessment", "ASSESSMENT"), 60 ("probable_root_cause", "ROOT CAUSE"), 61 ("recommended_actions", "ACTIONS"), 62 ("final_decision_json", "FINAL JSON"), 63 ("validation", "VALIDATED"), 64 ("api_handoff", "API HANDOFF"), 65 ] 66 67 STAGE_TO_MILESTONE = { 68 "transport": "transport", 69 "situation_assessment": "situation_assessment", 70 "probable_root_cause": "probable_root_cause", 71 "recommended_actions": "recommended_actions", 72 "final_decision_json": "final_decision_json", 73 } 74 75 76 @dataclass(slots=True) 77 class ApiSnapshot: 78 title: str 79 body: str 80 ok: bool 81 82 83 @dataclass(slots=True) 84 class OutcomeState: 85 decision: FinalDecision | None = None 86 parse_error: str | None = None 87 generation_error: str | None = None 88 89 90 class WowDemoTUI: 91 def __init__( 92 self, 93 scenario: str, 94 endpoint: str, 95 save: bool, 96 *, 97 api_base: str | None, 98 title_seconds: float, 99 pause_scale: float, 100 profile: Literal["recording", "live"], 101 ) -> None: 102 self.scenario = scenario 103 self.endpoint = endpoint 104 self.save = save 105 self.api_base = api_base.rstrip("/") if api_base else None 106 self.title_seconds = max(title_seconds, 0.0) 107 self.pause_scale = max(pause_scale, 0.0) 108 self.profile = profile 109 self.console = Console() 110 self.surface: dict[str, Any] | None = None 111 self.runtime_probe: dict[str, Any] = {} 112 self.api_snapshot = ApiSnapshot("API Handoff", "API handoff disabled for this run.", False) 113 self.outcome = OutcomeState() 114 self.stream_text = Text(style=INK) 115 self.final_decision_json = "{}" 116 self.status_lines: list[str] = [] 117 self.milestones = {key: False for key, _ in MILESTONE_ORDER} 118 self.milestone_notes: list[str] = [] 119 self.act_title = "Act I Runtime Discovery" 120 self.metrics: dict[str, Any] = { 121 "selected_rpc": "-", 122 "time_to_first_update_ms": "-", 123 "end_to_end_latency_ms": "-", 124 "output_bytes_received": "-", 125 "chunk_count": "-", 126 "finish_reason": "-", 127 } 128 self._stream_started_at: float | None = None 129 self._first_chunk_seen_at: float | None = None 130 131 def _pause(self, seconds: float) -> None: 132 if seconds <= 0 or self.pause_scale <= 0: 133 return 134 profile_factor = 1.0 if self.profile == "recording" else 0.45 135 time.sleep(seconds * self.pause_scale * profile_factor) 136 137 def _append_status(self, line: str) -> None: 138 stamp = time.strftime("%H:%M:%S") 139 self.status_lines.append(f"[{stamp}] {line}") 140 self.status_lines = self.status_lines[-8:] 141 142 def _mark_milestone(self, key: str, note: str) -> None: 143 if not self.milestones.get(key): 144 self.milestones[key] = True 145 self.milestone_notes.append(note) 146 self.milestone_notes = self.milestone_notes[-5:] 147 148 def _is_compact(self) -> bool: 149 return self.console.size.width < 145 150 151 def _service_name(self) -> str: 152 if not self.surface or not self.surface.get("services"): 153 return "vllm.grpc.engine.VllmEngine" 154 return self.surface["services"][0]["full_name"] 155 156 def _proto_packages_label(self) -> str: 157 if not self.surface: 158 return "pending" 159 return ", ".join(self.surface.get("protobuf_packages", [])) 160 161 def _selected_rpc(self) -> str: 162 value = self.metrics.get("selected_rpc", "-") 163 if isinstance(value, str) and value != "-": 164 return value 165 if not self.surface: 166 return "/vllm.grpc.engine.VllmEngine/Generate" 167 for service in self.surface["services"]: 168 for method in service["methods"]: 169 if method["name"] == "Generate": 170 return method["full_name"] 171 return "/vllm.grpc.engine.VllmEngine/Generate" 172 173 def _model_label(self) -> str: 174 model_info = self.runtime_probe.get("model_info", {}) 175 if isinstance(model_info, dict): 176 served_model_name = model_info.get("served_model_name") 177 model_path = model_info.get("model_path") 178 if isinstance(served_model_name, str) and served_model_name: 179 return served_model_name 180 if isinstance(model_path, str) and model_path: 181 return model_path 182 return "unavailable" 183 184 def _server_type(self) -> str: 185 server_info = self.runtime_probe.get("server_info", {}) 186 if isinstance(server_info, dict): 187 server_type = server_info.get("server_type") 188 if isinstance(server_type, str) and server_type: 189 return server_type 190 return "unknown" 191 192 def _tokenizer_label(self) -> str: 193 tokenizer = self.runtime_probe.get("tokenizer", {}) 194 if not isinstance(tokenizer, dict) or not tokenizer: 195 return "pending" 196 if tokenizer.get("implemented"): 197 return "streaming tokenizer RPC available" 198 code = tokenizer.get("code") 199 if code: 200 return f"adaptive local decode ({code})" 201 return "adaptive local decode" 202 203 def _health_label(self) -> str: 204 health = self.runtime_probe.get("health", {}) 205 if isinstance(health, dict) and health: 206 if health.get("healthy") is True: 207 return "healthy" 208 message = health.get("message") 209 if message: 210 return str(message) 211 error = self.runtime_probe.get("health_error") 212 if error: 213 return "probe failed" 214 return "pending" 215 216 def _summary_badge(self, label: str, value: str, style: str) -> Panel: 217 return Panel( 218 Group( 219 Text(label, style="bold white"), 220 Text(value, style=style, justify="center"), 221 ), 222 border_style=style, 223 padding=(0, 1), 224 ) 225 226 def _hero_stat(self, label: str, value: str, style: str) -> Panel: 227 return Panel( 228 Align.center( 229 Group( 230 Text(value, style=style, justify="center"), 231 Text(label, style=DIM, justify="center"), 232 ), 233 vertical="middle", 234 ), 235 border_style=style, 236 padding=(0, 1), 237 ) 238 239 def _show_title_card(self) -> None: 240 if self.title_seconds <= 0: 241 return 242 self.console.clear() 243 hero_title = ( 244 Text("NeuroWave // vLLM gRPC", style=ACCENT, justify="center") 245 if self._is_compact() 246 else Text(TITLE_ART, style=ACCENT) 247 ) 248 subtitle = Text() 249 subtitle.append("vLLM gRPC", style=ACCENT) 250 subtitle.append(" live inference with ", style=INK) 251 subtitle.append("streaming proof", style=SUCCESS) 252 subtitle.append(" and ", style=INK) 253 subtitle.append("validated JSON handoff", style=AMBER) 254 badges = Columns( 255 [ 256 self._summary_badge("PROTO", self._proto_packages_label(), ACCENT), 257 self._summary_badge("RPC", self._selected_rpc(), ACCENT), 258 self._summary_badge("MODEL", self._model_label(), SUCCESS), 259 self._summary_badge("CONTRACT", "SamplingParams.json_schema", AMBER), 260 ], 261 expand=True, 262 equal=True, 263 ) 264 details = Table.grid(expand=True, padding=(0, 2)) 265 details.add_column(style="bold white", ratio=1) 266 details.add_column(style=DIM, ratio=2) 267 details.add_row("Scenario", self.scenario) 268 details.add_row("Endpoint", self.endpoint) 269 details.add_row("Server", self._server_type()) 270 details.add_row("API", self.api_base or "disabled") 271 details.add_row("Profile", self.profile) 272 card = Panel( 273 Group( 274 Align.center(hero_title), 275 Align.center(Text("NeuroWave Demo Surface", style="bold white")), 276 Align.center(subtitle), 277 Rule(style="bright_cyan"), 278 badges, 279 Rule(style="bright_black"), 280 details, 281 ), 282 title="[bold white]Act I Runtime Discovery", 283 border_style="bright_blue", 284 padding=(1, 2), 285 ) 286 self.console.print(card) 287 self._pause(self.title_seconds) 288 289 def _header_panel(self) -> Panel: 290 text = Text() 291 text.append("NeuroWave", style=ACCENT) 292 text.append(" ", style=MUTED) 293 text.append("vLLM gRPC Demo", style="bold white") 294 text.append(" ", style=MUTED) 295 text.append(self.act_title, style=AMBER) 296 text.append("\n") 297 text.append(f"Scenario {self.scenario}", style=INK) 298 text.append(" | ", style=MUTED) 299 text.append(f"Endpoint {self.endpoint}", style=INK) 300 text.append(" | ", style=MUTED) 301 text.append(f"Model {self._model_label()}", style=INK) 302 text.append(" | ", style=MUTED) 303 text.append(f"Profile {self.profile}", style=INK) 304 return Panel(text, border_style="bright_blue") 305 306 def _hero_panel(self) -> Panel: 307 service_lines = Table.grid(expand=True, padding=(0, 1)) 308 service_lines.add_column(style="bold white", ratio=1) 309 service_lines.add_column(style=INK, ratio=2) 310 service_lines.add_row("Service", self._service_name()) 311 service_lines.add_row("Selected RPC", self._selected_rpc()) 312 service_lines.add_row("Mode", "unary request -> server stream") 313 service_lines.add_row("Proto", self._proto_packages_label()) 314 service_lines.add_row("Constraint", "GenerateRequest + SamplingParams.json_schema") 315 service_lines.add_row("Runtime health", self._health_label()) 316 service_lines.add_row("Tokenizer", self._tokenizer_label()) 317 headline = Text() 318 headline.append("Typed runtime discovery ", style=INK) 319 headline.append("proves the live surface", style=ACCENT) 320 headline.append(", then the stream converts it into ", style=INK) 321 headline.append("validated action JSON", style=AMBER) 322 return Panel( 323 Group( 324 Text("Transport Proof Rail", style="bold white"), 325 headline, 326 Rule(style="bright_black"), 327 service_lines, 328 ), 329 title="[bold white]gRPC Proof", 330 border_style="bright_cyan", 331 padding=(1, 1), 332 ) 333 334 def _metrics_panel(self) -> RenderableType: 335 stats = Columns( 336 [ 337 self._hero_stat("TTFT", str(self.metrics["time_to_first_update_ms"]), ACCENT), 338 self._hero_stat("LATENCY", str(self.metrics["end_to_end_latency_ms"]), SUCCESS), 339 self._hero_stat("BYTES", str(self.metrics["output_bytes_received"]), AMBER), 340 self._hero_stat("CHUNKS", str(self.metrics["chunk_count"]), "bold magenta"), 341 ], 342 expand=True, 343 equal=True, 344 ) 345 lower = Table.grid(expand=True, padding=(0, 1)) 346 lower.add_column(style="bold white") 347 lower.add_column(style=DIM) 348 lower.add_row("Finish reason", str(self.metrics["finish_reason"])) 349 lower.add_row("API handoff", self.api_base or "disabled") 350 lower.add_row("Saved output", str(FINAL_DECISION_PATH.name if self.save else "disabled")) 351 return Panel( 352 Group(stats, Rule(style="bright_black"), lower), 353 title="[bold white]Hero Stats", 354 border_style="bright_green", 355 padding=(1, 1), 356 ) 357 358 def _milestones_panel(self) -> Panel: 359 grid = Table.grid(expand=True, padding=(0, 1)) 360 columns = 1 if self._is_compact() else 2 361 for _ in range(columns): 362 grid.add_column(ratio=1) 363 entries = [] 364 for key, label in MILESTONE_ORDER: 365 style = "black on bright_cyan" if self.milestones[key] else "white on grey23" 366 entries.append(Text(f" {label} ", style=style)) 367 if columns == 1: 368 for entry in entries: 369 grid.add_row(entry) 370 else: 371 for index in range(0, len(entries), 2): 372 left = entries[index] 373 right = entries[index + 1] if index + 1 < len(entries) else Text("") 374 grid.add_row(left, right) 375 notes = Text( 376 "\n".join(self.milestone_notes) if self.milestone_notes else "Milestones will light up as the stream advances.", 377 style=DIM, 378 ) 379 return Panel( 380 Group(grid, Rule(style="bright_black"), notes), 381 title="[bold white]Sequence Markers", 382 border_style="magenta", 383 padding=(1, 1), 384 ) 385 386 def _stream_panel(self) -> Panel: 387 if self.stream_text.plain.strip(): 388 body: RenderableType = self.stream_text 389 else: 390 body = Text("Awaiting the first gRPC chunk...", style=DIM) 391 return Panel( 392 body, 393 title=f"[bold white]Act II Live Stream ({self.metrics['chunk_count']} chunks)", 394 border_style="bright_green", 395 padding=(1, 1), 396 ) 397 398 def _status_panel(self) -> Panel: 399 body = Text("\n".join(self.status_lines) if self.status_lines else "Ready.", style=INK) 400 return Panel(body, title="[bold white]Director Notes", border_style="bright_magenta", padding=(1, 1)) 401 402 def _decision_panel(self) -> Panel: 403 if self.outcome.generation_error: 404 return Panel( 405 Text(self.outcome.generation_error, style=DANGER), 406 title="[bold white]Act III Outcome", 407 border_style="bright_red", 408 padding=(1, 1), 409 ) 410 if self.outcome.parse_error: 411 return Panel( 412 Group( 413 Text("Live stream completed, but structured validation failed.", style=AMBER), 414 Rule(style="bright_black"), 415 Text(self.outcome.parse_error, style=INK), 416 ), 417 title="[bold white]Act III Outcome", 418 border_style="bright_yellow", 419 padding=(1, 1), 420 ) 421 if self.outcome.decision is None: 422 return Panel( 423 Text("Validated decision will appear here after parsing.", style=DIM), 424 title="[bold white]Act III Outcome", 425 border_style="bright_yellow", 426 padding=(1, 1), 427 ) 428 decision = self.outcome.decision 429 metadata = Table.grid(expand=True, padding=(0, 2)) 430 metadata.add_column(style="bold white") 431 metadata.add_column(style=INK) 432 metadata.add_row("Severity", decision.severity.upper()) 433 metadata.add_row("Confidence", f"{decision.confidence:.2f}") 434 metadata.add_row("Change risk", decision.change_risk.upper()) 435 metadata.add_row("Escalation", decision.escalation_team) 436 437 actions = Text() 438 for step in decision.recommended_actions[:4]: 439 actions.append(f"- {step.priority} {step.action}\n", style=AMBER) 440 actions.append(f" {step.owner}: {step.rationale}\n", style=DIM) 441 442 impacted = ", ".join(decision.impacted_assets[:5]) 443 return Panel( 444 Group( 445 Text(decision.executive_summary, style=INK), 446 Rule(style="bright_black"), 447 metadata, 448 Rule(style="bright_black"), 449 Text(f"Impacted assets: {impacted}", style=DIM), 450 Rule(style="bright_black"), 451 Text("Top Actions", style="bold white"), 452 actions, 453 ), 454 title="[bold white]Act III Outcome", 455 border_style="bright_yellow", 456 padding=(1, 1), 457 ) 458 459 def _api_or_json_panel(self) -> Panel: 460 if self.api_snapshot.ok: 461 renderable: RenderableType = JSON(self.api_snapshot.body) 462 title = self.api_snapshot.title 463 border_style = "bright_green" 464 elif self.final_decision_json.strip() != "{}": 465 renderable = Syntax(self.final_decision_json, "json", theme="monokai", line_numbers=False) 466 title = "Raw JSON" 467 border_style = "bright_black" 468 else: 469 renderable = Text(self.api_snapshot.body, style=DIM) 470 title = self.api_snapshot.title 471 border_style = "bright_black" 472 return Panel(renderable, title=f"[bold white]{title}", border_style=border_style, padding=(1, 1)) 473 474 def layout(self) -> Layout: 475 layout = Layout() 476 if self._is_compact(): 477 layout.split_column( 478 Layout(name="header", size=4), 479 Layout(name="hero", size=12), 480 Layout(name="metrics", size=10), 481 Layout(name="stream", ratio=3), 482 Layout(name="milestones", size=12), 483 Layout(name="outcome", size=16), 484 ) 485 layout["header"].update(self._header_panel()) 486 layout["hero"].update(self._hero_panel()) 487 layout["metrics"].update(self._metrics_panel()) 488 layout["stream"].update(self._stream_panel()) 489 layout["milestones"].update(self._milestones_panel()) 490 layout["outcome"].split_row( 491 Layout(name="decision", ratio=2), 492 Layout(name="api", ratio=1), 493 ) 494 layout["decision"].update(self._decision_panel()) 495 layout["api"].update(self._api_or_json_panel()) 496 return layout 497 498 layout.split_column( 499 Layout(name="header", size=4), 500 Layout(name="body"), 501 Layout(name="footer", size=16), 502 ) 503 layout["body"].split_row( 504 Layout(name="left", ratio=5), 505 Layout(name="right", ratio=3), 506 ) 507 layout["left"].split_column( 508 Layout(name="hero", size=11), 509 Layout(name="stream", ratio=3), 510 ) 511 layout["right"].split_column( 512 Layout(name="metrics", size=11), 513 Layout(name="milestones", size=12), 514 Layout(name="status", ratio=1), 515 ) 516 layout["footer"].split_row( 517 Layout(name="decision", ratio=2), 518 Layout(name="api", ratio=1), 519 ) 520 layout["header"].update(self._header_panel()) 521 layout["hero"].update(self._hero_panel()) 522 layout["stream"].update(self._stream_panel()) 523 layout["metrics"].update(self._metrics_panel()) 524 layout["milestones"].update(self._milestones_panel()) 525 layout["status"].update(self._status_panel()) 526 layout["decision"].update(self._decision_panel()) 527 layout["api"].update(self._api_or_json_panel()) 528 return layout 529 530 def _refresh(self, live: Live) -> None: 531 live.update(self.layout()) 532 533 def _fetch_api_json(self, path: str) -> ApiSnapshot: 534 if not self.api_base: 535 return ApiSnapshot("API Handoff", "API handoff disabled for this run.", False) 536 url = f"{self.api_base}{path}" 537 try: 538 with urlopen(url, timeout=3.0) as response: 539 payload = response.read().decode("utf-8") 540 parsed = json.loads(payload) 541 return ApiSnapshot(f"API Handoff {path}", json.dumps(parsed), True) 542 except HTTPError as exc: 543 return ApiSnapshot( 544 f"API Handoff {path}", 545 f"HTTP {exc.code} from {url}. Wrapper API is not ready for handoff yet.", 546 False, 547 ) 548 except (URLError, TimeoutError, json.JSONDecodeError) as exc: 549 return ApiSnapshot( 550 f"API Handoff {path}", 551 f"Unavailable: {exc}", 552 False, 553 ) 554 555 def _handle_stage_update(self, update: TriageUpdate, live: Live) -> None: 556 milestone = STAGE_TO_MILESTONE.get(update.stage) 557 if milestone: 558 self._mark_milestone(milestone, update.text) 559 self._append_status(update.text) 560 self._refresh(live) 561 562 def run(self) -> None: 563 incident = get_incident(self.scenario) 564 self.surface = discover_surface(endpoint=self.endpoint) 565 self.runtime_probe = self.surface["runtime_probe"] 566 write_summary(self.surface) 567 self.metrics["selected_rpc"] = self._selected_rpc() 568 self.api_snapshot = self._fetch_api_json("/health") 569 self._mark_milestone("discovery", "Installed protobufs and runtime descriptors resolved.") 570 self._append_status("Discovered installed protobuf descriptors and live runtime surface.") 571 self._append_status(f"Selected RPC path {self.metrics['selected_rpc']}.") 572 self._append_status(f"Tokenizer strategy: {self._tokenizer_label()}.") 573 self._show_title_card() 574 575 with Live(self.layout(), refresh_per_second=8, screen=True) as live: 576 self.act_title = "Act II Live Inference" 577 self._append_status("Opening the live vLLM gRPC stream.") 578 self._mark_milestone("schema_lock", "GenerateRequest includes SamplingParams.json_schema.") 579 self._refresh(live) 580 self._pause(0.7) 581 582 client = VllmGrpcClient(endpoint=self.endpoint, surface=self.surface) 583 self._stream_started_at = time.perf_counter() 584 585 def on_text(chunk: str) -> None: 586 self.metrics["chunk_count"] = int(self.metrics["chunk_count"]) + 1 if self.metrics["chunk_count"] != "-" else 1 587 if self._first_chunk_seen_at is None: 588 self._first_chunk_seen_at = time.perf_counter() 589 ttft_ms = (self._first_chunk_seen_at - self._stream_started_at) * 1000 590 self.metrics["time_to_first_update_ms"] = f"{ttft_ms:.2f} ms" 591 self._mark_milestone("first_chunk", "First streamed token chunk received.") 592 self._append_status("First streamed chunk landed in the terminal.") 593 self.stream_text.append(chunk.replace("<|im_end|>", ""), style=INK) 594 self._refresh(live) 595 596 generation = None 597 try: 598 generation = client.generate_incident( 599 incident, 600 stream=True, 601 on_text=on_text, 602 on_update=lambda update: self._handle_stage_update(update, live), 603 ) 604 except (VllmGrpcError, Exception) as exc: 605 self.outcome.generation_error = str(exc) 606 self.act_title = "Act III Degraded Outcome" 607 self._append_status("Live gRPC generation failed; rendered degraded state instead of crashing.") 608 self._refresh(live) 609 self._pause(1.2) 610 finally: 611 client.close() 612 613 if generation is not None: 614 self.metrics["chunk_count"] = generation.chunk_count 615 self.metrics["time_to_first_update_ms"] = ( 616 f"{generation.time_to_first_update_ms:.2f} ms" 617 if generation.time_to_first_update_ms is not None 618 else "-" 619 ) 620 self.metrics["end_to_end_latency_ms"] = f"{generation.end_to_end_latency_ms:.2f} ms" 621 self.metrics["output_bytes_received"] = generation.output_bytes_received 622 self.metrics["finish_reason"] = generation.finish_reason or "-" 623 624 self.act_title = "Act III Structured Outcome" 625 try: 626 decision = parse_final_decision(generation.raw_output, incident.incident_id) 627 self.outcome.decision = decision 628 self.final_decision_json = decision.model_dump_json(indent=2) 629 self._mark_milestone("validation", "Final JSON validated against the application contract.") 630 self._append_status("Structured output validated against the application contract.") 631 if self.save: 632 save_final_decision(decision) 633 self._append_status(f"Saved validated output to {FINAL_DECISION_PATH}.") 634 except ValueError as exc: 635 self.outcome.parse_error = str(exc) 636 self.final_decision_json = generation.raw_output 637 self._append_status("Structured validation failed; retaining raw output for inspection.") 638 639 self._refresh(live) 640 self._pause(0.8) 641 642 self.api_snapshot = self._fetch_api_json("/last-decision") 643 if self.api_snapshot.ok: 644 self._mark_milestone("api_handoff", "Wrapper API fetched the latest validated decision.") 645 self._append_status("Fetched downstream handoff view from the wrapper API.") 646 else: 647 self._append_status("API handoff unavailable; keeping local decision view visible.") 648 self._refresh(live) 649 self._pause(1.4) 650 651 self.console.print() 652 self.console.print(Rule("[bold bright_cyan]NeuroWave vLLM gRPC Demo")) 653 self.console.print(f"Scenario: {self.scenario}") 654 self.console.print(f"Selected RPC: {self.metrics['selected_rpc']}") 655 self.console.print(f"TTFT: {self.metrics['time_to_first_update_ms']}") 656 self.console.print(f"End-to-end latency: {self.metrics['end_to_end_latency_ms']}") 657 self.console.print(f"Output bytes: {self.metrics['output_bytes_received']}") 658 self.console.print(f"Chunks: {self.metrics['chunk_count']}") 659 self.console.print(f"Finish reason: {self.metrics['finish_reason']}") 660 if self.api_base: 661 self.console.print(f"API handoff: {self.api_base}/last-decision") 662 self.console.print() 663 if self.outcome.decision is not None: 664 self.console.print(Syntax(self.final_decision_json, "json", theme="monokai", line_numbers=False)) 665 elif self.outcome.parse_error: 666 self.console.print(f"[bold yellow]{self.outcome.parse_error}[/bold yellow]") 667 elif self.outcome.generation_error: 668 self.console.print(f"[bold red]{self.outcome.generation_error}[/bold red]") 669 670 671 def main() -> None: 672 parser = argparse.ArgumentParser(description="Run the polished Incident Commander demo TUI.") 673 parser.add_argument("--scenario", choices=scenario_names(), default="industrial") 674 parser.add_argument("--endpoint", default="localhost:8000") 675 parser.add_argument("--api-base", default=None, help="Optional API base URL for /health and /last-decision handoff.") 676 parser.add_argument("--title-seconds", type=float, default=1.6, help="How long to keep the branded title card on screen.") 677 parser.add_argument("--pause-scale", type=float, default=1.0, help="Multiply built-in stage pauses by this value.") 678 parser.add_argument( 679 "--profile", 680 choices=("recording", "live"), 681 default="recording", 682 help="Recording emphasizes staged reveals; live shortens pauses for resilience.", 683 ) 684 parser.add_argument( 685 "--no-save", 686 action="store_true", 687 help="Do not write outputs/final_decision.json.", 688 ) 689 args = parser.parse_args() 690 691 WowDemoTUI( 692 scenario=args.scenario, 693 endpoint=args.endpoint, 694 save=not args.no_save, 695 api_base=args.api_base, 696 title_seconds=args.title_seconds, 697 pause_scale=args.pause_scale, 698 profile=args.profile, 699 ).run() 700 701 702 if __name__ == "__main__": 703 try: 704 main() 705 except KeyboardInterrupt: 706 sys.exit(130)