cafe-core 0.0.1__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 (52) hide show
  1. cafe/__init__.py +184 -0
  2. cafe/_async.py +36 -0
  3. cafe/_env.py +32 -0
  4. cafe/cli.py +323 -0
  5. cafe/datasets.py +97 -0
  6. cafe/design/__init__.py +19 -0
  7. cafe/design/factorial.py +76 -0
  8. cafe/design/fractional.py +210 -0
  9. cafe/evaluation.py +515 -0
  10. cafe/examples/__init__.py +5 -0
  11. cafe/examples/qa.py +72 -0
  12. cafe/examples/toy.py +86 -0
  13. cafe/execution/__init__.py +18 -0
  14. cafe/execution/checkpoint.py +103 -0
  15. cafe/execution/progress.py +44 -0
  16. cafe/execution/results.py +175 -0
  17. cafe/execution/runner.py +199 -0
  18. cafe/judging/__init__.py +23 -0
  19. cafe/judging/judge.py +144 -0
  20. cafe/judging/prompts.py +164 -0
  21. cafe/judging/ratings.py +134 -0
  22. cafe/judging/rubric.py +113 -0
  23. cafe/judging/runner.py +122 -0
  24. cafe/judging/structured.py +101 -0
  25. cafe/llm.py +168 -0
  26. cafe/py.typed +0 -0
  27. cafe/rubrics.py +104 -0
  28. cafe/stats/__init__.py +48 -0
  29. cafe/stats/_format.py +21 -0
  30. cafe/stats/_frame.py +56 -0
  31. cafe/stats/clmm.R +80 -0
  32. cafe/stats/descriptive.py +106 -0
  33. cafe/stats/glmm.R +84 -0
  34. cafe/stats/inferential.py +362 -0
  35. cafe/stats/logistic.py +365 -0
  36. cafe/stats/ordinal.py +216 -0
  37. cafe/stats/pareto.py +198 -0
  38. cafe/stats/plots.py +296 -0
  39. cafe/stats/reliability.py +421 -0
  40. cafe/stats/stability.py +101 -0
  41. cafe/study.py +291 -0
  42. cafe/system.py +101 -0
  43. cafe/techniques/__init__.py +16 -0
  44. cafe/techniques/composed.py +143 -0
  45. cafe/techniques/context.py +147 -0
  46. cafe/techniques/pipe.py +170 -0
  47. cafe/techniques/registry.py +23 -0
  48. cafe_core-0.0.1.dist-info/METADATA +116 -0
  49. cafe_core-0.0.1.dist-info/RECORD +52 -0
  50. cafe_core-0.0.1.dist-info/WHEEL +4 -0
  51. cafe_core-0.0.1.dist-info/entry_points.txt +2 -0
  52. cafe_core-0.0.1.dist-info/licenses/LICENSE +201 -0
cafe/__init__.py ADDED
@@ -0,0 +1,184 @@
1
+ """CAFE core — a design-of-experiments evaluation engine for compound AI systems.
2
+
3
+ CAFE *measures* compound AI systems; it does not implement them. You provide a
4
+ black box ``run(config, item) -> output`` and declare the factors to vary; CAFE
5
+ generates the design, executes every configuration with replication, and judges
6
+ and attributes the results.
7
+
8
+ Quick start (no API keys needed)::
9
+
10
+ from cafe.examples import build_example_study
11
+ results = build_example_study().run()
12
+ print(results.summary())
13
+
14
+ Or define your own::
15
+
16
+ import cafe
17
+
18
+ async def my_system(config, item):
19
+ return f"answer for {item} using {config['model']}"
20
+
21
+ study = cafe.Study(
22
+ name="my-study",
23
+ system=my_system,
24
+ factors=[cafe.Factor("model", ["a", "b"])],
25
+ dataset=["question 1", "question 2"],
26
+ )
27
+ results = study.run()
28
+ """
29
+
30
+ from cafe import (
31
+ datasets,
32
+ design,
33
+ evaluation,
34
+ execution,
35
+ judging,
36
+ llm,
37
+ rubrics,
38
+ stats,
39
+ study,
40
+ system,
41
+ techniques,
42
+ )
43
+ from cafe.design import (
44
+ FractionalDesign,
45
+ fractional_factorial,
46
+ fractional_factorial_design,
47
+ full_factorial,
48
+ generate,
49
+ single,
50
+ size,
51
+ )
52
+ from cafe.techniques import (
53
+ Pipeline,
54
+ PipelineView,
55
+ pipeline,
56
+ stage_report,
57
+ )
58
+ from cafe.evaluation import Evaluation, Preflight, evaluate, preflight
59
+ from cafe.execution import estimate, run_study
60
+ from cafe.judging import (
61
+ JUDGE_PRESETS,
62
+ Judge,
63
+ JudgeOutput,
64
+ LLMJudge,
65
+ Rating,
66
+ Ratings,
67
+ build_judge_prompt,
68
+ judge_results,
69
+ )
70
+ from cafe.llm import LLMError, complete, set_model_cost
71
+ from cafe.execution.results import Observation, Results, config_id, config_label
72
+ from cafe.judging.rubric import ANSWER_QUALITY_1_5, Level, Rubric, ScaleType
73
+ from cafe.stats import (
74
+ Attribution,
75
+ CLMMResult,
76
+ Effects,
77
+ HumanRatings,
78
+ JudgeStability,
79
+ Logistic,
80
+ ParetoResult,
81
+ Reliability,
82
+ answer_sheet,
83
+ attribute,
84
+ check_r,
85
+ fit_clmm,
86
+ fit_effects,
87
+ fit_logistic,
88
+ human_evaluation,
89
+ human_ratings,
90
+ judge_stability,
91
+ krippendorff_alpha,
92
+ pareto,
93
+ ratings_from_human,
94
+ reliability,
95
+ )
96
+ from cafe.study import Factor, FactorType, Study
97
+ from cafe.system import System, as_system
98
+
99
+ __version__ = "0.0.1"
100
+
101
+ __all__ = [
102
+ # ── data ──
103
+ "datasets",
104
+ "rubrics",
105
+ # ── public submodules (for `cafe.<tab>`, `from cafe import *`, API docs) ──
106
+ "design",
107
+ "evaluation",
108
+ "execution",
109
+ "judging",
110
+ "llm",
111
+ "stats",
112
+ "study",
113
+ "system",
114
+ "techniques",
115
+ # ── define an experiment ──
116
+ "Study",
117
+ "Factor",
118
+ "FactorType",
119
+ "System",
120
+ "as_system",
121
+ # ── composed pipelines (techniques) ──
122
+ "Pipeline",
123
+ "PipelineView",
124
+ "stage_report",
125
+ "pipeline",
126
+ # ── run it ──
127
+ "evaluate", # the complete pipeline: answers -> judge -> attribution
128
+ "preflight", # cheap pre-run check + cost estimate
129
+ "run_study", # lower-level: answers only
130
+ "Evaluation",
131
+ "Preflight",
132
+ "Results",
133
+ "Observation",
134
+ "estimate",
135
+ # ── designs ──
136
+ "generate",
137
+ "size",
138
+ "full_factorial",
139
+ "single",
140
+ "fractional_factorial",
141
+ "fractional_factorial_design",
142
+ "FractionalDesign",
143
+ "config_id",
144
+ "config_label",
145
+ # ── judging ──
146
+ "complete",
147
+ "set_model_cost",
148
+ "LLMError",
149
+ "Rubric",
150
+ "Level",
151
+ "ScaleType",
152
+ "ANSWER_QUALITY_1_5",
153
+ "LLMJudge",
154
+ "Judge",
155
+ "JudgeOutput",
156
+ "JUDGE_PRESETS",
157
+ "judge_results",
158
+ "build_judge_prompt",
159
+ "Rating",
160
+ "Ratings",
161
+ # ── statistics ──
162
+ "attribute",
163
+ "Attribution",
164
+ "fit_effects",
165
+ "Effects",
166
+ "fit_clmm",
167
+ "CLMMResult",
168
+ "check_r",
169
+ "pareto",
170
+ "ParetoResult",
171
+ "judge_stability",
172
+ "JudgeStability",
173
+ "fit_logistic",
174
+ "Logistic",
175
+ "reliability",
176
+ "Reliability",
177
+ "human_ratings",
178
+ "HumanRatings",
179
+ "answer_sheet",
180
+ "krippendorff_alpha",
181
+ "ratings_from_human",
182
+ "human_evaluation",
183
+ "__version__",
184
+ ]
cafe/_async.py ADDED
@@ -0,0 +1,36 @@
1
+ """Run a coroutine to completion from sync code, in a script *or* a notebook.
2
+
3
+ If no event loop is running (a plain script) we just ``asyncio.run``. If one is already
4
+ running (e.g. a Jupyter cell), running another loop on the same thread would raise
5
+ ``asyncio.run() cannot be called from a running event loop`` — so we run it on a short-lived
6
+ worker thread and join. This is what lets ``study.evaluate()`` / ``result.rejudge()`` be
7
+ plain sync calls everywhere.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import threading
14
+ from typing import Any, Callable
15
+
16
+
17
+ def run_blocking(make_coro: Callable[[], Any]) -> Any:
18
+ try:
19
+ asyncio.get_running_loop()
20
+ except RuntimeError:
21
+ return asyncio.run(make_coro())
22
+
23
+ box: dict[str, Any] = {}
24
+
25
+ def _worker() -> None:
26
+ try:
27
+ box["result"] = asyncio.run(make_coro())
28
+ except BaseException as exc: # noqa: BLE001 — re-raised on the caller's thread
29
+ box["error"] = exc
30
+
31
+ thread = threading.Thread(target=_worker, name="cafe-run")
32
+ thread.start()
33
+ thread.join()
34
+ if "error" in box:
35
+ raise box["error"]
36
+ return box["result"]
cafe/_env.py ADDED
@@ -0,0 +1,32 @@
1
+ """Tiny zero-dependency .env loader.
2
+
3
+ Looks for the nearest ``.env`` file (current directory upward) and loads its
4
+ ``KEY=VALUE`` lines into ``os.environ`` without overwriting anything already set.
5
+ Kept dependency-free on purpose — the engine never requires python-dotenv.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+
14
+ def load_env(start: str | None = None) -> str | None:
15
+ """Load the nearest ``.env`` into the environment. Returns the path used, or None.
16
+
17
+ Existing environment variables win (we only ``setdefault``), so real env vars
18
+ and CI secrets always override a local ``.env``.
19
+ """
20
+ base = Path(start or os.getcwd()).resolve()
21
+ for cur in [base, *base.parents]:
22
+ candidate = cur / ".env"
23
+ if candidate.is_file():
24
+ for raw in candidate.read_text(encoding="utf-8").splitlines():
25
+ line = raw.strip()
26
+ if not line or line.startswith("#") or "=" not in line:
27
+ continue
28
+ key, value = line.split("=", 1)
29
+ value = value.strip().strip('"').strip("'")
30
+ os.environ.setdefault(key.strip(), value)
31
+ return str(candidate)
32
+ return None
cafe/cli.py ADDED
@@ -0,0 +1,323 @@
1
+ """cafe — a design-of-experiments evaluation engine for compound AI systems.
2
+
3
+ Commands:
4
+ cafe run example run the bundled toy study
5
+ cafe run path/to/study.py run a study defined in a Python file
6
+ cafe validate [target] expand the design and report its size (no execution)
7
+ cafe doctor check the environment (Python, R stats engine, LLM access)
8
+ cafe version print the version
9
+
10
+ Flags for `run`: --smoke (preflight), --reps, --concurrency,
11
+ --checkpoint PATH (resumable), --out PATH (write results JSONL).
12
+
13
+ The CLI is the headless/CI path; it shares the exact engine the library uses.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import asyncio
20
+ import importlib.util
21
+ import statistics
22
+ import sys
23
+ from collections import defaultdict
24
+ from pathlib import Path
25
+
26
+ from cafe import design, run_study
27
+ from cafe.execution.results import Observation, Results, config_label
28
+ from cafe.study import Study
29
+
30
+
31
+ def _load_study(target: str) -> Study:
32
+ """Resolve a CLI target into a Study.
33
+
34
+ ``example`` -> the bundled toy study. Otherwise ``target`` is a path to a
35
+ .py file exposing a module-level ``study`` or a ``build_study()``/
36
+ ``build_example_study()`` callable.
37
+ """
38
+ if target == "example":
39
+ from cafe.examples import build_example_study
40
+
41
+ return build_example_study()
42
+
43
+ path = Path(target)
44
+ if not path.exists():
45
+ raise SystemExit(f"cafe: no such study target: {target!r}")
46
+
47
+ spec = importlib.util.spec_from_file_location("cafe_user_study", path)
48
+ if spec is None or spec.loader is None:
49
+ raise SystemExit(f"cafe: could not import {target!r}")
50
+ module = importlib.util.module_from_spec(spec)
51
+ spec.loader.exec_module(module)
52
+
53
+ for attr in ("study", "STUDY"):
54
+ obj = getattr(module, attr, None)
55
+ if isinstance(obj, Study):
56
+ return obj
57
+ for attr in ("build_study", "build_example_study"):
58
+ fn = getattr(module, attr, None)
59
+ if callable(fn):
60
+ obj = fn()
61
+ if isinstance(obj, Study):
62
+ return obj
63
+ raise SystemExit(
64
+ f"cafe: {target!r} must define a `study` (Study) or a `build_study()` callable"
65
+ )
66
+
67
+
68
+ def _progress(_obs: Observation, done: int, total: int) -> None:
69
+ bar_w = 24
70
+ filled = int(bar_w * done / total) if total else bar_w
71
+ bar = "█" * filled + "·" * (bar_w - filled)
72
+ sys.stderr.write(f"\r running [{bar}] {done}/{total}")
73
+ sys.stderr.flush()
74
+ if done == total:
75
+ sys.stderr.write("\n")
76
+
77
+
78
+ def _mean_meta(obs: list[Observation], key: str) -> float | None:
79
+ vals = [o.metadata[key] for o in obs if key in o.metadata and o.metadata[key] is not None]
80
+ return round(statistics.fmean(vals), 3) if vals else None
81
+
82
+
83
+ def _print_results_table(results: Results) -> None:
84
+ by_config: dict[str, list[Observation]] = defaultdict(list)
85
+ for o in results.observations:
86
+ by_config[config_label(o.config)].append(o)
87
+
88
+ # Only show sim_q when some observation actually reports it (the toy example does;
89
+ # real Mode-A systems don't, and an always-"-" column is just clutter).
90
+ show_simq = any("sim_quality" in (o.metadata or {}) for o in results.observations)
91
+
92
+ rows: list[tuple[str, ...]] = []
93
+ header = ("configuration", "n", "err", "lat_s", "cost$") + (("sim_q",) if show_simq else ())
94
+ for label in sorted(by_config):
95
+ obs = by_config[label]
96
+ n_err = sum(1 for o in obs if not o.ok)
97
+ lat = _mean_meta(obs, "latency_s")
98
+ cost = _mean_meta(obs, "cost_usd")
99
+ row = (
100
+ label,
101
+ str(len(obs)),
102
+ str(n_err),
103
+ "-" if lat is None else f"{lat:.3f}",
104
+ "-" if cost is None else f"{cost:.4f}",
105
+ )
106
+ if show_simq:
107
+ simq = _mean_meta(obs, "sim_quality")
108
+ row += ("-" if simq is None else f"{simq:.3f}",)
109
+ rows.append(row)
110
+
111
+ widths = [max(len(header[i]), *(len(r[i]) for r in rows)) for i in range(len(header))]
112
+ fmt = " ".join("{:<" + str(w) + "}" for w in widths)
113
+ print()
114
+ print(" " + fmt.format(*header))
115
+ print(" " + fmt.format(*("-" * w for w in widths)))
116
+ for r in rows:
117
+ print(" " + fmt.format(*r))
118
+ print()
119
+
120
+
121
+ def _cmd_run(args: argparse.Namespace) -> int:
122
+ study = _load_study(args.target)
123
+ n_inputs = 1 if args.smoke else max(1, len(study.dataset))
124
+ n_reps = 1 if args.smoke else (args.reps or study.replications)
125
+ n_cells = design.size(study) * n_inputs * n_reps
126
+ mode = "SMOKE (1 input, 1 rep)" if args.smoke else f"{n_reps} reps"
127
+ print(f"study : {study.name}")
128
+ print(f"factors : {', '.join(f'{f.name}{f.levels}' for f in study.factors) or '(none)'}")
129
+ print(f"design : {study.design} -> {design.size(study)} configs")
130
+ print(f"inputs : {len(study.dataset)} mode: {mode}")
131
+ print(f"cells : {n_cells}")
132
+
133
+ results = asyncio.run(
134
+ run_study(
135
+ study,
136
+ replications=args.reps,
137
+ concurrency=args.concurrency,
138
+ checkpoint_path=args.checkpoint,
139
+ smoke=args.smoke,
140
+ on_progress=_progress,
141
+ )
142
+ )
143
+
144
+ s = results.summary()
145
+ _print_results_table(results)
146
+ print(
147
+ f" done: {s['n_answers']} obs over {s['n_configs']} configs, "
148
+ f"{s['n_errors']} errors, {s['total_compute_s']}s compute"
149
+ )
150
+ if args.out:
151
+ results.to_jsonl(args.out)
152
+ print(f" wrote {args.out}")
153
+ return 1 if s["n_errors"] else 0
154
+
155
+
156
+ def _cmd_validate(args: argparse.Namespace) -> int:
157
+ study = _load_study(args.target)
158
+ configs = design.generate(study)
159
+ print(f"ok: study {study.name!r} is valid")
160
+ print(f" design : {study.design} -> {len(configs)} configurations")
161
+ print(f" factors : {len(study.factors)} inputs: {len(study.dataset)}")
162
+ for c in configs[: min(8, len(configs))]:
163
+ print(f" - {config_label(c)}")
164
+ if len(configs) > 8:
165
+ print(f" ... (+{len(configs) - 8} more)")
166
+ return 0
167
+
168
+
169
+ def _cmd_doctor(args: argparse.Namespace) -> int:
170
+ """Check the CAFE environment: Python, the R statistics engine, and LLM access.
171
+
172
+ With ``--app``, also checks the web-app prerequisites (Docker + Docker Compose).
173
+ """
174
+ import os
175
+ import shutil
176
+ import subprocess
177
+
178
+ from cafe import __version__
179
+ from cafe.stats import check_glmer, check_r
180
+
181
+ def mark(ok: bool) -> str:
182
+ return "✓" if ok else "✗"
183
+
184
+ issues = 0
185
+ print("cafe doctor — environment check\n")
186
+
187
+ # --- Runtime -------------------------------------------------------------
188
+ py = sys.version_info
189
+ py_ok = py >= (3, 11)
190
+ if not py_ok:
191
+ issues += 1
192
+ print("Runtime")
193
+ print(f" {mark(py_ok)} Python {py.major}.{py.minor}.{py.micro} (>= 3.11 required)")
194
+ print(f" ✓ cafe {__version__}")
195
+ print()
196
+
197
+ # --- Statistics engine (R) -----------------------------------------------
198
+ print("Statistics engine (required — the mixed-effects models run in R)")
199
+ if shutil.which("Rscript") is None:
200
+ print(" ✗ R not found")
201
+ print(" fix → install R (e.g. `sudo apt install r-base` or `brew install r`),")
202
+ print(" then: Rscript -e 'install.packages(c(\"ordinal\", \"lme4\"))'")
203
+ issues += 1
204
+ else:
205
+ try:
206
+ proc = subprocess.run(["Rscript", "-e", "cat(R.version.string)"],
207
+ capture_output=True, text=True, timeout=30)
208
+ ver = proc.stdout.strip() or "R (version unknown)"
209
+ except Exception: # noqa: BLE001
210
+ ver = "R (version unknown)"
211
+ print(f" ✓ {ver} (Rscript on PATH)")
212
+ for pkg, (ok, _msg), label in (
213
+ ("ordinal", check_r(), "ordinal CLMM"),
214
+ ("lme4", check_glmer(), "binary logistic GLMM"),
215
+ ):
216
+ print(f" {mark(ok)} {pkg:<9}{label}")
217
+ if not ok:
218
+ print(f" fix → Rscript -e 'install.packages(\"{pkg}\")'")
219
+ issues += 1
220
+ print()
221
+
222
+ # --- LLM access ----------------------------------------------------------
223
+ print("LLM access (required for real studies; the bundled example needs none)")
224
+ keys = ("OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY",
225
+ "GEMINI_API_KEY", "OLLAMA_API_KEY")
226
+ any_key = False
227
+ for k in keys:
228
+ present = bool(os.environ.get(k))
229
+ any_key = any_key or present
230
+ print(f" {'✓' if present else '–'} {k}{'' if present else ' not set'}")
231
+ if not any_key:
232
+ print(" set at least one to run studies with a live judge")
233
+ print()
234
+
235
+ # --- Containers (web app) — only with --app ------------------------------
236
+ if getattr(args, "app", False):
237
+ print("Containers (for the self-hostable web app: `docker compose up`)")
238
+ if shutil.which("docker") is None:
239
+ print(" ✗ docker not found — install Docker Engine / Docker Desktop")
240
+ issues += 1
241
+ else:
242
+ try:
243
+ proc = subprocess.run(["docker", "version", "--format", "{{.Server.Version}}"],
244
+ capture_output=True, text=True, timeout=10)
245
+ if proc.returncode == 0 and proc.stdout.strip():
246
+ print(f" ✓ docker {proc.stdout.strip()} (daemon running)")
247
+ else:
248
+ print(" ✗ docker installed but the daemon isn't responding — start Docker")
249
+ issues += 1
250
+ except Exception: # noqa: BLE001
251
+ print(" ✗ docker present but not responding")
252
+ issues += 1
253
+ try:
254
+ proc = subprocess.run(["docker", "compose", "version", "--short"],
255
+ capture_output=True, text=True, timeout=10)
256
+ if proc.returncode == 0 and proc.stdout.strip():
257
+ print(f" ✓ docker compose {proc.stdout.strip()}")
258
+ else:
259
+ print(" ✗ 'docker compose' (v2) not available — update Docker")
260
+ issues += 1
261
+ except Exception: # noqa: BLE001
262
+ print(" ✗ 'docker compose' (v2) not available")
263
+ issues += 1
264
+ print()
265
+
266
+ # --- Summary -------------------------------------------------------------
267
+ if issues == 0:
268
+ print("All set — CAFE's statistics and execution layers are ready.")
269
+ return 0
270
+ word = "issue" if issues == 1 else "issues"
271
+ print(f"{issues} {word} — install the items marked ✗ above.")
272
+ return 1
273
+
274
+
275
+ def _cmd_version(_args: argparse.Namespace) -> int:
276
+ from cafe import __version__
277
+
278
+ print(f"cafe {__version__}")
279
+ return 0
280
+
281
+
282
+ def main(argv: list[str] | None = None) -> int:
283
+ from cafe._env import load_env
284
+ load_env() # load the nearest .env so `doctor` / `run` see the same keys a study does
285
+ parser = argparse.ArgumentParser(
286
+ prog="cafe", description=__doc__,
287
+ formatter_class=argparse.RawDescriptionHelpFormatter, # keep the docstring's layout
288
+ )
289
+ parser.add_argument("--version", action="store_true", help="print version and exit")
290
+ sub = parser.add_subparsers(dest="command")
291
+
292
+ p_run = sub.add_parser("run", help="run a study (or `example`)")
293
+ p_run.add_argument("target", help="'example' or a path to a .py study file")
294
+ p_run.add_argument("--smoke", action="store_true", help="preflight: 1 input, 1 rep, no judging")
295
+ p_run.add_argument("--reps", type=int, default=None, help="replications per (config, input)")
296
+ p_run.add_argument("--concurrency", type=int, default=8, help="max cells in flight")
297
+ p_run.add_argument("--checkpoint", default=None, help="resumable checkpoint JSONL path")
298
+ p_run.add_argument("--out", default=None, help="write results to this JSONL path")
299
+ p_run.set_defaults(func=_cmd_run)
300
+
301
+ p_val = sub.add_parser("validate", help="expand a study's design without running it")
302
+ p_val.add_argument("target", nargs="?", default="example", help="'example' or a .py study file")
303
+ p_val.set_defaults(func=_cmd_validate)
304
+
305
+ p_doc = sub.add_parser("doctor", help="check the environment (Python, R stats engine, LLM access)")
306
+ p_doc.add_argument("--app", action="store_true",
307
+ help="also check the web-app prerequisites (Docker + Docker Compose)")
308
+ p_doc.set_defaults(func=_cmd_doctor)
309
+
310
+ p_ver = sub.add_parser("version", help="print version and exit")
311
+ p_ver.set_defaults(func=_cmd_version)
312
+
313
+ args = parser.parse_args(argv)
314
+ if args.version:
315
+ return _cmd_version(args)
316
+ if not getattr(args, "command", None):
317
+ parser.print_help()
318
+ return 0
319
+ return args.func(args)
320
+
321
+
322
+ if __name__ == "__main__": # pragma: no cover
323
+ raise SystemExit(main())
cafe/datasets.py ADDED
@@ -0,0 +1,97 @@
1
+ """Load public benchmarks as CAFE datasets.
2
+
3
+ Each loader returns a list of items shaped for ``Study(dataset=...)``:
4
+ ``{"id", "text", "reference", ...}`` — so the reference-guided judge has a gold
5
+ answer to score against. Needs the ``datasets`` extra (HuggingFace ``datasets``):
6
+ ``pip install 'cafe-core[datasets]'``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import random
12
+ from typing import Any
13
+
14
+
15
+ def _quiet_hf() -> None:
16
+ """Best-effort: silence HuggingFace's download chatter (the 'unauthenticated requests
17
+ to the HF Hub / set HF_TOKEN' warning, progress logs). Purely cosmetic for showcasing;
18
+ never let it break the actual load."""
19
+ import warnings
20
+
21
+ warnings.filterwarnings("ignore", message=r".*unauthenticated requests to the HF Hub.*")
22
+ for mod in ("huggingface_hub", "datasets"):
23
+ try:
24
+ __import__(mod).logging.set_verbosity_error()
25
+ except Exception: # noqa: BLE001 — cosmetic only
26
+ pass
27
+ try:
28
+ import logging
29
+
30
+ logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
31
+ except Exception: # noqa: BLE001
32
+ pass
33
+
34
+
35
+ def _load_hf(name: str, *args: Any, **kwargs: Any):
36
+ try:
37
+ from datasets import load_dataset
38
+ except ImportError as exc: # pragma: no cover
39
+ raise ImportError(
40
+ "dataset loaders need the 'datasets' extra: pip install 'cafe-core[datasets]'"
41
+ ) from exc
42
+ _quiet_hf()
43
+ return load_dataset(name, *args, **kwargs)
44
+
45
+
46
+ def _sample(rows: list[Any], n: int | None, *, shuffle: bool, seed: int) -> list[Any]:
47
+ rows = list(rows)
48
+ if shuffle:
49
+ random.Random(seed).shuffle(rows)
50
+ return rows if n is None else rows[:n]
51
+
52
+
53
+ def load_truthfulqa(
54
+ n: int | None = 20,
55
+ *,
56
+ split: str = "train",
57
+ categories: list[str] | None = None,
58
+ shuffle: bool = True,
59
+ seed: int = 0,
60
+ ) -> list[dict[str, Any]]:
61
+ """TruthfulQA — adversarial questions that elicit common misconceptions.
62
+
63
+ Reference-based: each item carries the gold ``Best Answer``. Returns items
64
+ ``{id, text, reference, category}``. ``categories`` filters by Category
65
+ (e.g. ``["Misconceptions", "Law"]``).
66
+ """
67
+ ds = _load_hf("domenicrosati/TruthfulQA", split=split)
68
+ rows = list(ds)
69
+ if categories:
70
+ wanted = {c.lower() for c in categories}
71
+ rows = [r for r in rows if str(r.get("Category", "")).lower() in wanted]
72
+ rows = _sample(rows, n, shuffle=shuffle, seed=seed)
73
+ return [
74
+ {
75
+ "id": f"tq{i}",
76
+ "text": r["Question"],
77
+ "reference": r["Best Answer"],
78
+ "category": r.get("Category"),
79
+ }
80
+ for i, r in enumerate(rows)
81
+ ]
82
+
83
+
84
+ def load_gsm8k(
85
+ n: int | None = 20,
86
+ *,
87
+ split: str = "test",
88
+ shuffle: bool = True,
89
+ seed: int = 0,
90
+ ) -> list[dict[str, Any]]:
91
+ """GSM8K — grade-school math word problems with an exact numeric reference."""
92
+ ds = _load_hf("openai/gsm8k", "main", split=split)
93
+ rows = _sample(list(ds), n, shuffle=shuffle, seed=seed)
94
+ return [
95
+ {"id": f"gsm{i}", "text": r["question"], "reference": r["answer"].split("####")[-1].strip()}
96
+ for i, r in enumerate(rows)
97
+ ]
@@ -0,0 +1,19 @@
1
+ """Design of experiments: turn factors into the configurations to run."""
2
+
3
+ from cafe.design.factorial import Config, full_factorial, generate, single, size
4
+ from cafe.design.fractional import (
5
+ FractionalDesign,
6
+ fractional_factorial,
7
+ fractional_factorial_design,
8
+ )
9
+
10
+ __all__ = [
11
+ "Config",
12
+ "generate",
13
+ "size",
14
+ "full_factorial",
15
+ "single",
16
+ "fractional_factorial",
17
+ "fractional_factorial_design",
18
+ "FractionalDesign",
19
+ ]