ruling 0.2.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.
- ruling-0.2.0/.gitignore +9 -0
- ruling-0.2.0/LICENSE +21 -0
- ruling-0.2.0/PKG-INFO +83 -0
- ruling-0.2.0/README.md +61 -0
- ruling-0.2.0/pyproject.toml +60 -0
- ruling-0.2.0/ruling/__init__.py +25 -0
- ruling-0.2.0/ruling/against.py +50 -0
- ruling-0.2.0/ruling/build.py +257 -0
- ruling-0.2.0/ruling/calibration.py +182 -0
- ruling-0.2.0/ruling/cascade.py +83 -0
- ruling-0.2.0/ruling/cli.py +397 -0
- ruling-0.2.0/ruling/codes.py +30 -0
- ruling-0.2.0/ruling/config.py +120 -0
- ruling-0.2.0/ruling/corpus.py +202 -0
- ruling-0.2.0/ruling/decision.py +246 -0
- ruling-0.2.0/ruling/decision_train.py +208 -0
- ruling-0.2.0/ruling/distill.py +131 -0
- ruling-0.2.0/ruling/engine.py +518 -0
- ruling-0.2.0/ruling/evals.py +459 -0
- ruling-0.2.0/ruling/failover.py +62 -0
- ruling-0.2.0/ruling/history.html +78 -0
- ruling-0.2.0/ruling/hosted.py +394 -0
- ruling-0.2.0/ruling/ledger.py +120 -0
- ruling-0.2.0/ruling/metrics.py +66 -0
- ruling-0.2.0/ruling/modernbert.py +163 -0
- ruling-0.2.0/ruling/peft.py +144 -0
- ruling-0.2.0/ruling/playground.html +217 -0
- ruling-0.2.0/ruling/prompt.py +134 -0
- ruling-0.2.0/ruling/questions.py +280 -0
- ruling-0.2.0/ruling/registry.py +195 -0
- ruling-0.2.0/ruling/retrain.py +125 -0
- ruling-0.2.0/ruling/sdk.py +52 -0
- ruling-0.2.0/ruling/server.py +198 -0
- ruling-0.2.0/ruling/states.py +200 -0
- ruling-0.2.0/ruling/thinking.py +75 -0
- ruling-0.2.0/ruling/train.py +481 -0
- ruling-0.2.0/ruling/vision.py +100 -0
- ruling-0.2.0/ruling/worlds.py +134 -0
ruling-0.2.0/.gitignore
ADDED
ruling-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bradAGI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ruling-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ruling
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Open-weight recreation of TypeSafe AI's Jev: typed, probabilistic decisions from a local model, no text generation.
|
|
5
|
+
Project-URL: Homepage, https://github.com/bradAGI/ruling
|
|
6
|
+
Project-URL: Source, https://github.com/bradAGI/ruling
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: fastapi>=0.115
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Requires-Dist: huggingface-hub>=0.25
|
|
13
|
+
Requires-Dist: mlx-lm>=0.31
|
|
14
|
+
Requires-Dist: numpy>=1.26
|
|
15
|
+
Requires-Dist: pyarrow>=25.0.1
|
|
16
|
+
Requires-Dist: pydantic>=2.7
|
|
17
|
+
Requires-Dist: transformers>=5.17.0
|
|
18
|
+
Requires-Dist: uvicorn>=0.30
|
|
19
|
+
Provides-Extra: vision
|
|
20
|
+
Requires-Dist: mlx-vlm>=0.7.8; extra == 'vision'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="assets/banner.png" alt="ruling: typed decisions, no text" width="720" />
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
## ruling: _typed decisions_, not generated text, from open models
|
|
28
|
+
|
|
29
|
+
**Documentation:** [ruling docs](docs/README.md)
|
|
30
|
+
|
|
31
|
+
----
|
|
32
|
+
|
|
33
|
+
ruling turns any open chat model into a decision engine like TypeSafe's
|
|
34
|
+
[Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev). You send
|
|
35
|
+
a state (text, JSON or images) and typed questions; you get back a probability
|
|
36
|
+
for every option you declared. Nothing is generated, so there is nothing to
|
|
37
|
+
parse and the model cannot invent an answer you did not list. It runs on your
|
|
38
|
+
own machine, serves TypeSafe's API so their client works against it unchanged,
|
|
39
|
+
and learns from the outcomes of the decisions it serves.
|
|
40
|
+
|
|
41
|
+
On the 256 public judgments that carry Jev's own answers, a 19 GB open model
|
|
42
|
+
with no training gets 231 right to Jev's 238, not a significant difference
|
|
43
|
+
(exact McNemar, p = 0.21); [the results](docs/results.md) also say where Jev
|
|
44
|
+
is ahead. Not affiliated with TypeSafe AI.
|
|
45
|
+
|
|
46
|
+
## Documentation: [docs/](docs/README.md)
|
|
47
|
+
|
|
48
|
+
**Please go to the [ruling docs](docs/README.md).** The usual starting points:
|
|
49
|
+
[Getting started](docs/getting-started.md),
|
|
50
|
+
[Choosing models](docs/models.md), [Fine-tuning](docs/fine-tuning.md) and the
|
|
51
|
+
[Reference](docs/reference.md) for every setting and API route.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
You need an Apple Silicon Mac, Python 3.12+, and [uv](https://docs.astral.sh/uv/).
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/bradAGI/ruling && cd ruling
|
|
59
|
+
uv sync # add --extra vision to ask about images
|
|
60
|
+
uv run ruling serve # downloads a 2.3 GB model on first start, then serves http://127.0.0.1:8010
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open `http://127.0.0.1:8010/` for the playground. On NVIDIA, see
|
|
64
|
+
[vLLM](docs/reference.md#on-nvidia-vllm). Before relying on an answer, read the
|
|
65
|
+
[limits](docs/getting-started.md#limits).
|
|
66
|
+
|
|
67
|
+
## Reading more
|
|
68
|
+
|
|
69
|
+
- [Results](docs/results.md) and the [full measurements](docs/measurements.md),
|
|
70
|
+
with the command behind every number.
|
|
71
|
+
- [How it works](docs/how-it-works.md): one prefix, many suffixes, and reading
|
|
72
|
+
logits instead of sampling.
|
|
73
|
+
- [Background](docs/background.md): what Jev is, the other open recreations,
|
|
74
|
+
and the papers behind the calibration.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT. ruling is an independent project with no connection to TypeSafe AI; "Jev"
|
|
79
|
+
and "System One" are their names, used here to say what this reproduces. The
|
|
80
|
+
`authored144` and `perturbations108` datasets and the WANLI row selection are
|
|
81
|
+
converted from TheoLeeCJ/openjev under MIT, with their copyright notice in
|
|
82
|
+
[THIRD_PARTY.md](THIRD_PARTY.md). WANLI is CC-BY-4.0 and SST-5 keeps its own
|
|
83
|
+
terms; neither is committed here. Model weights keep their own licenses.
|
ruling-0.2.0/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/banner.png" alt="ruling: typed decisions, no text" width="720" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
## ruling: _typed decisions_, not generated text, from open models
|
|
6
|
+
|
|
7
|
+
**Documentation:** [ruling docs](docs/README.md)
|
|
8
|
+
|
|
9
|
+
----
|
|
10
|
+
|
|
11
|
+
ruling turns any open chat model into a decision engine like TypeSafe's
|
|
12
|
+
[Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev). You send
|
|
13
|
+
a state (text, JSON or images) and typed questions; you get back a probability
|
|
14
|
+
for every option you declared. Nothing is generated, so there is nothing to
|
|
15
|
+
parse and the model cannot invent an answer you did not list. It runs on your
|
|
16
|
+
own machine, serves TypeSafe's API so their client works against it unchanged,
|
|
17
|
+
and learns from the outcomes of the decisions it serves.
|
|
18
|
+
|
|
19
|
+
On the 256 public judgments that carry Jev's own answers, a 19 GB open model
|
|
20
|
+
with no training gets 231 right to Jev's 238, not a significant difference
|
|
21
|
+
(exact McNemar, p = 0.21); [the results](docs/results.md) also say where Jev
|
|
22
|
+
is ahead. Not affiliated with TypeSafe AI.
|
|
23
|
+
|
|
24
|
+
## Documentation: [docs/](docs/README.md)
|
|
25
|
+
|
|
26
|
+
**Please go to the [ruling docs](docs/README.md).** The usual starting points:
|
|
27
|
+
[Getting started](docs/getting-started.md),
|
|
28
|
+
[Choosing models](docs/models.md), [Fine-tuning](docs/fine-tuning.md) and the
|
|
29
|
+
[Reference](docs/reference.md) for every setting and API route.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
You need an Apple Silicon Mac, Python 3.12+, and [uv](https://docs.astral.sh/uv/).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/bradAGI/ruling && cd ruling
|
|
37
|
+
uv sync # add --extra vision to ask about images
|
|
38
|
+
uv run ruling serve # downloads a 2.3 GB model on first start, then serves http://127.0.0.1:8010
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Open `http://127.0.0.1:8010/` for the playground. On NVIDIA, see
|
|
42
|
+
[vLLM](docs/reference.md#on-nvidia-vllm). Before relying on an answer, read the
|
|
43
|
+
[limits](docs/getting-started.md#limits).
|
|
44
|
+
|
|
45
|
+
## Reading more
|
|
46
|
+
|
|
47
|
+
- [Results](docs/results.md) and the [full measurements](docs/measurements.md),
|
|
48
|
+
with the command behind every number.
|
|
49
|
+
- [How it works](docs/how-it-works.md): one prefix, many suffixes, and reading
|
|
50
|
+
logits instead of sampling.
|
|
51
|
+
- [Background](docs/background.md): what Jev is, the other open recreations,
|
|
52
|
+
and the papers behind the calibration.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT. ruling is an independent project with no connection to TypeSafe AI; "Jev"
|
|
57
|
+
and "System One" are their names, used here to say what this reproduces. The
|
|
58
|
+
`authored144` and `perturbations108` datasets and the WANLI row selection are
|
|
59
|
+
converted from TheoLeeCJ/openjev under MIT, with their copyright notice in
|
|
60
|
+
[THIRD_PARTY.md](THIRD_PARTY.md). WANLI is CC-BY-4.0 and SST-5 keeps its own
|
|
61
|
+
terms; neither is committed here. Model weights keep their own licenses.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ruling"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Open-weight recreation of TypeSafe AI's Jev: typed, probabilistic decisions from a local model, no text generation."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"mlx-lm>=0.31",
|
|
10
|
+
"fastapi>=0.115",
|
|
11
|
+
"uvicorn>=0.30",
|
|
12
|
+
"pydantic>=2.7",
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
"numpy>=1.26",
|
|
15
|
+
"huggingface-hub>=0.25",
|
|
16
|
+
"pyarrow>=25.0.1",
|
|
17
|
+
"transformers>=5.17.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/bradAGI/ruling"
|
|
22
|
+
Source = "https://github.com/bradAGI/ruling"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
ruling = "ruling.cli:main"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
vision = [
|
|
29
|
+
"mlx-vlm>=0.7.8",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"mlx-vlm>=0.7.8",
|
|
35
|
+
"peft>=0.21.2",
|
|
36
|
+
"pytest>=8",
|
|
37
|
+
"pytest-playwright>=0.7",
|
|
38
|
+
"pytest-timeout>=2.4.0",
|
|
39
|
+
"torch>=2.14.0",
|
|
40
|
+
"typesafe-sdk>=0.6.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["ruling"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build]
|
|
51
|
+
include = ["ruling/**"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
# unit/ and integration/ hold same-named modules; importlib mode keeps them distinct.
|
|
56
|
+
addopts = "--import-mode=importlib"
|
|
57
|
+
markers = [
|
|
58
|
+
"integration: loads a real model; slow on first run",
|
|
59
|
+
"heavy: trains a model or spawns a server; skipped on CI runners without a GPU",
|
|
60
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""ruling: typed, probabilistic decisions from a local open-weight model."""
|
|
2
|
+
|
|
3
|
+
from ruling.questions import (
|
|
4
|
+
Choice,
|
|
5
|
+
ChoiceAnswer,
|
|
6
|
+
Noul,
|
|
7
|
+
NoulAnswer,
|
|
8
|
+
Score,
|
|
9
|
+
ScoreAnswer,
|
|
10
|
+
SystemOneRequest,
|
|
11
|
+
SystemOneResponse,
|
|
12
|
+
)
|
|
13
|
+
from ruling.sdk import RulingClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Choice",
|
|
17
|
+
"ChoiceAnswer",
|
|
18
|
+
"Noul",
|
|
19
|
+
"NoulAnswer",
|
|
20
|
+
"RulingClient",
|
|
21
|
+
"Score",
|
|
22
|
+
"ScoreAnswer",
|
|
23
|
+
"SystemOneRequest",
|
|
24
|
+
"SystemOneResponse",
|
|
25
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Send a dataset to the hosted TypeSafe API so Jev's live answers can sit next to ruling's.
|
|
2
|
+
|
|
3
|
+
TypeSafe's HTTP request and response shapes are the same as ruling's, so this
|
|
4
|
+
is a thin client: bearer token, one POST per record, answers converted to
|
|
5
|
+
distributions over the record's canonical keys. Nothing but the record's state
|
|
6
|
+
and questions is sent.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from ruling.evals import Distribution, Record, answer_distribution
|
|
16
|
+
from ruling.questions import option_keys
|
|
17
|
+
|
|
18
|
+
API_KEY_ENV = "TYPESAFE_API_KEY"
|
|
19
|
+
BASE_URL_ENV = "TYPESAFE_BASE_URL"
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.typesafe.ai"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def live_answers(
|
|
24
|
+
records: list[Record],
|
|
25
|
+
base_url: str | None = None,
|
|
26
|
+
api_key: str | None = None,
|
|
27
|
+
model: str | None = None,
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
):
|
|
30
|
+
"""Yield (record index, answer distributions by question id) from the hosted API, one request per record."""
|
|
31
|
+
base_url = base_url or os.environ.get(BASE_URL_ENV, DEFAULT_BASE_URL)
|
|
32
|
+
api_key = api_key or os.environ.get(API_KEY_ENV)
|
|
33
|
+
headers = {"authorization": f"Bearer {api_key}"} if api_key else {}
|
|
34
|
+
with httpx.Client(base_url=base_url, headers=headers, timeout=timeout) as client:
|
|
35
|
+
for index, record in enumerate(records):
|
|
36
|
+
body = {"state": record.state,
|
|
37
|
+
"questions": {qid: q.model_dump(exclude_none=True) for qid, q in record.questions.items()}}
|
|
38
|
+
if model is not None:
|
|
39
|
+
body["model"] = model
|
|
40
|
+
response = client.post("/v1/systemone", json=body)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
yield index, {qid: answer_distribution(answer, option_keys(record.questions[qid]))
|
|
43
|
+
for qid, answer in response.json()["answers"].items()}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def collect_live(records: list[Record], base_url: str | None = None, api_key: str | None = None,
|
|
47
|
+
model: str | None = None, timeout: float = 30.0) -> dict[tuple[int, str], Distribution]:
|
|
48
|
+
return {(index, qid): distribution
|
|
49
|
+
for index, answers in live_answers(records, base_url, api_key, model, timeout)
|
|
50
|
+
for qid, distribution in answers.items()}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Build labeled datasets from pinned public sources.
|
|
2
|
+
|
|
3
|
+
Nothing here is committed; the builders download a pinned revision and write
|
|
4
|
+
records in the ruling dataset format. WANLI rows follow the 256-row selection
|
|
5
|
+
published by TheoLeeCJ/openjev so results stay comparable. SST-5 gives an
|
|
6
|
+
ordinal five-level Score benchmark. The TypeSafe and Every builders carry
|
|
7
|
+
Jev's own published answers as references, so agreement with Jev can be
|
|
8
|
+
reported on identical inputs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
import re
|
|
18
|
+
import zipfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from huggingface_hub import hf_hub_download
|
|
23
|
+
|
|
24
|
+
from ruling.evals import Distribution, answer_distribution
|
|
25
|
+
|
|
26
|
+
DATASETS = Path(__file__).resolve().parents[1] / "datasets"
|
|
27
|
+
|
|
28
|
+
WANLI = {"repo": "alisawuffles/WANLI", "revision": "61c95318fd71c55b6ba355d76253254615f387ec", "file": "test.jsonl"}
|
|
29
|
+
SST5 = {"repo": "SetFit/sst5", "revision": "e51bdcd8cd3a30da231967c1a249ba59361279a3", "file": "test.jsonl"}
|
|
30
|
+
WANLI_SELECTION = DATASETS / "wanli256-selection.jsonl"
|
|
31
|
+
TYPESAFE_SELECTION = DATASETS / "typesafe102-selection.jsonl"
|
|
32
|
+
TYPESAFE_CASES = "https://evals.typesafe.ai/{workflow}-cases.js"
|
|
33
|
+
EVERY_EXPERIMENTS = "https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/experiments.json"
|
|
34
|
+
EVERY_ARCHIVE = "https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip"
|
|
35
|
+
EVERY_EXPERIMENTS_GENERATED_AT = "2026-08-28T19:55:23.191Z"
|
|
36
|
+
EVERY_ARCHIVE_SHA256 = "9fbf42e9d9e7cd3b072e0271a0959dbfdca85618e93fe4f0ca519d683c418bf0"
|
|
37
|
+
|
|
38
|
+
NLI_DESCRIPTIONS = {
|
|
39
|
+
"supported": "The evidence establishes the claim",
|
|
40
|
+
"insufficient": "The evidence does not establish either",
|
|
41
|
+
"contradicted": "The evidence establishes the opposite",
|
|
42
|
+
}
|
|
43
|
+
NLI_LABELS = {"entailment": "supported", "neutral": "insufficient", "contradiction": "contradicted"}
|
|
44
|
+
SST5_LEVELS = ["very negative", "negative", "neutral", "positive", "very positive"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _rows(source: dict) -> list[dict]:
|
|
48
|
+
path = hf_hub_download(source["repo"], source["file"], repo_type="dataset", revision=source["revision"])
|
|
49
|
+
return [json.loads(line) for line in Path(path).read_text().splitlines() if line.strip()]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fetch(url: str) -> bytes:
|
|
53
|
+
response = httpx.get(url, timeout=60.0, follow_redirects=True)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
return response.content
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_wanli() -> list[dict]:
|
|
59
|
+
"""256 NLI rows as a 3-way Choice plus a Noul on entailment."""
|
|
60
|
+
by_id = {str(row["id"]): row for row in _rows(WANLI)}
|
|
61
|
+
selection = [json.loads(line) for line in WANLI_SELECTION.read_text().splitlines() if line.strip()]
|
|
62
|
+
records = []
|
|
63
|
+
for item in selection:
|
|
64
|
+
row = by_id[str(item["upstream"]["source_id"])]
|
|
65
|
+
gold = NLI_LABELS[row["gold"]]
|
|
66
|
+
records.append({
|
|
67
|
+
"family": item["family"],
|
|
68
|
+
"source_id": row["id"],
|
|
69
|
+
"state": row["premise"],
|
|
70
|
+
"questions": {
|
|
71
|
+
"verdict": {"type": "choice",
|
|
72
|
+
"instructions": "Assess the claim using only the supplied evidence: " + row["hypothesis"],
|
|
73
|
+
"criteria": {key: NLI_DESCRIPTIONS[key] for key in item["option_ids"]}},
|
|
74
|
+
"supported": {"type": "noul",
|
|
75
|
+
"instructions": "The evidence establishes this claim: " + row["hypothesis"]},
|
|
76
|
+
},
|
|
77
|
+
"labels": {"verdict": gold, "supported": gold == "supported"},
|
|
78
|
+
})
|
|
79
|
+
if len(records) != 256:
|
|
80
|
+
raise ValueError(f"expected 256 WANLI rows, built {len(records)}")
|
|
81
|
+
return records
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_sst5(limit: int, seed: int = 0) -> list[dict]:
|
|
85
|
+
"""A seeded sample of SST-5 test sentences as a five-level Score."""
|
|
86
|
+
rows = _rows(SST5)
|
|
87
|
+
sample = random.Random(seed).sample(rows, limit)
|
|
88
|
+
return [{
|
|
89
|
+
"family": "sentiment",
|
|
90
|
+
"state": row["text"],
|
|
91
|
+
"questions": {"sentiment": {"type": "score", "instructions": "What is the sentiment of the review sentence?",
|
|
92
|
+
"criteria": SST5_LEVELS}},
|
|
93
|
+
"labels": {"sentiment": int(row["label"])},
|
|
94
|
+
} for row in sample]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_typesafe() -> list[dict]:
|
|
98
|
+
"""The 102 TypeSafe public-eval rows TheoLeeCJ aligned, with Jev's published answers.
|
|
99
|
+
|
|
100
|
+
Labels are the argmax of TypeSafe's reference, the mean of GPT-6 Astra and
|
|
101
|
+
Claude Fable 5.1 at high reasoning, so they measure agreement with big
|
|
102
|
+
models, not ground truth. The selection manifest pins a hash of each
|
|
103
|
+
workflow payload and the build refuses to run if a payload has changed.
|
|
104
|
+
"""
|
|
105
|
+
selection = [json.loads(line) for line in TYPESAFE_SELECTION.read_text().splitlines() if line.strip()]
|
|
106
|
+
payloads: dict[str, dict] = {}
|
|
107
|
+
for workflow in sorted({item["upstream"]["workflow"] for item in selection}):
|
|
108
|
+
text = _fetch(TYPESAFE_CASES.format(workflow=workflow)).decode()
|
|
109
|
+
prefix = "__VIEWER_DATA__("
|
|
110
|
+
if not text.startswith(prefix):
|
|
111
|
+
raise ValueError(f"unexpected wrapper in TypeSafe {workflow} payload")
|
|
112
|
+
payload, _ = json.JSONDecoder().raw_decode(text[len(prefix):])
|
|
113
|
+
digest = hashlib.sha256(json.dumps(payload, indent=2).encode()).hexdigest()
|
|
114
|
+
pinned = {item["upstream"]["snapshot_sha256"] for item in selection if item["upstream"]["workflow"] == workflow}
|
|
115
|
+
if digest not in pinned:
|
|
116
|
+
raise ValueError(f"TypeSafe {workflow} payload changed since the selection was frozen")
|
|
117
|
+
payloads[workflow] = payload["eval"]
|
|
118
|
+
|
|
119
|
+
records = []
|
|
120
|
+
for item in selection:
|
|
121
|
+
up = item["upstream"]
|
|
122
|
+
evaluation = payloads[up["workflow"]]
|
|
123
|
+
case = evaluation["cases"][up["case_id"]]
|
|
124
|
+
source_question = evaluation["questions"][up["question_index"]]
|
|
125
|
+
if source_question["type"] == "noul":
|
|
126
|
+
criteria = source_question.get("criteria") or {}
|
|
127
|
+
question = {"type": "noul", "instructions": source_question["instructions"],
|
|
128
|
+
"criteria": {"true": criteria.get("true"), "false": criteria.get("false")}}
|
|
129
|
+
keys, source_keys = ["yes", "no"], ["true", "false"]
|
|
130
|
+
elif source_question["type"] == "choice":
|
|
131
|
+
question = {"type": "choice", "instructions": source_question["instructions"],
|
|
132
|
+
"criteria": source_question["criteria"]}
|
|
133
|
+
keys = source_keys = list(question["criteria"])
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError("the frozen selection excludes score questions")
|
|
136
|
+
if source_keys != item["option_ids"]:
|
|
137
|
+
raise ValueError(f"option order changed for {item['id']}")
|
|
138
|
+
|
|
139
|
+
sets = case["reference_answers"][up["node"]][up["question_id"]]["sets"]
|
|
140
|
+
reference = dict(zip(keys, _mean_reference(sets, source_keys)))
|
|
141
|
+
top = max(reference, key=reference.get)
|
|
142
|
+
if sum(1 for v in reference.values() if abs(v - reference[top]) < 1e-12) != 1:
|
|
143
|
+
raise ValueError(f"reference argmax is not unique for {item['id']}")
|
|
144
|
+
published = next(
|
|
145
|
+
(node["answers"][up["question_id"]] for node in case["models"]["typesafe"]["nodes"]
|
|
146
|
+
if node["node"] == up["node"] and node.get("doc") == up["document_index"]
|
|
147
|
+
and node.get("questions", {}).get(up["question_id"]) == up["question_index"]
|
|
148
|
+
and up["question_id"] in node.get("answers", {})),
|
|
149
|
+
None,
|
|
150
|
+
)
|
|
151
|
+
if published is None:
|
|
152
|
+
raise ValueError(f"no published Jev answer for {item['id']}")
|
|
153
|
+
records.append({
|
|
154
|
+
"family": item["family"],
|
|
155
|
+
"source_id": item["id"],
|
|
156
|
+
"state": evaluation["documents"][up["document_index"]],
|
|
157
|
+
"questions": {up["question_id"]: question},
|
|
158
|
+
"labels": {up["question_id"]: (top == "yes") if question["type"] == "noul" else top},
|
|
159
|
+
"references": {up["question_id"]: {"jev": answer_distribution(published, keys), "reference": reference}},
|
|
160
|
+
})
|
|
161
|
+
if len(records) != 102:
|
|
162
|
+
raise ValueError(f"expected 102 TypeSafe rows, built {len(records)}")
|
|
163
|
+
return records
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _mean_reference(sets: list[dict], keys: list[str]) -> list[float]:
|
|
167
|
+
total = [0.0] * len(keys)
|
|
168
|
+
for answer in sets:
|
|
169
|
+
if answer.get("probabilities"):
|
|
170
|
+
raw = {str(k): float(v) for k, v in answer["probabilities"].items()}
|
|
171
|
+
norm = sum(raw.get(k, 0.0) for k in keys)
|
|
172
|
+
for i, key in enumerate(keys):
|
|
173
|
+
total[i] += raw.get(key, 0.0) / norm
|
|
174
|
+
else:
|
|
175
|
+
value = answer["value"]
|
|
176
|
+
key = str(value).lower() if isinstance(value, bool) else str(value)
|
|
177
|
+
total[keys.index(key)] += 1.0
|
|
178
|
+
return [value / len(sets) for value in total]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def build_every() -> list[dict]:
|
|
182
|
+
"""Every's parallel judgment lab: Jev's recorded answers on identical inputs.
|
|
183
|
+
|
|
184
|
+
One record per document with all of that experiment's questions. Gold
|
|
185
|
+
labels exist for the judge grid and the two retrieval grids (author
|
|
186
|
+
labels); the other experiments carry only Jev's answers, so they measure
|
|
187
|
+
agreement. The AI-writing checker is skipped because only excerpts of its
|
|
188
|
+
documents are published.
|
|
189
|
+
"""
|
|
190
|
+
data = json.loads(_fetch(EVERY_EXPERIMENTS))
|
|
191
|
+
if data["generated_at"] != EVERY_EXPERIMENTS_GENERATED_AT:
|
|
192
|
+
raise ValueError(f"Every experiments file changed: generated_at is {data['generated_at']}")
|
|
193
|
+
archive_bytes = _fetch(EVERY_ARCHIVE)
|
|
194
|
+
if hashlib.sha256(archive_bytes).hexdigest() != EVERY_ARCHIVE_SHA256:
|
|
195
|
+
raise ValueError("Every source archive changed")
|
|
196
|
+
archive = zipfile.ZipFile(io.BytesIO(archive_bytes))
|
|
197
|
+
script = archive.read("scripts/run-experiments.mjs").decode()
|
|
198
|
+
policy = json.loads(re.search(r'const SUPPORT_POLICY = ("[^\n]+");', script).group(1))
|
|
199
|
+
experiments = {e["id"]: e for e in data["experiments"]}
|
|
200
|
+
records: list[dict] = []
|
|
201
|
+
|
|
202
|
+
grid = experiments["judge-grid"]
|
|
203
|
+
cells = {(c["output"], c["question"]): c for c in grid["typesafe"]["cells"]}
|
|
204
|
+
for document in grid["documents"]:
|
|
205
|
+
questions = {qid: {"type": "noul", "instructions": text} for qid, text in grid["questions"].items()}
|
|
206
|
+
records.append({
|
|
207
|
+
"family": "every_judge-grid",
|
|
208
|
+
"state": {"policy": policy, "candidate_response": document["response"]},
|
|
209
|
+
"questions": questions,
|
|
210
|
+
"labels": {qid: bool(document["expected"][qid]) for qid in questions},
|
|
211
|
+
"references": {qid: {"jev": _noul(cells[document["id"], qid]["value"])} for qid in questions},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
for name, folder in (("code-rag", "code-repo"), ("company-brain", "company-brain")):
|
|
215
|
+
by_document: dict[str, dict] = {}
|
|
216
|
+
for query in experiments[name]["grid"]:
|
|
217
|
+
for cell in query["cells"]:
|
|
218
|
+
entry = by_document.setdefault(cell["document"], {"questions": {}, "labels": {}, "references": {}})
|
|
219
|
+
entry["questions"][query["id"]] = {"type": "noul", "instructions": query["question"]}
|
|
220
|
+
entry["labels"][query["id"]] = bool(cell["relevant"])
|
|
221
|
+
entry["references"][query["id"]] = {"jev": _noul(cell["value"])}
|
|
222
|
+
for document, entry in by_document.items():
|
|
223
|
+
records.append({"family": f"every_{name}", "state": archive.read(f"fixtures/{folder}/{document}").decode(), **entry})
|
|
224
|
+
|
|
225
|
+
for name in ("action-firewall", "vc-diligence"):
|
|
226
|
+
experiment = experiments[name]
|
|
227
|
+
questions = {qid: {"type": "noul", "instructions": text} for qid, text in experiment["questions"].items()}
|
|
228
|
+
for row in experiment["rows"]:
|
|
229
|
+
records.append({"family": f"every_{name}", "state": row["text"], "questions": questions,
|
|
230
|
+
"references": {qid: {"jev": _noul(row["signals"][qid])} for qid in questions}})
|
|
231
|
+
|
|
232
|
+
for name in ("customer-voice", "ceo-radar", "inbox-urgency", "persona-panel"):
|
|
233
|
+
experiment = experiments[name]
|
|
234
|
+
questions = {qid: ({"type": "noul", "instructions": spec} if isinstance(spec, str) else spec)
|
|
235
|
+
for qid, spec in experiment["questions"].items()}
|
|
236
|
+
for row in experiment["rows"]:
|
|
237
|
+
references = {qid: {"jev": answer_distribution(answer, _keys(questions[qid]))}
|
|
238
|
+
for qid, answer in row["typed_answers"].items()}
|
|
239
|
+
records.append({"family": f"every_{name}", "state": row["document"], "questions": questions,
|
|
240
|
+
"references": references})
|
|
241
|
+
return records
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _noul(probability: float) -> Distribution:
|
|
245
|
+
return {"yes": float(probability), "no": 1.0 - float(probability)}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _keys(question: dict) -> list[str]:
|
|
249
|
+
if question["type"] == "choice":
|
|
250
|
+
return list(question["criteria"])
|
|
251
|
+
if question["type"] == "score":
|
|
252
|
+
return [str(i) for i in range(len(question["criteria"]))]
|
|
253
|
+
return ["yes", "no"]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def write(records: list[dict], output: Path) -> None:
|
|
257
|
+
output.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in records))
|