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,7 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""MAS Lab controller — daemon, workers, and HTTP API."""
|
|
4
|
+
from mas.lab.controller.api import ControllerAPI
|
|
5
|
+
from mas.lab.controller.lab_registry import LabRegistry, get_lab_registry
|
|
6
|
+
|
|
7
|
+
__all__ = ["ControllerAPI", "LabRegistry", "get_lab_registry"]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# Reference entry point for running the controller HTTP API directly (PR #5).
|
|
4
|
+
# Prefer: mas-lab control start or mas-lab serve
|
|
5
|
+
from mas.lab.controller.fastapi_app import app # noqa: F401
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""In-process agent chat for the controller UI.
|
|
4
|
+
|
|
5
|
+
Runs agents via v2 ``SessionController`` (no subprocess / stdout capture) and
|
|
6
|
+
returns structured interaction fields: user query in, agent response or error out.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal, Optional
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_STATUS = Literal["ok", "error"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class AgentTurnResult:
|
|
26
|
+
status: _STATUS
|
|
27
|
+
response: str = ""
|
|
28
|
+
error_message: str = ""
|
|
29
|
+
error_detail: str = ""
|
|
30
|
+
session_id: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_error(raw: str) -> tuple[str, str]:
|
|
34
|
+
"""Return (short_message, full_detail) from a raw agent/LLM error string."""
|
|
35
|
+
full = (raw or "").strip()
|
|
36
|
+
if not full:
|
|
37
|
+
return "Agent execution failed.", ""
|
|
38
|
+
|
|
39
|
+
short = full
|
|
40
|
+
|
|
41
|
+
msg_match = re.search(r"['\"]message['\"]\s*:\s*['\"]([^'\"]+)['\"]", full)
|
|
42
|
+
if msg_match:
|
|
43
|
+
short = msg_match.group(1)
|
|
44
|
+
elif "ExceededBudget" in full:
|
|
45
|
+
budget_match = re.search(r"(ExceededBudget:[^\n'\"]+)", full)
|
|
46
|
+
if budget_match:
|
|
47
|
+
short = budget_match.group(1).strip()
|
|
48
|
+
|
|
49
|
+
if len(short) > 240:
|
|
50
|
+
short = short[:237] + "..."
|
|
51
|
+
|
|
52
|
+
return short, full
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _SilentDisplay:
|
|
56
|
+
"""No-op conversation display for embedded UI turns."""
|
|
57
|
+
|
|
58
|
+
def on_user(self, text: str, *, turn_id: str = "") -> None:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
def on_agent(self, text: str) -> None:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
def on_turn_error(self, message: str, *, detail: str = "") -> None:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
def on_hitl_request(self, request: object) -> None:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
def on_working(self, message: str = "Agent working…") -> None:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def end_working(self) -> None:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def on_system(self, message: str) -> None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
def on_error(self, message: str) -> None:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _load_workspace_dotenv(base_dir: Path) -> None:
|
|
84
|
+
"""Load optional .env files; container env from docker/.env still wins (override=False)."""
|
|
85
|
+
import os
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
from dotenv import load_dotenv
|
|
89
|
+
except ImportError:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
candidates = [
|
|
93
|
+
Path("/workspace/.env"),
|
|
94
|
+
base_dir / ".env",
|
|
95
|
+
Path(os.environ.get("MAS_WORKSPACE_ROOT", "")) / ".env",
|
|
96
|
+
]
|
|
97
|
+
for path in candidates:
|
|
98
|
+
if path.is_file():
|
|
99
|
+
load_dotenv(path, override=False)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _quiet_logging_for_chat() -> None:
|
|
103
|
+
"""Suppress exchange logs; UI chat must not capture CLI-style stdout."""
|
|
104
|
+
import os
|
|
105
|
+
|
|
106
|
+
from mas.ctl.logging_setup import setup_logging
|
|
107
|
+
|
|
108
|
+
os.environ.pop("LOG_LEVEL", None)
|
|
109
|
+
setup_logging(verbosity=0)
|
|
110
|
+
logging.getLogger("mas.runtime").setLevel(logging.WARNING)
|
|
111
|
+
logging.getLogger("mas.ctl").setLevel(logging.WARNING)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_agent_turn(
|
|
115
|
+
manifest_yaml: str,
|
|
116
|
+
query: str,
|
|
117
|
+
*,
|
|
118
|
+
base_dir: Path,
|
|
119
|
+
flavour: Optional[str] = None,
|
|
120
|
+
session_id: Optional[str] = None,
|
|
121
|
+
) -> AgentTurnResult:
|
|
122
|
+
"""Execute one user → agent turn in-process via v2 SessionController."""
|
|
123
|
+
from mas.ctl.infra.resolve import resolve_infra_refs
|
|
124
|
+
from mas.ctl.runtime_cli import pattern_from_manifest
|
|
125
|
+
from mas.ctl.session.bootstrap import InstantiationOptions, instantiate_runtime
|
|
126
|
+
from mas.ctl.session.controller import ConversationConfig, SessionController
|
|
127
|
+
from mas.ctl.session.hitl_config import resolve_hitl_from_manifest
|
|
128
|
+
from mas.ctl.ui.turn_result import turn_to_agent_result
|
|
129
|
+
from mas.ctl.workspace.config import (
|
|
130
|
+
UserConfig,
|
|
131
|
+
WorkspaceConfig,
|
|
132
|
+
merge_infra_interceptors,
|
|
133
|
+
merge_infra_refs,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
_quiet_logging_for_chat()
|
|
137
|
+
_load_workspace_dotenv(base_dir)
|
|
138
|
+
|
|
139
|
+
base_dir = Path(base_dir)
|
|
140
|
+
sid = session_id or f"ui:{uuid.uuid4()}"
|
|
141
|
+
prompt = (query or "").strip()
|
|
142
|
+
if not prompt:
|
|
143
|
+
return AgentTurnResult(
|
|
144
|
+
status="error",
|
|
145
|
+
error_message="No query provided.",
|
|
146
|
+
session_id=sid,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
current_manifest = yaml.safe_load(manifest_yaml)
|
|
151
|
+
except yaml.YAMLError as exc:
|
|
152
|
+
return AgentTurnResult(
|
|
153
|
+
status="error",
|
|
154
|
+
error_message=f"Invalid manifest YAML: {exc}",
|
|
155
|
+
error_detail=str(exc),
|
|
156
|
+
session_id=sid,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if not isinstance(current_manifest, dict):
|
|
160
|
+
return AgentTurnResult(
|
|
161
|
+
status="error",
|
|
162
|
+
error_message="Manifest must be a YAML mapping.",
|
|
163
|
+
session_id=sid,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
workspace = WorkspaceConfig.load(base_dir)
|
|
168
|
+
user = UserConfig.load()
|
|
169
|
+
pattern = pattern_from_manifest(current_manifest)
|
|
170
|
+
merged_infra = merge_infra_refs(
|
|
171
|
+
workspace_refs=workspace.effective_infra_refs,
|
|
172
|
+
user_refs=[user.default_infra] if user.default_infra else [],
|
|
173
|
+
cli_refs=[],
|
|
174
|
+
workspace_found=workspace.found,
|
|
175
|
+
)
|
|
176
|
+
resolved_infra = resolve_infra_refs(
|
|
177
|
+
merged_infra,
|
|
178
|
+
anchor=base_dir,
|
|
179
|
+
workspace=workspace,
|
|
180
|
+
user=user,
|
|
181
|
+
interceptors=merge_infra_interceptors(
|
|
182
|
+
workspace_interceptors=workspace.infra_interceptors,
|
|
183
|
+
cli_interceptors=[],
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
hitl_responder, hitl_terminal = resolve_hitl_from_manifest(
|
|
187
|
+
current_manifest, session_interactive=False
|
|
188
|
+
)
|
|
189
|
+
instance, _store = instantiate_runtime(
|
|
190
|
+
InstantiationOptions(
|
|
191
|
+
pattern_plugin_id=pattern,
|
|
192
|
+
agent_manifest=current_manifest,
|
|
193
|
+
manifest_dir=base_dir,
|
|
194
|
+
resolved_infra=resolved_infra,
|
|
195
|
+
workspace=workspace,
|
|
196
|
+
validate_manifests=False,
|
|
197
|
+
),
|
|
198
|
+
hitl=hitl_responder,
|
|
199
|
+
)
|
|
200
|
+
controller = SessionController(
|
|
201
|
+
instance=instance,
|
|
202
|
+
display=_SilentDisplay(),
|
|
203
|
+
hitl_terminal=hitl_terminal,
|
|
204
|
+
config=ConversationConfig(single_turn=True),
|
|
205
|
+
verbose=0,
|
|
206
|
+
session_id=sid,
|
|
207
|
+
)
|
|
208
|
+
turn = controller.run_turn(prompt, turn_id=sid, auto_hitl=True)
|
|
209
|
+
mapped = turn_to_agent_result(turn)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
logger.debug("Agent session failed", exc_info=True)
|
|
212
|
+
short, full = format_error(str(exc))
|
|
213
|
+
return AgentTurnResult(
|
|
214
|
+
status="error",
|
|
215
|
+
error_message=short,
|
|
216
|
+
error_detail=full,
|
|
217
|
+
session_id=sid,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return AgentTurnResult(
|
|
221
|
+
status=mapped.status,
|
|
222
|
+
response=mapped.response,
|
|
223
|
+
error_message=mapped.error_message,
|
|
224
|
+
error_detail=mapped.error_detail,
|
|
225
|
+
session_id=sid,
|
|
226
|
+
)
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""ControllerAPI — pure Python backend for CLI and HTTP."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from mas.lab.controller.manifest_store import ManifestStore
|
|
12
|
+
from mas.lab.controller.registry import WorkerRegistry
|
|
13
|
+
from mas.lab.controller.worker_model import WorkerKind, WorkerStatus
|
|
14
|
+
from mas.lab.controller.workers import (
|
|
15
|
+
WorkerRunner,
|
|
16
|
+
run_application_worker,
|
|
17
|
+
run_benchmark_worker,
|
|
18
|
+
run_experiment_worker,
|
|
19
|
+
run_pipeline_worker,
|
|
20
|
+
)
|
|
21
|
+
from mas.runtime.spec.source import load_yaml_file
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_INSTANCE: "ControllerAPI | None" = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_controller_api(api: Optional["ControllerAPI"]) -> None:
|
|
29
|
+
global _INSTANCE
|
|
30
|
+
_INSTANCE = api
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def peek_controller_api() -> "ControllerAPI | None":
|
|
34
|
+
return _INSTANCE
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_controller_api(workspace: Any = None) -> "ControllerAPI":
|
|
38
|
+
global _INSTANCE
|
|
39
|
+
if _INSTANCE is None:
|
|
40
|
+
_INSTANCE = ControllerAPI(workspace)
|
|
41
|
+
return _INSTANCE
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ControllerAPI:
|
|
45
|
+
"""Shared API surface for Unix-socket CLI and HTTP UI."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, workspace: Any = None) -> None:
|
|
48
|
+
self.workspace = workspace
|
|
49
|
+
self.workers = WorkerRegistry()
|
|
50
|
+
self.runner = WorkerRunner(self.workers)
|
|
51
|
+
self.manifests = ManifestStore(workspace)
|
|
52
|
+
|
|
53
|
+
# -- Service ----------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def status(self) -> Dict[str, Any]:
|
|
56
|
+
from mas.lab.controller import daemon as daemon_mod
|
|
57
|
+
|
|
58
|
+
active = self.workers.list_workers(
|
|
59
|
+
status=WorkerStatus.RUNNING,
|
|
60
|
+
)
|
|
61
|
+
return {
|
|
62
|
+
"status": "ok",
|
|
63
|
+
"workers": len(self.workers.list_workers()),
|
|
64
|
+
"running": len(active),
|
|
65
|
+
"libraries": len(self.manifests.libraries()),
|
|
66
|
+
"sessions": daemon_mod._sessions.status(),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# -- Workers ----------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def list_workers(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
kind: Optional[str] = None,
|
|
75
|
+
status: Optional[str] = None,
|
|
76
|
+
) -> List[Dict[str, Any]]:
|
|
77
|
+
kind_enum = WorkerKind(kind) if kind else None
|
|
78
|
+
status_enum = WorkerStatus(status) if status else None
|
|
79
|
+
return [w.to_job_summary() for w in self.workers.list_workers(kind=kind_enum, status=status_enum)]
|
|
80
|
+
|
|
81
|
+
def get_worker(self, worker_id: str) -> Optional[Dict[str, Any]]:
|
|
82
|
+
record = self.workers.get(worker_id)
|
|
83
|
+
return record.to_job_detail() if record else None
|
|
84
|
+
|
|
85
|
+
def cancel_worker(self, worker_id: str) -> bool:
|
|
86
|
+
return self.runner.cancel(worker_id)
|
|
87
|
+
|
|
88
|
+
def drop_worker(self, worker_id: str) -> bool:
|
|
89
|
+
self.runner.cancel(worker_id)
|
|
90
|
+
return self.workers.drop(worker_id)
|
|
91
|
+
|
|
92
|
+
def drop_workers(self, *, origin: str | None = None, terminal_only: bool = True) -> int:
|
|
93
|
+
from mas.lab.controller.worker_model import _ui_origin
|
|
94
|
+
|
|
95
|
+
removed = 0
|
|
96
|
+
for record in list(self.workers.list_workers()):
|
|
97
|
+
if terminal_only and record.status not in {
|
|
98
|
+
WorkerStatus.COMPLETED,
|
|
99
|
+
WorkerStatus.FAILED,
|
|
100
|
+
WorkerStatus.CANCELLED,
|
|
101
|
+
}:
|
|
102
|
+
continue
|
|
103
|
+
job_origin = _ui_origin(record.spec)
|
|
104
|
+
if origin is not None and job_origin != origin:
|
|
105
|
+
continue
|
|
106
|
+
if self.drop_worker(record.id):
|
|
107
|
+
removed += 1
|
|
108
|
+
return removed
|
|
109
|
+
|
|
110
|
+
def submit_benchmark(self, spec: dict) -> Dict[str, Any]:
|
|
111
|
+
record = run_benchmark_worker(self.workers, self.runner, spec)
|
|
112
|
+
return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
|
|
113
|
+
|
|
114
|
+
def submit_experiment(self, spec: dict) -> Dict[str, Any]:
|
|
115
|
+
live_spec = {**spec, "live": True, "progress": False}
|
|
116
|
+
record = run_experiment_worker(self.workers, self.runner, live_spec)
|
|
117
|
+
return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
|
|
118
|
+
|
|
119
|
+
def submit_application(self, spec: dict) -> Dict[str, Any]:
|
|
120
|
+
record = run_application_worker(self.workers, self.runner, spec)
|
|
121
|
+
return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
|
|
122
|
+
|
|
123
|
+
def submit_pipeline(self, spec: dict) -> Dict[str, Any]:
|
|
124
|
+
record = run_pipeline_worker(self.workers, self.runner, spec)
|
|
125
|
+
return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
|
|
126
|
+
|
|
127
|
+
# -- Jobs (UI alias) --------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def list_jobs(self, status: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
130
|
+
return self.list_workers(status=status)
|
|
131
|
+
|
|
132
|
+
def get_job(self, job_id: str) -> Optional[Dict[str, Any]]:
|
|
133
|
+
return self.get_worker(job_id)
|
|
134
|
+
|
|
135
|
+
# -- Libraries / manifests ----------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def list_libraries(self) -> List[Dict[str, str]]:
|
|
138
|
+
self.manifests.refresh()
|
|
139
|
+
return self.manifests.libraries()
|
|
140
|
+
|
|
141
|
+
def list_runtime_runners(self) -> List[Dict[str, str]]:
|
|
142
|
+
"""Return registered ``mas.lab.runners`` plugins for UI / CLI."""
|
|
143
|
+
from mas.lab.runners.factory import RunnerFactory
|
|
144
|
+
|
|
145
|
+
return [{"id": rid, "label": rid} for rid in RunnerFactory.available()]
|
|
146
|
+
|
|
147
|
+
def validate_manifest_yaml(self, manifest_yaml: str, *, base_dir: Path | None = None) -> Dict[str, Any]:
|
|
148
|
+
from mas.lab.controller.manifest_validation import validate_manifest_yaml_content
|
|
149
|
+
|
|
150
|
+
root = base_dir or Path.cwd()
|
|
151
|
+
return validate_manifest_yaml_content(manifest_yaml, base_dir=root, resolve_refs=True)
|
|
152
|
+
|
|
153
|
+
def list_experiments(self, library: str) -> List[Dict[str, Any]]:
|
|
154
|
+
summaries: List[Dict[str, Any]] = []
|
|
155
|
+
for item in self.manifests.list_yaml_resources(library, "experiments"):
|
|
156
|
+
try:
|
|
157
|
+
data = load_yaml_file(Path(item["file"]))
|
|
158
|
+
meta = data.get("metadata") or data.get("experiment") or {}
|
|
159
|
+
summaries.append(
|
|
160
|
+
{
|
|
161
|
+
"name": item["name"],
|
|
162
|
+
"description": meta.get("description", ""),
|
|
163
|
+
"version": str(meta.get("version", "")),
|
|
164
|
+
"scenarios": list((data.get("scenarios") or {}).keys()),
|
|
165
|
+
"dataset": str(data.get("dataset") or ""),
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
except Exception:
|
|
169
|
+
summaries.append(
|
|
170
|
+
{
|
|
171
|
+
"name": item["name"],
|
|
172
|
+
"description": "",
|
|
173
|
+
"version": "",
|
|
174
|
+
"scenarios": [],
|
|
175
|
+
"dataset": "",
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
return summaries
|
|
179
|
+
|
|
180
|
+
def get_experiment_content(self, library: str, name: str) -> Dict[str, str]:
|
|
181
|
+
return {"name": name, "content": self.manifests.read_text(library, "experiments", name)}
|
|
182
|
+
|
|
183
|
+
def save_experiment(self, library: str, name: str, content: str) -> None:
|
|
184
|
+
self.manifests.write_text(library, "experiments", name, content)
|
|
185
|
+
|
|
186
|
+
def delete_experiment(self, library: str, name: str) -> None:
|
|
187
|
+
self.manifests.delete_resource(library, "experiments", name)
|
|
188
|
+
|
|
189
|
+
def list_pipelines(self, library: str) -> List[Dict[str, Any]]:
|
|
190
|
+
out: List[Dict[str, Any]] = []
|
|
191
|
+
for item in self.manifests.list_yaml_resources(library, "pipelines"):
|
|
192
|
+
try:
|
|
193
|
+
data = load_yaml_file(Path(item["file"]))
|
|
194
|
+
md = data.get("metadata") or {}
|
|
195
|
+
steps = []
|
|
196
|
+
for step in data.get("steps") or []:
|
|
197
|
+
steps.append(
|
|
198
|
+
{
|
|
199
|
+
"name": step.get("name", step.get("id", "")),
|
|
200
|
+
"type": step.get("type", ""),
|
|
201
|
+
"depends_on": list(step.get("depends_on") or []),
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
out.append(
|
|
205
|
+
{
|
|
206
|
+
"filename": item["name"],
|
|
207
|
+
"name": md.get("name") or item["name"],
|
|
208
|
+
"description": md.get("description", ""),
|
|
209
|
+
"steps": steps,
|
|
210
|
+
"experiment": str(data.get("experiment") or ""),
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
out.append(
|
|
215
|
+
{
|
|
216
|
+
"filename": item["name"],
|
|
217
|
+
"name": item["name"],
|
|
218
|
+
"description": "",
|
|
219
|
+
"steps": [],
|
|
220
|
+
"experiment": "",
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
return out
|
|
224
|
+
|
|
225
|
+
def get_pipeline_content(self, library: str, name: str) -> Dict[str, str]:
|
|
226
|
+
return {"name": name, "content": self.manifests.read_text(library, "pipelines", name)}
|
|
227
|
+
|
|
228
|
+
def save_pipeline(self, library: str, name: str, content: str) -> None:
|
|
229
|
+
self.manifests.write_text(library, "pipelines", name, content)
|
|
230
|
+
|
|
231
|
+
def delete_pipeline(self, library: str, name: str) -> None:
|
|
232
|
+
self.manifests.delete_resource(library, "pipelines", name)
|
|
233
|
+
|
|
234
|
+
def list_overlays(self, library: str) -> List[Dict[str, Any]]:
|
|
235
|
+
out: List[Dict[str, Any]] = []
|
|
236
|
+
for item in self.manifests.list_yaml_resources(library, "overlays"):
|
|
237
|
+
try:
|
|
238
|
+
data = load_yaml_file(Path(item["file"]))
|
|
239
|
+
out.append({"name": item["name"], "description": data.get("description", "")})
|
|
240
|
+
except Exception:
|
|
241
|
+
out.append({"name": item["name"]})
|
|
242
|
+
return out
|
|
243
|
+
|
|
244
|
+
def get_overlay_content(self, library: str, name: str) -> Dict[str, str]:
|
|
245
|
+
return {"name": name, "content": self.manifests.read_text(library, "overlays", name)}
|
|
246
|
+
|
|
247
|
+
def save_overlay(self, library: str, name: str, content: str) -> None:
|
|
248
|
+
self.manifests.write_text(library, "overlays", name, content)
|
|
249
|
+
|
|
250
|
+
def delete_overlay(self, library: str, name: str) -> None:
|
|
251
|
+
self.manifests.delete_resource(library, "overlays", name)
|
|
252
|
+
|
|
253
|
+
def list_datasets(self, library: str) -> List[Dict[str, Any]]:
|
|
254
|
+
out: List[Dict[str, Any]] = []
|
|
255
|
+
for item in self.manifests.list_yaml_resources(library, "datasets"):
|
|
256
|
+
try:
|
|
257
|
+
data = load_yaml_file(Path(item["file"]))
|
|
258
|
+
md = data.get("metadata") or {}
|
|
259
|
+
out.append({"name": item["name"], "description": md.get("description", "")})
|
|
260
|
+
except Exception:
|
|
261
|
+
out.append({"name": item["name"], "description": ""})
|
|
262
|
+
return out
|
|
263
|
+
|
|
264
|
+
def get_dataset_content(self, library: str, name: str) -> Dict[str, str]:
|
|
265
|
+
return {"name": name, "content": self.manifests.read_text(library, "datasets", name)}
|
|
266
|
+
|
|
267
|
+
def save_dataset(self, library: str, name: str, content: str) -> None:
|
|
268
|
+
self.manifests.write_text(library, "datasets", name, content)
|
|
269
|
+
|
|
270
|
+
def delete_dataset(self, library: str, name: str) -> None:
|
|
271
|
+
self.manifests.delete_resource(library, "datasets", name)
|
|
272
|
+
|
|
273
|
+
def list_scenarios(self, library: str) -> List[Dict[str, str]]:
|
|
274
|
+
return [
|
|
275
|
+
{"name": item["name"], "path": item["path"]}
|
|
276
|
+
for item in self.manifests.list_yaml_resources(library, "scenarios")
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
def config_files(self, library: str) -> Dict[str, Dict[str, str]]:
|
|
280
|
+
return self.manifests.config_files(library)
|
|
281
|
+
|
|
282
|
+
def list_tools(self, library: str) -> List[Dict[str, str]]:
|
|
283
|
+
from mas.lab.controller.artifact_discovery import discover_tools
|
|
284
|
+
|
|
285
|
+
return discover_tools(self.manifests.library_root(library))
|
|
286
|
+
|
|
287
|
+
def list_skills(self, library: str) -> List[Dict[str, str]]:
|
|
288
|
+
from mas.lab.controller.artifact_discovery import discover_skills
|
|
289
|
+
|
|
290
|
+
return discover_skills(self.manifests.library_root(library))
|
|
291
|
+
|
|
292
|
+
def pipeline_step_types(self) -> Dict[str, Any]:
|
|
293
|
+
from mas.lab.controller.lab_registry import get_lab_registry
|
|
294
|
+
|
|
295
|
+
return get_lab_registry(self.workspace).pipeline_step_types()
|
|
296
|
+
|
|
297
|
+
def registry_catalog(self) -> Dict[str, Any]:
|
|
298
|
+
from mas.lab.controller.lab_registry import get_lab_registry
|
|
299
|
+
|
|
300
|
+
return get_lab_registry(self.workspace).catalog()
|
|
301
|
+
|
|
302
|
+
def run_agent_job(self, library: str, body: dict) -> Dict[str, Any]:
|
|
303
|
+
root = self.manifests.library_root(library)
|
|
304
|
+
agents_dir = root / "agents"
|
|
305
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=agents_dir, delete=False) as fh:
|
|
307
|
+
fh.write(body.get("manifest_yaml") or "")
|
|
308
|
+
spec_path = fh.name
|
|
309
|
+
spec = {
|
|
310
|
+
"prompt": body.get("query") or "",
|
|
311
|
+
"spec_path": spec_path,
|
|
312
|
+
"config": {},
|
|
313
|
+
"endpoint": f"/api/libraries/{library}/run",
|
|
314
|
+
"command": f"run agent ({library})",
|
|
315
|
+
}
|
|
316
|
+
return self.submit_application(spec)
|
|
317
|
+
|
|
318
|
+
def run_mas_job(self, library: str, body: dict) -> Dict[str, Any]:
|
|
319
|
+
root = self.manifests.library_root(library)
|
|
320
|
+
mas_dir = root / "apps"
|
|
321
|
+
mas_dir.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=mas_dir, delete=False) as fh:
|
|
323
|
+
fh.write(body.get("manifest_yaml") or "")
|
|
324
|
+
spec_path = fh.name
|
|
325
|
+
spec = {
|
|
326
|
+
"prompt": body.get("query") or "",
|
|
327
|
+
"spec_path": spec_path,
|
|
328
|
+
"config": {"overlays": body.get("overlays") or []},
|
|
329
|
+
"endpoint": f"/api/libraries/{library}/run-mas",
|
|
330
|
+
"command": f"run mas ({library})",
|
|
331
|
+
}
|
|
332
|
+
return self.submit_application(spec)
|
|
333
|
+
|
|
334
|
+
def run_benchmark_job(self, library: str, body: dict) -> Dict[str, Any]:
|
|
335
|
+
content = body.get("experiment_yaml") or ""
|
|
336
|
+
root = self.manifests.library_root(library)
|
|
337
|
+
exp_dir = root / "experiments"
|
|
338
|
+
exp_dir.mkdir(parents=True, exist_ok=True)
|
|
339
|
+
with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=exp_dir, delete=False) as fh:
|
|
340
|
+
fh.write(content)
|
|
341
|
+
experiment_yaml = fh.name
|
|
342
|
+
spec = {
|
|
343
|
+
"experiment_yaml": experiment_yaml,
|
|
344
|
+
"progress": body.get("progress", True),
|
|
345
|
+
"max_runs": body.get("n_runs"),
|
|
346
|
+
"endpoint": f"/api/libraries/{library}/benchmark/run",
|
|
347
|
+
}
|
|
348
|
+
result = self.submit_benchmark(spec)
|
|
349
|
+
result["command"] = f"mas-lab benchmark run {experiment_yaml}"
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
def run_pipeline_job(self, library: str, body: dict) -> Dict[str, Any]:
|
|
353
|
+
root = self.manifests.library_root(library)
|
|
354
|
+
pipe_dir = root / "pipelines"
|
|
355
|
+
pipe_dir.mkdir(parents=True, exist_ok=True)
|
|
356
|
+
with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=pipe_dir, delete=False) as fh:
|
|
357
|
+
fh.write(body.get("pipeline_yaml") or "")
|
|
358
|
+
pipeline_yaml = fh.name
|
|
359
|
+
spec = {
|
|
360
|
+
"pipeline_yaml": pipeline_yaml,
|
|
361
|
+
"only": body.get("only"),
|
|
362
|
+
"command": f"pipeline run {pipeline_yaml}",
|
|
363
|
+
}
|
|
364
|
+
return self.submit_pipeline(spec)
|