Skip to content

Commit 48fa303

Browse files
committed
DPL MCP: add ability to inspect analyses on hyperloop
1 parent bb3cfe5 commit 48fa303

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Framework/Core/scripts/hyperloop-server/hyperloop_server.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,148 @@ async def fetch_one(wid: str) -> dict | None:
277277
return "\n".join(lines)
278278

279279

280+
# ---------------------------------------------------------------------------
281+
# Analysis / wagon browsing
282+
#
283+
# These mirror the alihyperloop web UI's analysis pages. Endpoint and param
284+
# names were taken from the frontend bundle (/hyperloop/assets/index-*.js);
285+
# unknown `lists` values silently return an empty array, and the wagon list
286+
# uses the *plural* `analysis_ids`.
287+
# ---------------------------------------------------------------------------
288+
289+
290+
@mcp.tool()
291+
async def list_analyses(username: str) -> str:
292+
"""List a user's Hyperloop analyses (id, name, JIRA, analyzers).
293+
294+
`username` is the CERN login of an analyzer (e.g. "eulisse").
295+
"""
296+
rows = await _get("analysis/list-analysis.jsp",
297+
{"lists": "analysis-by-username", "username": username})
298+
if not isinstance(rows, list) or not rows:
299+
return f"No analyses found for user '{username}'."
300+
lines = [f"Analyses for {username}:\n",
301+
f"{'ID':>7} {'Svc':<3} {'JIRA':<14} Name"]
302+
lines.append("-" * 70)
303+
for a in rows:
304+
svc = "yes" if a.get("service_analysis") else ""
305+
lines.append(f"{a.get('id'):>7} {svc:<3} {str(a.get('jira_id') or ''):<14} "
306+
f"{a.get('name')}")
307+
return "\n".join(lines)
308+
309+
310+
@mcp.tool()
311+
async def analysis_wagons(analysis_id: int) -> str:
312+
"""List the wagons of an analysis (wagon id, name, last test train id)."""
313+
data = await _get("analysis/wagons-by-analyses.jsp",
314+
{"analysis_ids": analysis_id})
315+
if not isinstance(data, dict) or not data:
316+
return f"No wagons found for analysis {analysis_id}."
317+
rows = sorted(data.values(), key=lambda w: str(w.get("name", "")).lower())
318+
lines = [f"{len(rows)} wagons in analysis {analysis_id}:\n",
319+
f"{'WagonID':>8} {'TrainID':>8} Name"]
320+
lines.append("-" * 70)
321+
for w in rows:
322+
lines.append(f"{w.get('id'):>8} {str(w.get('train_id') or '-'):>8} "
323+
f"{w.get('name')}")
324+
return "\n".join(lines)
325+
326+
327+
@mcp.tool()
328+
async def wagon_config(wagon_id: int, device: str = "") -> str:
329+
"""Show a wagon's merged configuration (device -> parameters).
330+
331+
If `device` is given, only devices whose name contains that substring are
332+
shown (e.g. "pid-tpc-service"); otherwise the device list + sizes is shown.
333+
"""
334+
cfg = await _get("analysis/wagon/download-configuration.jsp",
335+
{"wagon_id": wagon_id})
336+
if not isinstance(cfg, dict) or not cfg:
337+
return f"No configuration for wagon {wagon_id}."
338+
devices = {k: v for k, v in cfg.items() if isinstance(v, dict)}
339+
if not device:
340+
lines = [f"Wagon {wagon_id}: {len(devices)} configured devices:\n"]
341+
for k in sorted(devices):
342+
lines.append(f" {k} ({len(devices[k])} params)")
343+
lines.append("\nPass device=<substring> to see a device's parameters.")
344+
return "\n".join(lines)
345+
matched = {k: v for k, v in devices.items() if device in k}
346+
if not matched:
347+
return f"Wagon {wagon_id}: no device matching '{device}'."
348+
lines = []
349+
for k in sorted(matched):
350+
lines.append(f"[{k}]")
351+
for p in sorted(matched[k]):
352+
lines.append(f" {p} = {matched[k][p]}")
353+
lines.append("")
354+
return "\n".join(lines).rstrip()
355+
356+
357+
@mcp.tool()
358+
async def find_wagons_by_config(analysis_id: int, param: str,
359+
value: str | None = None) -> str:
360+
"""Find wagons in an analysis whose config sets a given parameter.
361+
362+
Scans every wagon's merged config for a device parameter whose name
363+
contains `param` (e.g. "useNetworkCorrection"). If `value` is given, only
364+
wagons where the parameter equals it are reported. Each hit resolves the
365+
wagon's dataset name(s) so Run 2 vs Run 3 is visible.
366+
367+
Example: find wagons running the TPC PID neural network ->
368+
find_wagons_by_config(50446, "pidTPC.useNetworkCorrection", "1")
369+
"""
370+
wagons = await _get("analysis/wagons-by-analyses.jsp",
371+
{"analysis_ids": analysis_id})
372+
if not isinstance(wagons, dict) or not wagons:
373+
return f"No wagons found for analysis {analysis_id}."
374+
375+
# wagon_id -> [dataset names], via the wagon<->dataset associations.
376+
assoc = await _get("analysis/wagondataset-by-analyses.jsp",
377+
{"analysis_ids": analysis_id})
378+
train_ids = {a.get("test_train_id") for a in (assoc or [])
379+
if a.get("test_train_id")}
380+
train_ds = {}
381+
for tid in train_ids:
382+
try:
383+
t = await _get("trains/train.jsp", {"train_id": tid})
384+
t = t[0] if isinstance(t, list) else t
385+
train_ds[tid] = t.get("dataset_name")
386+
except Exception:
387+
pass
388+
wagon_ds: dict = {}
389+
for a in (assoc or []):
390+
ds = train_ds.get(a.get("test_train_id"))
391+
if ds:
392+
wagon_ds.setdefault(str(a.get("wagon_id")), set()).add(ds)
393+
394+
hits = []
395+
for wid, w in wagons.items():
396+
try:
397+
cfg = await _get("analysis/wagon/download-configuration.jsp",
398+
{"wagon_id": wid})
399+
except Exception:
400+
continue
401+
for dev, c in cfg.items() if isinstance(cfg, dict) else []:
402+
if not isinstance(c, dict):
403+
continue
404+
for p, v in c.items():
405+
if param not in p:
406+
continue
407+
if value is not None and str(v) != str(value):
408+
continue
409+
ds = ", ".join(sorted(wagon_ds.get(str(wid), []))) or "?"
410+
hits.append((w.get("name"), wid, dev, p, str(v), ds))
411+
412+
if not hits:
413+
cond = f"{param}={value}" if value is not None else param
414+
return f"No wagons in analysis {analysis_id} match {cond}."
415+
lines = [f"Wagons in analysis {analysis_id} matching '{param}'"
416+
+ (f"={value}" if value is not None else "") + ":\n"]
417+
for name, wid, dev, p, v, ds in hits:
418+
lines.append(f" {str(name)[:34]:34} wagon {wid:>6} | {dev} | {p}={v} | {ds}")
419+
return "\n".join(lines)
420+
421+
280422
def main():
281423
import argparse
282424
global PROXY, TOKEN, API

0 commit comments

Comments
 (0)