mas-lab-component-controller 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mas/lab/controller/__init__.py +7 -0
- mas/lab/controller/__main__.py +5 -0
- mas/lab/controller/agent_chat.py +226 -0
- mas/lab/controller/api.py +364 -0
- mas/lab/controller/artifact_discovery.py +107 -0
- mas/lab/controller/artifacts.py +112 -0
- mas/lab/controller/cli.py +493 -0
- mas/lab/controller/client.py +271 -0
- mas/lab/controller/config.py +75 -0
- mas/lab/controller/constants.py +30 -0
- mas/lab/controller/daemon.py +190 -0
- mas/lab/controller/deps.py +172 -0
- mas/lab/controller/fastapi_app.py +166 -0
- mas/lab/controller/http_server.py +243 -0
- mas/lab/controller/io_capture.py +55 -0
- mas/lab/controller/item_service.py +625 -0
- mas/lab/controller/job_store.py +209 -0
- mas/lab/controller/jobs.py +375 -0
- mas/lab/controller/kinds.py +149 -0
- mas/lab/controller/lab_registry.py +1076 -0
- mas/lab/controller/manifest_layout.py +832 -0
- mas/lab/controller/manifest_store.py +214 -0
- mas/lab/controller/manifest_validation.py +90 -0
- mas/lab/controller/models.py +215 -0
- mas/lab/controller/path_safety.py +68 -0
- mas/lab/controller/pipeline_validation.py +79 -0
- mas/lab/controller/registry.py +83 -0
- mas/lab/controller/routes/__init__.py +82 -0
- mas/lab/controller/routes/_api.py +15 -0
- mas/lab/controller/routes/apps.py +190 -0
- mas/lab/controller/routes/artifacts.py +37 -0
- mas/lab/controller/routes/benchmark.py +307 -0
- mas/lab/controller/routes/choices.py +141 -0
- mas/lab/controller/routes/datasets.py +91 -0
- mas/lab/controller/routes/design_patterns.py +121 -0
- mas/lab/controller/routes/eval_output.py +31 -0
- mas/lab/controller/routes/experiments.py +194 -0
- mas/lab/controller/routes/health.py +178 -0
- mas/lab/controller/routes/items.py +187 -0
- mas/lab/controller/routes/jobs.py +107 -0
- mas/lab/controller/routes/libraries.py +144 -0
- mas/lab/controller/routes/overlays.py +164 -0
- mas/lab/controller/routes/pipelines.py +139 -0
- mas/lab/controller/routes/registry.py +40 -0
- mas/lab/controller/routes/run.py +215 -0
- mas/lab/controller/routes/runs.py +392 -0
- mas/lab/controller/routes/scopes.py +139 -0
- mas/lab/controller/routes/uploads.py +24 -0
- mas/lab/controller/routes/validate.py +36 -0
- mas/lab/controller/run_bridge.py +276 -0
- mas/lab/controller/run_store.py +190 -0
- mas/lab/controller/schema_registry.py +109 -0
- mas/lab/controller/schemas/pipeline-step-types-post.json +764 -0
- mas/lab/controller/schemas/pipeline-step-types-pre.json +35 -0
- mas/lab/controller/scope_catalog.py +266 -0
- mas/lab/controller/scope_service.py +134 -0
- mas/lab/controller/sessions.py +63 -0
- mas/lab/controller/settings.py +65 -0
- mas/lab/controller/ui_stream/__init__.py +13 -0
- mas/lab/controller/ui_stream/app.py +2147 -0
- mas/lab/controller/ui_stream/artefact_bind.py +308 -0
- mas/lab/controller/ui_stream/auth.py +136 -0
- mas/lab/controller/ui_stream/bus.py +156 -0
- mas/lab/controller/ui_stream/export.py +400 -0
- mas/lab/controller/ui_stream/governance.py +141 -0
- mas/lab/controller/ui_stream/ingest.py +300 -0
- mas/lab/controller/ui_stream/inspect.py +55 -0
- mas/lab/controller/ui_stream/layouts.py +161 -0
- mas/lab/controller/ui_stream/metrics.py +274 -0
- mas/lab/controller/ui_stream/plot_folds.py +609 -0
- mas/lab/controller/ui_stream/plugin_registry.py +139 -0
- mas/lab/controller/ui_stream/refs.py +259 -0
- mas/lab/controller/ui_stream/replay.py +236 -0
- mas/lab/controller/ui_stream/replay_cli.py +707 -0
- mas/lab/controller/ui_stream/run_index.py +337 -0
- mas/lab/controller/ui_stream/spec_load.py +48 -0
- mas/lab/controller/ui_stream/spec_snapshot.py +313 -0
- mas/lab/controller/ui_stream/stream.py +95 -0
- mas/lab/controller/ui_stream/streams.py +42 -0
- mas/lab/controller/ui_stream/timeline.py +183 -0
- mas/lab/controller/ui_stream/ui_trace.py +258 -0
- mas/lab/controller/uploads.py +61 -0
- mas/lab/controller/worker_model.py +137 -0
- mas/lab/controller/workers.py +296 -0
- mas/lab/controller/workspace_library.py +70 -0
- mas_lab_component_controller-0.2.0.dist-info/METADATA +21 -0
- mas_lab_component_controller-0.2.0.dist-info/RECORD +89 -0
- mas_lab_component_controller-0.2.0.dist-info/WHEEL +4 -0
- mas_lab_component_controller-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""LLM-as-judge eval-output endpoint."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
|
|
9
|
+
from mas.lab.controller.models import EvalOutputRequest
|
|
10
|
+
from mas.lab.controller.routes._api import deps, jobs, LIBRARIES_DIR, validate_pipeline_yaml
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post("/api/libraries/{library_name}/eval-output", tags=["Libraries"], status_code=202)
|
|
16
|
+
async def eval_output(library_name: str, req: EvalOutputRequest):
|
|
17
|
+
"""Run LLM-as-judge evaluation on an events file. Returns a job_id for polling."""
|
|
18
|
+
lib_dir = deps.get_library_path(library_name)
|
|
19
|
+
|
|
20
|
+
cmd = ["mas-lab", "eval", req.events_file]
|
|
21
|
+
for metric in req.metrics:
|
|
22
|
+
cmd.extend(["--metric", metric])
|
|
23
|
+
cmd.extend(["--model", req.model])
|
|
24
|
+
job = jobs.submit_job(
|
|
25
|
+
endpoint=f"/api/libraries/{library_name}/eval-output",
|
|
26
|
+
cmd=cmd,
|
|
27
|
+
cwd=lib_dir,
|
|
28
|
+
timeout=req.timeout,
|
|
29
|
+
request_body=req.model_dump(),
|
|
30
|
+
)
|
|
31
|
+
return {"job_id": job.id, "status": job.status.value, "command": job.command}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Library-scoped experiment definition CRUD endpoints."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException
|
|
10
|
+
from mas.lab.controller.api import get_controller_api
|
|
11
|
+
from mas.lab.controller.models import ExperimentRunRequest, SaveExperimentRequest
|
|
12
|
+
from mas.lab.controller.routes._api import deps
|
|
13
|
+
from mas.lab.controller.routes.run import _run_pre_steps
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_experiment_path(lib_dir: Path, name: str) -> Path:
|
|
19
|
+
"""Resolve an experiment by path, filename, stem, or YAML ``experiment.name``."""
|
|
20
|
+
raw = Path(name)
|
|
21
|
+
candidates: list[Path] = []
|
|
22
|
+
if raw.is_absolute():
|
|
23
|
+
candidates.append(raw)
|
|
24
|
+
else:
|
|
25
|
+
candidates.append(lib_dir / name)
|
|
26
|
+
candidates.append(lib_dir / "experiments" / name)
|
|
27
|
+
if not name.endswith((".yaml", ".yml")):
|
|
28
|
+
candidates.extend(
|
|
29
|
+
[
|
|
30
|
+
lib_dir / f"{name}.yaml",
|
|
31
|
+
lib_dir / "experiments" / f"{name}.yaml",
|
|
32
|
+
lib_dir / "experiment.yaml",
|
|
33
|
+
lib_dir / "experiments" / "experiment.yaml",
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
for path in candidates:
|
|
37
|
+
if path.is_file():
|
|
38
|
+
return path
|
|
39
|
+
wanted = name
|
|
40
|
+
if wanted.endswith(".yaml"):
|
|
41
|
+
wanted = wanted[:-5]
|
|
42
|
+
elif wanted.endswith(".yml"):
|
|
43
|
+
wanted = wanted[:-4]
|
|
44
|
+
for path in _experiment_yaml_files(lib_dir):
|
|
45
|
+
if path.stem == wanted or path.name == name:
|
|
46
|
+
return path
|
|
47
|
+
if _experiment_yaml_name(path) == wanted:
|
|
48
|
+
return path
|
|
49
|
+
raise HTTPException(status_code=404, detail=f"Experiment '{name}' not found")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _experiment_yaml_files(lib_dir: Path):
|
|
53
|
+
if not lib_dir.is_dir():
|
|
54
|
+
return
|
|
55
|
+
for path in (lib_dir / "experiment.yaml", lib_dir / "experiment.yml"):
|
|
56
|
+
if path.is_file():
|
|
57
|
+
yield path
|
|
58
|
+
experiments = lib_dir / "experiments"
|
|
59
|
+
if experiments.is_dir():
|
|
60
|
+
yield from sorted(experiments.glob("*.yaml"))
|
|
61
|
+
yield from sorted(experiments.glob("*.yml"))
|
|
62
|
+
yield from sorted(experiments.glob("**/experiment.yaml"))
|
|
63
|
+
yield from sorted(experiments.glob("**/experiment.yml"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _experiment_yaml_name(path: Path) -> str:
|
|
67
|
+
try:
|
|
68
|
+
from mas.runtime.spec.source import load_yaml_file
|
|
69
|
+
|
|
70
|
+
doc = load_yaml_file(path)
|
|
71
|
+
except Exception:
|
|
72
|
+
return path.stem
|
|
73
|
+
block = doc.get("experiment") if isinstance(doc, dict) else None
|
|
74
|
+
if isinstance(block, dict) and block.get("name"):
|
|
75
|
+
return str(block["name"])
|
|
76
|
+
return path.stem
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.post("/api/libraries/{library_name}/experiment/run", tags=["Libraries"], status_code=202)
|
|
80
|
+
async def experiment_run(library_name: str, req: ExperimentRunRequest):
|
|
81
|
+
"""Start an in-process experiment worker that streams live to demo-ui."""
|
|
82
|
+
lib_dir = deps.get_library_path(library_name)
|
|
83
|
+
_run_pre_steps(lib_dir, req.pre)
|
|
84
|
+
experiment_path = resolve_experiment_path(lib_dir, req.experiment_yaml)
|
|
85
|
+
spec = {
|
|
86
|
+
"experiment_yaml": str(experiment_path),
|
|
87
|
+
"progress": False,
|
|
88
|
+
"live": True,
|
|
89
|
+
"single_run": True,
|
|
90
|
+
"force": req.force,
|
|
91
|
+
"max_runs": req.max_runs or 1,
|
|
92
|
+
"query": req.query,
|
|
93
|
+
"infra_refs": list(req.infra_refs or []),
|
|
94
|
+
"demo_ui_ingest_url": req.demo_ui_ingest_url,
|
|
95
|
+
"demo_ui_ref": req.demo_ui_ref or {},
|
|
96
|
+
"endpoint": f"/api/libraries/{library_name}/experiment/run",
|
|
97
|
+
"command": f"mas-lab experiment run {experiment_path}",
|
|
98
|
+
}
|
|
99
|
+
result = get_controller_api().submit_experiment(spec)
|
|
100
|
+
result["command"] = spec["command"]
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.get("/api/libraries/{library_name}/experiments", tags=["Libraries"])
|
|
105
|
+
async def list_library_experiments(library_name: str):
|
|
106
|
+
"""List available experiment definitions with metadata."""
|
|
107
|
+
experiments = deps.get_manifest_store().list_experiments(library_name)
|
|
108
|
+
for exp in experiments:
|
|
109
|
+
exp.setdefault("library", library_name)
|
|
110
|
+
return {"experiments": experiments}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@router.post("/api/libraries/{library_name}/experiments", tags=["Libraries"], status_code=201)
|
|
114
|
+
async def create_experiment(library_name: str, req: SaveExperimentRequest):
|
|
115
|
+
"""Create a new experiment definition in the library's experiments/ directory."""
|
|
116
|
+
lib_dir = deps.get_library_path(library_name)
|
|
117
|
+
exp_dir = lib_dir / "experiments"
|
|
118
|
+
exp_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
|
|
120
|
+
filename = f"{req.name}.yaml" if not req.name.endswith(".yaml") else req.name
|
|
121
|
+
file_path = exp_dir / filename
|
|
122
|
+
|
|
123
|
+
if file_path.exists():
|
|
124
|
+
raise HTTPException(status_code=409, detail=f"Experiment '{req.name}' already exists")
|
|
125
|
+
|
|
126
|
+
file_path.write_text(req.content, encoding="utf-8")
|
|
127
|
+
deps.get_manifest_store().invalidate()
|
|
128
|
+
return {"name": req.name, "filename": filename}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@router.get("/api/libraries/{library_name}/experiments/{experiment_name}", tags=["Libraries"])
|
|
132
|
+
async def get_library_experiment(library_name: str, experiment_name: str):
|
|
133
|
+
"""Return the content of a specific experiment definition."""
|
|
134
|
+
store = deps.get_manifest_store()
|
|
135
|
+
lib_dir = deps.get_library_path(library_name)
|
|
136
|
+
filename = f"{experiment_name}.yaml" if not experiment_name.endswith(".yaml") else experiment_name
|
|
137
|
+
candidates = [
|
|
138
|
+
lib_dir / "experiments" / filename,
|
|
139
|
+
lib_dir / filename,
|
|
140
|
+
]
|
|
141
|
+
candidates.extend(
|
|
142
|
+
p for p in store._registry._iter_experiment_files(lib_dir)
|
|
143
|
+
if p.name == filename or p.stem == experiment_name
|
|
144
|
+
)
|
|
145
|
+
file_path = next((p for p in candidates if p.exists()), None)
|
|
146
|
+
|
|
147
|
+
if file_path is None:
|
|
148
|
+
raise HTTPException(status_code=404, detail=f"Experiment '{experiment_name}' not found")
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"name": experiment_name,
|
|
152
|
+
"filename": file_path.name,
|
|
153
|
+
"content": file_path.read_text(encoding="utf-8"),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.put("/api/libraries/{library_name}/experiments/{experiment_name}", tags=["Libraries"])
|
|
158
|
+
async def update_experiment(library_name: str, experiment_name: str, req: SaveExperimentRequest):
|
|
159
|
+
"""Update an existing experiment definition. Supports rename via req.name."""
|
|
160
|
+
lib_dir = deps.get_library_path(library_name)
|
|
161
|
+
exp_dir = lib_dir / "experiments"
|
|
162
|
+
|
|
163
|
+
old_filename = f"{experiment_name}.yaml" if not experiment_name.endswith(".yaml") else experiment_name
|
|
164
|
+
old_path = exp_dir / old_filename
|
|
165
|
+
|
|
166
|
+
if not old_path.exists():
|
|
167
|
+
raise HTTPException(status_code=404, detail=f"Experiment '{experiment_name}' not found")
|
|
168
|
+
|
|
169
|
+
new_filename = f"{req.name}.yaml" if not req.name.endswith(".yaml") else req.name
|
|
170
|
+
new_path = exp_dir / new_filename
|
|
171
|
+
|
|
172
|
+
if req.name != experiment_name and new_path.exists():
|
|
173
|
+
raise HTTPException(status_code=409, detail=f"Experiment '{req.name}' already exists")
|
|
174
|
+
|
|
175
|
+
if old_path != new_path:
|
|
176
|
+
old_path.unlink()
|
|
177
|
+
new_path.write_text(req.content, encoding="utf-8")
|
|
178
|
+
deps.get_manifest_store().invalidate()
|
|
179
|
+
return {"name": req.name, "filename": new_filename}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.delete("/api/libraries/{library_name}/experiments/{experiment_name}", tags=["Libraries"])
|
|
183
|
+
async def delete_library_experiment(library_name: str, experiment_name: str):
|
|
184
|
+
"""Delete an experiment definition."""
|
|
185
|
+
lib_dir = deps.get_library_path(library_name)
|
|
186
|
+
filename = f"{experiment_name}.yaml" if not experiment_name.endswith(".yaml") else experiment_name
|
|
187
|
+
file_path = lib_dir / "experiments" / filename
|
|
188
|
+
|
|
189
|
+
if not file_path.exists():
|
|
190
|
+
raise HTTPException(status_code=404, detail=f"Experiment '{experiment_name}' not found")
|
|
191
|
+
|
|
192
|
+
file_path.unlink()
|
|
193
|
+
deps.get_manifest_store().invalidate()
|
|
194
|
+
return {"deleted": experiment_name}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Health, info, schema, and metrics endpoints."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json as _json
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
10
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
11
|
+
from mas.lab.controller.constants import (
|
|
12
|
+
PIPELINE_STEP_TYPES_POST_PATH,
|
|
13
|
+
PIPELINE_STEP_TYPES_PRE_PATH,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/health")
|
|
20
|
+
async def health():
|
|
21
|
+
return {"status": "ok"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get("/api/health")
|
|
25
|
+
async def api_health():
|
|
26
|
+
return {"status": "ok"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/api/info")
|
|
30
|
+
async def info(request: Request):
|
|
31
|
+
"""Capabilities the UI uses to bootstrap a connection."""
|
|
32
|
+
settings = getattr(request.app.state, "settings", None)
|
|
33
|
+
auth = getattr(settings, "auth_mode", "none") if settings else "none"
|
|
34
|
+
api_version = getattr(settings, "api_version", "0.3.0") if settings else "0.3.0"
|
|
35
|
+
cors_ok = True
|
|
36
|
+
if settings is not None:
|
|
37
|
+
cors_ok = bool(settings.cors_allow_origins)
|
|
38
|
+
from mas.lab.controller.deps import store_from_request
|
|
39
|
+
from mas.lab.controller.routes.scopes import _kinds_for
|
|
40
|
+
from mas.lab.controller.scope_catalog import get_scope_catalog
|
|
41
|
+
|
|
42
|
+
catalog = get_scope_catalog(store_from_request(request))
|
|
43
|
+
edit_targets = [
|
|
44
|
+
{
|
|
45
|
+
"id": scope.id,
|
|
46
|
+
"kind": scope.kind,
|
|
47
|
+
"name": scope.name,
|
|
48
|
+
"origin": scope.origin,
|
|
49
|
+
"kinds": _kinds_for(scope),
|
|
50
|
+
"owner": scope.owner,
|
|
51
|
+
}
|
|
52
|
+
for scope in catalog.all()
|
|
53
|
+
if scope.writable
|
|
54
|
+
]
|
|
55
|
+
return {
|
|
56
|
+
"api_version": api_version,
|
|
57
|
+
"features": [
|
|
58
|
+
"scopes",
|
|
59
|
+
"items",
|
|
60
|
+
"runs",
|
|
61
|
+
"artifacts",
|
|
62
|
+
"uploads",
|
|
63
|
+
"choices",
|
|
64
|
+
"sse",
|
|
65
|
+
"edit_targets",
|
|
66
|
+
"create_scope",
|
|
67
|
+
],
|
|
68
|
+
"auth": auth,
|
|
69
|
+
"cors_ok": cors_ok,
|
|
70
|
+
"writable_default": any(scope.writable for scope in catalog.all()),
|
|
71
|
+
"edit_targets": edit_targets,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.get("/api/schemas", tags=["Schemas"])
|
|
76
|
+
async def list_manifest_schemas():
|
|
77
|
+
"""List manifest schemas resolved from installed packages (runtime, bench, core).
|
|
78
|
+
|
|
79
|
+
mas-lab-ui should fetch schemas here instead of bundling copies.
|
|
80
|
+
"""
|
|
81
|
+
from mas.lab.controller.schema_registry import list_schemas
|
|
82
|
+
|
|
83
|
+
return {"schemas": list_schemas()}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get("/api/schemas/{schema_id}", tags=["Schemas"])
|
|
87
|
+
async def get_manifest_schema(
|
|
88
|
+
schema_id: str,
|
|
89
|
+
format: str | None = None,
|
|
90
|
+
resolved: bool = Query(False),
|
|
91
|
+
):
|
|
92
|
+
"""Return a manifest schema by registry id.
|
|
93
|
+
|
|
94
|
+
Query params:
|
|
95
|
+
- ``format=json`` — JSON body (required for ``resolved=1``).
|
|
96
|
+
- ``resolved=1`` — JSON Schema with local ``./`` refs inlined (for UI Ajv).
|
|
97
|
+
Uses the same loader as ``mas.ctl.validate`` — no filesystem access in the browser.
|
|
98
|
+
"""
|
|
99
|
+
import yaml as _yaml
|
|
100
|
+
from mas.ctl.validate.schemas import load_schema, schema_path_for_kind
|
|
101
|
+
from mas.lab.controller.schema_registry import read_schema_text
|
|
102
|
+
|
|
103
|
+
want_resolved = resolved
|
|
104
|
+
want_json = (format or "").strip().lower() == "json" or want_resolved
|
|
105
|
+
|
|
106
|
+
if want_resolved:
|
|
107
|
+
kind = schema_id.replace("-", "_")
|
|
108
|
+
if schema_path_for_kind(kind) is None:
|
|
109
|
+
raise HTTPException(status_code=404, detail=f"No resolved schema for: {schema_id}")
|
|
110
|
+
try:
|
|
111
|
+
schema_dict = load_schema(kind)
|
|
112
|
+
except KeyError as exc:
|
|
113
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
114
|
+
return JSONResponse(schema_dict)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
entry, text = read_schema_text(schema_id)
|
|
118
|
+
except KeyError:
|
|
119
|
+
raise HTTPException(status_code=404, detail=f"Unknown schema: {schema_id}") from None
|
|
120
|
+
except FileNotFoundError as exc:
|
|
121
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
122
|
+
|
|
123
|
+
if want_json and entry.format == "yaml":
|
|
124
|
+
text = _json.dumps(_yaml.safe_load(text), indent=2)
|
|
125
|
+
media = "application/json"
|
|
126
|
+
elif entry.format == "json" or want_json:
|
|
127
|
+
media = "application/json"
|
|
128
|
+
else:
|
|
129
|
+
media = "text/yaml"
|
|
130
|
+
return PlainTextResponse(content=text, media_type=media)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@router.get("/api/pipeline-step-types")
|
|
134
|
+
async def get_pipeline_step_types(phase: str | None = None):
|
|
135
|
+
"""Return the registry of all pipeline step types with config schemas for the React UI.
|
|
136
|
+
|
|
137
|
+
Query params:
|
|
138
|
+
phase: "pre" | "post" — return only pre-phase or post-phase step types.
|
|
139
|
+
If omitted, returns the post-phase step types.
|
|
140
|
+
"""
|
|
141
|
+
if phase == "pre":
|
|
142
|
+
path = PIPELINE_STEP_TYPES_PRE_PATH
|
|
143
|
+
else:
|
|
144
|
+
path = PIPELINE_STEP_TYPES_POST_PATH
|
|
145
|
+
return _json.loads(path.read_text(encoding="utf-8"))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router.get("/api/metrics/eval", tags=["Pipelines"])
|
|
149
|
+
async def get_eval_metrics():
|
|
150
|
+
"""Return available metrics for the annotate_metrics pipeline step.
|
|
151
|
+
|
|
152
|
+
Keys are the metric_class values to use in pipeline YAML config.
|
|
153
|
+
Values are human-readable display names.
|
|
154
|
+
Used by step type: annotate_metrics (config.metric_class).
|
|
155
|
+
"""
|
|
156
|
+
return {
|
|
157
|
+
"mas.lab.components.evaluation.deepeval_wrapper.AnswerRelevancyMetric": "Answer Relevancy",
|
|
158
|
+
"mas.lab.dataset.evaluator.BiasMetric": "Bias",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@router.get("/api/metrics/mce", tags=["Pipelines"])
|
|
163
|
+
async def get_mce_metrics():
|
|
164
|
+
"""Return available MCE metrics for eval_batch steps.
|
|
165
|
+
|
|
166
|
+
Keys are the metric name strings to use in pipeline YAML config.metrics array.
|
|
167
|
+
Values are human-readable display names.
|
|
168
|
+
Used by step type: eval_batch (config.metrics).
|
|
169
|
+
"""
|
|
170
|
+
return {
|
|
171
|
+
"answer_relevancy": "Answer Relevancy",
|
|
172
|
+
"goal_success_rate": "Goal Success Rate",
|
|
173
|
+
"groundedness": "Groundedness",
|
|
174
|
+
"response_completeness": "Response Completeness",
|
|
175
|
+
"task_delegation": "Task Delegation Accuracy",
|
|
176
|
+
"tool_utilization_accuracy": "Tool Utilization Accuracy",
|
|
177
|
+
"duration": "Duration",
|
|
178
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Item catalog: list/get/create/update/fork/rename/delete/check."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
10
|
+
from mas.lab.controller import item_service
|
|
11
|
+
from mas.lab.controller.deps import store_from_request
|
|
12
|
+
from mas.lab.controller.kinds import list_kinds
|
|
13
|
+
from mas.lab.controller.scope_catalog import get_scope_catalog
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateItemRequest(BaseModel):
|
|
20
|
+
kind: str
|
|
21
|
+
name: str
|
|
22
|
+
version: Optional[str] = None
|
|
23
|
+
template: Optional[str] = None
|
|
24
|
+
parent: Optional[str] = None
|
|
25
|
+
upload_id: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UpdateItemRequest(BaseModel):
|
|
29
|
+
manifest: str
|
|
30
|
+
children: dict[str, str] = Field(default_factory=dict)
|
|
31
|
+
revision: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ForkItemRequest(BaseModel):
|
|
35
|
+
version: Optional[str] = None
|
|
36
|
+
name: Optional[str] = None
|
|
37
|
+
target_scope: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RenameItemRequest(BaseModel):
|
|
41
|
+
name: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CheckItemRequest(BaseModel):
|
|
45
|
+
kind: str
|
|
46
|
+
name: str
|
|
47
|
+
version: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ValidateScopeRequest(BaseModel):
|
|
51
|
+
kind: Optional[str] = None
|
|
52
|
+
manifest: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _raise(exc: item_service.ItemError) -> None:
|
|
56
|
+
raise HTTPException(status_code=exc.status, detail=exc.detail) from exc
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/api/kinds", tags=["Kinds"])
|
|
60
|
+
async def get_kinds():
|
|
61
|
+
return {"kinds": list_kinds()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.get("/api/scopes/{scope_id}/items", tags=["Items"])
|
|
65
|
+
async def list_items(
|
|
66
|
+
scope_id: str,
|
|
67
|
+
request: Request,
|
|
68
|
+
kind: str = Query(...),
|
|
69
|
+
q: Optional[str] = None,
|
|
70
|
+
):
|
|
71
|
+
store = store_from_request(request)
|
|
72
|
+
try:
|
|
73
|
+
scope = get_scope_catalog(store).get(scope_id)
|
|
74
|
+
except KeyError as exc:
|
|
75
|
+
raise HTTPException(status_code=404, detail=f"scope {scope_id!r} not found") from exc
|
|
76
|
+
try:
|
|
77
|
+
groups = item_service.group_items(scope, kind, q=q)
|
|
78
|
+
except KeyError as exc:
|
|
79
|
+
raise HTTPException(status_code=400, detail=f"unknown kind {kind!r}") from exc
|
|
80
|
+
return {"items": groups}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.get("/api/scopes/{scope_id}/items/{kind}/{item_id}", tags=["Items"])
|
|
84
|
+
async def get_item(scope_id: str, kind: str, item_id: str, request: Request):
|
|
85
|
+
try:
|
|
86
|
+
return item_service.get_item(scope_id, kind, item_id, store=store_from_request(request))
|
|
87
|
+
except item_service.ItemError as exc:
|
|
88
|
+
_raise(exc)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.post("/api/scopes/{scope_id}/items", tags=["Items"], status_code=201)
|
|
92
|
+
async def create_item(scope_id: str, req: CreateItemRequest, request: Request):
|
|
93
|
+
try:
|
|
94
|
+
return item_service.create_item(
|
|
95
|
+
scope_id,
|
|
96
|
+
kind=req.kind,
|
|
97
|
+
name=req.name,
|
|
98
|
+
version=req.version,
|
|
99
|
+
template=req.template,
|
|
100
|
+
parent=req.parent,
|
|
101
|
+
upload_id=req.upload_id,
|
|
102
|
+
store=store_from_request(request),
|
|
103
|
+
)
|
|
104
|
+
except item_service.ItemError as exc:
|
|
105
|
+
_raise(exc)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.put("/api/scopes/{scope_id}/items/{kind}/{item_id}", tags=["Items"])
|
|
109
|
+
async def update_item(scope_id: str, kind: str, item_id: str, req: UpdateItemRequest, request: Request):
|
|
110
|
+
try:
|
|
111
|
+
return item_service.update_item(
|
|
112
|
+
scope_id,
|
|
113
|
+
kind,
|
|
114
|
+
item_id,
|
|
115
|
+
manifest=req.manifest,
|
|
116
|
+
children=req.children,
|
|
117
|
+
revision=req.revision,
|
|
118
|
+
store=store_from_request(request),
|
|
119
|
+
)
|
|
120
|
+
except item_service.ItemError as exc:
|
|
121
|
+
_raise(exc)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.post("/api/scopes/{scope_id}/items/{kind}/{item_id}/fork", tags=["Items"], status_code=201)
|
|
125
|
+
async def fork_item(scope_id: str, kind: str, item_id: str, req: ForkItemRequest, request: Request):
|
|
126
|
+
try:
|
|
127
|
+
return item_service.fork_item(
|
|
128
|
+
scope_id,
|
|
129
|
+
kind,
|
|
130
|
+
item_id,
|
|
131
|
+
version=req.version,
|
|
132
|
+
name=req.name,
|
|
133
|
+
target_scope=req.target_scope,
|
|
134
|
+
store=store_from_request(request),
|
|
135
|
+
)
|
|
136
|
+
except item_service.ItemError as exc:
|
|
137
|
+
_raise(exc)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.post("/api/scopes/{scope_id}/items/{kind}/{item_id}/rename", tags=["Items"])
|
|
141
|
+
async def rename_item(scope_id: str, kind: str, item_id: str, req: RenameItemRequest, request: Request):
|
|
142
|
+
try:
|
|
143
|
+
return item_service.rename_item(scope_id, kind, item_id, name=req.name, store=store_from_request(request))
|
|
144
|
+
except item_service.ItemError as exc:
|
|
145
|
+
_raise(exc)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router.delete("/api/scopes/{scope_id}/items/{kind}/{item_id}", tags=["Items"])
|
|
149
|
+
async def delete_item(scope_id: str, kind: str, item_id: str, request: Request):
|
|
150
|
+
try:
|
|
151
|
+
item_service.delete_item(scope_id, kind, item_id, store=store_from_request(request))
|
|
152
|
+
except item_service.ItemError as exc:
|
|
153
|
+
_raise(exc)
|
|
154
|
+
return {"ok": True}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.post("/api/scopes/{scope_id}/items/check", tags=["Items"])
|
|
158
|
+
async def check_item(scope_id: str, req: CheckItemRequest, request: Request):
|
|
159
|
+
return item_service.check_name(
|
|
160
|
+
scope_id, req.kind, req.name, req.version, store=store_from_request(request)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.get("/api/resolve", tags=["Items"])
|
|
165
|
+
async def resolve(ref: str, request: Request):
|
|
166
|
+
try:
|
|
167
|
+
return item_service.resolve_ref(ref, store=store_from_request(request))
|
|
168
|
+
except item_service.ItemError as exc:
|
|
169
|
+
_raise(exc)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@router.post("/api/scopes/{scope_id}/validate", tags=["Items"])
|
|
173
|
+
async def validate_in_scope(scope_id: str, req: ValidateScopeRequest, request: Request):
|
|
174
|
+
from mas.lab.controller.manifest_validation import validate_manifest_yaml_content
|
|
175
|
+
from mas.lab.controller.scope_catalog import get_scope_catalog as catalog_of
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
scope = catalog_of(store_from_request(request)).get(scope_id)
|
|
179
|
+
except KeyError as exc:
|
|
180
|
+
raise HTTPException(status_code=404, detail=f"scope {scope_id!r} not found") from exc
|
|
181
|
+
result = validate_manifest_yaml_content(
|
|
182
|
+
req.manifest,
|
|
183
|
+
base_dir=scope.root,
|
|
184
|
+
kind=req.kind,
|
|
185
|
+
resolve_refs=True,
|
|
186
|
+
)
|
|
187
|
+
return result
|