metrik-bench 0.1.0__tar.gz
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.
- metrik_bench-0.1.0/.gitignore +89 -0
- metrik_bench-0.1.0/PKG-INFO +52 -0
- metrik_bench-0.1.0/README.md +36 -0
- metrik_bench-0.1.0/pyproject.toml +35 -0
- metrik_bench-0.1.0/src/metrik/bench/__init__.py +29 -0
- metrik_bench-0.1.0/src/metrik/bench/py.typed +0 -0
- metrik_bench-0.1.0/src/metrik/bench/runtime.py +169 -0
- metrik_bench-0.1.0/src/metrik/bench/tasks.py +72 -0
- metrik_bench-0.1.0/tests/test_runtime.py +140 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Metrik — .gitignore
|
|
2
|
+
#
|
|
3
|
+
# NOTE: the previous version of this file contained `docs/*`, `.gitignore`, and `.claude`,
|
|
4
|
+
# which excluded the entire documentation tree — i.e. every Phase 0/1 deliverable — from
|
|
5
|
+
# version control. Replaced deliberately. docs/ is the project's primary output right now.
|
|
6
|
+
|
|
7
|
+
# --- Build output that must never be committed ---
|
|
8
|
+
# ADR 016: diagram sources live in docs/diagrams/*.mmd; rendered images are build output.
|
|
9
|
+
docs/static/diagrams/
|
|
10
|
+
# ADR 015: generated reference docs are committed on purpose (drift detection), so they
|
|
11
|
+
# are NOT ignored here — see docs/reference/.
|
|
12
|
+
|
|
13
|
+
# --- Python ---
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
*.so
|
|
17
|
+
.Python
|
|
18
|
+
build/
|
|
19
|
+
dist/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.eggs/
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
env/
|
|
25
|
+
|
|
26
|
+
# Tooling caches
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
.hypothesis/
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
htmlcov/
|
|
34
|
+
coverage.xml
|
|
35
|
+
.benchmarks/
|
|
36
|
+
.tox/
|
|
37
|
+
.nox/
|
|
38
|
+
|
|
39
|
+
# uv (ADR 013) — uv.lock IS committed; the cache is not.
|
|
40
|
+
.uv/
|
|
41
|
+
|
|
42
|
+
# --- Node / web (ADR 009) ---
|
|
43
|
+
node_modules/
|
|
44
|
+
.next/
|
|
45
|
+
out/
|
|
46
|
+
.turbo/
|
|
47
|
+
*.tsbuildinfo
|
|
48
|
+
.pnpm-store/
|
|
49
|
+
|
|
50
|
+
# --- Tauri (ADR 010) ---
|
|
51
|
+
apps/desktop/src-tauri/target/
|
|
52
|
+
apps/desktop/src-tauri/gen/
|
|
53
|
+
|
|
54
|
+
# --- Docs site (ADR 015) ---
|
|
55
|
+
docs/.docusaurus/
|
|
56
|
+
docs/build/
|
|
57
|
+
|
|
58
|
+
# --- Metrik runtime data ---
|
|
59
|
+
# The artifact store and blob store (ADR 006). Content-addressed, machine-generated,
|
|
60
|
+
# gigabytes. Never in git.
|
|
61
|
+
.metrik/
|
|
62
|
+
*.metrik-store/
|
|
63
|
+
# Local config may hold API keys (ADR 018).
|
|
64
|
+
metrik.local.toml
|
|
65
|
+
.env
|
|
66
|
+
.env.*
|
|
67
|
+
!.env.example
|
|
68
|
+
|
|
69
|
+
# Model weights and checkpoints — always too large, often license-encumbered (plan.md §5.7).
|
|
70
|
+
*.safetensors
|
|
71
|
+
*.gguf
|
|
72
|
+
*.onnx
|
|
73
|
+
*.pt
|
|
74
|
+
*.pth
|
|
75
|
+
*.bin
|
|
76
|
+
# ...except the tiny-random fixtures, which are committed on purpose (ADR 017).
|
|
77
|
+
!tests/fixtures/**
|
|
78
|
+
|
|
79
|
+
# --- Editors and OS ---
|
|
80
|
+
.vscode/
|
|
81
|
+
!.vscode/extensions.json
|
|
82
|
+
.idea/
|
|
83
|
+
*.swp
|
|
84
|
+
.DS_Store
|
|
85
|
+
Thumbs.db
|
|
86
|
+
desktop.ini
|
|
87
|
+
|
|
88
|
+
# --- Local agent/tooling settings ---
|
|
89
|
+
.claude/settings.local.json
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metrik-bench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Metrik Benchmark Lab — sandboxed evaluation, or a refusal. Never an unsandboxed run.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Asmodeus14/Metrik
|
|
6
|
+
Project-URL: Repository, https://github.com/Asmodeus14/Metrik
|
|
7
|
+
Project-URL: Issues, https://github.com/Asmodeus14/Metrik/issues
|
|
8
|
+
Author: The Metrik Authors
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: metrik-sdk==0.1.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# metrik-bench
|
|
18
|
+
|
|
19
|
+
Sandboxed evaluation — or a refusal. Never an unsandboxed run.
|
|
20
|
+
|
|
21
|
+
```console
|
|
22
|
+
$ metrik bench --check # what isolation is available here, and what is not
|
|
23
|
+
$ metrik bench <model> --task humaneval
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Why this package is unusual
|
|
27
|
+
|
|
28
|
+
HumanEval and MBPP ask a model to write code and then **run it**. A benchmark runner that
|
|
29
|
+
`exec()`s model output on your machine is a remote-code-execution primitive with extra steps.
|
|
30
|
+
|
|
31
|
+
[ADR 019](../../docs/planning/adr/019-benchmark-sandbox.md) answers that by refusing rather
|
|
32
|
+
than degrading, and records the clause as **not revisitable**. There is no in-process fallback,
|
|
33
|
+
no `--i-know-what-im-doing` flag, and no configuration that produces one.
|
|
34
|
+
|
|
35
|
+
That is enforced in two places, deliberately:
|
|
36
|
+
|
|
37
|
+
- **The type.** `BenchResult` cannot be constructed for a code-executing task without a sandbox
|
|
38
|
+
attestation, and `isolation: "none"` fails validation. A runner that finds no container
|
|
39
|
+
runtime can emit nothing at all.
|
|
40
|
+
- **The runner.** It checks for a runtime *before* generating anything, so the refusal arrives
|
|
41
|
+
in a second rather than after an hour of generation.
|
|
42
|
+
|
|
43
|
+
## What a refusal looks like
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
error: humaneval executes model-generated code and no container runtime is available
|
|
47
|
+
-> Install Docker Desktop and start it, then re-run. `metrik bench --check` shows
|
|
48
|
+
what was probed. Tasks that do not execute code (mmlu) run without a container.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Note the last sentence: MMLU is multiple choice and needs no container. Requiring one for
|
|
52
|
+
everything would push people to disable the check they actually need.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# metrik-bench
|
|
2
|
+
|
|
3
|
+
Sandboxed evaluation — or a refusal. Never an unsandboxed run.
|
|
4
|
+
|
|
5
|
+
```console
|
|
6
|
+
$ metrik bench --check # what isolation is available here, and what is not
|
|
7
|
+
$ metrik bench <model> --task humaneval
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Why this package is unusual
|
|
11
|
+
|
|
12
|
+
HumanEval and MBPP ask a model to write code and then **run it**. A benchmark runner that
|
|
13
|
+
`exec()`s model output on your machine is a remote-code-execution primitive with extra steps.
|
|
14
|
+
|
|
15
|
+
[ADR 019](../../docs/planning/adr/019-benchmark-sandbox.md) answers that by refusing rather
|
|
16
|
+
than degrading, and records the clause as **not revisitable**. There is no in-process fallback,
|
|
17
|
+
no `--i-know-what-im-doing` flag, and no configuration that produces one.
|
|
18
|
+
|
|
19
|
+
That is enforced in two places, deliberately:
|
|
20
|
+
|
|
21
|
+
- **The type.** `BenchResult` cannot be constructed for a code-executing task without a sandbox
|
|
22
|
+
attestation, and `isolation: "none"` fails validation. A runner that finds no container
|
|
23
|
+
runtime can emit nothing at all.
|
|
24
|
+
- **The runner.** It checks for a runtime *before* generating anything, so the refusal arrives
|
|
25
|
+
in a second rather than after an hour of generation.
|
|
26
|
+
|
|
27
|
+
## What a refusal looks like
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
error: humaneval executes model-generated code and no container runtime is available
|
|
31
|
+
-> Install Docker Desktop and start it, then re-run. `metrik bench --check` shows
|
|
32
|
+
what was probed. Tasks that do not execute code (mmlu) run without a container.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Note the last sentence: MMLU is multiple choice and needs no container. Requiring one for
|
|
36
|
+
everything would push people to disable the check they actually need.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "metrik-bench"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Metrik Benchmark Lab — sandboxed evaluation, or a refusal. Never an unsandboxed run."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "The Metrik Authors" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# No harness dependency yet. `lm-eval-harness` arrives pinned (ADR 022) with the first task
|
|
20
|
+
# that needs it; this package currently owns runtime detection and the refusal path, which
|
|
21
|
+
# deliberately need nothing but the standard library.
|
|
22
|
+
dependencies = [
|
|
23
|
+
"metrik-sdk==0.1.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/Asmodeus14/Metrik"
|
|
28
|
+
Repository = "https://github.com/Asmodeus14/Metrik"
|
|
29
|
+
Issues = "https://github.com/Asmodeus14/Metrik/issues"
|
|
30
|
+
|
|
31
|
+
[tool.uv.sources]
|
|
32
|
+
metrik-sdk = { workspace = true }
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/metrik"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Metrik Benchmark Lab: sandboxed evaluation, or a refusal.
|
|
2
|
+
|
|
3
|
+
The runner is not implemented yet, and the order is deliberate -- the gate ships before the
|
|
4
|
+
thing it gates (ADR 019). What exists is runtime detection, the task catalogue, and the
|
|
5
|
+
refusal path, so that a machine which cannot contain model-generated code establishes that
|
|
6
|
+
before anything is generated rather than after.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from metrik.bench.runtime import (
|
|
12
|
+
CANDIDATES,
|
|
13
|
+
Availability,
|
|
14
|
+
RuntimeInfo,
|
|
15
|
+
detect_runtimes,
|
|
16
|
+
require_runtime,
|
|
17
|
+
)
|
|
18
|
+
from metrik.bench.tasks import TASKS, TaskSpec, resolve_task
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CANDIDATES",
|
|
22
|
+
"TASKS",
|
|
23
|
+
"Availability",
|
|
24
|
+
"RuntimeInfo",
|
|
25
|
+
"TaskSpec",
|
|
26
|
+
"detect_runtimes",
|
|
27
|
+
"require_runtime",
|
|
28
|
+
"resolve_task",
|
|
29
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Finding a container runtime, or establishing that there is not one.
|
|
2
|
+
|
|
3
|
+
``artifacts.md`` §6.12, ADR 019. The one job here is to answer *"can this machine contain
|
|
4
|
+
model-generated code?"* honestly, before anything is generated.
|
|
5
|
+
|
|
6
|
+
Two distinctions this makes that a naive check does not.
|
|
7
|
+
|
|
8
|
+
**Installed is not running.** ``docker --version`` answers from the CLI binary alone and
|
|
9
|
+
succeeds happily while Docker Desktop is stopped. The question that matters is whether a
|
|
10
|
+
container can actually be started, which only ``docker info`` answers -- it talks to the
|
|
11
|
+
daemon. Treating the binary as proof is how a runner concludes it is safe and then discovers
|
|
12
|
+
otherwise at the first sample.
|
|
13
|
+
|
|
14
|
+
**Absent is not broken.** "No runtime installed" and "runtime installed but the daemon is
|
|
15
|
+
down" need different instructions, so they are different results rather than one boolean.
|
|
16
|
+
|
|
17
|
+
Probing is a capability check, never a hardcoded binary name: ``SandboxAttestation.runtime``
|
|
18
|
+
records whichever was used, so adding Podman later is an entry in :data:`CANDIDATES` rather
|
|
19
|
+
than a change to anything that consumes the result.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from enum import StrEnum
|
|
28
|
+
from typing import Final
|
|
29
|
+
|
|
30
|
+
from metrik.sdk import UnsupportedError
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CANDIDATES",
|
|
34
|
+
"PROBE_TIMEOUT_S",
|
|
35
|
+
"Availability",
|
|
36
|
+
"RuntimeInfo",
|
|
37
|
+
"detect_runtimes",
|
|
38
|
+
"require_runtime",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
#: Runtimes probed, in preference order. Docker first per ``plan.md`` §13 decision 4 -- it is
|
|
42
|
+
#: what most contributors already have. Podman is probed anyway because it costs one
|
|
43
|
+
#: subprocess and a machine that has it is a machine that can run benchmarks.
|
|
44
|
+
CANDIDATES: Final[tuple[str, ...]] = ("docker", "podman")
|
|
45
|
+
|
|
46
|
+
#: A stopped Docker Desktop can leave `docker info` hanging on a named pipe. A benchmark run
|
|
47
|
+
#: waiting indefinitely to find out whether it may start is worse than a fast refusal.
|
|
48
|
+
PROBE_TIMEOUT_S: Final = 10.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Availability(StrEnum):
|
|
52
|
+
"""Why a runtime can or cannot be used."""
|
|
53
|
+
|
|
54
|
+
READY = "ready"
|
|
55
|
+
#: Binary present, daemon unreachable. The common Docker Desktop case, and the one where
|
|
56
|
+
#: telling someone to "install Docker" wastes their time.
|
|
57
|
+
NOT_RUNNING = "not_running"
|
|
58
|
+
NOT_INSTALLED = "not_installed"
|
|
59
|
+
#: Present and responding, but the probe itself failed -- a permissions problem, usually.
|
|
60
|
+
UNUSABLE = "unusable"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class RuntimeInfo:
|
|
65
|
+
"""What one candidate runtime looks like on this machine."""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
availability: Availability
|
|
69
|
+
version: str | None = None
|
|
70
|
+
detail: str = ""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def ready(self) -> bool:
|
|
74
|
+
return self.availability is Availability.READY
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def remediation(self) -> str:
|
|
78
|
+
"""What this specific machine needs, rather than generic advice."""
|
|
79
|
+
if self.availability is Availability.NOT_RUNNING:
|
|
80
|
+
return f"{self.name} is installed but not responding. Start it and re-run."
|
|
81
|
+
if self.availability is Availability.NOT_INSTALLED:
|
|
82
|
+
return f"Install {self.name}."
|
|
83
|
+
if self.availability is Availability.UNUSABLE:
|
|
84
|
+
return f"{self.name} responded with an error: {self.detail}"
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _run(command: list[str]) -> tuple[int, str]:
|
|
89
|
+
"""Run a probe, tolerating every way it can fail to answer."""
|
|
90
|
+
try:
|
|
91
|
+
completed = subprocess.run(
|
|
92
|
+
command,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
text=True,
|
|
95
|
+
timeout=PROBE_TIMEOUT_S,
|
|
96
|
+
check=False,
|
|
97
|
+
)
|
|
98
|
+
except subprocess.TimeoutExpired:
|
|
99
|
+
return 124, "timed out"
|
|
100
|
+
except OSError as exc:
|
|
101
|
+
return 127, str(exc)
|
|
102
|
+
output = (completed.stdout or completed.stderr or "").strip()
|
|
103
|
+
return completed.returncode, output.splitlines()[0] if output else ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _probe(name: str) -> RuntimeInfo:
|
|
107
|
+
if shutil.which(name) is None:
|
|
108
|
+
return RuntimeInfo(name, Availability.NOT_INSTALLED, detail="not on PATH")
|
|
109
|
+
|
|
110
|
+
# `info`, not `--version`. The version comes from the CLI binary and says nothing about
|
|
111
|
+
# whether a container can be started; `info` requires the daemon to answer.
|
|
112
|
+
code, output = _run([name, "info", "--format", "{{.ServerVersion}}"])
|
|
113
|
+
if code == 0 and output:
|
|
114
|
+
return RuntimeInfo(name, Availability.READY, version=output)
|
|
115
|
+
if code == 124:
|
|
116
|
+
return RuntimeInfo(
|
|
117
|
+
name,
|
|
118
|
+
Availability.NOT_RUNNING,
|
|
119
|
+
detail=f"`{name} info` timed out after {PROBE_TIMEOUT_S:g}s",
|
|
120
|
+
)
|
|
121
|
+
# A non-zero `info` with the binary present almost always means the daemon is down; the
|
|
122
|
+
# message is kept so an unusual cause is still visible rather than flattened.
|
|
123
|
+
return RuntimeInfo(name, Availability.NOT_RUNNING, detail=output or f"`{name} info` failed")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def detect_runtimes() -> list[RuntimeInfo]:
|
|
127
|
+
"""Probe every candidate and report all of them.
|
|
128
|
+
|
|
129
|
+
All of them, not the first that works: ``metrik bench --check`` has to be able to say what
|
|
130
|
+
was looked for and what each one said, or "no runtime available" is unactionable.
|
|
131
|
+
"""
|
|
132
|
+
return [_probe(name) for name in CANDIDATES]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def require_runtime(*, task_id: str) -> RuntimeInfo:
|
|
136
|
+
"""Return a usable runtime, or refuse.
|
|
137
|
+
|
|
138
|
+
The refusal is the feature. ADR 019 permits no in-process fallback and no flag that enables
|
|
139
|
+
one, so this raises and the caller stops -- there is deliberately no second return value
|
|
140
|
+
meaning "carry on unsafely".
|
|
141
|
+
|
|
142
|
+
Called *before* generation, so a machine that cannot contain the run finds out in a second
|
|
143
|
+
rather than after an hour of decoding.
|
|
144
|
+
"""
|
|
145
|
+
found = detect_runtimes()
|
|
146
|
+
for runtime in found:
|
|
147
|
+
if runtime.ready:
|
|
148
|
+
return runtime
|
|
149
|
+
|
|
150
|
+
installed = [r for r in found if r.availability is not Availability.NOT_INSTALLED]
|
|
151
|
+
if installed:
|
|
152
|
+
# Something is there but not answering. Say which, and what to do about that one --
|
|
153
|
+
# "install Docker" to someone who has Docker installed reads as a broken tool.
|
|
154
|
+
lines = "; ".join(f"{r.name}: {r.detail}" for r in installed)
|
|
155
|
+
remediation = " ".join(r.remediation for r in installed)
|
|
156
|
+
else:
|
|
157
|
+
lines = "none of " + ", ".join(CANDIDATES) + " is installed"
|
|
158
|
+
remediation = (
|
|
159
|
+
"Install Docker Desktop and start it (plan.md sec. 13 decision 4), or Podman. "
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
raise UnsupportedError(
|
|
163
|
+
f"{task_id} executes model-generated code and no container runtime is available ({lines})",
|
|
164
|
+
remediation=(
|
|
165
|
+
f"{remediation}Run `metrik bench --check` to see what was probed. Tasks that do "
|
|
166
|
+
f"not execute code still run without a container. There is no unsandboxed mode "
|
|
167
|
+
f"(ADR 019)."
|
|
168
|
+
),
|
|
169
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""The task catalogue: which benchmarks exist, and which of them run code.
|
|
2
|
+
|
|
3
|
+
``executes_code`` is declared per task and never inferred from the id. A pattern match on
|
|
4
|
+
names like ``*eval*`` would be wrong in both directions -- MMLU contains "eval" in plenty of
|
|
5
|
+
spellings and does not execute anything, while a new agentic benchmark might execute
|
|
6
|
+
everything and be called something else entirely. The failure that matters is a
|
|
7
|
+
code-executing task defaulting to "safe", so nothing here guesses.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
from metrik.sdk import UserError
|
|
16
|
+
|
|
17
|
+
__all__ = ["TASKS", "TaskSpec", "resolve_task"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class TaskSpec:
|
|
22
|
+
"""A benchmark this build knows how to run."""
|
|
23
|
+
|
|
24
|
+
id: str
|
|
25
|
+
version: str
|
|
26
|
+
#: The field that gates sandboxing (``artifacts.md`` §6.12). Declared, never inferred.
|
|
27
|
+
executes_code: bool
|
|
28
|
+
summary: str
|
|
29
|
+
n_items: int | None = None
|
|
30
|
+
harness: str = "lm_eval_harness"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TASKS: Final[dict[str, TaskSpec]] = {
|
|
34
|
+
"humaneval": TaskSpec(
|
|
35
|
+
id="humaneval",
|
|
36
|
+
version="1.0",
|
|
37
|
+
executes_code=True,
|
|
38
|
+
summary="164 Python problems; generated solutions are executed against unit tests.",
|
|
39
|
+
n_items=164,
|
|
40
|
+
harness="metrik_code_exec",
|
|
41
|
+
),
|
|
42
|
+
"mbpp": TaskSpec(
|
|
43
|
+
id="mbpp",
|
|
44
|
+
version="1.0",
|
|
45
|
+
executes_code=True,
|
|
46
|
+
summary="Mostly Basic Python Problems; generated solutions are executed.",
|
|
47
|
+
n_items=500,
|
|
48
|
+
harness="metrik_code_exec",
|
|
49
|
+
),
|
|
50
|
+
"mmlu": TaskSpec(
|
|
51
|
+
id="mmlu",
|
|
52
|
+
version="1.0",
|
|
53
|
+
executes_code=False,
|
|
54
|
+
summary="Multiple choice across 57 subjects. Nothing is executed.",
|
|
55
|
+
n_items=14042,
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_task(task_id: str) -> TaskSpec:
|
|
61
|
+
"""Look up a task, or fail with the list of what exists.
|
|
62
|
+
|
|
63
|
+
An unknown task is refused rather than treated as a custom one. Accepting it would mean
|
|
64
|
+
guessing ``executes_code``, and the safe-looking guess is the dangerous one.
|
|
65
|
+
"""
|
|
66
|
+
spec = TASKS.get(task_id.lower())
|
|
67
|
+
if spec is None:
|
|
68
|
+
raise UserError(
|
|
69
|
+
f"unknown task {task_id!r}",
|
|
70
|
+
remediation=f"Known tasks: {', '.join(sorted(TASKS))}. Run `metrik bench --check`.",
|
|
71
|
+
)
|
|
72
|
+
return spec
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Runtime detection and the refusal path.
|
|
2
|
+
|
|
3
|
+
The property under test is that there is **no way through** without a runtime. ADR 019 permits
|
|
4
|
+
no in-process fallback and no flag enabling one, so `require_runtime` either returns something
|
|
5
|
+
usable or raises -- there is deliberately no third outcome.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from metrik.bench import Availability, RuntimeInfo, detect_runtimes, require_runtime, resolve_task
|
|
17
|
+
from metrik.bench import runtime as runtime_module
|
|
18
|
+
from metrik.sdk import UnsupportedError, UserError
|
|
19
|
+
|
|
20
|
+
pytestmark = pytest.mark.unit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fake_probe(monkeypatch: pytest.MonkeyPatch, results: dict[str, RuntimeInfo]) -> None:
|
|
24
|
+
monkeypatch.setattr(
|
|
25
|
+
runtime_module,
|
|
26
|
+
"_probe",
|
|
27
|
+
lambda name: results.get(name, RuntimeInfo(name, Availability.NOT_INSTALLED)),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# -- the refusal -------------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_no_runtime_means_refusal(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
35
|
+
_fake_probe(monkeypatch, {})
|
|
36
|
+
with pytest.raises(UnsupportedError, match="no container runtime is available"):
|
|
37
|
+
require_runtime(task_id="humaneval")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_the_refusal_says_there_is_no_unsandboxed_mode(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
41
|
+
"""The message has to close the door, or the next question is "how do I skip it"."""
|
|
42
|
+
_fake_probe(monkeypatch, {})
|
|
43
|
+
with pytest.raises(UnsupportedError) as caught:
|
|
44
|
+
require_runtime(task_id="humaneval")
|
|
45
|
+
assert "no unsandboxed mode" in (caught.value.remediation or "")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_an_installed_but_stopped_runtime_gets_different_advice(
|
|
49
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Telling someone with Docker installed to "install Docker" reads as a broken tool."""
|
|
52
|
+
_fake_probe(
|
|
53
|
+
monkeypatch,
|
|
54
|
+
{"docker": RuntimeInfo("docker", Availability.NOT_RUNNING, detail="daemon unreachable")},
|
|
55
|
+
)
|
|
56
|
+
with pytest.raises(UnsupportedError) as caught:
|
|
57
|
+
require_runtime(task_id="humaneval")
|
|
58
|
+
remediation = caught.value.remediation or ""
|
|
59
|
+
assert "not responding" in remediation
|
|
60
|
+
assert "Install docker." not in remediation
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_a_ready_runtime_is_returned(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
64
|
+
_fake_probe(monkeypatch, {"docker": RuntimeInfo("docker", Availability.READY, version="27.0")})
|
|
65
|
+
assert require_runtime(task_id="humaneval").name == "docker"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_the_second_candidate_is_used_when_the_first_is_down(
|
|
69
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
70
|
+
) -> None:
|
|
71
|
+
_fake_probe(
|
|
72
|
+
monkeypatch,
|
|
73
|
+
{
|
|
74
|
+
"docker": RuntimeInfo("docker", Availability.NOT_RUNNING),
|
|
75
|
+
"podman": RuntimeInfo("podman", Availability.READY, version="5.1"),
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
assert require_runtime(task_id="humaneval").name == "podman"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# -- installed is not running --------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_a_present_binary_with_a_dead_daemon_is_not_ready(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
85
|
+
"""`docker --version` succeeds with Docker Desktop stopped. Only `info` reaches the daemon,
|
|
86
|
+
and only the daemon can start a container."""
|
|
87
|
+
monkeypatch.setattr(shutil, "which", lambda _n: "/usr/bin/docker")
|
|
88
|
+
monkeypatch.setattr(
|
|
89
|
+
runtime_module, "_run", lambda _c: (1, "Cannot connect to the Docker daemon")
|
|
90
|
+
)
|
|
91
|
+
probed = runtime_module._probe("docker")
|
|
92
|
+
assert probed.availability is Availability.NOT_RUNNING
|
|
93
|
+
assert not probed.ready
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_a_hanging_probe_becomes_a_refusal_not_a_hang(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
97
|
+
"""A stopped Docker Desktop can leave `info` blocked on a named pipe. Waiting forever to
|
|
98
|
+
find out whether a run may start is worse than refusing quickly."""
|
|
99
|
+
|
|
100
|
+
def _timeout(*_a: Any, **_k: Any) -> Any:
|
|
101
|
+
raise subprocess.TimeoutExpired(cmd="docker", timeout=runtime_module.PROBE_TIMEOUT_S)
|
|
102
|
+
|
|
103
|
+
monkeypatch.setattr(shutil, "which", lambda _n: "/usr/bin/docker")
|
|
104
|
+
monkeypatch.setattr(subprocess, "run", _timeout)
|
|
105
|
+
probed = runtime_module._probe("docker")
|
|
106
|
+
assert probed.availability is Availability.NOT_RUNNING
|
|
107
|
+
assert "timed out" in probed.detail
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_a_missing_binary_is_reported_as_absent(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
111
|
+
monkeypatch.setattr(shutil, "which", lambda _n: None)
|
|
112
|
+
assert runtime_module._probe("docker").availability is Availability.NOT_INSTALLED
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_detection_reports_every_candidate(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
116
|
+
"""Not just the first that works: `--check` cannot explain "nothing available" without
|
|
117
|
+
saying what was looked for."""
|
|
118
|
+
_fake_probe(monkeypatch, {})
|
|
119
|
+
assert [r.name for r in detect_runtimes()] == list(runtime_module.CANDIDATES)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# -- the task catalogue -------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
("task_id", "executes"), [("humaneval", True), ("mbpp", True), ("mmlu", False)]
|
|
127
|
+
)
|
|
128
|
+
def test_executes_code_is_declared_per_task(task_id: str, executes: bool) -> None:
|
|
129
|
+
assert resolve_task(task_id).executes_code is executes
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_an_unknown_task_is_refused_rather_than_assumed_safe() -> None:
|
|
133
|
+
"""Accepting it would mean guessing `executes_code`, and the safe-looking guess is the
|
|
134
|
+
dangerous one."""
|
|
135
|
+
with pytest.raises(UserError, match="unknown task"):
|
|
136
|
+
resolve_task("some-new-agentic-benchmark")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_task_lookup_is_case_insensitive() -> None:
|
|
140
|
+
assert resolve_task("HumanEval").id == "humaneval"
|