Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ The same read-only view is available through
`inferrsi.apps.api.read_attempt_view`. The legacy `status --run-id` form
reads the legacy experiment directory.

Use the same reference files with `trace`, `report` or `dashboard`.
`trace` prints the view plus recorded steps, checks and error codes. An
unknown step time stays null. `report` and offline `dashboard` accept
`--output <file.html>` and present the same facts and claims. With
`dashboard --live`, the loopback page and its state endpoint serve the
snapshot pinned at startup. The campaign Dashboard remains available through
`--input`, including its reviewed translation overlay.

## Campaign summary figure

### Summarize a campaign figure — `tools/campaign_summary_chart.py`
Expand Down
19 changes: 17 additions & 2 deletions src/inferrsi/apps/views/attempt.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@
"process_outcome", "exit_code", "recorded_at", "prepared_ref",
)
_CLAIM_FIELDS = ("claim_id", "validation_scope", "verdict", "reasons", "missing_evidence")
_TRACE_FIELDS = {
"steps": (
"step_id", "status", "started_at", "ended_at", "elapsed_seconds", "exit_code", "reasons",
),
"checks": ("check_id", "subject", "phase", "status", "reasons"),
"errors": ("code", "stage", "retryable"),
}


def read_attempt_view(
record_ref: dict, *, storage_config: dict, assessment_ref: dict | None = None,
include_trace: bool = False,
) -> dict:
"""Return fixed execution facts and claim outcomes for one record revision."""
"""Return fixed facts, claim outcomes and optional trace for one record revision."""
try:
_check_ref(record_ref, "attempt_id", "record_version")
if record_ref["artifact"]["media_type"] != "application/json":
Expand All @@ -44,6 +52,10 @@ def read_attempt_view(
execution["cleanup_status"] = record["cleanup"]["status"]
candidate = record["candidate"]
execution["candidate_id"] = candidate["candidate_id"] if candidate is not None else None
trace = {
group: [{key: item[key] for key in fields} for item in record[group]]
for group, fields in _TRACE_FIELDS.items()
} if include_trace else None
except (KeyError, TypeError, ValueError):
raise PublicError(
"ArtifactIntegrityError", stage="attempt_view",
Expand Down Expand Up @@ -82,7 +94,10 @@ def read_attempt_view(
message="Invalid archived assessment for this record.",
) from None

return {
view = {
"record_ref": deepcopy(record_ref), "execution": execution,
"assessment_ref": deepcopy(assessment_ref), "assessment": evaluation,
}
if include_trace:
view["trace"] = trace
return view
87 changes: 87 additions & 0 deletions src/inferrsi/apps/views/attempt_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Render a fixed attempt for the report and Dashboard."""

from html import escape


def _text(value: object) -> str:
if value is None:
return "Unknown"
if isinstance(value, list):
return escape(", ".join(str(item) for item in value)) if value else "—"
return escape(str(value))


def _table(rows: list[dict], columns: tuple[tuple[str, str], ...]) -> str:
if not rows:
return "<p>No entries recorded.</p>"
head = "".join(f'<th scope="col">{label}</th>' for _, label in columns)
body = [
"<tr>" + "".join(f"<td>{_text(row[key])}</td>" for key, _ in columns) + "</tr>"
for row in rows
]
return (
f'<div class="scroll"><table><thead><tr>{head}</tr></thead>'
f'<tbody>{"".join(body)}</tbody></table></div>'
)


def render_attempt(view: dict, *, title: str) -> str:
"""Present the shared read model without exposing raw log or artifact paths."""
execution, assessment, trace = view["execution"], view["assessment"], view["trace"]
facts = (
("Run", execution["run_id"]),
("Attempt", execution["attempt_id"]),
("Record revision", execution["record_version"]),
("Record SHA-256", view["record_ref"]["artifact"]["sha256"]),
("Recorded at", execution["recorded_at"]),
("Candidate", execution["candidate_id"]),
("Phase", execution["phase"]),
("Process outcome", execution["process_outcome"]),
("Exit code", execution["exit_code"]),
("Cleanup", execution["cleanup_status"]),
)
summary = "".join(f"<dt>{label}</dt><dd>{_text(value)}</dd>" for label, value in facts)
claims = "<p>No archived assessment selected.</p>"
if assessment is not None:
digest = _text(view["assessment_ref"]["artifact"]["sha256"])
claims = f"<p>Assessment SHA-256 <code>{digest}</code></p>" + _table(
assessment["claims"],
(("claim_id", "Claim"), ("validation_scope", "Scope"), ("verdict", "Verdict"),
("reasons", "Reasons"), ("missing_evidence", "Missing evidence")),
)

steps = _table(
trace["steps"],
(("step_id", "Step"), ("status", "Status"), ("started_at", "Started"),
("ended_at", "Ended"), ("elapsed_seconds", "Seconds"), ("exit_code", "Exit code"),
("reasons", "Reasons")),
)
checks = _table(
trace["checks"],
(("check_id", "Check"), ("subject", "Subject"), ("phase", "Phase"), ("status", "Status"),
("reasons", "Reasons")),
)
errors = _table(
trace["errors"], (("code", "Code"), ("stage", "Stage"), ("retryable", "Retryable")),
)
name = escape(title)
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="InferRSI {name}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
<title>InferRSI {name}</title>
<style>
body{{font:16px/1.6 system-ui,sans-serif;max-width:1050px;margin:40px auto;
padding:0 20px;background:#fafafa;color:#182432}}
section{{margin:24px 0;padding:18px;background:white;border:1px solid #ddd}}
dl{{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:4px 20px}}
dd{{margin:0;overflow-wrap:anywhere}}dt,th{{font-weight:650}}.scroll{{overflow:auto}}
table{{border-collapse:collapse;width:100%}}td,th{{padding:8px;text-align:left;
vertical-align:top;border-bottom:1px solid #ddd}}code{{overflow-wrap:anywhere}}
</style></head>
<body><main><h1>InferRSI {name}</h1>
<section><h2>Execution</h2><dl>{summary}</dl></section>
<section><h2>Archived assessment</h2>{claims}</section>
<section><h2>Trace</h2><h3>Steps</h3>{steps}<h3>Checks</h3>{checks}<h3>Errors</h3>{errors}</section>
</main></body></html>"""
17 changes: 16 additions & 1 deletion src/inferrsi/apps/views/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from inferrsi.harness.records.evidence import inside, sanitized_text
from inferrsi.summary_chart import SpecError, build_svg

from .attempt_html import render_attempt
from .dashboard_data import _number, load_dashboard
from .dashboard_timeline import temporal_chart

Expand Down Expand Up @@ -334,6 +335,8 @@ def dashboard_snapshot(source: Path, translations: Path | None = None) -> dict:

def render_dashboard(snapshot: dict, *, live: bool = False) -> str:
"""Render a snapshot; only the local live view enables same-origin requests."""
if "fixed_attempt" in snapshot["data"]:
return render_attempt(snapshot["data"]["fixed_attempt"], title="fixed attempt Dashboard")
view = {**snapshot["data"], "live": live}
payload = json.dumps(view, ensure_ascii=False, allow_nan=False)
# The JSON data block cannot terminate its script element or create markup.
Expand Down Expand Up @@ -366,6 +369,18 @@ def write_dashboard(source: Path, destination: Path | None = None,
source = Path(source).resolve()
document = render_dashboard(dashboard_snapshot(source, translations))
output = Path(destination) if destination is not None else source / "dashboard.html"
return _write_view_document(output, document, generator="InferRSI dashboard")


def write_attempt_document(view: dict, destination: Path, *, kind: str) -> Path:
"""Export the shared attempt view as the selected HTML document."""
title = "fixed attempt Dashboard" if kind == "dashboard" else "fixed attempt report"
return _write_view_document(
destination, render_attempt(view, title=title), generator=f"InferRSI {title}",
)


def _write_view_document(output: Path, document: str, *, generator: str) -> Path:
output.parent.mkdir(parents=True, exist_ok=True)
if output.is_symlink():
raise ValueError("dashboard output cannot be a symbolic link")
Expand All @@ -375,7 +390,7 @@ def write_dashboard(source: Path, destination: Path | None = None,
if output.suffix.lower() != ".html":
raise ValueError("refusing to overwrite an existing non-dashboard file")
with output.open("r", encoding="utf-8", errors="replace") as existing:
if '<meta name="generator" content="InferRSI dashboard">' not in existing.read(1024):
if f'<meta name="generator" content="{generator}">' not in existing.read(1024):
raise ValueError("refusing to overwrite an existing non-dashboard file")
temporary = None
try:
Expand Down
29 changes: 22 additions & 7 deletions src/inferrsi/apps/views/dashboard_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import sys
from collections import Counter
from copy import deepcopy
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

Expand Down Expand Up @@ -44,14 +45,25 @@ def _compact_contexts(snapshot: dict) -> dict:
class DashboardServer(ThreadingHTTPServer):
"""Only expose the generated view, never the underlying directory."""

def __init__(self, source: Path, translations: Path | None, port: int):
def __init__(
self, source: Path | None, translations: Path | None, port: int,
*, fixed_attempt: dict | None = None,
):
if not 0 <= port <= 65535:
raise ValueError("dashboard port must be between 0 and 65535")
self.source = Path(source).resolve()
if (source is None) == (fixed_attempt is None):
raise ValueError("select a Dashboard directory or a fixed attempt")
self.source = Path(source).resolve() if source is not None else None
self.translations = Path(translations).resolve() if translations else None
dashboard_snapshot(self.source, self.translations)
self._fixed_attempt = deepcopy(fixed_attempt)
self.snapshot()
super().__init__(("127.0.0.1", port), DashboardHandler)

def snapshot(self) -> dict:
if self._fixed_attempt is not None:
return {"data": {"fixed_attempt": self._fixed_attempt}, "chart": "", "temporal": ""}
return dashboard_snapshot(self.source, self.translations)


class DashboardHandler(BaseHTTPRequestHandler):
server: DashboardServer
Expand Down Expand Up @@ -107,7 +119,7 @@ def do_GET(self) -> None:
self._reply(403, b"Invalid host")
return
events = re.fullmatch(r"/api/profile-events/([0-9a-f]{64})", self.path)
if events:
if events and self.server.source is not None:
from inferrsi.harness.records.profile_timeline import profile_event_table

payload = profile_event_table(self.server.source, events.group(1))
Expand All @@ -120,7 +132,7 @@ def do_GET(self) -> None:
self._reply(404, b"Not found")
return
try:
snapshot = dashboard_snapshot(self.server.source, self.server.translations)
snapshot = self.server.snapshot()
if self.path == "/":
html = render_dashboard(_compact_contexts(snapshot), live=True)
body, encoding = self._representation(html.encode("utf-8"))
Expand All @@ -142,9 +154,12 @@ def do_GET(self) -> None:
self._reply(503, b"Dashboard snapshot is temporarily unavailable")


def serve_dashboard(source: Path, translations: Path | None = None, port: int = 8765) -> None:
def serve_dashboard(
source: Path | None, translations: Path | None = None, port: int = 8765,
*, fixed_attempt: dict | None = None,
) -> None:
"""Serve until interrupted; the listening socket never leaves loopback."""
with DashboardServer(source, translations, port) as server:
with DashboardServer(source, translations, port, fixed_attempt=fixed_attempt) as server:
print(json.dumps({"dashboard": f"http://127.0.0.1:{server.server_port}/"}), flush=True)
try:
server.serve_forever(poll_interval=0.25)
Expand Down
34 changes: 25 additions & 9 deletions src/inferrsi/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@
from .runspec import RunSpec


def _fixed_attempt_args(
parser: argparse.ArgumentParser, *, legacy_flag: str | None = None,
) -> None:
source = parser
if legacy_flag is not None:
source = parser.add_mutually_exclusive_group(required=True)
if legacy_flag == "--input":
source.add_argument("--input", type=Path, help="Run or campaign directory")
else:
source.add_argument(legacy_flag)
source.add_argument(
"--record-ref", type=Path, required=legacy_flag is None,
help="JSON file containing a RecordRef",
)
parser.add_argument("--storage-config", type=Path, help="Trusted storage config JSON")
parser.add_argument("--assessment-ref", type=Path, help="Optional AssessmentRef JSON")


def _common(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--repo", default=".", help="Prepared target Git checkout")
parser.add_argument("--repo-type", choices=REPO_TYPES, default="vllm_omni")
Expand Down Expand Up @@ -211,20 +229,18 @@ def parser() -> argparse.ArgumentParser:
name,
help="Read a recorded experiment" if name == "status" else "Regenerate its HTML report",
)
if name == "status":
source = child.add_mutually_exclusive_group(required=True)
source.add_argument("--run-id")
source.add_argument("--record-ref", type=Path, help="JSON file containing a RecordRef")
child.add_argument("--storage-config", type=Path, help="Trusted storage config JSON")
child.add_argument("--assessment-ref", type=Path, help="Optional AssessmentRef JSON")
else:
child.add_argument("--run-id", required=True)
_fixed_attempt_args(child, legacy_flag="--run-id")
if name == "report":
child.add_argument("--output", type=Path, help="HTML output for a fixed RecordRef")
child.add_argument("--output-dir", default="runs")

trace = commands.add_parser("trace", help="Read recorded steps, checks and errors")
_fixed_attempt_args(trace)

dashboard = commands.add_parser(
"dashboard", help="Export an offline dashboard for one run or recorded campaign"
)
dashboard.add_argument("--input", type=Path, required=True, help="Run or campaign directory")
_fixed_attempt_args(dashboard, legacy_flag="--input")
dashboard.add_argument(
"--output", type=Path, help="HTML output (default: input/dashboard.html)"
)
Expand Down
Loading
Loading