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,83 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Worker registry — thread-safe in-process store."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from mas.lab.controller.worker_model import WorkerKind, WorkerRecord, WorkerStatus, new_worker_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkerRegistry:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self._workers: Dict[str, WorkerRecord] = {}
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def create(
|
|
18
|
+
self,
|
|
19
|
+
kind: WorkerKind,
|
|
20
|
+
*,
|
|
21
|
+
spec: Optional[dict] = None,
|
|
22
|
+
command: str = "",
|
|
23
|
+
endpoint: str = "",
|
|
24
|
+
parent_id: Optional[str] = None,
|
|
25
|
+
worker_id: Optional[str] = None,
|
|
26
|
+
) -> WorkerRecord:
|
|
27
|
+
prefix = {"application": "app", "benchmark": "bench", "pipeline": "pipe"}.get(
|
|
28
|
+
kind.value, "w"
|
|
29
|
+
)
|
|
30
|
+
record = WorkerRecord(
|
|
31
|
+
id=worker_id or new_worker_id(prefix),
|
|
32
|
+
kind=kind,
|
|
33
|
+
spec=dict(spec or {}),
|
|
34
|
+
command=command,
|
|
35
|
+
endpoint=endpoint,
|
|
36
|
+
parent_id=parent_id,
|
|
37
|
+
)
|
|
38
|
+
with self._lock:
|
|
39
|
+
self._workers[record.id] = record
|
|
40
|
+
return record
|
|
41
|
+
|
|
42
|
+
def get(self, worker_id: str) -> Optional[WorkerRecord]:
|
|
43
|
+
with self._lock:
|
|
44
|
+
return self._workers.get(worker_id)
|
|
45
|
+
|
|
46
|
+
def list_workers(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
kind: Optional[WorkerKind] = None,
|
|
50
|
+
status: Optional[WorkerStatus] = None,
|
|
51
|
+
) -> List[WorkerRecord]:
|
|
52
|
+
with self._lock:
|
|
53
|
+
items = list(self._workers.values())
|
|
54
|
+
if kind is not None:
|
|
55
|
+
items = [w for w in items if w.kind == kind]
|
|
56
|
+
if status is not None:
|
|
57
|
+
items = [w for w in items if w.status == status]
|
|
58
|
+
items.sort(key=lambda w: w.created_at, reverse=True)
|
|
59
|
+
return items
|
|
60
|
+
|
|
61
|
+
def update(self, worker_id: str, **fields: object) -> Optional[WorkerRecord]:
|
|
62
|
+
with self._lock:
|
|
63
|
+
record = self._workers.get(worker_id)
|
|
64
|
+
if record is None:
|
|
65
|
+
return None
|
|
66
|
+
for key, value in fields.items():
|
|
67
|
+
if hasattr(record, key):
|
|
68
|
+
setattr(record, key, value)
|
|
69
|
+
return record
|
|
70
|
+
|
|
71
|
+
def cancel(self, worker_id: str) -> bool:
|
|
72
|
+
record = self.get(worker_id)
|
|
73
|
+
if record is None:
|
|
74
|
+
return False
|
|
75
|
+
if record.status in (WorkerStatus.COMPLETED, WorkerStatus.FAILED, WorkerStatus.CANCELLED):
|
|
76
|
+
return True
|
|
77
|
+
record.status = WorkerStatus.CANCELLED
|
|
78
|
+
record.finished_at = record.finished_at or __import__("time").time()
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def drop(self, worker_id: str) -> bool:
|
|
82
|
+
with self._lock:
|
|
83
|
+
return self._workers.pop(worker_id, None) is not None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""FastAPI route registration for the MAS Lab controller API."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from mas.lab.controller.routes import (
|
|
9
|
+
apps,
|
|
10
|
+
artifacts,
|
|
11
|
+
benchmark,
|
|
12
|
+
choices,
|
|
13
|
+
datasets,
|
|
14
|
+
design_patterns,
|
|
15
|
+
eval_output,
|
|
16
|
+
experiments,
|
|
17
|
+
health,
|
|
18
|
+
items,
|
|
19
|
+
jobs,
|
|
20
|
+
libraries,
|
|
21
|
+
overlays,
|
|
22
|
+
pipelines,
|
|
23
|
+
registry,
|
|
24
|
+
run,
|
|
25
|
+
runs,
|
|
26
|
+
scopes,
|
|
27
|
+
uploads,
|
|
28
|
+
validate,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_DEPRECATED_PREFIXES = (
|
|
32
|
+
"/api/libraries",
|
|
33
|
+
"/api/jobs",
|
|
34
|
+
"/api/experiments",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def register_routes(app: FastAPI) -> None:
|
|
39
|
+
"""Attach all API routers to the application."""
|
|
40
|
+
app.include_router(jobs.router)
|
|
41
|
+
app.include_router(registry.router)
|
|
42
|
+
app.include_router(libraries.router)
|
|
43
|
+
app.include_router(overlays.router)
|
|
44
|
+
app.include_router(validate.router)
|
|
45
|
+
app.include_router(run.router)
|
|
46
|
+
app.include_router(datasets.router)
|
|
47
|
+
app.include_router(benchmark.router)
|
|
48
|
+
app.include_router(experiments.router)
|
|
49
|
+
app.include_router(pipelines.router)
|
|
50
|
+
app.include_router(eval_output.router)
|
|
51
|
+
app.include_router(apps.router)
|
|
52
|
+
app.include_router(health.router)
|
|
53
|
+
app.include_router(scopes.router)
|
|
54
|
+
app.include_router(items.router)
|
|
55
|
+
app.include_router(choices.router)
|
|
56
|
+
app.include_router(design_patterns.router)
|
|
57
|
+
app.include_router(runs.router)
|
|
58
|
+
app.include_router(artifacts.router)
|
|
59
|
+
app.include_router(uploads.router)
|
|
60
|
+
_mark_app_deprecated(app)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _iter_api_routes(node) -> list:
|
|
64
|
+
routes = []
|
|
65
|
+
for route in getattr(node, "routes", []) or []:
|
|
66
|
+
original = getattr(route, "original_router", None)
|
|
67
|
+
if original is not None:
|
|
68
|
+
routes.extend(_iter_api_routes(original))
|
|
69
|
+
continue
|
|
70
|
+
nested = getattr(route, "app", None)
|
|
71
|
+
if nested is not None and hasattr(nested, "routes") and nested is not node:
|
|
72
|
+
routes.extend(_iter_api_routes(nested))
|
|
73
|
+
routes.append(route)
|
|
74
|
+
return routes
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _mark_app_deprecated(app: FastAPI) -> None:
|
|
78
|
+
for route in _iter_api_routes(app):
|
|
79
|
+
path = getattr(route, "path", "") or ""
|
|
80
|
+
if any(path == prefix or path.startswith(prefix + "/") for prefix in _DEPRECATED_PREFIXES):
|
|
81
|
+
if hasattr(route, "deprecated"):
|
|
82
|
+
route.deprecated = True
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared imports for route modules — patch ``mas.lab.controller.deps`` in tests."""
|
|
4
|
+
|
|
5
|
+
from mas.lab.controller import deps, jobs
|
|
6
|
+
from mas.lab.controller.constants import LIBRARIES_DIR, MAS_LAB_ROOT
|
|
7
|
+
from mas.lab.controller.pipeline_validation import validate_pipeline_yaml
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"LIBRARIES_DIR",
|
|
11
|
+
"MAS_LAB_ROOT",
|
|
12
|
+
"deps",
|
|
13
|
+
"jobs",
|
|
14
|
+
"validate_pipeline_yaml",
|
|
15
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""MAS app resource CRUD endpoints."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
from fastapi import APIRouter, HTTPException
|
|
12
|
+
|
|
13
|
+
from mas.ctl.overlay.canvas import apply_ui_canvas_to_yaml, persist_ui_canvas_yaml
|
|
14
|
+
from mas.lab.controller.lab_registry import LabRegistry
|
|
15
|
+
from mas.lab.controller.models import SaveMASResourceRequest
|
|
16
|
+
from mas.lab.controller.routes._api import deps, jobs, LIBRARIES_DIR, validate_pipeline_yaml
|
|
17
|
+
|
|
18
|
+
router = APIRouter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _persist_ui_canvas(app_dir: Path, mas_yaml: str, agents: dict[str, str], mas_name: str) -> tuple[str, dict[str, str]]:
|
|
22
|
+
"""Move canvas ``x-*`` fields from MAS/agent YAML onto a layout-only Overlay."""
|
|
23
|
+
return persist_ui_canvas_yaml(app_dir, mas_yaml, agents, mas_name=mas_name)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_mas_resource(apps_dir: Path, mas_name: str, mas_yaml: str, agents: dict[str, str]) -> dict:
|
|
27
|
+
"""Write a MAS manifest + agent files to disk. Returns summary."""
|
|
28
|
+
app_dir = apps_dir / mas_name
|
|
29
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
mas_yaml, agents = _persist_ui_canvas(app_dir, mas_yaml, agents, mas_name)
|
|
31
|
+
|
|
32
|
+
mas_file = app_dir / "mas.yaml"
|
|
33
|
+
mas_file.write_text(mas_yaml, encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
created_agents = []
|
|
36
|
+
if agents:
|
|
37
|
+
agents_dir = app_dir / "agents"
|
|
38
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
for agent_name, agent_content in agents.items():
|
|
40
|
+
filename = f"{agent_name}.yaml" if not agent_name.endswith(".yaml") else agent_name
|
|
41
|
+
(agents_dir / filename).write_text(agent_content, encoding="utf-8")
|
|
42
|
+
created_agents.append(filename)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"mas_name": mas_name,
|
|
46
|
+
"mas_file": f"apps/{mas_name}/mas.yaml",
|
|
47
|
+
"agents": sorted(created_agents),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post("/api/libraries/{library_name}/apps", tags=["Libraries"], status_code=201)
|
|
52
|
+
async def save_mas_resource(library_name: str, req: SaveMASResourceRequest):
|
|
53
|
+
"""Save a MAS manifest + agent files under apps/<mas_name>/."""
|
|
54
|
+
lib_dir = deps.get_library_path(library_name)
|
|
55
|
+
apps_dir = lib_dir / "apps"
|
|
56
|
+
app_dir = apps_dir / req.mas_name
|
|
57
|
+
|
|
58
|
+
if (app_dir / "mas.yaml").exists():
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
status_code=409,
|
|
61
|
+
detail=f"MAS resource '{req.mas_name}' already exists",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
result = _write_mas_resource(apps_dir, req.mas_name, req.mas_yaml, req.agents)
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.put("/api/libraries/{library_name}/apps/{old_mas_name}", tags=["Libraries"])
|
|
69
|
+
async def update_mas_resource(library_name: str, old_mas_name: str, req: SaveMASResourceRequest):
|
|
70
|
+
"""Update a MAS resource. Overwrites mas.yaml and agents/ only, preserving other app contents."""
|
|
71
|
+
lib_dir = deps.get_library_path(library_name)
|
|
72
|
+
apps_dir = lib_dir / "apps"
|
|
73
|
+
old_app_dir = apps_dir / old_mas_name
|
|
74
|
+
|
|
75
|
+
if not (old_app_dir / "mas.yaml").exists():
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=404,
|
|
78
|
+
detail=f"MAS resource '{old_mas_name}' not found",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
renaming = req.mas_name != old_mas_name
|
|
82
|
+
if renaming:
|
|
83
|
+
new_app_dir = apps_dir / req.mas_name
|
|
84
|
+
if new_app_dir.exists() and (new_app_dir / "mas.yaml").exists():
|
|
85
|
+
raise HTTPException(
|
|
86
|
+
status_code=409,
|
|
87
|
+
detail=f"MAS resource '{req.mas_name}' already exists",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if renaming:
|
|
91
|
+
target_dir = apps_dir / req.mas_name
|
|
92
|
+
old_app_dir.rename(target_dir)
|
|
93
|
+
else:
|
|
94
|
+
target_dir = old_app_dir
|
|
95
|
+
|
|
96
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
mas_yaml, agents = _persist_ui_canvas(target_dir, req.mas_yaml, req.agents, req.mas_name)
|
|
98
|
+
(target_dir / "mas.yaml").write_text(mas_yaml, encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
agents_dir = target_dir / "agents"
|
|
101
|
+
if agents_dir.exists():
|
|
102
|
+
shutil.rmtree(agents_dir)
|
|
103
|
+
if agents:
|
|
104
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
for agent_name, agent_content in agents.items():
|
|
106
|
+
filename = f"{agent_name}.yaml" if not agent_name.endswith(".yaml") else agent_name
|
|
107
|
+
(agents_dir / filename).write_text(agent_content, encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"mas_name": req.mas_name,
|
|
111
|
+
"mas_file": f"apps/{req.mas_name}/mas.yaml",
|
|
112
|
+
"agents": sorted(
|
|
113
|
+
f"{a}.yaml" if not a.endswith(".yaml") else a for a in agents
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.get("/api/libraries/{library_name}/apps", tags=["Libraries"])
|
|
119
|
+
async def list_mas_resources(library_name: str):
|
|
120
|
+
"""List all MAS resources with their manifests and agents."""
|
|
121
|
+
return {"mas_resources": deps.get_manifest_store().collect_mas_resources(library_name)}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.get("/api/libraries/{library_name}/apps/{mas_name}", tags=["Libraries"])
|
|
125
|
+
async def get_mas_resource(library_name: str, mas_name: str):
|
|
126
|
+
"""Return a single MAS resource (manifest + agents)."""
|
|
127
|
+
resources = deps.get_manifest_store().collect_mas_resources(library_name)
|
|
128
|
+
entry = resources.get(mas_name)
|
|
129
|
+
if entry is None:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=404,
|
|
132
|
+
detail=f"MAS resource '{mas_name}' not found",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
mas_yaml = entry["mas_yaml"]
|
|
136
|
+
agents = dict(entry.get("agents") or {})
|
|
137
|
+
store = deps.get_manifest_store()
|
|
138
|
+
lib_dir = deps.get_library_path(library_name)
|
|
139
|
+
mas_path = entry.get("path")
|
|
140
|
+
mas_dir = (lib_dir / mas_path).parent if mas_path else lib_dir / "apps" / mas_name
|
|
141
|
+
registry = getattr(store, "_registry", None)
|
|
142
|
+
if not isinstance(registry, LabRegistry):
|
|
143
|
+
registry = LabRegistry._ephemeral()
|
|
144
|
+
target_name = mas_name
|
|
145
|
+
try:
|
|
146
|
+
mas_doc = yaml.safe_load(mas_yaml) or {}
|
|
147
|
+
if isinstance(mas_doc, dict):
|
|
148
|
+
target_name = str((mas_doc.get("metadata") or {}).get("name") or mas_name).strip() or mas_name
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
seen: set[Path] = set()
|
|
152
|
+
matched: list[tuple[Path, dict]] = []
|
|
153
|
+
for name in (target_name, mas_name):
|
|
154
|
+
for path, doc in registry.layout_overlays_targeting(lib_dir, kind="MAS", name=name):
|
|
155
|
+
key = path.resolve()
|
|
156
|
+
if key in seen:
|
|
157
|
+
continue
|
|
158
|
+
seen.add(key)
|
|
159
|
+
matched.append((path, doc))
|
|
160
|
+
mas_yaml, agents, overlay_path = apply_ui_canvas_to_yaml(
|
|
161
|
+
mas_dir, mas_yaml, agents, overlays=matched
|
|
162
|
+
)
|
|
163
|
+
body: dict = {
|
|
164
|
+
"mas_name": mas_name,
|
|
165
|
+
"mas_yaml": mas_yaml,
|
|
166
|
+
"agents": agents,
|
|
167
|
+
}
|
|
168
|
+
if overlay_path is not None:
|
|
169
|
+
try:
|
|
170
|
+
body["ui_canvas_overlay"] = str(overlay_path.relative_to(lib_dir))
|
|
171
|
+
except ValueError:
|
|
172
|
+
body["ui_canvas_overlay"] = str(overlay_path)
|
|
173
|
+
return body
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@router.delete("/api/libraries/{library_name}/apps/{mas_name}", tags=["Libraries"])
|
|
177
|
+
async def delete_mas_resource(library_name: str, mas_name: str):
|
|
178
|
+
"""Delete a MAS app folder (manifest + agents)."""
|
|
179
|
+
lib_dir = deps.get_library_path(library_name)
|
|
180
|
+
apps_dir = lib_dir / "apps"
|
|
181
|
+
app_dir = apps_dir / mas_name
|
|
182
|
+
|
|
183
|
+
if not (app_dir / "mas.yaml").exists():
|
|
184
|
+
raise HTTPException(
|
|
185
|
+
status_code=404,
|
|
186
|
+
detail=f"MAS resource '{mas_name}' not found",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
shutil.rmtree(app_dir)
|
|
190
|
+
return {"deleted": mas_name}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Serve run artifacts by issued id only."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from fastapi.responses import FileResponse, PlainTextResponse
|
|
9
|
+
from mas.lab.controller.artifacts import get_artifact_registry
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
_TEXT_TYPES = {
|
|
14
|
+
"text/plain",
|
|
15
|
+
"text/csv",
|
|
16
|
+
"text/yaml",
|
|
17
|
+
"text/html",
|
|
18
|
+
"text/xml",
|
|
19
|
+
"application/json",
|
|
20
|
+
"application/yaml",
|
|
21
|
+
"image/svg+xml",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/api/artifacts/{artifact_id}", tags=["Artifacts"])
|
|
26
|
+
async def get_artifact(artifact_id: str, download: bool = False):
|
|
27
|
+
art = get_artifact_registry().get(artifact_id)
|
|
28
|
+
if art is None or art.path is None or not art.path.is_file():
|
|
29
|
+
raise HTTPException(status_code=404, detail="artifact not found")
|
|
30
|
+
if download or art.media_type not in _TEXT_TYPES and not art.media_type.startswith("text/"):
|
|
31
|
+
return FileResponse(
|
|
32
|
+
path=str(art.path),
|
|
33
|
+
media_type=art.media_type,
|
|
34
|
+
filename=art.name,
|
|
35
|
+
)
|
|
36
|
+
text = art.path.read_text(encoding="utf-8", errors="replace")
|
|
37
|
+
return PlainTextResponse(text, media_type=art.media_type or "text/plain")
|