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