/ app / demo_tui.py
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)