"""Local browser dashboard for the SemIf agent. A pure-stdlib HTTP server on localhost serving a Redux-DevTools-style inspector over the agent's decision flow. Reads the decision log (`decisions.jsonl`) plus the run lifecycle trace (`runs.jsonl`), exposes the static skill tree, the dream cost report, and two write endpoints: submit a request and relabel a decision (human override). The scheduler's engine and LLM are built lazily, so the dashboard runs on the thin dev box in replay mode (reads logs; submit degrades to a JSON error) and in live mode on the box with SemIf + a local LLM. python -m semif_agent.cli dashboard [--port 8765] """ from __future__ import annotations import json import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from .dream import dream from .scheduler import Scheduler from .skills import build_skills, build_tree STATIC_DIR = Path(__file__).parent / "static" JSON_HEADERS = {"Content-Type": "application/json; charset=utf-8"} def _phase(row: dict) -> str: return (row.get("extra") or {}).get("phase", "") def _enrich_decision(row: dict, costs: dict) -> dict: out = dict(row) out["phase"] = _phase(row) cost = costs.get(row.get("id")) if cost is not None: out["cost"] = cost return out def build_payload(scheduler: Scheduler) -> dict: """Group logged decisions by run, attach lifecycle events and dream costs.""" events_by_run = scheduler.trace.runs() rows = scheduler.log.read() costs = {} for cost_row in dream(scheduler.log).rows: costs[cost_row.decision_id] = { "predicted": cost_row.predicted, "nll": cost_row.nll, "weight": cost_row.weight, "correct": cost_row.correct, } by_run: dict[str, dict] = {} for row in rows: run_id = (row.get("extra") or {}).get("run_id", "?") run = by_run.setdefault( run_id, {"run_id": run_id, "events": [], "decisions": [], "first_ts": None} ) run["first_ts"] = row["ts"] if run["first_ts"] is None else min(run["first_ts"], row["ts"]) run["decisions"].append(_enrich_decision(row, costs)) for run_id, events in events_by_run.items(): run = by_run.setdefault( run_id, {"run_id": run_id, "events": [], "decisions": [], "first_ts": None} ) run["events"] = events event_ts = [e["ts"] for e in events if "ts" in e] if event_ts: earliest = min(event_ts) run["first_ts"] = earliest if run["first_ts"] is None else min(run["first_ts"], earliest) runs = sorted(by_run.values(), key=lambda r: (r["first_ts"] is None, r["first_ts"] or 0)) for run in runs: run["decisions"].sort(key=lambda d: d["ts"]) return {"runs": runs} def build_dream_report(scheduler: Scheduler) -> dict: report = dream(scheduler.log) return { "rows": len(report.rows), "skipped": report.skipped, "human_overrides": report.human_overrides, "cross_entropy": report.cross_entropy, "accuracy": report.accuracy, "ece": report.ece, } def build_status(scheduler: Scheduler) -> dict: current = ( {"skill": scheduler.current.skill, "request_id": scheduler.current.request.id} if scheduler.current else None ) queue = [ {"id": request.id, "weight": weight, "text": request.text[:80]} for weight, request in scheduler.queue.items() ] return { "current": current, "queue": queue, "tau": scheduler.tau, "queue_max": scheduler.queue.max_size, } def build_tree_payload(scheduler: Scheduler) -> dict: tree = scheduler.tree return { "categories": { category: [{"name": skill.name, "description": skill.description} for skill in skills] for category, skills in sorted(tree.items()) } } class DashboardHandler(BaseHTTPRequestHandler): scheduler: Scheduler lock: threading.Lock = threading.Lock() server_version = "semif-dashboard/0.1" # ---- helpers ---- def _send(self, code: int, payload: dict, headers: dict | None = None): body = json.dumps(payload).encode("utf-8") self.send_response(code) for key, value in JSON_HEADERS.items(): self.send_header(key, value) if headers: for key, value in headers.items(): self.send_header(key, value) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _send_error(self, code: int, message: str): self._send(code, {"error": message}) def _read_json(self) -> dict: length = int(self.headers.get("Content-Length") or 0) if length <= 0: return {} return json.loads(self.rfile.read(length).decode("utf-8")) def _serve_static(self, rel: str): if not rel or rel == "/": rel = "index.html" target = (STATIC_DIR / rel).resolve() if not str(target).startswith(str(STATIC_DIR.resolve())): self._send_error(403, "forbidden") return if not target.is_file(): self._send_error(404, "not found") return body = target.read_bytes() content_type = { ".html": "text/html; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", }.get(target.suffix, "application/octet-stream") self.send_response(200) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) # ---- routing ---- def do_GET(self): path = self.path.split("?", 1)[0].rstrip("/") or "/" if path == "/" or path.startswith("/static/"): rel = path.removeprefix("/static/").removeprefix("/") self._serve_static(rel) return if path == "/api/trace": self._send(200, build_payload(self.scheduler)) return if path == "/api/tree": self._send(200, build_tree_payload(self.scheduler)) return if path == "/api/dream": self._send(200, build_dream_report(self.scheduler)) return if path == "/api/status": self._send(200, build_status(self.scheduler)) return self._send_error(404, "no such endpoint") def do_POST(self): path = self.path.split("?", 1)[0] if path == "/api/submit": with self.lock: try: body = self._read_json() status, detail = self.scheduler.submit( str(body.get("text", "")), source=str(body.get("source", "dashboard")) ) except Exception as exc: self._send_error(500, str(exc)) return self._send(200, {"status": status, "detail": detail}) return if path == "/api/relabel": with self.lock: try: body = self._read_json() ok = self.scheduler.log.relabel(str(body.get("id", "")), str(body.get("outcome", ""))) except Exception as exc: self._send_error(500, str(exc)) return self._send(200, {"ok": ok}) return self._send_error(404, "no such endpoint") def log_message(self, format, *args): pass def serve(scheduler: Scheduler, port: int = 8765, host: str = "127.0.0.1") -> None: handler = type("Handler", (DashboardHandler,), {"scheduler": scheduler, "lock": threading.Lock()}) server = ThreadingHTTPServer((host, port), handler) bound = server.server_address[0] url = f"http://{bound}:{server.server_address[1]}/" if bound != "0.0.0.0" else _lan_url(port) print(f"semif dashboard on {url} (Ctrl-C to stop)") try: server.serve_forever() except KeyboardInterrupt: pass finally: server.server_close() def _lan_url(port: int) -> str: import socket try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(("8.8.8.8", 80)) ip = sock.getsockname()[0] sock.close() except OSError: ip = "127.0.0.1" return f"http://{ip}:{port}/"