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,107 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Discover tools and skills from a library directory (filesystem scan)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def discover_tools(base_dir: Path, namespaces: list[str] | None = None) -> list[dict]:
|
|
13
|
+
"""Scan library tree for Tool manifests and ToolProvider infra entries."""
|
|
14
|
+
if not namespaces:
|
|
15
|
+
namespaces = ["global"]
|
|
16
|
+
|
|
17
|
+
tools: dict[str, str] = {}
|
|
18
|
+
|
|
19
|
+
for ns in namespaces:
|
|
20
|
+
if ns == "global":
|
|
21
|
+
tools_dir = base_dir / "tools"
|
|
22
|
+
if tools_dir.exists():
|
|
23
|
+
for tf in sorted(tools_dir.rglob("*.tool.yaml")):
|
|
24
|
+
data = yaml.safe_load(tf.read_text(encoding="utf-8"))
|
|
25
|
+
if (data or {}).get("kind") != "Tool":
|
|
26
|
+
continue
|
|
27
|
+
meta = (data or {}).get("metadata", {})
|
|
28
|
+
name = meta.get("name")
|
|
29
|
+
if name:
|
|
30
|
+
tools[f"global/{name}"] = meta.get("description", "")
|
|
31
|
+
|
|
32
|
+
infra_dir = base_dir / "infra"
|
|
33
|
+
if infra_dir.exists():
|
|
34
|
+
for infra_file in infra_dir.rglob("*.yaml"):
|
|
35
|
+
data = yaml.safe_load(infra_file.read_text(encoding="utf-8"))
|
|
36
|
+
if (data or {}).get("kind") == "ToolProvider":
|
|
37
|
+
for name in (data or {}).get("spec", {}).get("tools", {}).keys():
|
|
38
|
+
tools.setdefault(f"global/{name}", "")
|
|
39
|
+
else:
|
|
40
|
+
app_tools_dir = base_dir / "apps" / ns / "tools"
|
|
41
|
+
if app_tools_dir.exists():
|
|
42
|
+
for tf in sorted(app_tools_dir.rglob("*.tool.yaml")):
|
|
43
|
+
data = yaml.safe_load(tf.read_text(encoding="utf-8"))
|
|
44
|
+
if (data or {}).get("kind") != "Tool":
|
|
45
|
+
continue
|
|
46
|
+
meta = (data or {}).get("metadata", {})
|
|
47
|
+
name = meta.get("name")
|
|
48
|
+
if name:
|
|
49
|
+
tools[name] = meta.get("description", "")
|
|
50
|
+
|
|
51
|
+
return sorted(
|
|
52
|
+
[{"name": n, "description": d} for n, d in tools.items()],
|
|
53
|
+
key=lambda t: t["name"],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def discover_skills(base_dir: Path, namespaces: list[str] | None = None) -> list[dict]:
|
|
58
|
+
"""Scan library tree for skill folders containing SKILL.md."""
|
|
59
|
+
if not namespaces:
|
|
60
|
+
namespaces = ["global"]
|
|
61
|
+
|
|
62
|
+
skills: dict[str, str] = {}
|
|
63
|
+
|
|
64
|
+
def _scan_skills_dir(skills_dir: Path, prefix: str) -> None:
|
|
65
|
+
if not skills_dir.exists():
|
|
66
|
+
return
|
|
67
|
+
for skill_path in sorted(skills_dir.iterdir()):
|
|
68
|
+
if not skill_path.is_dir():
|
|
69
|
+
continue
|
|
70
|
+
skill_md = skill_path / "SKILL.md"
|
|
71
|
+
if not skill_md.exists():
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
75
|
+
name = skill_path.name
|
|
76
|
+
description = ""
|
|
77
|
+
|
|
78
|
+
if content.startswith("---"):
|
|
79
|
+
parts = content.split("---", 2)
|
|
80
|
+
if len(parts) >= 3:
|
|
81
|
+
try:
|
|
82
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
83
|
+
if frontmatter:
|
|
84
|
+
name = frontmatter.get("name", name)
|
|
85
|
+
description = frontmatter.get("description", "").strip()
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
if not description:
|
|
90
|
+
for line in content.splitlines():
|
|
91
|
+
if line.startswith("# "):
|
|
92
|
+
description = line[2:].strip()
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
display = f"{prefix}{name}" if prefix else name
|
|
96
|
+
skills.setdefault(display, description)
|
|
97
|
+
|
|
98
|
+
for ns in namespaces:
|
|
99
|
+
if ns == "global":
|
|
100
|
+
_scan_skills_dir(base_dir / "skills", "global/")
|
|
101
|
+
else:
|
|
102
|
+
_scan_skills_dir(base_dir / "apps" / ns / "skills", "")
|
|
103
|
+
|
|
104
|
+
return sorted(
|
|
105
|
+
[{"name": n, "description": d} for n, d in skills.items()],
|
|
106
|
+
key=lambda s: s["name"],
|
|
107
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Opaque artifact ids for run outputs. Paths never leave this module."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import mimetypes
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _artifact_id(run_id: str, relative: str) -> str:
|
|
15
|
+
digest = hashlib.sha256(f"{run_id}:{relative}".encode("utf-8")).hexdigest()
|
|
16
|
+
return digest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Artifact:
|
|
21
|
+
artifact_id: str
|
|
22
|
+
name: str
|
|
23
|
+
kind: str
|
|
24
|
+
size: int
|
|
25
|
+
media_type: str
|
|
26
|
+
children: list["Artifact"] = field(default_factory=list)
|
|
27
|
+
# Internal
|
|
28
|
+
path: Path | None = None
|
|
29
|
+
|
|
30
|
+
def public_dict(self) -> dict[str, Any]:
|
|
31
|
+
payload: dict[str, Any] = {
|
|
32
|
+
"artifact_id": self.artifact_id,
|
|
33
|
+
"name": self.name,
|
|
34
|
+
"kind": self.kind,
|
|
35
|
+
"size": self.size,
|
|
36
|
+
"media_type": self.media_type,
|
|
37
|
+
}
|
|
38
|
+
if self.children:
|
|
39
|
+
payload["children"] = [c.public_dict() for c in self.children]
|
|
40
|
+
return payload
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ArtifactRegistry:
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._by_id: dict[str, Artifact] = {}
|
|
46
|
+
self._by_run: dict[str, list[str]] = {}
|
|
47
|
+
|
|
48
|
+
def register_tree(self, run_id: str, root: Path) -> list[Artifact]:
|
|
49
|
+
if not root.exists():
|
|
50
|
+
return []
|
|
51
|
+
artifacts: list[Artifact] = []
|
|
52
|
+
for path in sorted(root.rglob("*")):
|
|
53
|
+
if not path.is_file():
|
|
54
|
+
continue
|
|
55
|
+
rel = path.relative_to(root).as_posix()
|
|
56
|
+
art = self.register_file(run_id, rel, path)
|
|
57
|
+
artifacts.append(art)
|
|
58
|
+
return artifacts
|
|
59
|
+
|
|
60
|
+
def register_file(self, run_id: str, name: str, path: Path) -> Artifact:
|
|
61
|
+
aid = _artifact_id(run_id, name)
|
|
62
|
+
media, _ = mimetypes.guess_type(name)
|
|
63
|
+
kind = "file"
|
|
64
|
+
suffix = Path(name).suffix.lower()
|
|
65
|
+
if suffix in {".csv"}:
|
|
66
|
+
kind = "csv"
|
|
67
|
+
elif suffix in {".json", ".jsonl"}:
|
|
68
|
+
kind = "json"
|
|
69
|
+
elif suffix in {".yaml", ".yml"}:
|
|
70
|
+
kind = "yaml"
|
|
71
|
+
elif suffix in {".html", ".htm"}:
|
|
72
|
+
kind = "html"
|
|
73
|
+
elif suffix in {".svg"}:
|
|
74
|
+
kind = "svg"
|
|
75
|
+
elif suffix in {".png", ".jpg", ".jpeg"}:
|
|
76
|
+
kind = "image"
|
|
77
|
+
art = Artifact(
|
|
78
|
+
artifact_id=aid,
|
|
79
|
+
name=Path(name).name,
|
|
80
|
+
kind=kind,
|
|
81
|
+
size=path.stat().st_size if path.exists() else 0,
|
|
82
|
+
media_type=media or "application/octet-stream",
|
|
83
|
+
path=path.resolve(),
|
|
84
|
+
)
|
|
85
|
+
self._by_id[aid] = art
|
|
86
|
+
self._by_run.setdefault(run_id, []).append(aid)
|
|
87
|
+
return art
|
|
88
|
+
|
|
89
|
+
def get(self, artifact_id: str) -> Artifact | None:
|
|
90
|
+
return self._by_id.get(artifact_id)
|
|
91
|
+
|
|
92
|
+
def for_run(self, run_id: str) -> list[Artifact]:
|
|
93
|
+
return [self._by_id[i] for i in self._by_run.get(run_id, []) if i in self._by_id]
|
|
94
|
+
|
|
95
|
+
def drop_run(self, run_id: str) -> None:
|
|
96
|
+
for aid in self._by_run.pop(run_id, []):
|
|
97
|
+
self._by_id.pop(aid, None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_registry: ArtifactRegistry | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_artifact_registry() -> ArtifactRegistry:
|
|
104
|
+
global _registry
|
|
105
|
+
if _registry is None:
|
|
106
|
+
_registry = ArtifactRegistry()
|
|
107
|
+
return _registry
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def set_artifact_registry(registry: ArtifactRegistry | None) -> None:
|
|
111
|
+
global _registry
|
|
112
|
+
_registry = registry
|