mas-lab-component-bench 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 (226) hide show
  1. mas/lab/benchmark/README.md +323 -0
  2. mas/lab/benchmark/__init__.py +65 -0
  3. mas/lab/benchmark/analysis.py +199 -0
  4. mas/lab/benchmark/cache/__init__.py +28 -0
  5. mas/lab/benchmark/cache/key_scheme.py +102 -0
  6. mas/lab/benchmark/cache/store.py +198 -0
  7. mas/lab/benchmark/cache/trace_store.py +530 -0
  8. mas/lab/benchmark/cli/__init__.py +41 -0
  9. mas/lab/benchmark/cli/analyze.py +114 -0
  10. mas/lab/benchmark/cli/artifacts.py +156 -0
  11. mas/lab/benchmark/cli/clean.py +171 -0
  12. mas/lab/benchmark/cli/common.py +38 -0
  13. mas/lab/benchmark/cli/declared.py +269 -0
  14. mas/lab/benchmark/cli/export_import.py +1020 -0
  15. mas/lab/benchmark/cli/list.py +109 -0
  16. mas/lab/benchmark/cli/manage.py +175 -0
  17. mas/lab/benchmark/cli/show.py +319 -0
  18. mas/lab/benchmark/cli/steps.py +217 -0
  19. mas/lab/benchmark/cli/tree.py +295 -0
  20. mas/lab/benchmark/codecs/__init__.py +94 -0
  21. mas/lab/benchmark/codecs/base.py +79 -0
  22. mas/lab/benchmark/dataset.py +267 -0
  23. mas/lab/benchmark/dataset_source.py +231 -0
  24. mas/lab/benchmark/engine.py +196 -0
  25. mas/lab/benchmark/execution/__init__.py +16 -0
  26. mas/lab/benchmark/execution/experiment_overlay.py +157 -0
  27. mas/lab/benchmark/execution/overrides.py +50 -0
  28. mas/lab/benchmark/execution/pipeline_attach.py +96 -0
  29. mas/lab/benchmark/execution/plan.py +173 -0
  30. mas/lab/benchmark/experiment.py +648 -0
  31. mas/lab/benchmark/golden/__init__.py +20 -0
  32. mas/lab/benchmark/golden/cache_backup.py +96 -0
  33. mas/lab/benchmark/golden/events.py +149 -0
  34. mas/lab/benchmark/golden/labs.py +132 -0
  35. mas/lab/benchmark/lock.py +224 -0
  36. mas/lab/benchmark/metadata.py +456 -0
  37. mas/lab/benchmark/metadata_integration.py +157 -0
  38. mas/lab/benchmark/migrate.py +207 -0
  39. mas/lab/benchmark/pipeline/__init__.py +101 -0
  40. mas/lab/benchmark/pipeline/adapter.py +52 -0
  41. mas/lab/benchmark/pipeline/artifact.py +312 -0
  42. mas/lab/benchmark/pipeline/artifact_paths.py +567 -0
  43. mas/lab/benchmark/pipeline/cache.py +304 -0
  44. mas/lab/benchmark/pipeline/channel.py +458 -0
  45. mas/lab/benchmark/pipeline/cli.py +264 -0
  46. mas/lab/benchmark/pipeline/config.py +30 -0
  47. mas/lab/benchmark/pipeline/core.py +495 -0
  48. mas/lab/benchmark/pipeline/counters.py +44 -0
  49. mas/lab/benchmark/pipeline/digest.py +39 -0
  50. mas/lab/benchmark/pipeline/executor.py +1044 -0
  51. mas/lab/benchmark/pipeline/expand.py +61 -0
  52. mas/lab/benchmark/pipeline/fanout.py +197 -0
  53. mas/lab/benchmark/pipeline/gather.py +45 -0
  54. mas/lab/benchmark/pipeline/incremental.py +115 -0
  55. mas/lab/benchmark/pipeline/keys.py +51 -0
  56. mas/lab/benchmark/pipeline/kinds.py +57 -0
  57. mas/lab/benchmark/pipeline/lib/__init__.py +7 -0
  58. mas/lab/benchmark/pipeline/lib/data_source.py +201 -0
  59. mas/lab/benchmark/pipeline/lib/plot_lib.py +615 -0
  60. mas/lab/benchmark/pipeline/lib/plot_specs/ablation_heatmap.yaml +51 -0
  61. mas/lab/benchmark/pipeline/lib/plot_specs/shapley_bars.yaml +62 -0
  62. mas/lab/benchmark/pipeline/lib/plot_specs/trajectory_gantt.yaml +68 -0
  63. mas/lab/benchmark/pipeline/lifecycle.py +83 -0
  64. mas/lab/benchmark/pipeline/live_sink.py +20 -0
  65. mas/lab/benchmark/pipeline/models.py +161 -0
  66. mas/lab/benchmark/pipeline/persist.py +60 -0
  67. mas/lab/benchmark/pipeline/resolver.py +198 -0
  68. mas/lab/benchmark/pipeline/resources.py +502 -0
  69. mas/lab/benchmark/pipeline/run_artifacts.py +183 -0
  70. mas/lab/benchmark/pipeline/scheduler.py +345 -0
  71. mas/lab/benchmark/pipeline/schema_validation.py +90 -0
  72. mas/lab/benchmark/pipeline/store.py +122 -0
  73. mas/lab/benchmark/pipeline/types.py +68 -0
  74. mas/lab/benchmark/pipeline/ui_bridge.py +221 -0
  75. mas/lab/benchmark/pipeline/ws.py +179 -0
  76. mas/lab/benchmark/plugins/__init__.py +3 -0
  77. mas/lab/benchmark/plugins/mas.py +62 -0
  78. mas/lab/benchmark/progress.py +256 -0
  79. mas/lab/benchmark/reporting.py +248 -0
  80. mas/lab/benchmark/reproducibility.py +243 -0
  81. mas/lab/benchmark/results.py +346 -0
  82. mas/lab/benchmark/run_manager/__init__.py +17 -0
  83. mas/lab/benchmark/run_manager/discovery.py +58 -0
  84. mas/lab/benchmark/run_manager/display.py +222 -0
  85. mas/lab/benchmark/run_manager/manager.py +507 -0
  86. mas/lab/benchmark/run_manager/models.py +65 -0
  87. mas/lab/benchmark/run_manager/persistence.py +39 -0
  88. mas/lab/benchmark/run_manager/pointer.py +20 -0
  89. mas/lab/benchmark/run_manager/stats.py +84 -0
  90. mas/lab/benchmark/runner.py +186 -0
  91. mas/lab/benchmark/runners/__init__.py +3 -0
  92. mas/lab/benchmark/schedule/__init__.py +13 -0
  93. mas/lab/benchmark/schedule/metadata.py +87 -0
  94. mas/lab/benchmark/schedule/pipeline.py +749 -0
  95. mas/lab/benchmark/schedule/pipeline_resolve.py +137 -0
  96. mas/lab/benchmark/schedule/resume.py +70 -0
  97. mas/lab/benchmark/schedule/run_batch/__init__.py +114 -0
  98. mas/lab/benchmark/schedule/run_batch/execute.py +810 -0
  99. mas/lab/benchmark/schedule/run_batch/finalize.py +183 -0
  100. mas/lab/benchmark/schedule/run_batch/load.py +381 -0
  101. mas/lab/benchmark/schedule/run_batch/pre_hooks.py +123 -0
  102. mas/lab/benchmark/schedule/run_batch/prepare.py +364 -0
  103. mas/lab/benchmark/schedule/run_discovery.py +157 -0
  104. mas/lab/benchmark/schema/__init__.py +37 -0
  105. mas/lab/benchmark/schema/normalized.py +210 -0
  106. mas/lab/benchmark/schema/validation.py +198 -0
  107. mas/lab/benchmark/service_manager.py +658 -0
  108. mas/lab/benchmark/stale_cleanup.py +269 -0
  109. mas/lab/benchmark/state.py +267 -0
  110. mas/lab/benchmark/storage.py +181 -0
  111. mas/lab/benchmark/worker.py +198 -0
  112. mas/lab/deprecations.py +103 -0
  113. mas/lab/flavour/load.py +20 -0
  114. mas/lab/flavour/resolve.py +51 -0
  115. mas/lab/infra/README.md +26 -0
  116. mas/lab/infra/datastore.py +51 -0
  117. mas/lab/infra/stores.py +63 -0
  118. mas/lab/inputs/__init__.py +264 -0
  119. mas/lab/inputs/expectations.py +45 -0
  120. mas/lab/inputs/fixtures.py +55 -0
  121. mas/lab/inputs/legacy.py +75 -0
  122. mas/lab/inputs/refs.py +100 -0
  123. mas/lab/inputs/schemas.py +82 -0
  124. mas/lab/lab/__init__.py +39 -0
  125. mas/lab/lab/config/__init__.py +163 -0
  126. mas/lab/lab/config/artifact_types.py +205 -0
  127. mas/lab/lab/config/execution.py +562 -0
  128. mas/lab/lab/config/experiment_base.py +596 -0
  129. mas/lab/lab/config/lab_config.py +52 -0
  130. mas/lab/lab/config/lab_context.py +179 -0
  131. mas/lab/lab/config/mas_experiment.py +216 -0
  132. mas/lab/lab/config/pipeline.py +360 -0
  133. mas/lab/lab/config/scenario.py +224 -0
  134. mas/lab/lab/config/scenario_loading.py +558 -0
  135. mas/lab/lab/config/ui.py +39 -0
  136. mas/lab/labs.py +693 -0
  137. mas/lab/manifest/README.md +14 -0
  138. mas/lab/manifest/__init__.py +29 -0
  139. mas/lab/manifest/load.py +303 -0
  140. mas/lab/manifests/__init__.py +26 -0
  141. mas/lab/manifests/composed_manifest.py +276 -0
  142. mas/lab/manifests/loader.py +49 -0
  143. mas/lab/manifests/ref_checks.py +271 -0
  144. mas/lab/manifests/schema/README.md +10 -0
  145. mas/lab/manifests/validator.py +320 -0
  146. mas/lab/manifests/versions.py +51 -0
  147. mas/lab/mas_validator.py +612 -0
  148. mas/lab/plots/__init__.py +69 -0
  149. mas/lab/plots/_trajectory_validator.py +318 -0
  150. mas/lab/plots/_viz_adapters.py +122 -0
  151. mas/lab/plots/assets/context_provenance.html +675 -0
  152. mas/lab/plots/assets/d3.min.js +2 -0
  153. mas/lab/plots/assets/live.html +135 -0
  154. mas/lab/plots/assets/mas.js +519 -0
  155. mas/lab/plots/assets/multilevel.html +3053 -0
  156. mas/lab/plots/communication_flow.py +375 -0
  157. mas/lab/plots/communication_flow_processor.yaml +17 -0
  158. mas/lab/plots/context_provenance.py +521 -0
  159. mas/lab/plots/html_demo.py +186 -0
  160. mas/lab/plots/kg_adapter.py +971 -0
  161. mas/lab/plots/kg_widget/__init__.py +208 -0
  162. mas/lab/plots/kg_widget/kg_composer.css +532 -0
  163. mas/lab/plots/kg_widget/kg_composer.js +1097 -0
  164. mas/lab/plots/kg_widget/kg_widget.css +398 -0
  165. mas/lab/plots/kg_widget/kg_widget.js +1423 -0
  166. mas/lab/plots/live_html.py +73 -0
  167. mas/lab/plots/loaders.py +78 -0
  168. mas/lab/plots/mas_plot.py +500 -0
  169. mas/lab/plots/message_graph/__init__.py +60 -0
  170. mas/lab/plots/message_graph/constants.py +50 -0
  171. mas/lab/plots/message_graph/extract.py +93 -0
  172. mas/lab/plots/message_graph/helpers.py +36 -0
  173. mas/lab/plots/message_graph/layout.py +49 -0
  174. mas/lab/plots/message_graph/plot.py +410 -0
  175. mas/lab/plots/message_graph/processor.py +71 -0
  176. mas/lab/plots/message_graph/theme.py +282 -0
  177. mas/lab/plots/multilevel_context_provenance.py +619 -0
  178. mas/lab/plots/multilevel_trajectory/__init__.py +118 -0
  179. mas/lab/plots/multilevel_trajectory/annotations.py +584 -0
  180. mas/lab/plots/multilevel_trajectory/chart_data.py +568 -0
  181. mas/lab/plots/multilevel_trajectory/constants.py +156 -0
  182. mas/lab/plots/multilevel_trajectory/dag.py +2357 -0
  183. mas/lab/plots/multilevel_trajectory/governance.py +263 -0
  184. mas/lab/plots/multilevel_trajectory/layout.py +81 -0
  185. mas/lab/plots/multilevel_trajectory/models.py +225 -0
  186. mas/lab/plots/multilevel_trajectory/plot.py +218 -0
  187. mas/lab/plots/multilevel_trajectory/records.py +991 -0
  188. mas/lab/plots/multilevel_trajectory/svg.py +419 -0
  189. mas/lab/plots/multilevel_trajectory/test_regression_verifier.py +149 -0
  190. mas/lab/plots/multilevel_trajectory/tree.py +910 -0
  191. mas/lab/plots/multilevel_trajectory_processor.py +164 -0
  192. mas/lab/plots/multilevel_trajectory_processor.yaml +17 -0
  193. mas/lab/plots/palette.py +84 -0
  194. mas/lab/plots/pipeline_diagram.py +504 -0
  195. mas/lab/plots/providers.py +138 -0
  196. mas/lab/plots/topology.py +463 -0
  197. mas/lab/plots/trajectory/__init__.py +11 -0
  198. mas/lab/plots/trajectory/extract.py +213 -0
  199. mas/lab/plots/trajectory/highlights.py +54 -0
  200. mas/lab/plots/trajectory/html.py +185 -0
  201. mas/lab/plots/trajectory/loading.py +77 -0
  202. mas/lab/plots/trajectory/mermaid.py +99 -0
  203. mas/lab/plots/trajectory/plot.py +115 -0
  204. mas/lab/plots/trajectory/svg.py +311 -0
  205. mas/lab/processors/__init__.py +63 -0
  206. mas/lab/processors/multilevel_trajectory_plotter.py +105 -0
  207. mas/lab/processors/multilevel_trajectory_plotter.yaml +23 -0
  208. mas/lab/processors/trajectory_annotator.py +86 -0
  209. mas/lab/processors/trajectory_annotator.yaml +16 -0
  210. mas/lab/processors/trajectory_loader.py +86 -0
  211. mas/lab/processors/trajectory_loader.yaml +16 -0
  212. mas/lab/processors/trajectory_plotter.py +129 -0
  213. mas/lab/processors/trajectory_plotter.yaml +25 -0
  214. mas/lab/processors/trajectory_plotter_native.py +116 -0
  215. mas/lab/processors/trajectory_plotter_native.yaml +22 -0
  216. mas/lab/verification/__init__.py +39 -0
  217. mas/lab/verification/contracts/__init__.py +33 -0
  218. mas/lab/verification/contracts/executor.py +423 -0
  219. mas/lab/verification/contracts/generator.py +216 -0
  220. mas/lab/verification/contracts/loader.py +133 -0
  221. mas/lab/verification/contracts/reporter.py +202 -0
  222. mas/lab/workspace.py +77 -0
  223. mas_lab_component_bench-0.2.0.dist-info/METADATA +19 -0
  224. mas_lab_component_bench-0.2.0.dist-info/RECORD +226 -0
  225. mas_lab_component_bench-0.2.0.dist-info/WHEEL +4 -0
  226. mas_lab_component_bench-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,323 @@
1
+ <!--
2
+ Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
3
+ SPDX-License-Identifier: Apache-2.0
4
+ -->
5
+ # Benchmark Framework - Reusable Components
6
+
7
+ **Composable primitives for agent evaluation.**
8
+
9
+ ## Philosophy
10
+
11
+ This framework follows these principles:
12
+
13
+ 1. **Declarative over imperative** - Configure via YAML/JSON, not code
14
+ 2. **Incremental over monolithic** - Add runs without rerunning everything
15
+ 3. **Composable over all-in-one** - Mix and match components
16
+ 4. **Human-readable over optimized** - Folder names you can understand
17
+ 5. **DataFrame-native over custom formats** - Leverage pandas/polars ecosystem
18
+
19
+ ## Components
20
+
21
+ ### 1. Dataset (`dataset.py`)
22
+
23
+ Loads and manipulates evaluation datasets.
24
+
25
+ ```python
26
+ from mas.lab.benchmark import Dataset
27
+
28
+ # Load from JSON
29
+ dataset = Dataset.from_json("math_problems.json")
30
+
31
+ # Filter by category
32
+ arithmetic = dataset.filter(category="arithmetic")
33
+
34
+ # Iterate
35
+ for item in dataset:
36
+ print(item.id, item.prompt)
37
+ ```
38
+
39
+ **JSON Format:**
40
+ ```json
41
+ {
42
+ "name": "dataset_name",
43
+ "version": "v1",
44
+ "items": [
45
+ {
46
+ "id": "001",
47
+ "prompt": "Question text",
48
+ "category": "category_name",
49
+ "expected_answer": "Answer",
50
+ "metadata_key": "metadata_value"
51
+ }
52
+ ]
53
+ }
54
+ ```
55
+
56
+ ### 2. ResultStorage (`storage.py`)
57
+
58
+ Manages human-readable folder structure.
59
+
60
+ ```python
61
+ from mas.lab.benchmark import ResultStorage, RunMetadata
62
+
63
+ storage = ResultStorage("./results")
64
+
65
+ # Generate run ID
66
+ run_id = storage.generate_run_id("cot", run_number=1)
67
+ # → "cot_run_001_20260218_143022"
68
+
69
+ # Save run
70
+ metadata = RunMetadata(
71
+ dataset_name="math",
72
+ item_id="001",
73
+ run_id=run_id,
74
+ pattern="cot",
75
+ timestamp="2026-02-18T14:30:22",
76
+ config={"model": "gpt-4"},
77
+ success=True,
78
+ latency_ms=1234.5
79
+ )
80
+
81
+ storage.save_run(
82
+ dataset_name="math",
83
+ item_id="001",
84
+ run_id=run_id,
85
+ metadata=metadata,
86
+ events_file=Path("events.jsonl"),
87
+ artifacts={"log": Path("agent.log")}
88
+ )
89
+
90
+ # List all runs
91
+ runs = storage.list_runs("math")
92
+ # → [("math", "001", "cot_run_001_20260218_143022"), ...]
93
+ ```
94
+
95
+ **Folder Structure:**
96
+ ```
97
+ results/
98
+ {dataset_name}/
99
+ {item_id}/
100
+ {run_id}/
101
+ metadata.json
102
+ events.jsonl
103
+ artifacts/
104
+ consolidated.parquet
105
+ ```
106
+
107
+ ### 3. MultiRunOrchestrator (`runner.py`)
108
+
109
+ Coordinates N runs per item across patterns.
110
+
111
+ ```python
112
+ from mas.lab.benchmark import MultiRunOrchestrator
113
+
114
+ orchestrator = MultiRunOrchestrator(
115
+ storage=storage,
116
+ n_runs=3,
117
+ pause_between_runs=1.0
118
+ )
119
+
120
+ # Define agent factory
121
+ async def agent_factory(pattern_name, pattern_config):
122
+ # Your agent creation logic
123
+ return agent
124
+
125
+ # Run benchmark
126
+ summary = await orchestrator.run_dataset(
127
+ dataset=dataset,
128
+ patterns={
129
+ "cot": {"manifest": "cot.yaml"},
130
+ "react": {"manifest": "react.yaml"}
131
+ },
132
+ agent_factory=agent_factory
133
+ )
134
+ ```
135
+
136
+ ### 4. ResultAnalyzer (`analysis.py`)
137
+
138
+ Consolidates results into DataFrame.
139
+
140
+ ```python
141
+ from mas.lab.benchmark import ResultAnalyzer
142
+
143
+ analyzer = ResultAnalyzer(storage)
144
+
145
+ # Consolidate all runs
146
+ df = analyzer.consolidate_results(
147
+ dataset_name="math",
148
+ include_events=True, # Parse event logs
149
+ cache=True # Save as Parquet
150
+ )
151
+
152
+ # Compute statistics
153
+ stats = analyzer.compute_statistics(df)
154
+ # pattern latency_ms_mean success_rate mean_tokens
155
+ # 0 cot 1234.5 0.95 567
156
+ # 1 react 890.2 0.92 423
157
+
158
+ # Compare patterns
159
+ comparison = analyzer.compare_patterns(df, ["cot", "react"], "latency_ms")
160
+ ```
161
+
162
+ **DataFrame Schema:**
163
+ ```python
164
+ df.columns
165
+ # ['dataset_name', 'item_id', 'run_id', 'pattern',
166
+ # 'timestamp', 'success', 'latency_ms', 'error',
167
+ # 'total_tokens', 'prompt_tokens', 'completion_tokens',
168
+ # 'tool_calls', 'llm_calls',
169
+ # 'config_*']
170
+ ```
171
+
172
+ ### 5. Pipeline plots (`plotnine` / `plot` steps)
173
+
174
+ Paper figures use **`type: plotnine`** in `experiment.yaml` experiment-level `post:`.
175
+ Ad-hoc charts from step data use **`type: plot`** with **`config.spec`** (plot library).
176
+
177
+ See [pipeline-steps.md](../../../../../../docs/pipeline-steps.md) and `labs/*/experiment.yaml` for examples.
178
+
179
+ ## Complete Example
180
+
181
+ ```python
182
+ import asyncio
183
+ from pathlib import Path
184
+ from mas.lab.benchmark import (
185
+ Dataset, ResultStorage, MultiRunOrchestrator,
186
+ ResultAnalyzer,
187
+ )
188
+
189
+ async def my_agent_factory(pattern_name, pattern_config):
190
+ # Your agent creation logic
191
+ pass
192
+
193
+ async def main():
194
+ # 1. Load dataset
195
+ dataset = Dataset.from_yaml("dataset.yaml")
196
+
197
+ # 2. Setup storage
198
+ storage = ResultStorage("./results")
199
+
200
+ # 3. Run benchmark
201
+ orchestrator = MultiRunOrchestrator(storage, n_runs=3)
202
+ await orchestrator.run_dataset(
203
+ dataset,
204
+ patterns={"cot": {}, "react": {}},
205
+ agent_factory=my_agent_factory
206
+ )
207
+
208
+ # 4. Analyze
209
+ analyzer = ResultAnalyzer(storage)
210
+ df = analyzer.consolidate_results(dataset.name)
211
+ stats = analyzer.compute_statistics(df)
212
+ print(stats)
213
+ # Plots: declare plotnine / plot steps in experiment.yaml post:
214
+
215
+ asyncio.run(main())
216
+ ```
217
+
218
+ ## Incremental Updates
219
+
220
+ ### Add Items
221
+ ```python
222
+ # Load existing dataset
223
+ dataset = Dataset.from_json("dataset.json")
224
+
225
+ # Add new items
226
+ dataset.items.append(DatasetItem(id="006", prompt="New question"))
227
+
228
+ # Save
229
+ dataset.to_json("dataset.json")
230
+
231
+ # Run benchmark (only new items executed)
232
+ await orchestrator.run_dataset(dataset, patterns, agent_factory)
233
+ ```
234
+
235
+ ### Add Runs
236
+ ```python
237
+ # Increase n_runs
238
+ orchestrator = MultiRunOrchestrator(storage, n_runs=5) # Was 3
239
+
240
+ # Run again - adds run_004, run_005 to existing items
241
+ await orchestrator.run_dataset(dataset, patterns, agent_factory)
242
+ ```
243
+
244
+ ### Add Patterns
245
+ ```python
246
+ # Add new pattern
247
+ patterns = {
248
+ "cot": {...},
249
+ "react": {...},
250
+ "planner": {...} # NEW
251
+ }
252
+
253
+ # Run again - only planner executed on all items
254
+ await orchestrator.run_dataset(dataset, patterns, agent_factory)
255
+ ```
256
+
257
+ ### Regenerate Plots
258
+ ```python
259
+ # Re-run pipeline post steps only (content-addressed trace cache skips LLM calls)
260
+ # mas-lab benchmark run experiment.yaml --force-post
261
+ ```
262
+
263
+ ## Advanced Usage
264
+
265
+ ### Custom Metrics
266
+
267
+ Extend `ResultAnalyzer._extract_event_metrics()`:
268
+
269
+ ```python
270
+ def _extract_event_metrics(self, events_file: Path) -> Dict[str, Any]:
271
+ metrics = super()._extract_event_metrics(events_file)
272
+
273
+ # Add custom metrics
274
+ metrics["my_custom_metric"] = ...
275
+
276
+ return metrics
277
+ ```
278
+
279
+ ### Custom Plots
280
+
281
+ Add a `plotnine` step in experiment-level `post:` or a plot-library YAML under
282
+ `pipeline/steps/plot_library/`. See `labs/design-space.lab/01-design-patterns/experiment.yaml`.
283
+
284
+ ### Parallel Execution
285
+
286
+ ```python
287
+ # Run patterns in parallel
288
+ import asyncio
289
+
290
+ async def run_pattern(pattern_name, pattern_config):
291
+ await orchestrator.run_dataset(
292
+ dataset,
293
+ patterns={pattern_name: pattern_config},
294
+ agent_factory=agent_factory
295
+ )
296
+
297
+ await asyncio.gather(
298
+ run_pattern("cot", {...}),
299
+ run_pattern("react", {...}),
300
+ )
301
+ ```
302
+
303
+ ## Dependencies
304
+
305
+ ```bash
306
+ pip install pandas pyarrow plotnine pyyaml
307
+ ```
308
+
309
+ ## Testing
310
+
311
+ ```bash
312
+ cd mas-lab
313
+ pytest tests/benchmark/
314
+ ```
315
+
316
+ ## Contributing
317
+
318
+ When adding new components:
319
+
320
+ 1. Follow existing patterns (DataClass for data, Class for behavior)
321
+ 2. Add docstrings with examples
322
+ 3. Write tests in `tests/benchmark/`
323
+ 4. Update this documentation
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Reusable benchmark infrastructure for MAS evaluations.
5
+
6
+ Provides composable primitives for:
7
+ - Dataset management
8
+ - Multi-run orchestration
9
+ - Incremental results storage
10
+ - DataFrame consolidation
11
+ - Declarative evaluation configuration
12
+ """
13
+
14
+ from mas.lab.benchmark.dataset import Dataset, DatasetItem
15
+ from mas.lab.benchmark.runner import MultiRunOrchestrator
16
+ from mas.lab.benchmark.storage import ResultStorage, RunMetadata
17
+ from mas.lab.benchmark.metadata import (
18
+ BenchmarkMetadata,
19
+ BenchmarkState,
20
+ BenchmarkStatus,
21
+ ScenarioResult,
22
+ ScenarioState,
23
+ )
24
+ from mas.lab.benchmark.state import EnhancedBenchmarkState
25
+ from mas.lab.benchmark.lock import BenchmarkLock, LockInfo
26
+
27
+ __all__ = [
28
+ "Dataset",
29
+ "DatasetItem",
30
+ "MultiRunOrchestrator",
31
+ "ResultStorage",
32
+ "RunMetadata",
33
+ "ResultAnalyzer",
34
+ "BenchmarkMetadata",
35
+ "BenchmarkState",
36
+ "BenchmarkStatus",
37
+ "ScenarioResult",
38
+ "ScenarioState",
39
+ "EnhancedBenchmarkState",
40
+ "BenchmarkLock",
41
+ "LockInfo",
42
+ "ExperimentResults",
43
+ "ScenarioView",
44
+ "ItemView",
45
+ "RunView",
46
+ ]
47
+
48
+
49
+ def __getattr__(name: str):
50
+ # Defer pandas imports so coverage tracing of pipeline subpackages does not
51
+ # reload numpy during test collection (pytest-cov / coverage.py on Py 3.11+).
52
+ if name == "ResultAnalyzer":
53
+ from mas.lab.benchmark.analysis import ResultAnalyzer
54
+
55
+ return ResultAnalyzer
56
+ if name in ("ExperimentResults", "ScenarioView", "ItemView", "RunView"):
57
+ from mas.lab.benchmark.results import (
58
+ ExperimentResults,
59
+ ItemView,
60
+ RunView,
61
+ ScenarioView,
62
+ )
63
+
64
+ return locals()[name]
65
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,199 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Result analysis and DataFrame consolidation.
5
+
6
+ Loads all run metadata and creates consolidated DataFrames for analysis.
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Dict, Any, List, Optional
12
+ import pandas as pd
13
+
14
+ from mas.lab.benchmark.storage import ResultStorage
15
+
16
+
17
+ class ResultAnalyzer:
18
+ """
19
+ Analyze benchmark results.
20
+
21
+ Features:
22
+ - Consolidate all runs into DataFrame
23
+ - Compute statistics per pattern
24
+ - Extract metrics from event logs
25
+ - Cache results as Parquet
26
+ """
27
+
28
+ def __init__(self, storage: ResultStorage):
29
+ self.storage = storage
30
+
31
+ def consolidate_results(
32
+ self,
33
+ dataset_name: str,
34
+ include_events: bool = False,
35
+ cache: bool = True
36
+ ) -> pd.DataFrame:
37
+ """
38
+ Consolidate all runs into DataFrame.
39
+
40
+ Args:
41
+ dataset_name: Dataset name
42
+ include_events: Parse event logs for detailed metrics
43
+ cache: Save as Parquet for incremental updates
44
+
45
+ Returns:
46
+ DataFrame with columns:
47
+ - dataset_name, item_id, run_id, pattern
48
+ - timestamp, success, latency_ms, error
49
+ - (if include_events) total_tokens, tool_calls, etc.
50
+ """
51
+ runs = self.storage.list_runs(dataset_name)
52
+
53
+ if not runs:
54
+ return pd.DataFrame()
55
+
56
+ rows = []
57
+ for ds_name, item_id, run_id in runs:
58
+ metadata = self.storage.load_run_metadata(ds_name, item_id, run_id)
59
+ if not metadata:
60
+ continue
61
+
62
+ row = {
63
+ "dataset_name": metadata.dataset_name,
64
+ "item_id": metadata.item_id,
65
+ "run_id": metadata.run_id,
66
+ "pattern": metadata.pattern,
67
+ "timestamp": metadata.timestamp,
68
+ "success": metadata.success,
69
+ "latency_ms": metadata.latency_ms,
70
+ "error": metadata.error,
71
+ }
72
+
73
+ # Add config fields
74
+ for key, value in metadata.config.items():
75
+ row[f"config_{key}"] = value
76
+
77
+ # Parse events if requested
78
+ if include_events:
79
+ run_dir = self.storage.get_run_dir(ds_name, item_id, run_id)
80
+ events_file = run_dir / "events.jsonl"
81
+ if events_file.exists():
82
+ event_metrics = self._extract_event_metrics(events_file)
83
+ row.update(event_metrics)
84
+
85
+ rows.append(row)
86
+
87
+ df = pd.DataFrame(rows)
88
+
89
+ # Cache as Parquet
90
+ if cache and not df.empty:
91
+ cache_path = self.storage.get_consolidated_path(dataset_name)
92
+ df.to_parquet(cache_path, index=False)
93
+
94
+ return df
95
+
96
+ def load_cached(self, dataset_name: str) -> Optional[pd.DataFrame]:
97
+ """Load cached consolidated DataFrame."""
98
+ cache_path = self.storage.get_consolidated_path(dataset_name)
99
+ if not cache_path.exists():
100
+ return None
101
+ return pd.read_parquet(cache_path)
102
+
103
+ def compute_statistics(self, df: pd.DataFrame) -> pd.DataFrame:
104
+ """
105
+ Compute aggregate statistics per pattern.
106
+
107
+ Returns:
108
+ DataFrame with pattern-level statistics:
109
+ - mean_latency_ms, std_latency_ms
110
+ - success_rate
111
+ - mean_tokens, mean_tool_calls (if available)
112
+ """
113
+ if df.empty:
114
+ return pd.DataFrame()
115
+
116
+ # Basic statistics
117
+ stats = df.groupby("pattern").agg({
118
+ "latency_ms": ["mean", "std", "min", "max"],
119
+ "success": "mean",
120
+ })
121
+
122
+ # Flatten column names
123
+ stats.columns = ["_".join(col).strip("_") for col in stats.columns]
124
+ stats = stats.rename(columns={"success_mean": "success_rate"})
125
+
126
+ # Token and tool metrics if available
127
+ if "total_tokens" in df.columns:
128
+ token_stats = df.groupby("pattern")["total_tokens"].agg(["mean", "std"])
129
+ token_stats.columns = ["mean_tokens", "std_tokens"]
130
+ stats = stats.join(token_stats)
131
+
132
+ if "tool_calls" in df.columns:
133
+ tool_stats = df.groupby("pattern")["tool_calls"].agg(["mean", "std"])
134
+ tool_stats.columns = ["mean_tool_calls", "std_tool_calls"]
135
+ stats = stats.join(tool_stats)
136
+
137
+ return stats.reset_index()
138
+
139
+ def _extract_event_metrics(self, events_file: Path) -> Dict[str, Any]:
140
+ """Extract metrics from event log."""
141
+ metrics = {
142
+ "total_tokens": 0,
143
+ "prompt_tokens": 0,
144
+ "completion_tokens": 0,
145
+ "tool_calls": 0,
146
+ "llm_calls": 0,
147
+ }
148
+
149
+ with open(events_file) as f:
150
+ for line in f:
151
+ if not line.strip():
152
+ continue
153
+ try:
154
+ event = json.loads(line)
155
+
156
+ # Count LLM calls
157
+ if event.get("kind") == "llm_response":
158
+ metrics["llm_calls"] += 1
159
+ usage = event.get("usage", {})
160
+ metrics["total_tokens"] += usage.get("total_tokens", 0)
161
+ metrics["prompt_tokens"] += usage.get("prompt_tokens", 0)
162
+ metrics["completion_tokens"] += usage.get("completion_tokens", 0)
163
+
164
+ # Count tool calls
165
+ elif event.get("kind") == "tool_start":
166
+ metrics["tool_calls"] += 1
167
+
168
+ except json.JSONDecodeError:
169
+ continue
170
+
171
+ return metrics
172
+
173
+ def get_item_runs(self, df: pd.DataFrame, item_id: str) -> pd.DataFrame:
174
+ """Get all runs for specific item."""
175
+ return df[df["item_id"] == item_id].copy()
176
+
177
+ def get_pattern_runs(self, df: pd.DataFrame, pattern: str) -> pd.DataFrame:
178
+ """Get all runs for specific pattern."""
179
+ return df[df["pattern"] == pattern].copy()
180
+
181
+ def compare_patterns(
182
+ self,
183
+ df: pd.DataFrame,
184
+ patterns: List[str],
185
+ metric: str = "latency_ms"
186
+ ) -> pd.DataFrame:
187
+ """
188
+ Compare patterns on specific metric.
189
+
190
+ Returns:
191
+ DataFrame with item_id, pattern, metric columns
192
+ """
193
+ filtered = df[df["pattern"].isin(patterns)]
194
+ return filtered.pivot_table(
195
+ index="item_id",
196
+ columns="pattern",
197
+ values=metric,
198
+ aggfunc="mean"
199
+ ).reset_index()
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Content-addressed trace cache helpers."""
4
+ from mas.lab.benchmark.cache.trace_store import (
5
+ compute_run_hash,
6
+ resolve_run_events_path,
7
+ trace_cache_roots,
8
+ extract_flavour_info,
9
+ get_trace_cache_dir,
10
+ link_trace_to_cache_entry,
11
+ materialize_config,
12
+ write_cache_inputs,
13
+ write_run_info,
14
+ write_run_result,
15
+ )
16
+
17
+ __all__ = [
18
+ "compute_run_hash",
19
+ "extract_flavour_info",
20
+ "get_trace_cache_dir",
21
+ "link_trace_to_cache_entry",
22
+ "materialize_config",
23
+ "resolve_run_events_path",
24
+ "trace_cache_roots",
25
+ "write_cache_inputs",
26
+ "write_run_info",
27
+ "write_run_result",
28
+ ]