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,274 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Live token/time aggregation from native records and metric artefacts."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from mas.lab.controller.ui_stream.refs import DataSourceRef
|
|
12
|
+
|
|
13
|
+
_END_KINDS = frozenset({"mas_call_end", "execution_end"})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def coerce_timestamp(value: Any) -> float | None:
|
|
17
|
+
"""Unix seconds from a numeric, numeric-string, or ISO-8601 timestamp."""
|
|
18
|
+
if isinstance(value, bool):
|
|
19
|
+
return None
|
|
20
|
+
if isinstance(value, (int, float)):
|
|
21
|
+
ts = float(value)
|
|
22
|
+
return ts / 1000.0 if ts > 1e12 else ts
|
|
23
|
+
text = str(value or "").strip()
|
|
24
|
+
if not text:
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
ts = float(text)
|
|
28
|
+
return ts / 1000.0 if ts > 1e12 else ts
|
|
29
|
+
except ValueError:
|
|
30
|
+
pass
|
|
31
|
+
try:
|
|
32
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()
|
|
33
|
+
except ValueError:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _as_int(value: Any) -> int:
|
|
38
|
+
try:
|
|
39
|
+
return int(value)
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _as_float(value: Any) -> float:
|
|
45
|
+
try:
|
|
46
|
+
return float(value)
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
return 0.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_SKIP_FLATTEN = frozenset(
|
|
52
|
+
{
|
|
53
|
+
"note",
|
|
54
|
+
"schema_version",
|
|
55
|
+
"snapshot_at",
|
|
56
|
+
"fragments_present",
|
|
57
|
+
"fragments_missing",
|
|
58
|
+
"scoring",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _family_dotted(metric_id: str) -> str | None:
|
|
64
|
+
"""``family_leaf`` → ``family.leaf`` for known eval families."""
|
|
65
|
+
for family in ("outcome", "stateful"):
|
|
66
|
+
prefix = family + "_"
|
|
67
|
+
if metric_id.startswith(prefix) and len(metric_id) > len(prefix):
|
|
68
|
+
return f"{family}.{metric_id[len(prefix):]}"
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _alias_eval_keys(out: dict[str, float | int]) -> dict[str, float | int]:
|
|
73
|
+
"""Mirror underscore session ids onto the live dotted KPI keys."""
|
|
74
|
+
aliased: dict[str, float | int] = dict(out)
|
|
75
|
+
for key, value in out.items():
|
|
76
|
+
leaf = key.rsplit(".", 1)[-1]
|
|
77
|
+
dotted = _family_dotted(leaf) or _family_dotted(key)
|
|
78
|
+
if dotted:
|
|
79
|
+
aliased.setdefault(dotted, value)
|
|
80
|
+
if leaf.startswith("challenge_"):
|
|
81
|
+
aliased.setdefault(leaf, value)
|
|
82
|
+
if (
|
|
83
|
+
key.startswith("scored.") or key.startswith("session.")
|
|
84
|
+
or leaf.startswith("challenge_")
|
|
85
|
+
or leaf.startswith("outcome_")
|
|
86
|
+
or leaf.startswith("stateful_")
|
|
87
|
+
) and not leaf.startswith("mce."):
|
|
88
|
+
aliased.setdefault(f"mce.{leaf}", value)
|
|
89
|
+
return aliased
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def flatten_metric_tree(value: Any, prefix: str = "", out: dict[str, float | int] | None = None) -> dict[str, float | int]:
|
|
93
|
+
"""Lift nested numeric/bool leaves (and ``{value: n}`` score objects) to dotted keys."""
|
|
94
|
+
out = out if out is not None else {}
|
|
95
|
+
leaf = prefix.rsplit(".", 1)[-1] if prefix else ""
|
|
96
|
+
if leaf in _SKIP_FLATTEN:
|
|
97
|
+
return out
|
|
98
|
+
if isinstance(value, dict):
|
|
99
|
+
raw = value.get("value")
|
|
100
|
+
if prefix and isinstance(raw, bool):
|
|
101
|
+
out[prefix] = int(raw)
|
|
102
|
+
return out
|
|
103
|
+
if prefix and isinstance(raw, (int, float)) and not isinstance(raw, bool):
|
|
104
|
+
out[prefix] = raw
|
|
105
|
+
return out
|
|
106
|
+
for key, child in value.items():
|
|
107
|
+
if str(key).startswith("_") or str(key) in _SKIP_FLATTEN:
|
|
108
|
+
continue
|
|
109
|
+
next_key = f"{prefix}.{key}" if prefix else str(key)
|
|
110
|
+
flatten_metric_tree(child, next_key, out)
|
|
111
|
+
return out
|
|
112
|
+
if prefix and isinstance(value, bool):
|
|
113
|
+
out[prefix] = int(value)
|
|
114
|
+
return out
|
|
115
|
+
if prefix and isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
116
|
+
out[prefix] = value
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def flatten_metric_payload(payload: dict[str, Any] | None) -> dict[str, float | int]:
|
|
121
|
+
"""Flatten a ``kind: metric`` record or a nested progress/MCE document."""
|
|
122
|
+
if not isinstance(payload, dict):
|
|
123
|
+
return {}
|
|
124
|
+
body = payload.get("metrics") if isinstance(payload.get("metrics"), dict) else payload
|
|
125
|
+
if not isinstance(body, dict):
|
|
126
|
+
return {}
|
|
127
|
+
return _alias_eval_keys(flatten_metric_tree(body))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def extract_cached_tokens(usage: dict[str, Any]) -> int:
|
|
131
|
+
details = usage.get("prompt_tokens_details")
|
|
132
|
+
if not isinstance(details, dict):
|
|
133
|
+
details = {}
|
|
134
|
+
return _as_int(
|
|
135
|
+
usage.get("cached_tokens")
|
|
136
|
+
or usage.get("cache_read_input_tokens")
|
|
137
|
+
or details.get("cached_tokens")
|
|
138
|
+
or details.get("cache_read_tokens")
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def extract_usage(payload: dict[str, Any]) -> dict[str, float | int]:
|
|
143
|
+
usage = payload.get("tokens_used") or payload.get("usage")
|
|
144
|
+
if not isinstance(usage, dict):
|
|
145
|
+
usage = {}
|
|
146
|
+
prompt = _as_int(usage.get("prompt_tokens") or usage.get("input_tokens"))
|
|
147
|
+
completion = _as_int(usage.get("completion_tokens") or usage.get("output_tokens"))
|
|
148
|
+
total = _as_int(usage.get("total_tokens")) or (prompt + completion)
|
|
149
|
+
cost = _as_float(usage.get("cost") or payload.get("cost"))
|
|
150
|
+
return {
|
|
151
|
+
"prompt_tokens": prompt,
|
|
152
|
+
"completion_tokens": completion,
|
|
153
|
+
"total_tokens": total,
|
|
154
|
+
"cached_tokens": extract_cached_tokens(usage),
|
|
155
|
+
"cost": cost,
|
|
156
|
+
"llm_calls": 1 if payload.get("kind") == "llm_call_end" else 0,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _public(bucket: dict[str, float | int]) -> dict[str, float | int]:
|
|
161
|
+
return {key: value for key, value in bucket.items() if not str(key).startswith("_")}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class MetricsStore:
|
|
165
|
+
def __init__(self) -> None:
|
|
166
|
+
self._by_key: dict[str, dict[str, float | int]] = {}
|
|
167
|
+
|
|
168
|
+
def _key(self, ref: DataSourceRef) -> str:
|
|
169
|
+
return "|".join(
|
|
170
|
+
[
|
|
171
|
+
ref.library or "",
|
|
172
|
+
ref.experiment or "",
|
|
173
|
+
ref.scenario or "",
|
|
174
|
+
ref.overlay or "",
|
|
175
|
+
ref.session or ref.run or "",
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _bucket(self, ref: DataSourceRef) -> dict[str, float | int]:
|
|
180
|
+
return self._by_key.setdefault(
|
|
181
|
+
self._key(ref),
|
|
182
|
+
{
|
|
183
|
+
"prompt_tokens": 0,
|
|
184
|
+
"completion_tokens": 0,
|
|
185
|
+
"total_tokens": 0,
|
|
186
|
+
"cached_tokens": 0,
|
|
187
|
+
"cost": 0.0,
|
|
188
|
+
"llm_calls": 0,
|
|
189
|
+
"n_llm_calls": 0,
|
|
190
|
+
"n_tool_calls": 0,
|
|
191
|
+
"duration_s": 0.0,
|
|
192
|
+
"elapsed_s": 0.0,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def apply_native(
|
|
197
|
+
self,
|
|
198
|
+
ref: DataSourceRef,
|
|
199
|
+
payload: dict[str, Any],
|
|
200
|
+
*,
|
|
201
|
+
wall: bool = False,
|
|
202
|
+
) -> dict[str, float | int]:
|
|
203
|
+
current = self._bucket(ref)
|
|
204
|
+
updated = False
|
|
205
|
+
kind = str(payload.get("kind") or "")
|
|
206
|
+
stopped = bool(current.get("_stopped"))
|
|
207
|
+
ts = coerce_timestamp(payload.get("timestamp", payload.get("ts")))
|
|
208
|
+
if not stopped:
|
|
209
|
+
event_span = None
|
|
210
|
+
if ts is not None:
|
|
211
|
+
tmin = current.get("_tmin")
|
|
212
|
+
tmax = current.get("_tmax")
|
|
213
|
+
current["_tmin"] = float(ts) if tmin is None else min(float(tmin), float(ts))
|
|
214
|
+
current["_tmax"] = float(ts) if tmax is None else max(float(tmax), float(ts))
|
|
215
|
+
event_span = round(float(current["_tmax"]) - float(current["_tmin"]), 3)
|
|
216
|
+
if wall:
|
|
217
|
+
now = time.time()
|
|
218
|
+
if current.get("_wall_start") is None:
|
|
219
|
+
current["_wall_start"] = now
|
|
220
|
+
wall_span = round(now - float(current["_wall_start"]), 3)
|
|
221
|
+
current["elapsed_s"] = wall_span
|
|
222
|
+
current["duration_s"] = max(wall_span, event_span or 0.0)
|
|
223
|
+
updated = True
|
|
224
|
+
elif event_span is not None:
|
|
225
|
+
current["duration_s"] = event_span
|
|
226
|
+
updated = True
|
|
227
|
+
if kind in _END_KINDS:
|
|
228
|
+
current["_stopped"] = 1
|
|
229
|
+
updated = True
|
|
230
|
+
if kind == "tool_call_start":
|
|
231
|
+
current["n_tool_calls"] = int(current.get("n_tool_calls") or 0) + 1
|
|
232
|
+
updated = True
|
|
233
|
+
if kind == "llm_call_end":
|
|
234
|
+
usage = extract_usage(payload)
|
|
235
|
+
if usage["total_tokens"] or usage["cost"] or usage["llm_calls"] or usage["cached_tokens"]:
|
|
236
|
+
for field in ("prompt_tokens", "completion_tokens", "total_tokens", "cached_tokens", "llm_calls"):
|
|
237
|
+
current[field] = int(current[field]) + int(usage[field])
|
|
238
|
+
current["n_llm_calls"] = int(current["llm_calls"])
|
|
239
|
+
current["cost"] = float(current["cost"]) + float(usage["cost"])
|
|
240
|
+
updated = True
|
|
241
|
+
out = _public(current)
|
|
242
|
+
if updated:
|
|
243
|
+
out["_updated"] = 1
|
|
244
|
+
return out
|
|
245
|
+
|
|
246
|
+
def apply_step(self, ref: DataSourceRef, payload: dict[str, Any]) -> dict[str, float | int]:
|
|
247
|
+
current = self._bucket(ref)
|
|
248
|
+
wall = current.get("_wall_start") is not None
|
|
249
|
+
keep_if_zero = {
|
|
250
|
+
"prompt_tokens",
|
|
251
|
+
"completion_tokens",
|
|
252
|
+
"total_tokens",
|
|
253
|
+
"cached_tokens",
|
|
254
|
+
"n_llm_calls",
|
|
255
|
+
"llm_calls",
|
|
256
|
+
"n_tool_calls",
|
|
257
|
+
"duration_s",
|
|
258
|
+
"elapsed_s",
|
|
259
|
+
}
|
|
260
|
+
for key, value in flatten_metric_payload(payload).items():
|
|
261
|
+
if str(key).startswith("_"):
|
|
262
|
+
continue
|
|
263
|
+
if wall and key in {"duration_s", "elapsed_s"}:
|
|
264
|
+
continue
|
|
265
|
+
if key in keep_if_zero and value == 0 and current.get(key):
|
|
266
|
+
continue
|
|
267
|
+
current[str(key)] = value
|
|
268
|
+
return _public(current)
|
|
269
|
+
|
|
270
|
+
def snapshot(self, ref: DataSourceRef) -> dict[str, float | int]:
|
|
271
|
+
return _public(self._bucket(ref))
|
|
272
|
+
|
|
273
|
+
def clear(self) -> None:
|
|
274
|
+
self._by_key.clear()
|