From bf075f3c43fd45c76b009e06987f3ecc58e5cb31 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Thu, 28 May 2026 21:46:30 +0200 Subject: [PATCH] DPL MCP: a server to investigate running trains on hyperloop --- .../hyperloop_server.cpython-314.pyc | Bin 0 -> 17267 bytes .../hyperloop-server/hyperloop_server.py | 261 ++++++++++++++++++ .../scripts/hyperloop-server/pyproject.toml | 19 ++ 3 files changed, 280 insertions(+) create mode 100644 Framework/Core/scripts/hyperloop-server/__pycache__/hyperloop_server.cpython-314.pyc create mode 100644 Framework/Core/scripts/hyperloop-server/hyperloop_server.py create mode 100644 Framework/Core/scripts/hyperloop-server/pyproject.toml diff --git a/Framework/Core/scripts/hyperloop-server/__pycache__/hyperloop_server.cpython-314.pyc b/Framework/Core/scripts/hyperloop-server/__pycache__/hyperloop_server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b69ae691c2064c4975f397c3e900dcc1375596d1 GIT binary patch literal 17267 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 dict[str, str]: + return {"Authorization": f"Bearer {TOKEN}"} + + +async def _get(path: str, params: dict | None = None) -> any: + hdrs = _headers() + hdrs["Accept-Encoding"] = "identity" + async with httpx.AsyncClient(timeout=30) as client: + r = await client.get(f"{API}/{path}", params=params, headers=hdrs) + r.raise_for_status() + return r.json() + + +def _fmt_bytes(n: float | None) -> str: + if n is None: + return "n/a" + for unit in ("B", "KB", "MB", "GB", "TB"): + if abs(n) < 1024: + return f"{n:.1f} {unit}" + n /= 1024 + return f"{n:.1f} PB" + + +def _fmt_time(seconds: float | None) -> str: + if seconds is None: + return "n/a" + if seconds < 60: + return f"{seconds:.0f}s" + if seconds < 3600: + return f"{seconds / 60:.1f}m" + return f"{seconds / 3600:.1f}h" + + +def _parse_job_status(raw: str | None) -> dict: + if not raw: + return {} + js = json.loads(raw) if isinstance(raw, str) else raw + done = sum(v for k, v in js.items() if k.startswith("DONE")) + total = js.get("TOTAL", 0) + errors = sum(v for k, v in js.items() + if k.startswith("ERROR") or k.startswith("EXPIRED") + or k.startswith("FAILED") or k.startswith("KILLED")) + active = sum(v for k, v in js.items() + if k.startswith("R") or k.startswith("A") or k.startswith("S")) + wait = total - done - errors - active + return {"total": total, "done": done, "errors": errors, + "active": active, "wait": max(0, wait)} + + +@mcp.tool() +async def list_ongoing_trains() -> str: + """List all currently running / ready Hyperloop train runs. + + Returns a compact table with train ID, dataset, state, job progress, + error rate, and package tag. One API call. + """ + trains = await _get("trains/all-trains.jsp", {"state": "ready"}) + if not trains: + return "No ongoing trains." + + lines = [] + lines.append(f"{'ID':>8} {'State':<11} {'Done/Total':>12} {'Err%':>5} " + f"{'Dataset':<40} {'Package'}") + lines.append("-" * 120) + + for t in sorted(trains, key=lambda x: _parse_job_status( + x.get("job_status")).get("total", 0), reverse=True): + js = _parse_job_status(t.get("job_status")) + total = js.get("total", 0) + done = js.get("done", 0) + errors = js.get("errors", 0) + err_pct = f"{100 * errors / total:.1f}" if total > 0 else "n/a" + pkg = (t.get("package_tag") or "").replace("O2Physics::", "") + ds = t.get("dataset_name", "") + if len(ds) > 40: + ds = ds[:37] + "..." + lines.append( + f"{t['id']:>8} {t.get('state', '?'):<11} " + f"{done:>6}/{total:<6} {err_pct:>5} " + f"{ds:<40} {pkg}" + ) + + lines.append(f"\nTotal: {len(trains)} trains") + return "\n".join(lines) + + +@mcp.tool() +async def train_detail(train_id: int) -> str: + """Get resource metrics for a specific train run. + + Shows CPU time, wall time, memory (PSS), throughput, input/output + sizes, target, and merge status. One API call. + """ + t = await _get("trains/train.jsp", {"train_id": train_id, "type": "ready"}) + + lines = [f"Train {t['id']}: {t.get('dataset_name', '?')}"] + lines.append(f" State: {t.get('state')}") + lines.append(f" Package: {t.get('package_tag')}") + lines.append(f" Target: {t.get('target')}") + lines.append(f" CPU cores: {t.get('cpu_cores')}") + lines.append(f" CPU time: {_fmt_time(t.get('cpu_time'))}") + lines.append(f" Wall time: {_fmt_time(t.get('wall_time'))}") + lines.append(f" PSS memory: {_fmt_bytes(t.get('mem_pss'))} avg, " + f"{_fmt_bytes(t.get('mem_pss_max'))} max") + lines.append(f" Private mem: {_fmt_bytes(t.get('mem_private'))} avg, " + f"{_fmt_bytes(t.get('mem_private_max'))} max") + lines.append(f" Input size: {_fmt_bytes(t.get('input_size'))}") + lines.append(f" Output size: {_fmt_bytes(t.get('output_size'))}") + + throughput = t.get("estimated_throughput") + if throughput: + lines.append(f" Throughput: {_fmt_bytes(throughput)}/s") + + events = t.get("events") + if events and events > 0: + lines.append(f" Events: {events}") + + lines.append(f" Created: {t.get('created')}") + lines.append(f" Username: {t.get('username')}") + + return "\n".join(lines) + + +@mcp.tool() +async def wagon_stats(train_id: int) -> str: + """Get per-wagon CPU and memory breakdown for a train. + + Fetches wagon IDs from the train, then retrieves grid statistics + for each wagon. Typically 10-20 wagons, one API call each. + """ + # First get train detail for dataset_id and wagons_timestamp + t = await _get("trains/train.jsp", {"train_id": train_id, "type": "ready"}) + dataset_id = t.get("dataset_id") + wagons_ts = t.get("wagons_timestamp") or t.get("dataset_timestamp") + + if not dataset_id or not wagons_ts: + return f"Cannot determine dataset/timestamp for train {train_id}" + + # Get wagon IDs + wagons_data = await _get("trains/wagons_derived_data.jsp", + {"train_id": train_id, + "wagons_timestamp": wagons_ts}) + wagon_ids = list(wagons_data.keys()) if isinstance(wagons_data, dict) else [] + if not wagon_ids: + return f"No wagons found for train {train_id}" + + # Fetch stats for each wagon concurrently + async def fetch_one(wid: str) -> dict | None: + try: + stats = await _get("analysis/wagon/wagon-dataset-grid-statistics.jsp", + {"wagon_id": wid, "dataset_id": dataset_id}) + if isinstance(stats, dict) and str(train_id) in stats: + return stats[str(train_id)] + except Exception: + pass + return None + + results = await asyncio.gather(*(fetch_one(wid) for wid in wagon_ids)) + + rows = [] + for wid, stat in zip(wagon_ids, results): + if stat is None: + continue + rows.append(stat) + + if not rows: + return f"No wagon statistics available for train {train_id}" + + # Sort by CPU time descending + rows.sort(key=lambda r: r.get("cpu_time") or 0, reverse=True) + + lines = [f"Wagon stats for train {train_id} " + f"({t.get('dataset_name', '?')}), {len(rows)} wagons:\n"] + lines.append(f"{'Wagon':<35} {'CPU time':>10} {'PSS avg':>10} " + f"{'PSS max':>10} {'Throughput':>12} {'Done%':>6}") + lines.append("-" * 90) + + total_cpu = 0 + for r in rows: + name = r.get("wagon_name", f"id={r.get('wagon_id', '?')}") + if len(name) > 35: + name = name[:32] + "..." + cpu = r.get("cpu_time") or 0 + total_cpu += cpu + pss_avg = _fmt_bytes(r.get("mem_pss")) + pss_max = _fmt_bytes(r.get("mem_pss_max")) + tp = _fmt_bytes(r.get("throughput")) + "/s" if r.get("throughput") else "n/a" + pct = r.get("percent_done") + pct_str = f"{pct}%" if pct is not None else "n/a" + lines.append(f"{name:<35} {_fmt_time(cpu / 1000):>10} {pss_avg:>10} " + f"{pss_max:>10} {tp:>12} {pct_str:>6}") + + lines.append("-" * 90) + lines.append(f"Total CPU: {_fmt_time(total_cpu / 1000)}") + return "\n".join(lines) + + +def main(): + import argparse + global PROXY, TOKEN, API + + parser = argparse.ArgumentParser(description="AliHyperloop MCP server") + parser.add_argument("--proxy", default=PROXY, help="Proxy base URL") + parser.add_argument("--token", default=TOKEN, help="Bearer token") + args = parser.parse_args() + + PROXY = args.proxy + TOKEN = args.token + API = f"{PROXY}/alihyperloop-data" + + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/Framework/Core/scripts/hyperloop-server/pyproject.toml b/Framework/Core/scripts/hyperloop-server/pyproject.toml new file mode 100644 index 0000000000000..c135a517396df --- /dev/null +++ b/Framework/Core/scripts/hyperloop-server/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "hyperloop-server" +version = "0.1.0" +description = "MCP server for monitoring AliHyperloop train runs" +requires-python = ">=3.11" +dependencies = [ + "mcp>=1.0.0", + "httpx>=0.27.0", +] + +[project.scripts] +hyperloop-server = "hyperloop_server:main" + +[tool.hatch.build.targets.wheel] +include = ["hyperloop_server.py"]