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.
Files changed (89) hide show
  1. mas/lab/controller/__init__.py +7 -0
  2. mas/lab/controller/__main__.py +5 -0
  3. mas/lab/controller/agent_chat.py +226 -0
  4. mas/lab/controller/api.py +364 -0
  5. mas/lab/controller/artifact_discovery.py +107 -0
  6. mas/lab/controller/artifacts.py +112 -0
  7. mas/lab/controller/cli.py +493 -0
  8. mas/lab/controller/client.py +271 -0
  9. mas/lab/controller/config.py +75 -0
  10. mas/lab/controller/constants.py +30 -0
  11. mas/lab/controller/daemon.py +190 -0
  12. mas/lab/controller/deps.py +172 -0
  13. mas/lab/controller/fastapi_app.py +166 -0
  14. mas/lab/controller/http_server.py +243 -0
  15. mas/lab/controller/io_capture.py +55 -0
  16. mas/lab/controller/item_service.py +625 -0
  17. mas/lab/controller/job_store.py +209 -0
  18. mas/lab/controller/jobs.py +375 -0
  19. mas/lab/controller/kinds.py +149 -0
  20. mas/lab/controller/lab_registry.py +1076 -0
  21. mas/lab/controller/manifest_layout.py +832 -0
  22. mas/lab/controller/manifest_store.py +214 -0
  23. mas/lab/controller/manifest_validation.py +90 -0
  24. mas/lab/controller/models.py +215 -0
  25. mas/lab/controller/path_safety.py +68 -0
  26. mas/lab/controller/pipeline_validation.py +79 -0
  27. mas/lab/controller/registry.py +83 -0
  28. mas/lab/controller/routes/__init__.py +82 -0
  29. mas/lab/controller/routes/_api.py +15 -0
  30. mas/lab/controller/routes/apps.py +190 -0
  31. mas/lab/controller/routes/artifacts.py +37 -0
  32. mas/lab/controller/routes/benchmark.py +307 -0
  33. mas/lab/controller/routes/choices.py +141 -0
  34. mas/lab/controller/routes/datasets.py +91 -0
  35. mas/lab/controller/routes/design_patterns.py +121 -0
  36. mas/lab/controller/routes/eval_output.py +31 -0
  37. mas/lab/controller/routes/experiments.py +194 -0
  38. mas/lab/controller/routes/health.py +178 -0
  39. mas/lab/controller/routes/items.py +187 -0
  40. mas/lab/controller/routes/jobs.py +107 -0
  41. mas/lab/controller/routes/libraries.py +144 -0
  42. mas/lab/controller/routes/overlays.py +164 -0
  43. mas/lab/controller/routes/pipelines.py +139 -0
  44. mas/lab/controller/routes/registry.py +40 -0
  45. mas/lab/controller/routes/run.py +215 -0
  46. mas/lab/controller/routes/runs.py +392 -0
  47. mas/lab/controller/routes/scopes.py +139 -0
  48. mas/lab/controller/routes/uploads.py +24 -0
  49. mas/lab/controller/routes/validate.py +36 -0
  50. mas/lab/controller/run_bridge.py +276 -0
  51. mas/lab/controller/run_store.py +190 -0
  52. mas/lab/controller/schema_registry.py +109 -0
  53. mas/lab/controller/schemas/pipeline-step-types-post.json +764 -0
  54. mas/lab/controller/schemas/pipeline-step-types-pre.json +35 -0
  55. mas/lab/controller/scope_catalog.py +266 -0
  56. mas/lab/controller/scope_service.py +134 -0
  57. mas/lab/controller/sessions.py +63 -0
  58. mas/lab/controller/settings.py +65 -0
  59. mas/lab/controller/ui_stream/__init__.py +13 -0
  60. mas/lab/controller/ui_stream/app.py +2147 -0
  61. mas/lab/controller/ui_stream/artefact_bind.py +308 -0
  62. mas/lab/controller/ui_stream/auth.py +136 -0
  63. mas/lab/controller/ui_stream/bus.py +156 -0
  64. mas/lab/controller/ui_stream/export.py +400 -0
  65. mas/lab/controller/ui_stream/governance.py +141 -0
  66. mas/lab/controller/ui_stream/ingest.py +300 -0
  67. mas/lab/controller/ui_stream/inspect.py +55 -0
  68. mas/lab/controller/ui_stream/layouts.py +161 -0
  69. mas/lab/controller/ui_stream/metrics.py +274 -0
  70. mas/lab/controller/ui_stream/plot_folds.py +609 -0
  71. mas/lab/controller/ui_stream/plugin_registry.py +139 -0
  72. mas/lab/controller/ui_stream/refs.py +259 -0
  73. mas/lab/controller/ui_stream/replay.py +236 -0
  74. mas/lab/controller/ui_stream/replay_cli.py +707 -0
  75. mas/lab/controller/ui_stream/run_index.py +337 -0
  76. mas/lab/controller/ui_stream/spec_load.py +48 -0
  77. mas/lab/controller/ui_stream/spec_snapshot.py +313 -0
  78. mas/lab/controller/ui_stream/stream.py +95 -0
  79. mas/lab/controller/ui_stream/streams.py +42 -0
  80. mas/lab/controller/ui_stream/timeline.py +183 -0
  81. mas/lab/controller/ui_stream/ui_trace.py +258 -0
  82. mas/lab/controller/uploads.py +61 -0
  83. mas/lab/controller/worker_model.py +137 -0
  84. mas/lab/controller/workers.py +296 -0
  85. mas/lab/controller/workspace_library.py +70 -0
  86. mas_lab_component_controller-0.2.0.dist-info/METADATA +21 -0
  87. mas_lab_component_controller-0.2.0.dist-info/RECORD +89 -0
  88. mas_lab_component_controller-0.2.0.dist-info/WHEEL +4 -0
  89. 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