mas-lab-library-lab 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 (66) hide show
  1. mas/library/lab/__init__.py +23 -0
  2. mas/library/lab/artifact_types.py +73 -0
  3. mas/library/lab/codecs/__init__.py +0 -0
  4. mas/library/lab/codecs/otel_codecs.py +456 -0
  5. mas/library/lab/eval/__init__.py +7 -0
  6. mas/library/lab/eval/mcev1.py +35 -0
  7. mas/library/lab/library.yaml +284 -0
  8. mas/library/lab/plots.py +408 -0
  9. mas/library/lab/plugins/design_patterns/__init__.py +16 -0
  10. mas/library/lab/plugins/design_patterns/staged_debate.py +63 -0
  11. mas/library/lab/plugins/design_patterns/supervised.py +61 -0
  12. mas/library/lab/plugins/design_patterns/verifier.py +94 -0
  13. mas/library/lab/plugins/design_patterns/voting.py +29 -0
  14. mas/library/lab/steps/README.md +36 -0
  15. mas/library/lab/steps/__init__.py +99 -0
  16. mas/library/lab/steps/_io.py +100 -0
  17. mas/library/lab/steps/data/analysis.py +130 -0
  18. mas/library/lab/steps/data/collect_dataframe.py +203 -0
  19. mas/library/lab/steps/data/dataset.py +67 -0
  20. mas/library/lab/steps/data/deserialize.py +138 -0
  21. mas/library/lab/steps/data/diff_trajectories.py +119 -0
  22. mas/library/lab/steps/data/embed_trajectories.py +221 -0
  23. mas/library/lab/steps/data/experiment.py +114 -0
  24. mas/library/lab/steps/data/gather_level.py +120 -0
  25. mas/library/lab/steps/data/generate_dataset.py +297 -0
  26. mas/library/lab/steps/data/join_dataframe.py +145 -0
  27. mas/library/lab/steps/data/processor.py +195 -0
  28. mas/library/lab/steps/data/serialize.py +326 -0
  29. mas/library/lab/steps/data/to_dataframe.py +196 -0
  30. mas/library/lab/steps/eval/annotate_metrics.py +208 -0
  31. mas/library/lab/steps/eval/collect_metrics.py +467 -0
  32. mas/library/lab/steps/eval/compute_ci.py +487 -0
  33. mas/library/lab/steps/eval/compute_drift.py +168 -0
  34. mas/library/lab/steps/eval/mce.py +502 -0
  35. mas/library/lab/steps/eval/metrics_files.py +86 -0
  36. mas/library/lab/steps/eval/metrics_to_dataframe.py +109 -0
  37. mas/library/lab/steps/eval/validate_outputs.py +53 -0
  38. mas/library/lab/steps/extract/mealy_stats.py +387 -0
  39. mas/library/lab/steps/extract/sys_stats.py +234 -0
  40. mas/library/lab/steps/extract/trace_stats.py +445 -0
  41. mas/library/lab/steps/extract/trajectories.py +309 -0
  42. mas/library/lab/steps/services/events_to_otel.py +261 -0
  43. mas/library/lab/steps/services/export_otel.py +196 -0
  44. mas/library/lab/steps/services/file_tail.py +253 -0
  45. mas/library/lab/steps/services/metric_event.py +67 -0
  46. mas/library/lab/steps/services/plot_stream.py +131 -0
  47. mas/library/lab/steps/services/service_start.py +168 -0
  48. mas/library/lab/steps/services/service_stop.py +77 -0
  49. mas/library/lab/steps/services/shell.py +56 -0
  50. mas/library/lab/steps/viz/ci_plot.py +381 -0
  51. mas/library/lab/steps/viz/metrics_comparison_plot.py +161 -0
  52. mas/library/lab/steps/viz/pipeline_diagram.py +564 -0
  53. mas/library/lab/steps/viz/plot.py +114 -0
  54. mas/library/lab/steps/viz/plot_communication_flow.py +86 -0
  55. mas/library/lab/steps/viz/plot_ggplot.py +189 -0
  56. mas/library/lab/steps/viz/plot_message_graph.py +98 -0
  57. mas/library/lab/steps/viz/plot_multilevel_trajectory.py +211 -0
  58. mas/library/lab/steps/viz/plot_trajectory.py +94 -0
  59. mas/library/lab/steps/viz/plotnine.py +527 -0
  60. mas/library/lab/ui/__init__.py +3 -0
  61. mas/library/lab/ui/assets/series.js +404 -0
  62. mas/library/lab/ui/series.py +134 -0
  63. mas_lab_library_lab-0.2.0.dist-info/METADATA +27 -0
  64. mas_lab_library_lab-0.2.0.dist-info/RECORD +66 -0
  65. mas_lab_library_lab-0.2.0.dist-info/WHEEL +4 -0
  66. mas_lab_library_lab-0.2.0.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,23 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """mas.library.lab package."""
4
+
5
+ from pathlib import Path
6
+
7
+ from mas._dist import installed_version
8
+
9
+
10
+ def package_root() -> Path:
11
+ """Return the directory containing ``library.yaml`` for this manifest library."""
12
+ here = Path(__file__).resolve().parent
13
+ for parent in [here, *here.parents]:
14
+ if (parent / "library.yaml").is_file():
15
+ return parent
16
+ return here
17
+
18
+
19
+ __version__ = installed_version(file=__file__)
20
+
21
+ from mas.library.lab.plots import execution_chain_graph # noqa: E402
22
+
23
+ __all__ = ["execution_chain_graph"]
@@ -0,0 +1,73 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Artifact kinds declared by the lab library.
4
+
5
+ Each class is a library.yaml ``type: artifact`` plugin
6
+ (``type`` / ``name`` / ``module`` / ``class``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from mas.lab.lab.config.artifact_types import ArtifactType
12
+
13
+
14
+ class Trace(ArtifactType):
15
+ path = "{run_dir}/traces/events.jsonl"
16
+ format = "jsonl"
17
+ stream = "native"
18
+ realtime = True
19
+ description = "Raw observability event stream from a single run."
20
+
21
+
22
+ class RunInfo(ArtifactType):
23
+ path = "{run_dir}/run_info.json"
24
+ format = "json"
25
+ description = "Run metadata (hash, model, timing, status)."
26
+
27
+
28
+ class Plot(ArtifactType):
29
+ path = "{level_dir}/plot.png"
30
+ format = "png"
31
+ description = "Visualization output (PNG, SVG, HTML, or PDF)."
32
+
33
+
34
+ class Metrics(ArtifactType):
35
+ path = "{level_dir}/metrics.json"
36
+ format = "json"
37
+ schema = "artefacts/metrics.schema.json"
38
+ stream = "metric"
39
+ realtime = True
40
+ description = "Quality metrics computed by evaluation providers."
41
+
42
+
43
+ class ChatSnapshot(ArtifactType):
44
+ path = "{run_dir}/custom.chat.jsonl"
45
+ format = "jsonl"
46
+ stream = "custom.chat"
47
+ plot = "chat"
48
+ realtime = True
49
+ description = "Folded chat cards for the chat HTML plot."
50
+
51
+
52
+ class TopologySnapshot(ArtifactType):
53
+ path = "{run_dir}/custom.topology.jsonl"
54
+ format = "jsonl"
55
+ stream = "custom.topology"
56
+ plot = "topology"
57
+ realtime = True
58
+ description = "Folded graph for the topology HTML plot."
59
+
60
+
61
+ class LoopSeries(ArtifactType):
62
+ path = "{run_dir}/plots/series.svg"
63
+ format = "svg"
64
+ stream = "series"
65
+ plot = "loop"
66
+ realtime = True
67
+ description = "Generic timeseries (html live; svg/png end-state)."
68
+
69
+
70
+ class DataFrame(ArtifactType):
71
+ path = "{level_dir}/data.csv"
72
+ format = "csv"
73
+ description = "Tidy CSV dataframe for analysis and plotting."
File without changes
@@ -0,0 +1,456 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+ """OTel codecs — encoder/decoder pairs for ``otel_traces`` and ``events`` artifact kinds.
5
+
6
+ Registered codecs
7
+ -----------------
8
+ * ``("otel_traces", "otlp")`` — Push spans to an OTLP HTTP endpoint (encode only).
9
+ * ``("otel_traces", "clickhouse")``— Query spans from ClickHouse (decode only).
10
+ * ``("otel_traces", "filesystem")``— Write/read spans as JSONL on disk (both).
11
+ * ``("events", "filesystem")``— Write/read raw events.jsonl on disk (both).
12
+
13
+ These codecs are registered declaratively via ``library-lab/library.yaml``'s
14
+ ``types:``/``plugins:`` block through the runtime plugin
15
+ registry (see :mod:`mas.runtime.registry.bootstrap` and
16
+ ``runtime/docs/plugin-registry-manifests.md``) — the same generic
17
+ manifest/fixpoint mechanism used for every other plugin category. There is
18
+ no bench-local codec registry; :func:`mas.lab.benchmark.codecs.get_codec`
19
+ is a thin lookup over ``mas.runtime.registry.get_registry()``. The classes
20
+ below are plain :class:`Codec` subclasses with no registration side effect
21
+ of their own — do not add a decorator-based registration call here, it
22
+ would just duplicate the manifest entry above.
23
+
24
+ Usage in a pipeline YAML::
25
+
26
+ # Push traces to OTLP collector
27
+ - type: serialize
28
+ name: push_otel
29
+ depends_on: [export_otel]
30
+ config:
31
+ artifact_kind: otel_traces
32
+ source_step: export_otel
33
+ source_key: spans
34
+ store:
35
+ type: otlp
36
+ uri: http://localhost:4318
37
+
38
+ # Read traces back from ClickHouse
39
+ - type: deserialize
40
+ name: load_traces
41
+ config:
42
+ artifact_kind: otel_traces
43
+ store:
44
+ type: clickhouse
45
+ host: localhost
46
+ port: 8123
47
+ user: admin
48
+ password_env: CLICKHOUSE_PASSWORD
49
+ database: default
50
+ opts:
51
+ session_id: "{session_id}"
52
+ """
53
+
54
+
55
+ import json
56
+ import logging
57
+ import os
58
+ import urllib.error
59
+ import urllib.parse
60
+ import urllib.request
61
+ import base64
62
+ from pathlib import Path
63
+ from typing import Any
64
+
65
+ from mas.lab.benchmark.codecs.base import Codec
66
+ from mas.lab.infra.datastore import DatastoreSpec
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # ClickHouse row → otel_sdk_spans format
73
+ # (canonical ClickHouse row → otel_sdk_spans conversion)
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def _ns_to_isoformat(ns: int) -> str:
77
+ """Convert Unix nanoseconds to ISO-8601 string (microsecond precision)."""
78
+ import datetime
79
+ us = ns // 1000
80
+ dt = (
81
+ datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
82
+ + datetime.timedelta(microseconds=us)
83
+ )
84
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
85
+
86
+
87
+ def _ch_status_to_sdk(code: str) -> str:
88
+ """Map ClickHouse OTel StatusCode string to SDK status_code string."""
89
+ code = code.upper()
90
+ if "OK" in code:
91
+ return "OK"
92
+ if "ERROR" in code:
93
+ return "ERROR"
94
+ return "UNSET"
95
+
96
+
97
+ def ch_row_to_sdk_span(row: dict[str, Any]) -> dict[str, Any]:
98
+ """Convert one ClickHouse otel_traces row to otel_sdk_spans.jsonl format.
99
+
100
+ Public so downstream code (normalize_otel etc.) can import it from here.
101
+ """
102
+ start_ns = int(row["StartNs"])
103
+ duration = int(row.get("Duration", 0))
104
+ end_ns = start_ns + duration
105
+
106
+ trace_id = "0x" + row["TraceId"].lower()
107
+ span_id = "0x" + row["SpanId"].lower()
108
+ parent_id = (
109
+ "0x" + row["ParentSpanId"].lower()
110
+ if row.get("ParentSpanId")
111
+ else None
112
+ )
113
+
114
+ attrs = dict(row.get("SpanAttributes") or {})
115
+ resource_attrs = dict(row.get("ResourceAttributes") or {})
116
+ resource_attrs.setdefault("service.name", row.get("ServiceName", ""))
117
+
118
+ return {
119
+ "name": row["SpanName"],
120
+ "context": {
121
+ "trace_id": trace_id,
122
+ "span_id": span_id,
123
+ "trace_state": "[]",
124
+ },
125
+ "kind": "SpanKind.INTERNAL",
126
+ "parent_id": parent_id,
127
+ "start_time": _ns_to_isoformat(start_ns),
128
+ "end_time": _ns_to_isoformat(end_ns),
129
+ "status": {
130
+ "status_code": _ch_status_to_sdk(row.get("StatusCode", "")),
131
+ },
132
+ "attributes": attrs,
133
+ "events": [],
134
+ "links": [],
135
+ "resource": {
136
+ "attributes": resource_attrs,
137
+ "schema_url": "",
138
+ },
139
+ }
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # ClickHouse connection helpers
144
+ # ---------------------------------------------------------------------------
145
+
146
+ def _resolve_ch_conn(store: DatastoreSpec) -> dict[str, Any]:
147
+ """Resolve ClickHouse connection from DatastoreSpec + connections.yaml fallback.
148
+
149
+ Resolution order:
150
+ 1. Fields set on *store* (host, port, user, database)
151
+ 2. ``store.password_env`` → env var value
152
+ 3. ``resolve_clickhouse_conn()`` for any missing fields (connections.yaml → env → defaults)
153
+ """
154
+ from mas.lab.connections import resolve_clickhouse_conn
155
+
156
+ password = ""
157
+ if store.password_env:
158
+ password = os.environ.get(store.password_env, "")
159
+
160
+ conn = resolve_clickhouse_conn(
161
+ host=store.host or None,
162
+ port=store.port or None,
163
+ user=store.user or None,
164
+ database=store.database or None,
165
+ )
166
+ if password:
167
+ conn["password"] = password
168
+ # URI override: parse host/port from store.uri if set
169
+ if store.uri:
170
+ conn["uri"] = store.uri
171
+ return conn
172
+
173
+
174
+ def _clickhouse_url(conn: dict[str, Any]) -> str:
175
+ """Build the ClickHouse HTTP base URL from a resolved connection dict."""
176
+ if conn.get("uri"):
177
+ return conn["uri"].rstrip("/")
178
+ return f"http://{conn['host']}:{conn['port']}"
179
+
180
+
181
+ def _clickhouse_query(
182
+ store: DatastoreSpec,
183
+ query: str,
184
+ timeout: int = 60,
185
+ ) -> list[dict[str, Any]]:
186
+ """Execute a ClickHouse query and return raw rows as dicts (FORMAT JSONEachRow)."""
187
+ conn = _resolve_ch_conn(store)
188
+ base_url = _clickhouse_url(conn)
189
+ database = conn.get("database", "default")
190
+
191
+ params = urllib.parse.urlencode({"database": database, "query": query})
192
+ url = f"{base_url}/?{params}"
193
+ req = urllib.request.Request(url)
194
+ user = conn.get("user", "")
195
+ password = conn.get("password", "")
196
+ if user or password:
197
+ creds = base64.b64encode(f"{user}:{password}".encode()).decode()
198
+ req.add_header("Authorization", f"Basic {creds}")
199
+
200
+ try:
201
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
202
+ body = resp.read().decode()
203
+ except urllib.error.HTTPError as exc:
204
+ body = exc.read().decode(errors="replace")
205
+ if exc.code == 403:
206
+ raise RuntimeError(
207
+ f"ClickHouse authentication failed at {base_url} (user={user!r}). "
208
+ f"Check store.password_env or set the env var."
209
+ ) from exc
210
+ raise RuntimeError(f"ClickHouse HTTP {exc.code}: {body}") from exc
211
+ except urllib.error.URLError as exc:
212
+ raise RuntimeError(
213
+ f"Could not connect to ClickHouse at {base_url}: {exc}"
214
+ ) from exc
215
+
216
+ return [json.loads(line) for line in body.splitlines() if line.strip()]
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # ("otel_traces", "otlp") — encode only
221
+ # ---------------------------------------------------------------------------
222
+
223
+ class OtlpOtelCodec(Codec):
224
+ """Push OTel spans to an OTLP HTTP endpoint (encode only).
225
+
226
+ The ``store`` spec must have ``type: otlp`` and ``uri`` (or ``host``/``port``)
227
+ pointing to the OTLP collector HTTP port (typically 4318).
228
+
229
+ ``artifact`` must be either:
230
+ - a list of event dicts (``events.jsonl`` format) — converted to OTLP spans
231
+ - a ``Path`` to an ``events.jsonl`` file — read and converted
232
+
233
+ Extra ``opts``::
234
+
235
+ service_name str OTel service.name resource attribute (default: "mas-runtime")
236
+ batch_size int Spans per HTTP request (default: 200)
237
+ dry_run bool Convert but do not push (default: False)
238
+ """
239
+
240
+ artifact_kind: str = "otel_traces"
241
+ store_type: str = "otlp"
242
+
243
+ def encode(self, artifact: Any, **opts: Any) -> None:
244
+ from mas.lab.telemetry.otlp_push import push_file, load_events
245
+
246
+ service_name: str = opts.get("service_name", "mas-runtime")
247
+ batch_size: int = int(opts.get("batch_size", 200))
248
+ dry_run: bool = bool(opts.get("dry_run", False))
249
+
250
+ # Determine OTLP endpoint
251
+ if self.store.uri:
252
+ endpoint = self.store.uri.rstrip("/")
253
+ else:
254
+ host = self.store.host or "localhost"
255
+ port = self.store.port or 4318
256
+ endpoint = f"http://{host}:{port}"
257
+
258
+ if isinstance(artifact, (str, Path)):
259
+ events = load_events(artifact)
260
+ elif isinstance(artifact, list):
261
+ events = artifact
262
+ else:
263
+ raise TypeError(
264
+ f"OtlpOtelCodec.encode: expected list[dict] or Path, got {type(artifact)!r}"
265
+ )
266
+
267
+ if dry_run:
268
+ logger.info("[dry-run] Would push %d events to %s", len(events), endpoint)
269
+ return
270
+
271
+ logger.info("OtlpOtelCodec: pushing %d events → %s", len(events), endpoint)
272
+ push_file(
273
+ events=events,
274
+ endpoint=endpoint,
275
+ service_name=service_name,
276
+ batch_size=batch_size,
277
+ )
278
+
279
+ def decode(self, **_: Any) -> Any:
280
+ raise NotImplementedError(
281
+ "OtlpOtelCodec supports encode only. "
282
+ "Use ClickHouseOtelCodec (store_type='clickhouse') to read OTel spans."
283
+ )
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # ("otel_traces", "clickhouse") — decode only
288
+ # ---------------------------------------------------------------------------
289
+
290
+ class ClickHouseOtelCodec(Codec):
291
+ """Query OTel spans from ClickHouse (decode only).
292
+
293
+ The ``store`` spec must have ``type: clickhouse`` with host/port/user/
294
+ password_env/database.
295
+
296
+ Extra ``opts`` for :meth:`decode`::
297
+
298
+ session_id str Filter by ``SpanAttributes['mas.session.id']``
299
+ run_id str Filter by ``SpanAttributes['mas.run.id']``
300
+ trace_id str Filter by ``TraceId`` column (hex, with/without 0x)
301
+ by str 'session' | 'run' | 'trace' — alternate selector
302
+ app_name str Filter by ``ServiceName``
303
+ table str ClickHouse table name (default: "otel_traces")
304
+ """
305
+
306
+ artifact_kind: str = "otel_traces"
307
+ store_type: str = "clickhouse"
308
+
309
+ def encode(self, artifact: Any, **_: Any) -> None:
310
+ raise NotImplementedError(
311
+ "ClickHouseOtelCodec supports decode only. "
312
+ "Use OtlpOtelCodec (store_type='otlp') to push OTel spans."
313
+ )
314
+
315
+ def decode(self, **opts: Any) -> list[dict[str, Any]]:
316
+ session_id: str | None = opts.get("session_id")
317
+ run_id: str | None = opts.get("run_id")
318
+ trace_id: str | None = opts.get("trace_id")
319
+ by: str = opts.get("by", "session")
320
+ app_name: str | None = opts.get("app_name")
321
+ table: str = opts.get("table", "otel_traces")
322
+
323
+ # Normalise: --by trace or trace_id kwarg
324
+ if by == "trace" and not trace_id:
325
+ trace_id = session_id or run_id
326
+ session_id = run_id = None
327
+
328
+ if not session_id and not run_id and not trace_id:
329
+ raise ValueError(
330
+ "ClickHouseOtelCodec.decode: provide session_id, run_id, or trace_id in opts."
331
+ )
332
+
333
+ database = self.store.database or "default"
334
+ db_table = f"{database}.{table}"
335
+ app_filter = f" AND ServiceName = '{app_name}'" if app_name else ""
336
+
337
+ if trace_id:
338
+ hex_id = trace_id.lower().replace("0x", "").zfill(32)
339
+ safe_hex = hex_id.replace("'", "\\'")
340
+ where_clause = f"TraceId = '{safe_hex}'{app_filter}"
341
+ log_key = f"trace_id={trace_id}"
342
+ else:
343
+ effective_id = session_id or run_id
344
+ attr_key = "mas.session.id" if session_id else "mas.run.id"
345
+ safe_id = (effective_id or "").replace("'", "\\'")
346
+ where_clause = f"SpanAttributes['{attr_key}'] = '{safe_id}'{app_filter}"
347
+ log_key = f"session_id={session_id} run_id={run_id}"
348
+
349
+ query = (
350
+ f"SELECT "
351
+ f"toUnixTimestamp64Nano(Timestamp) AS StartNs, "
352
+ f"Duration, TraceId, SpanId, ParentSpanId, SpanName, SpanKind, "
353
+ f"ServiceName, ResourceAttributes, SpanAttributes, StatusCode "
354
+ f"FROM {db_table} "
355
+ f"WHERE {where_clause} "
356
+ f"ORDER BY StartNs "
357
+ f"FORMAT JSONEachRow"
358
+ )
359
+
360
+ logger.info(
361
+ "ClickHouseOtelCodec.decode: %s table=%s",
362
+ log_key, db_table,
363
+ )
364
+ raw_rows = _clickhouse_query(self.store, query)
365
+ return [ch_row_to_sdk_span(row) for row in raw_rows]
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # ("otel_traces", "filesystem") — encode + decode
370
+ # ---------------------------------------------------------------------------
371
+
372
+ class FilesystemOtelCodec(Codec):
373
+ """Write/read OTel spans as JSONL on the local filesystem.
374
+
375
+ The ``store`` spec must have ``type: filesystem`` and ``path`` pointing to
376
+ a directory. Individual files are named via ``opts.filename``.
377
+
378
+ Extra ``opts``::
379
+
380
+ filename str File name within store.path (default: "otel_spans.jsonl")
381
+ """
382
+
383
+ artifact_kind: str = "otel_traces"
384
+ store_type: str = "filesystem"
385
+
386
+ def encode(self, artifact: Any, **opts: Any) -> None:
387
+ filename: str = opts.get("filename", "otel_spans.jsonl")
388
+ base = Path(self.store.path)
389
+ base.mkdir(parents=True, exist_ok=True)
390
+ out = base / filename
391
+
392
+ if isinstance(artifact, (str, Path)):
393
+ import shutil
394
+ shutil.copy2(artifact, out)
395
+ logger.info("FilesystemOtelCodec: copied %s → %s", artifact, out)
396
+ elif isinstance(artifact, list):
397
+ with open(out, "w") as fh:
398
+ for item in artifact:
399
+ fh.write(json.dumps(item) + "\n")
400
+ logger.info("FilesystemOtelCodec: wrote %d spans → %s", len(artifact), out)
401
+ else:
402
+ raise TypeError(
403
+ f"FilesystemOtelCodec.encode: expected list[dict] or Path, got {type(artifact)!r}"
404
+ )
405
+
406
+ def decode(self, **opts: Any) -> list[dict[str, Any]]:
407
+ filename: str = opts.get("filename", "otel_spans.jsonl")
408
+ path = Path(self.store.path) / filename
409
+ if not path.exists():
410
+ raise FileNotFoundError(f"FilesystemOtelCodec: file not found: {path}")
411
+ with open(path) as fh:
412
+ return [json.loads(line) for line in fh if line.strip()]
413
+
414
+
415
+ # ---------------------------------------------------------------------------
416
+ # ("events", "filesystem") — encode + decode
417
+ # ---------------------------------------------------------------------------
418
+
419
+ class FilesystemEventsCodec(Codec):
420
+ """Write/read raw events (``events.jsonl`` format) on the local filesystem.
421
+
422
+ Extra ``opts``::
423
+
424
+ filename str File name within store.path (default: "events.jsonl")
425
+ """
426
+
427
+ artifact_kind: str = "events"
428
+ store_type: str = "filesystem"
429
+
430
+ def encode(self, artifact: Any, **opts: Any) -> None:
431
+ filename: str = opts.get("filename", "events.jsonl")
432
+ base = Path(self.store.path)
433
+ base.mkdir(parents=True, exist_ok=True)
434
+ out = base / filename
435
+
436
+ if isinstance(artifact, (str, Path)):
437
+ import shutil
438
+ shutil.copy2(artifact, out)
439
+ logger.info("FilesystemEventsCodec: copied %s → %s", artifact, out)
440
+ elif isinstance(artifact, list):
441
+ with open(out, "w") as fh:
442
+ for item in artifact:
443
+ fh.write(json.dumps(item) + "\n")
444
+ logger.info("FilesystemEventsCodec: wrote %d events → %s", len(artifact), out)
445
+ else:
446
+ raise TypeError(
447
+ f"FilesystemEventsCodec.encode: expected list[dict] or Path, got {type(artifact)!r}"
448
+ )
449
+
450
+ def decode(self, **opts: Any) -> list[dict[str, Any]]:
451
+ filename: str = opts.get("filename", "events.jsonl")
452
+ path = Path(self.store.path) / filename
453
+ if not path.exists():
454
+ raise FileNotFoundError(f"FilesystemEventsCodec: file not found: {path}")
455
+ with open(path) as fh:
456
+ return [json.loads(line) for line in fh if line.strip()]
@@ -0,0 +1,7 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Eval providers shipped by the lab library."""
4
+
5
+ from .mcev1 import MCEv1Provider
6
+
7
+ __all__ = ["MCEv1Provider"]
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Public MCE v1 provider plugin entry point."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+
11
+ class MCEv1Provider:
12
+ """Delegates to mas.library.eval MCE v1 provider."""
13
+
14
+ name = "mcev1"
15
+
16
+ def __init__(self) -> None:
17
+ from mas.library.eval.evaluator import get_provider
18
+
19
+ self._delegate = get_provider("mce_v1")
20
+
21
+ def compute(
22
+ self,
23
+ kg_path: Path,
24
+ metric_names: List[str],
25
+ *,
26
+ response_agent_id: Optional[str] = None,
27
+ ) -> Dict[str, dict]:
28
+ return self._delegate.compute(
29
+ kg_path=kg_path,
30
+ metric_names=metric_names,
31
+ response_agent_id=response_agent_id,
32
+ )
33
+
34
+ def available_metrics(self) -> List[str]:
35
+ return self._delegate.available_metrics()