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,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