repeatability-evals 0.2.0__tar.gz → 0.3.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.
- repeatability_evals-0.3.0/CHANGELOG.md +50 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/PKG-INFO +33 -10
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/README.md +32 -9
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/examples/compare_versions_demo.py +2 -2
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/pyproject.toml +1 -1
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/__init__.py +17 -2
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/cli.py +44 -5
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/compare.py +157 -40
- repeatability_evals-0.3.0/src/repeatability_evals/dataset.py +118 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/metrics.py +109 -17
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/prompts.py +19 -7
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/sampling.py +12 -5
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/similarity/__init__.py +10 -2
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/similarity/base.py +28 -5
- repeatability_evals-0.3.0/src/repeatability_evals/similarity/embedding.py +116 -0
- repeatability_evals-0.3.0/src/repeatability_evals/similarity/judge.py +330 -0
- repeatability_evals-0.3.0/src/repeatability_evals/stats.py +71 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_cli.py +1 -1
- repeatability_evals-0.3.0/tests/test_robustness.py +310 -0
- repeatability_evals-0.2.0/src/repeatability_evals/dataset.py +0 -80
- repeatability_evals-0.2.0/src/repeatability_evals/similarity/embedding.py +0 -56
- repeatability_evals-0.2.0/src/repeatability_evals/similarity/judge.py +0 -185
- repeatability_evals-0.2.0/src/repeatability_evals/stats.py +0 -27
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/.github/workflows/ci.yml +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/.github/workflows/publish.yml +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/.gitignore +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/LICENSE +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/benchmarks/sampling_error.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/__main__.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/py.typed +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/similarity/ranking.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/similarity/sets.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/similarity/text.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/src/repeatability_evals/types.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_compare.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_dataset.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_judge.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_metrics.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_sampling.py +0 -0
- {repeatability_evals-0.2.0 → repeatability_evals-0.3.0}/tests/test_similarity.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Dataset means no longer count single-run groups.** Groups with fewer than 2 items were scored
|
|
7
|
+
1.0 and averaged in, inflating `evaluate_groups` results. They are now excluded and listed in
|
|
8
|
+
`DatasetResult.degenerate`.
|
|
9
|
+
- **`acompare_versions(concurrency=N)` is a real global cap.** Previously each input and each metric
|
|
10
|
+
had its own semaphore, so `concurrency=4` could issue ~24 simultaneous judge calls.
|
|
11
|
+
- **Judge prompts are filled in one pass.** A response containing `{item_b}` could pull the other
|
|
12
|
+
response into its own slot. Responses are now also enclosed in tags with an instruction to
|
|
13
|
+
treat them as data (`render_prompt` is public).
|
|
14
|
+
- **Score parsing ignores scale text.** `"Score (1-5): 4"` parsed as 1 and `"2 out of 5"` could
|
|
15
|
+
parse as 5. JSON is now found even after prose containing braces, and rubric scores may be
|
|
16
|
+
nested under `"scores"` or top level.
|
|
17
|
+
- `EmbeddingCosine` cache was not thread-safe under the async metrics (possible `KeyError`).
|
|
18
|
+
- `TotalAgreement` crashed on unhashable items (dicts, lists); they are compared as canonical JSON.
|
|
19
|
+
- `cycle_pairs` with a truncated budget now always covers `min(N, 2 * budget)` items (odd N missed
|
|
20
|
+
one).
|
|
21
|
+
- `__version__` is read from package metadata (0.2.0 on PyPI reported 0.1.0).
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Noise-aware per-input flags in `compare_versions`.** An input now counts as changed only if
|
|
25
|
+
its drift exceeds `tolerance` *and* a permutation test on its pair scores (cross-version pairs
|
|
26
|
+
less alike than within-version pairs) gives `p_value <= alpha` (default 0.2; `alpha=1.0`
|
|
27
|
+
restores the old drift-only rule). On simulated noisy-but-unchanged systems (30% of runs
|
|
28
|
+
differ, 4 runs per side) the share of inputs flagged by chance drops from ~21% to ~5%, and in
|
|
29
|
+
`examples/compare_versions_demo.py` the flagged set now matches the 36 truly changed inputs
|
|
30
|
+
exactly (previously 5 false positives). The dataset-level verdict is unchanged in method.
|
|
31
|
+
`InputComparison.p_value` and CLI `--alpha` expose it.
|
|
32
|
+
- `on_error="skip"` on `PairwiseConsistency`, `evaluate_groups`, `aevaluate_groups`,
|
|
33
|
+
`compare_versions`, `acompare_versions` and the CLI (`--on-error skip`): failing comparisons are
|
|
34
|
+
dropped and reported instead of aborting a long run. `AllComparisonsFailed` when nothing is left.
|
|
35
|
+
- Judges: `retries` (default 2) for unparsable replies, `skip_identical` (default on: identical
|
|
36
|
+
outputs score 1.0 with no model call) and an LRU `cache_size` (default 10,000). Symmetric async
|
|
37
|
+
judging runs both orders concurrently.
|
|
38
|
+
- `Similarity.prepare` / `aprepare` hook, called with every item of a group; `EmbeddingCosine` uses
|
|
39
|
+
it to embed a group in one batch, and accepts an async `aembed`.
|
|
40
|
+
- `call_limit(n)` context manager and `aevaluate_groups(max_calls=...)` for global rate limiting.
|
|
41
|
+
- `FunctionSimilarity` passes `context` to functions that accept it.
|
|
42
|
+
- CLI reads JSON Lines (`{"id", "output", "query"}` per line).
|
|
43
|
+
- HTML reports are now structured pages (tables, headings, light/dark) instead of preformatted
|
|
44
|
+
markdown.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- CLI exit codes: `0` no meaningful change, `3` inconclusive, `4` changed (were 0/1/2, which
|
|
48
|
+
collided with Python and argparse error codes).
|
|
49
|
+
- Judge prompts wrap the input and responses in `<input>`, `<response_a>`, `<response_b>` tags.
|
|
50
|
+
The rubric wording is unchanged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: repeatability-evals
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Consistency evals for non-deterministic systems: LLM generation, agentic retrieval, agent workflows.
|
|
5
5
|
Project-URL: Homepage, https://github.com/krishna-gogineni-765/repeatability-evals
|
|
6
6
|
Project-URL: Issues, https://github.com/krishna-gogineni-765/repeatability-evals/issues
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
**Consistency evals for non-deterministic systems: LLM generation, agentic retrieval, agent workflows.**
|
|
23
23
|
|
|
24
24
|
Most evals ask *"is the output correct?"* and need gold labels to answer. This library asks
|
|
25
|
-
two questions that are extremely important in enterprise
|
|
25
|
+
two questions that are extremely important in enterprise settings and need **no gold labels**:
|
|
26
26
|
|
|
27
27
|
1. **Is my system consistent/repeatable with itself?** Same input, N runs: do the answers, the retrieved
|
|
28
28
|
documents, the tool-call traces agree?
|
|
@@ -54,7 +54,7 @@ report.save("report.md") # also .html / .json
|
|
|
54
54
|
```
|
|
55
55
|
# v1 vs v2: consistency report
|
|
56
56
|
CHANGED: the candidate's outputs differ from the baseline's by more than run-to-run noise explains.
|
|
57
|
-
120 inputs compared;
|
|
57
|
+
120 inputs compared; 30% changed beyond the noise floor.
|
|
58
58
|
|
|
59
59
|
| v1 vs itself (noise floor) | 0.925 |
|
|
60
60
|
| v2 vs itself | 0.948 |
|
|
@@ -66,13 +66,13 @@ CHANGED: the candidate's outputs differ from the baseline's by more than run-to-
|
|
|
66
66
|
| specifics | +0.115 |
|
|
67
67
|
| supporting_detail | +0.100 |
|
|
68
68
|
| core_facts | +0.057 |
|
|
69
|
-
Changed inputs by main driver: 21 mainly in `format`,
|
|
69
|
+
Changed inputs by main driver: 21 mainly in `format`, 15 mainly in `specifics`.
|
|
70
70
|
|
|
71
|
-
### `q000`: drift +0.62 (cross 0.38 vs expected 1.00)
|
|
71
|
+
### `q000`: drift +0.62 (cross 0.38 vs expected 1.00, p = 0.001)
|
|
72
72
|
- Main driver: `specifics`
|
|
73
73
|
- Judge: Retention period differs.
|
|
74
74
|
- v1: Retention for gold is 30 days.
|
|
75
|
-
- v2: -
|
|
75
|
+
- v2: - The retention period of gold is 90 days. - Applies to all sites.
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
*(Real output of `examples/compare_versions_demo.py`, which uses simulated runs and a scripted
|
|
@@ -81,8 +81,12 @@ stand-in for the LLM judge so it runs without an API key.)*
|
|
|
81
81
|
The key idea is the **noise floor**. A non-deterministic system never matches itself perfectly,
|
|
82
82
|
so comparing v2 to v1 naively overstates change. We measure how much v1 differs from *itself*
|
|
83
83
|
and v2 from *itself*, and report only the **extra** disagreement across versions (`drift`).
|
|
84
|
-
Two equally noisy systems show no drift; a real change does.
|
|
85
|
-
|
|
84
|
+
Two equally noisy systems show no drift; a real change does. Each input is also screened with a
|
|
85
|
+
permutation test (are its cross-version pairs less alike than its within-version pairs?), so a
|
|
86
|
+
noisy system does not get a long list of inputs flagged by chance. In the demo above, 36 inputs
|
|
87
|
+
were changed by construction and exactly those 36 are flagged. The report also says *what kind*
|
|
88
|
+
of change it is, since format drift and changed numbers deserve different decisions. Use 3 or
|
|
89
|
+
more runs per side, 5 if the system is noisy.
|
|
86
90
|
|
|
87
91
|
From the shell, with no code and no model (offline n-gram similarity), or with your own LLM:
|
|
88
92
|
|
|
@@ -92,8 +96,10 @@ repeatability-evals compare v1.json v2.json --out report.md
|
|
|
92
96
|
repeatability-evals compare v1.json v2.json --judge mypkg.llm:complete --rubric --out report.html
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
Input is JSON
|
|
96
|
-
|
|
99
|
+
Input is JSON, `{"input id": ["run 1", "run 2", ...]}`, or JSON Lines with one run per line,
|
|
100
|
+
`{"id": "...", "output": "...", "query": "..."}`, which is the shape most request logs already
|
|
101
|
+
have. The exit code is `0` (no meaningful change), `3` (inconclusive) or `4` (changed), so it can
|
|
102
|
+
gate a CI pipeline.
|
|
97
103
|
|
|
98
104
|
## Usage pattern 2: Measure self-consistency
|
|
99
105
|
|
|
@@ -134,6 +140,23 @@ your own `Dimension`s to change them. `LLMJudge` is the single 1-5 semantic-equi
|
|
|
134
140
|
`symmetric=True` judges both orders to cancel position bias. **Validate any judge against human
|
|
135
141
|
ratings on your own data before relying on it.**
|
|
136
142
|
|
|
143
|
+
### Running it at scale
|
|
144
|
+
|
|
145
|
+
LLM-judged evaluations over thousands of inputs should not fall over on one bad reply, and
|
|
146
|
+
should not pay for comparisons whose answer is obvious:
|
|
147
|
+
|
|
148
|
+
* **Failures**: judges re-ask when a reply has no parsable score (`retries=2`). Pass
|
|
149
|
+
`on_error="skip"` to metrics, `evaluate_groups` or `compare_versions` to drop a comparison that
|
|
150
|
+
still fails (timeout, API error) and keep going; skipped pairs and inputs are reported.
|
|
151
|
+
* **Cost**: identical outputs, which deterministic systems produce constantly, score 1.0 without a
|
|
152
|
+
model call (`skip_identical=True`), and repeated comparisons are cached. `EmbeddingCosine`
|
|
153
|
+
embeds each group in one batch.
|
|
154
|
+
* **Rate limits**: `concurrency` on `acompare_versions` (and `max_calls` on `aevaluate_groups`)
|
|
155
|
+
caps simultaneous judge calls across the whole run.
|
|
156
|
+
* **Prompt safety**: outputs are inserted into the judge prompt in one pass, inside tags, and the
|
|
157
|
+
judge is told to treat them as data, so placeholder-like text in a response cannot change the
|
|
158
|
+
prompt's structure. (No prompt fully rules out injection; review judge reasons on outliers.)
|
|
159
|
+
|
|
137
160
|
### Sampling cost
|
|
138
161
|
|
|
139
162
|
All pairs for 10 runs is 45 judge calls; the default sampler uses 10. On synthetic data the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Consistency evals for non-deterministic systems: LLM generation, agentic retrieval, agent workflows.**
|
|
4
4
|
|
|
5
5
|
Most evals ask *"is the output correct?"* and need gold labels to answer. This library asks
|
|
6
|
-
two questions that are extremely important in enterprise
|
|
6
|
+
two questions that are extremely important in enterprise settings and need **no gold labels**:
|
|
7
7
|
|
|
8
8
|
1. **Is my system consistent/repeatable with itself?** Same input, N runs: do the answers, the retrieved
|
|
9
9
|
documents, the tool-call traces agree?
|
|
@@ -35,7 +35,7 @@ report.save("report.md") # also .html / .json
|
|
|
35
35
|
```
|
|
36
36
|
# v1 vs v2: consistency report
|
|
37
37
|
CHANGED: the candidate's outputs differ from the baseline's by more than run-to-run noise explains.
|
|
38
|
-
120 inputs compared;
|
|
38
|
+
120 inputs compared; 30% changed beyond the noise floor.
|
|
39
39
|
|
|
40
40
|
| v1 vs itself (noise floor) | 0.925 |
|
|
41
41
|
| v2 vs itself | 0.948 |
|
|
@@ -47,13 +47,13 @@ CHANGED: the candidate's outputs differ from the baseline's by more than run-to-
|
|
|
47
47
|
| specifics | +0.115 |
|
|
48
48
|
| supporting_detail | +0.100 |
|
|
49
49
|
| core_facts | +0.057 |
|
|
50
|
-
Changed inputs by main driver: 21 mainly in `format`,
|
|
50
|
+
Changed inputs by main driver: 21 mainly in `format`, 15 mainly in `specifics`.
|
|
51
51
|
|
|
52
|
-
### `q000`: drift +0.62 (cross 0.38 vs expected 1.00)
|
|
52
|
+
### `q000`: drift +0.62 (cross 0.38 vs expected 1.00, p = 0.001)
|
|
53
53
|
- Main driver: `specifics`
|
|
54
54
|
- Judge: Retention period differs.
|
|
55
55
|
- v1: Retention for gold is 30 days.
|
|
56
|
-
- v2: -
|
|
56
|
+
- v2: - The retention period of gold is 90 days. - Applies to all sites.
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
*(Real output of `examples/compare_versions_demo.py`, which uses simulated runs and a scripted
|
|
@@ -62,8 +62,12 @@ stand-in for the LLM judge so it runs without an API key.)*
|
|
|
62
62
|
The key idea is the **noise floor**. A non-deterministic system never matches itself perfectly,
|
|
63
63
|
so comparing v2 to v1 naively overstates change. We measure how much v1 differs from *itself*
|
|
64
64
|
and v2 from *itself*, and report only the **extra** disagreement across versions (`drift`).
|
|
65
|
-
Two equally noisy systems show no drift; a real change does.
|
|
66
|
-
|
|
65
|
+
Two equally noisy systems show no drift; a real change does. Each input is also screened with a
|
|
66
|
+
permutation test (are its cross-version pairs less alike than its within-version pairs?), so a
|
|
67
|
+
noisy system does not get a long list of inputs flagged by chance. In the demo above, 36 inputs
|
|
68
|
+
were changed by construction and exactly those 36 are flagged. The report also says *what kind*
|
|
69
|
+
of change it is, since format drift and changed numbers deserve different decisions. Use 3 or
|
|
70
|
+
more runs per side, 5 if the system is noisy.
|
|
67
71
|
|
|
68
72
|
From the shell, with no code and no model (offline n-gram similarity), or with your own LLM:
|
|
69
73
|
|
|
@@ -73,8 +77,10 @@ repeatability-evals compare v1.json v2.json --out report.md
|
|
|
73
77
|
repeatability-evals compare v1.json v2.json --judge mypkg.llm:complete --rubric --out report.html
|
|
74
78
|
```
|
|
75
79
|
|
|
76
|
-
Input is JSON
|
|
77
|
-
|
|
80
|
+
Input is JSON, `{"input id": ["run 1", "run 2", ...]}`, or JSON Lines with one run per line,
|
|
81
|
+
`{"id": "...", "output": "...", "query": "..."}`, which is the shape most request logs already
|
|
82
|
+
have. The exit code is `0` (no meaningful change), `3` (inconclusive) or `4` (changed), so it can
|
|
83
|
+
gate a CI pipeline.
|
|
78
84
|
|
|
79
85
|
## Usage pattern 2: Measure self-consistency
|
|
80
86
|
|
|
@@ -115,6 +121,23 @@ your own `Dimension`s to change them. `LLMJudge` is the single 1-5 semantic-equi
|
|
|
115
121
|
`symmetric=True` judges both orders to cancel position bias. **Validate any judge against human
|
|
116
122
|
ratings on your own data before relying on it.**
|
|
117
123
|
|
|
124
|
+
### Running it at scale
|
|
125
|
+
|
|
126
|
+
LLM-judged evaluations over thousands of inputs should not fall over on one bad reply, and
|
|
127
|
+
should not pay for comparisons whose answer is obvious:
|
|
128
|
+
|
|
129
|
+
* **Failures**: judges re-ask when a reply has no parsable score (`retries=2`). Pass
|
|
130
|
+
`on_error="skip"` to metrics, `evaluate_groups` or `compare_versions` to drop a comparison that
|
|
131
|
+
still fails (timeout, API error) and keep going; skipped pairs and inputs are reported.
|
|
132
|
+
* **Cost**: identical outputs, which deterministic systems produce constantly, score 1.0 without a
|
|
133
|
+
model call (`skip_identical=True`), and repeated comparisons are cached. `EmbeddingCosine`
|
|
134
|
+
embeds each group in one batch.
|
|
135
|
+
* **Rate limits**: `concurrency` on `acompare_versions` (and `max_calls` on `aevaluate_groups`)
|
|
136
|
+
caps simultaneous judge calls across the whole run.
|
|
137
|
+
* **Prompt safety**: outputs are inserted into the judge prompt in one pass, inside tags, and the
|
|
138
|
+
judge is told to treat them as data, so placeholder-like text in a response cannot change the
|
|
139
|
+
prompt's structure. (No prompt fully rules out injection; review judge reasons on outliers.)
|
|
140
|
+
|
|
118
141
|
### Sampling cost
|
|
119
142
|
|
|
120
143
|
All pairs for 10 runs is 45 judge calls; the default sampler uses 10. On synthetic data the
|
|
@@ -38,8 +38,8 @@ v2 = version(lambda q: 90 if q % 8 == 0 else 30, lambda q: q % 5 == 0)
|
|
|
38
38
|
|
|
39
39
|
def judge(prompt):
|
|
40
40
|
"""Scripted stand-in for an LLM: scores each dimension by simple rules."""
|
|
41
|
-
a = prompt.split("
|
|
42
|
-
b = prompt.split("
|
|
41
|
+
a = prompt.split("<response_a>")[1].split("</response_a>")[0]
|
|
42
|
+
b = prompt.split("<response_b>")[1].split("</response_b>")[0]
|
|
43
43
|
num = lambda t: re.findall(r"\d+", t) # noqa: E731
|
|
44
44
|
bullets = lambda t: t.lstrip().startswith("-") # noqa: E731
|
|
45
45
|
scores = {d.name: 5 for d in DEFAULT_DIMENSIONS}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repeatability-evals"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Consistency evals for non-deterministic systems: LLM generation, agentic retrieval, agent workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"""repeatability-evals: consistency evals for non-deterministic LLM systems."""
|
|
2
2
|
from .compare import InputComparison, VersionComparison, acompare_versions, compare_versions
|
|
3
3
|
from .dataset import DatasetResult, aevaluate_groups, evaluate_groups
|
|
4
|
-
from .metrics import
|
|
4
|
+
from .metrics import (
|
|
5
|
+
AllComparisonsFailed,
|
|
6
|
+
ClusterConsistency,
|
|
7
|
+
ConsistencyMetric,
|
|
8
|
+
PairwiseConsistency,
|
|
9
|
+
TotalAgreement,
|
|
10
|
+
call_limit,
|
|
11
|
+
)
|
|
5
12
|
from .prompts import DEFAULT_DIMENSIONS, SEMANTIC_EQUIVALENCE_PROMPT, Dimension, build_rubric_prompt
|
|
6
13
|
from .sampling import AllPairs, AnchorPairs, CrossSampler, CycleSampler, PairSampler, RandomPairs, cycle_pairs
|
|
7
14
|
from .similarity import (
|
|
@@ -20,13 +27,21 @@ from .similarity import (
|
|
|
20
27
|
RubricJudge,
|
|
21
28
|
Similarity,
|
|
22
29
|
as_similarity,
|
|
30
|
+
render_prompt,
|
|
23
31
|
)
|
|
24
32
|
from .stats import bootstrap_ci
|
|
25
33
|
from .types import ConsistencyResult, PairScore, SimilarityResult
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
try:
|
|
36
|
+
from importlib.metadata import PackageNotFoundError
|
|
37
|
+
from importlib.metadata import version as _version
|
|
38
|
+
|
|
39
|
+
__version__ = _version("repeatability-evals")
|
|
40
|
+
except PackageNotFoundError: # running from a source checkout that is not installed
|
|
41
|
+
__version__ = "0+unknown"
|
|
28
42
|
|
|
29
43
|
__all__ = [
|
|
44
|
+
"call_limit", "AllComparisonsFailed", "render_prompt",
|
|
30
45
|
"compare_versions", "acompare_versions", "VersionComparison", "InputComparison", "CrossSampler",
|
|
31
46
|
"ConsistencyMetric", "PairwiseConsistency", "ClusterConsistency", "TotalAgreement",
|
|
32
47
|
"evaluate_groups", "aevaluate_groups", "DatasetResult", "bootstrap_ci",
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
repeatability-evals compare baseline.json candidate.json --out report.md
|
|
4
4
|
|
|
5
5
|
Input files are JSON: ``{"<input id>": ["run 1 output", "run 2 output", ...]}`` or
|
|
6
|
-
``{"<input id>": {"query": "...", "outputs": [...]}}
|
|
6
|
+
``{"<input id>": {"query": "...", "outputs": [...]}}``, or JSON Lines (``.jsonl``) with one run
|
|
7
|
+
per line: ``{"id": "...", "output": "...", "query": "..."}``, the shape most request logs already
|
|
8
|
+
have. By default outputs are compared with
|
|
7
9
|
n-gram overlap (no model needed, runs fully offline). For semantic comparison pass
|
|
8
10
|
``--judge package.module:function`` where the function takes a prompt string and returns the
|
|
9
11
|
model's reply (it may be ``async``); add ``--rubric`` for the multi-dimension judge.
|
|
12
|
+
|
|
13
|
+
Exit status: 0 no meaningful change, 3 inconclusive, 4 changed (1 and 2 are left to Python and
|
|
14
|
+
argparse for errors), so the command can gate a CI pipeline.
|
|
10
15
|
"""
|
|
11
16
|
from __future__ import annotations
|
|
12
17
|
|
|
@@ -21,8 +26,34 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
21
26
|
from .compare import acompare_versions, compare_versions
|
|
22
27
|
from .similarity import EditSimilarity, ExactMatch, LLMJudge, NgramJaccard, RougeL, RubricJudge
|
|
23
28
|
|
|
29
|
+
EXIT_CODES = {"no_meaningful_change": 0, "inconclusive": 3, "changed": 4}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_jsonl(path: str) -> Tuple[Dict[str, List[Any]], Dict[str, Dict[str, Any]]]:
|
|
33
|
+
runs: Dict[str, List[Any]] = {}
|
|
34
|
+
contexts: Dict[str, Dict[str, Any]] = {}
|
|
35
|
+
with open(path, encoding="utf-8") as f:
|
|
36
|
+
for n, line in enumerate(f, 1):
|
|
37
|
+
if not line.strip():
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
row = json.loads(line)
|
|
41
|
+
except ValueError as e:
|
|
42
|
+
raise SystemExit(f"{path}:{n}: invalid JSON ({e})") from e
|
|
43
|
+
if not isinstance(row, dict) or "id" not in row or not ("output" in row or "outputs" in row):
|
|
44
|
+
raise SystemExit(f'{path}:{n}: each line needs "id" and "output" (or "outputs")')
|
|
45
|
+
key = str(row["id"])
|
|
46
|
+
outs = row["outputs"] if "outputs" in row else [row["output"]]
|
|
47
|
+
runs.setdefault(key, []).extend(outs)
|
|
48
|
+
if row.get("query") is not None:
|
|
49
|
+
contexts.setdefault(key, {"query": row["query"]})
|
|
50
|
+
return runs, contexts
|
|
51
|
+
|
|
24
52
|
|
|
25
53
|
def load_runs(path: str) -> Tuple[Dict[str, List[Any]], Dict[str, Dict[str, Any]]]:
|
|
54
|
+
"""Read runs from ``.json`` (object keyed by input id) or ``.jsonl`` (one run per line)."""
|
|
55
|
+
if path.endswith(".jsonl"):
|
|
56
|
+
return _load_jsonl(path)
|
|
26
57
|
with open(path, encoding="utf-8") as f:
|
|
27
58
|
data = json.load(f)
|
|
28
59
|
if not isinstance(data, dict):
|
|
@@ -34,8 +65,10 @@ def load_runs(path: str) -> Tuple[Dict[str, List[Any]], Dict[str, Dict[str, Any]
|
|
|
34
65
|
runs[key] = list(val.get("outputs", []))
|
|
35
66
|
if val.get("query") is not None:
|
|
36
67
|
contexts[key] = {"query": val["query"]}
|
|
37
|
-
|
|
68
|
+
elif isinstance(val, list):
|
|
38
69
|
runs[key] = list(val)
|
|
70
|
+
else:
|
|
71
|
+
raise SystemExit(f"{path}: input {key!r} must map to a list of runs or an object")
|
|
39
72
|
return runs, contexts
|
|
40
73
|
|
|
41
74
|
|
|
@@ -59,9 +92,13 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
59
92
|
c.add_argument("--rubric", action="store_true", help="use the 5-dimension RubricJudge")
|
|
60
93
|
c.add_argument("--symmetric", action="store_true", help="judge both orders (2x cost)")
|
|
61
94
|
c.add_argument("--tolerance", type=float, default=0.1, help="per-input drift counted as changed")
|
|
95
|
+
c.add_argument("--alpha", type=float, default=0.2,
|
|
96
|
+
help="per-input significance level for 'changed' (1.0 = drift threshold only)")
|
|
62
97
|
c.add_argument("--acceptable-drift", type=float, default=0.05, help="mean drift you would ship")
|
|
63
98
|
c.add_argument("--concurrency", type=int, default=8)
|
|
64
99
|
c.add_argument("--no-text", action="store_true", help="omit raw outputs from the report")
|
|
100
|
+
c.add_argument("--on-error", choices=["raise", "skip"], default="raise",
|
|
101
|
+
help="skip comparisons that fail (bad judge reply, timeout) instead of stopping")
|
|
65
102
|
c.add_argument("--names", nargs=2, metavar=("BASE", "CAND"), default=["baseline", "candidate"])
|
|
66
103
|
c.add_argument("--out", help="write report here (.md, .html or .json); default: print markdown")
|
|
67
104
|
args = ap.parse_args(argv)
|
|
@@ -78,9 +115,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
78
115
|
else:
|
|
79
116
|
sim = {"ngram": NgramJaccard(2), "rouge": RougeL(), "edit": EditSimilarity(),
|
|
80
117
|
"exact": ExactMatch()}[args.similarity]
|
|
81
|
-
kwargs: Dict[str, Any] = dict(contexts=contexts, tolerance=args.tolerance,
|
|
118
|
+
kwargs: Dict[str, Any] = dict(contexts=contexts, tolerance=args.tolerance, alpha=args.alpha,
|
|
82
119
|
acceptable_drift=args.acceptable_drift, names=tuple(args.names),
|
|
83
|
-
include_examples=not args.no_text)
|
|
120
|
+
include_examples=not args.no_text, on_error=args.on_error)
|
|
84
121
|
if judge_fn is not None:
|
|
85
122
|
report = asyncio.run(acompare_versions(base, cand, sim, concurrency=args.concurrency, **kwargs))
|
|
86
123
|
else:
|
|
@@ -91,7 +128,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
91
128
|
f"{report.share_changed:.0%} of {report.n_inputs} inputs changed -> {args.out}")
|
|
92
129
|
else:
|
|
93
130
|
print(report.to_markdown(include_text=not args.no_text))
|
|
94
|
-
|
|
131
|
+
if report.failed_inputs:
|
|
132
|
+
print(f"warning: {len(report.failed_inputs)} inputs failed and were skipped", file=sys.stderr)
|
|
133
|
+
return EXIT_CODES[report.verdict]
|
|
95
134
|
|
|
96
135
|
|
|
97
136
|
if __name__ == "__main__":
|